Fix #2338 - Add ViewComponentResult

Makes it easier to render a view component from inside a controller. This
makes it possible to implement behavior where an ajax request refreshes
part of a page.
This commit is contained in:
Ryan Nowak 2015-07-22 12:20:49 -07:00
parent 67474d8cbc
commit e91ce4560f
13 changed files with 741 additions and 29 deletions

View File

@ -375,8 +375,7 @@ Global
{F21E225B-190B-4DAA-8B0A-05986D231F56}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F21E225B-190B-4DAA-8B0A-05986D231F56}.Release|x86.ActiveCfg = Release|Any CPU
{F21E225B-190B-4DAA-8B0A-05986D231F56}.Release|x86.Build.0 = Release|Any CPU
{3F8B8FC1-9FE4-4788-8991-367113E8D7AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F8B8FC1-9FE4-4788-8991-367113E8D7AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F8B8FC1-9FE4-4788-8991-367113E8D7AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F8B8FC1-9FE4-4788-8991-367113E8D7AD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{3F8B8FC1-9FE4-4788-8991-367113E8D7AD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{3F8B8FC1-9FE4-4788-8991-367113E8D7AD}.Debug|x86.ActiveCfg = Debug|Any CPU

View File

@ -324,6 +324,46 @@ namespace Microsoft.AspNet.Mvc
};
}
/// <summary>
/// Creates a <see cref="ViewComponentResult"/> by specifying the name of a view component to render.
/// </summary>
/// <param name="componentName">
/// The view component name. Can be a view component
/// <see cref="ViewComponents.ViewComponentDescriptor.ShortName"/> or
/// <see cref="ViewComponents.ViewComponentDescriptor.FullName"/>.</param>
/// <param name="arguments">The arguments to pass to the view component.</param>
/// <returns>The created <see cref="ViewComponentResult"/> object for the response.</returns>
[NonAction]
public virtual ViewComponentResult ViewComponent(string componentName, params object[] arguments)
{
return new ViewComponentResult()
{
ViewComponentName = componentName,
Arguments = arguments,
ViewData = ViewData,
TempData = TempData
};
}
/// <summary>
/// Creates a <see cref="ViewComponentResult"/> by specifying the <see cref="Type"/> of a view component to
/// render.
/// </summary>
/// <param name="componentType">The view component <see cref="Type"/>.</param>
/// <param name="arguments">The arguments to pass to the view component.</param>
/// <returns>The created <see cref="ViewComponentResult"/> object for the response.</returns>
[NonAction]
public virtual ViewComponentResult ViewComponent(Type componentType, params object[] arguments)
{
return new ViewComponentResult()
{
ViewComponentType = componentType,
Arguments = arguments,
ViewData = ViewData,
TempData = TempData
};
}
/// <summary>
/// Creates a <see cref="ContentResult"/> object by specifying a <paramref name="content"/> string.
/// </summary>

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
{
public class NullView : IView
{
public static readonly NullView Instance = new NullView();
public string Path => string.Empty;
public Task RenderAsync([NotNull] ViewContext context)
{
return Task.FromResult(0);
}
}
}

View File

@ -778,6 +778,22 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGenerator_FieldNameCannotBeNullOrEmpty"), p0, p1, p2, p3, p4);
}
/// <summary>
/// Either the '{0}' or '{1}' property must be set in order to invoke a view component.
/// </summary>
internal static string ViewComponentResult_NameOrTypeMustBeSet
{
get { return GetString("ViewComponentResult_NameOrTypeMustBeSet"); }
}
/// <summary>
/// Either the '{0}' or '{1}' property must be set in order to invoke a view component.
/// </summary>
internal static string FormatViewComponentResult_NameOrTypeMustBeSet(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponentResult_NameOrTypeMustBeSet"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -262,4 +262,7 @@
<data name="HtmlGenerator_FieldNameCannotBeNullOrEmpty" xml:space="preserve">
<value>The name of an HTML field cannot be null or empty. Instead use methods {0}.{1} or {2}.{3} with a non-empty {4} argument value.</value>
</data>
<data name="ViewComponentResult_NameOrTypeMustBeSet" xml:space="preserve">
<value>Either the '{0}' or '{1}' property must be set in order to invoke a view component.</value>
</data>
</root>

View File

@ -0,0 +1,125 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// An <see cref="IActionResult"/> which renders a view component to the response.
/// </summary>
public class ViewComponentResult : ActionResult
{
/// <summary>
/// Gets or sets the arguments provided to the view component.
/// </summary>
public object[] Arguments { get; set; }
/// <summary>
/// Gets or sets the <see cref="MediaTypeHeaderValue"/> representing the Content-Type header of the response.
/// </summary>
public MediaTypeHeaderValue ContentType { get; set; }
/// <summary>
/// Gets or sets the HTTP status code.
/// </summary>
public int? StatusCode { get; set; }
/// <summary>
/// Gets or sets the <see cref="ITempDataDictionary"/> for this result.
/// </summary>
public ITempDataDictionary TempData { get; set; }
/// <summary>
/// Gets or sets the name of the view component to invoke. Will be ignored if <see cref="ViewComponentType"/>
/// is set to a non-<c>null</c> value.
/// </summary>
public string ViewComponentName { get; set; }
/// <summary>
/// Gets or sets the type of the view component to invoke.
/// </summary>
public Type ViewComponentType { get; set; }
/// <summary>
/// Gets or sets the <see cref="ViewDataDictionary"/> for this result.
/// </summary>
public ViewDataDictionary ViewData { get; set; }
/// <summary>
/// Gets or sets the <see cref="IViewEngine"/> used to locate views.
/// </summary>
/// <remarks>When <c>null</c>, an instance of <see cref="ICompositeViewEngine"/> from
/// <c>ActionContext.HttpContext.RequestServices</c> is used.</remarks>
public IViewEngine ViewEngine { get; set; }
/// <inheritdoc />
public override async Task ExecuteResultAsync(ActionContext context)
{
var response = context.HttpContext.Response;
var services = context.HttpContext.RequestServices;
var htmlHelperOptions = services.GetRequiredService<IOptions<MvcViewOptions>>().Options.HtmlHelperOptions;
var viewComponentHelper = services.GetRequiredService<IViewComponentHelper>();
var viewData = ViewData;
if (viewData == null)
{
var modelMetadataProvider = services.GetRequiredService<IModelMetadataProvider>();
viewData = new ViewDataDictionary(modelMetadataProvider, context.ModelState);
}
var contentType = ContentType ?? ViewExecutor.DefaultContentType;
if (contentType.Encoding == null)
{
// Do not modify the user supplied content type, so copy it instead
contentType = contentType.Copy();
contentType.Encoding = Encoding.UTF8;
}
if (StatusCode != null)
{
response.StatusCode = StatusCode.Value;
}
response.ContentType = contentType.ToString();
using (var writer = new HttpResponseStreamWriter(response.Body, contentType.Encoding))
{
var viewContext = new ViewContext(
context,
NullView.Instance,
viewData,
TempData,
writer,
htmlHelperOptions);
(viewComponentHelper as ICanHasViewContext)?.Contextualize(viewContext);
if (ViewComponentType == null && ViewComponentName == null)
{
throw new InvalidOperationException(Resources.FormatViewComponentResult_NameOrTypeMustBeSet(
nameof(ViewComponentName),
nameof(ViewComponentType)));
}
else if (ViewComponentType == null)
{
await viewComponentHelper.RenderInvokeAsync(ViewComponentName, Arguments);
}
else
{
await viewComponentHelper.RenderInvokeAsync(ViewComponentType, Arguments);
}
}
}
}
}

View File

@ -14,11 +14,10 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
public static class ViewExecutor
{
private const int BufferSize = 1024;
private static readonly MediaTypeHeaderValue DefaultContentType = new MediaTypeHeaderValue("text/html")
public static readonly MediaTypeHeaderValue DefaultContentType = new MediaTypeHeaderValue("text/html")
{
Encoding = Encoding.UTF8
};
}.CopyAsReadOnly();
/// <summary>
/// Asynchronously renders the specified <paramref name="view"/> to the response body.
@ -37,32 +36,17 @@ namespace Microsoft.AspNet.Mvc
{
var response = actionContext.HttpContext.Response;
var contentTypeHeader = contentType;
Encoding encoding;
if (contentTypeHeader == null)
contentType = contentType ?? DefaultContentType;
if (contentType.Encoding == null)
{
contentTypeHeader = DefaultContentType;
encoding = Encoding.UTF8;
}
else
{
if (contentTypeHeader.Encoding == null)
{
// Do not modify the user supplied content type, so copy it instead
contentTypeHeader = contentTypeHeader.Copy();
contentTypeHeader.Encoding = Encoding.UTF8;
encoding = Encoding.UTF8;
}
else
{
encoding = contentTypeHeader.Encoding;
}
// Do not modify the user supplied content type, so copy it instead
contentType = contentType.Copy();
contentType.Encoding = Encoding.UTF8;
}
response.ContentType = contentTypeHeader.ToString();
response.ContentType = contentType.ToString();
using (var writer = new HttpResponseStreamWriter(response.Body, encoding))
using (var writer = new HttpResponseStreamWriter(response.Body, contentType.Encoding))
{
var viewContext = new ViewContext(
actionContext,
@ -72,7 +56,8 @@ namespace Microsoft.AspNet.Mvc
writer,
htmlHelperOptions);
await view.RenderAsync(viewContext); }
await view.RenderAsync(viewContext);
}
}
}
}

View File

@ -64,6 +64,19 @@ ViewWithSyncComponents Invoke: hello from viewdatacomponent"
Assert.Equal("10", body.Trim());
}
[Fact]
public async Task ViewComponents_InvokeWithViewComponentResult()
{
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
var client = server.CreateClient();
// Act
var body = await client.GetStringAsync("http://localhost/ViewComponentResult/Invoke?number=31");
// Assert
Assert.Equal("31", body.Trim());
}
[Theory]
[InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingWhere", "Where")]
[InlineData("http://localhost/Home/ViewComponentWithEnumerableModelUsingSelect", "Select")]

View File

@ -553,6 +553,52 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(actionContext.ModelState, controller2.ViewData.ModelState);
}
[Fact]
public void ViewComponent_WithName()
{
// Arrange
var controller = new TestabilityController();
// Act
var result = controller.ViewComponent("TagCloud");
// Assert
Assert.NotNull(result);
Assert.Equal("TagCloud", result.ViewComponentName);
}
[Fact]
public void ViewComponent_WithType()
{
// Arrange
var controller = new TestabilityController();
// Act
var result = controller.ViewComponent(typeof(TagCloudViewComponent));
// Assert
Assert.NotNull(result);
Assert.Equal(typeof(TagCloudViewComponent), result.ViewComponentType);
}
[Fact]
public void ViewComponent_WithArguments()
{
// Arrange
var controller = new TestabilityController();
// Act
var result = controller.ViewComponent(typeof(TagCloudViewComponent), "Hi", "There");
// Assert
Assert.NotNull(result);
Assert.Equal(typeof(TagCloudViewComponent), result.ViewComponentType);
Assert.Equal(new object[] { "Hi", "There" }, result.Arguments);
}
public static IEnumerable<object[]> TestabilityViewTestData
{
get
@ -666,5 +712,9 @@ namespace Microsoft.AspNet.Mvc
public string Property1 { get; set; }
public string Property2 { get; set; }
}
private class TagCloudViewComponent
{
}
}
}

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc
}
[Fact]
public async Task ViewResult_UsesFindPartialViewOnSpecifiedViewEngineToLocateViews()
public async Task PartialViewResult_UsesFindPartialViewOnSpecifiedViewEngineToLocateViews()
{
// Arrange
var viewName = "myview";
@ -125,6 +125,33 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(expectedContentTypeHeaderValue, httpContext.Response.ContentType);
}
[Fact]
public async Task PartialViewResult_SetsStatusCode()
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
viewEngine.Setup(e => e.FindPartialView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view));
var viewResult = new PartialViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
StatusCode = 404,
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
Assert.Equal(404, httpContext.Response.StatusCode);
}
[Fact]
public async Task ExecuteResultAsync_UsesActionDescriptorName_IfViewNameIsNull()
{

View File

@ -0,0 +1,391 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewComponents;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class ViewComponentResultTest
{
[Fact]
public async Task ExecuteResultAsync_Throws_IfNameOrTypeIsNotSet()
{
// Arrange
var expected =
"Either the 'ViewComponentName' or 'ViewComponentType' " +
"property must be set in order to invoke a view component.";
var actionContext = CreateActionContext();
var viewComponentResult = new ViewComponentResult();
// Act and Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => viewComponentResult.ExecuteResultAsync(actionContext));
Assert.Equal(expected, exception.Message);
}
[Fact]
public async Task ExecuteResultAsync_Throws_IfViewComponentCouldNotBeFound_ByName()
{
// Arrange
var expected = "A view component named 'Text' could not be found.";
var actionContext = CreateActionContext();
var viewComponentResult = new ViewComponentResult
{
ViewComponentName = "Text",
};
// Act and Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => viewComponentResult.ExecuteResultAsync(actionContext));
Assert.Equal(expected, exception.Message);
}
[Fact]
public async Task ExecuteResultAsync_Throws_IfViewComponentCouldNotBeFound_ByType()
{
// Arrange
var expected = $"A view component named '{typeof(TextViewComponent).FullName}' could not be found.";
var services = CreateServices();
services.AddSingleton<IViewComponentSelector>();
var actionContext = CreateActionContext();
var viewComponentResult = new ViewComponentResult
{
ViewComponentType = typeof(TextViewComponent),
};
// Act and Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => viewComponentResult.ExecuteResultAsync(actionContext));
Assert.Equal(expected, exception.Message);
}
[Fact]
public async Task ExecuteResultAsync_ExecutesSyncViewComponent()
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
ViewComponentName = "Text",
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
var body = ReadBody(actionContext.HttpContext.Response);
Assert.Equal("Hello, World!", body);
}
[Fact]
public async Task ExecuteResultAsync_ExecutesAsyncViewComponent()
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.AsyncText",
ShortName = "AsyncText",
Type = typeof(AsyncTextViewComponent),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
ViewComponentName = "AsyncText",
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
var body = ReadBody(actionContext.HttpContext.Response);
Assert.Equal("Hello-Async, World!", body);
}
[Fact]
public async Task ExecuteResultAsync_ExecutesViewComponent_ByShortName()
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
ViewComponentName = "Text",
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
var body = ReadBody(actionContext.HttpContext.Response);
Assert.Equal("Hello, World!", body);
}
[Fact]
public async Task ExecuteResultAsync_ExecutesViewComponent_ByFullName()
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
ViewComponentName = "Full.Name.Text",
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
var body = ReadBody(actionContext.HttpContext.Response);
Assert.Equal("Hello, World!", body);
}
[Fact]
public async Task ExecuteResultAsync_ExecutesViewComponent_ByType()
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
ViewComponentType = typeof(TextViewComponent),
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
var body = ReadBody(actionContext.HttpContext.Response);
Assert.Equal("Hello, World!", body);
}
[Fact]
public async Task ExecuteResultAsync_SetsStatusCode()
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
};
var actionContext = CreateActionContext(descriptor);
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
ViewComponentType = typeof(TextViewComponent),
StatusCode = 404,
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
Assert.Equal(404, actionContext.HttpContext.Response.StatusCode);
}
public static TheoryData<MediaTypeHeaderValue, string> ViewComponentResultContentTypeData
{
get
{
return new TheoryData<MediaTypeHeaderValue, string>
{
{
null,
"text/html; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo"),
"text/foo; charset=utf-8"
},
{
MediaTypeHeaderValue.Parse("text/foo;p1=p1-value"),
"text/foo; p1=p1-value; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo") { Encoding = Encoding.ASCII },
"text/foo; charset=us-ascii"
}
};
}
}
[Theory]
[MemberData(nameof(ViewComponentResultContentTypeData))]
public async Task ViewComponentResult_SetsContentTypeHeader(
MediaTypeHeaderValue contentType,
string expectedContentTypeHeaderValue)
{
// Arrange
var descriptor = new ViewComponentDescriptor()
{
FullName = "Full.Name.Text",
ShortName = "Text",
Type = typeof(TextViewComponent),
};
var actionContext = CreateActionContext(descriptor);
var contentTypeBeforeViewResultExecution = contentType?.ToString();
var viewComponentResult = new ViewComponentResult()
{
Arguments = new object[] { "World!" },
ViewComponentName = "Text",
ContentType = contentType
};
// Act
await viewComponentResult.ExecuteResultAsync(actionContext);
// Assert
Assert.Equal(expectedContentTypeHeaderValue, actionContext.HttpContext.Response.ContentType);
// Check if the original instance provided by the user has not changed.
// Since we do not have access to the new instance created within the view executor,
// check if at least the content is the same.
var contentTypeAfterViewResultExecution = contentType?.ToString();
Assert.Equal(contentTypeBeforeViewResultExecution, contentTypeAfterViewResultExecution);
}
private IServiceCollection CreateServices(params ViewComponentDescriptor[] descriptors)
{
var services = new ServiceCollection();
services.AddSingleton<IOptions<MvcViewOptions>, TestOptionsManager<MvcViewOptions>>();
services.AddTransient<IViewComponentHelper, DefaultViewComponentHelper>();
services.AddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
services.AddSingleton<IViewComponentDescriptorCollectionProvider, DefaultViewComponentDescriptorCollectionProvider>();
services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
services.AddSingleton<ITypeActivatorCache, DefaultTypeActivatorCache>();
services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
services.AddInstance<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors));
services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
return services;
}
private HttpContext CreateHttpContext(params ViewComponentDescriptor[] descriptors)
{
var services = CreateServices(descriptors);
var httpContext = new DefaultHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.RequestServices = services.BuildServiceProvider();
return httpContext;
}
private ActionContext CreateActionContext(params ViewComponentDescriptor[] descriptors)
{
return new ActionContext(CreateHttpContext(descriptors), new RouteData(), new ActionDescriptor());
}
private class FixedSetViewComponentDescriptorProvider : IViewComponentDescriptorProvider
{
private readonly ViewComponentDescriptor[] _descriptors;
public FixedSetViewComponentDescriptorProvider(params ViewComponentDescriptor[] descriptors)
{
_descriptors = descriptors ?? new ViewComponentDescriptor[0];
}
public IEnumerable<ViewComponentDescriptor> GetViewComponents()
{
return _descriptors;
}
}
private class TextViewComponent : ViewComponent
{
public HtmlString Invoke(string name)
{
return new HtmlString("Hello, " + name);
}
}
private class AsyncTextViewComponent : ViewComponent
{
public HtmlString Invoke()
{
// Should never run.
throw null;
}
public Task<HtmlString> InvokeAsync(string name)
{
return Task.FromResult(new HtmlString("Hello-Async, " + name));
}
}
private static string ReadBody(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(response.Body))
{
return reader.ReadToEnd();
}
}
}
}

View File

@ -137,6 +137,33 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(contentTypeBeforeViewResultExecution, contentTypeAfterViewResultExecution);
}
[Fact]
public async Task ViewResult_SetsStatusCode()
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
viewEngine.Setup(e => e.FindView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view));
var viewResult = new ViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
StatusCode = 404,
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
Assert.Equal(404, httpContext.Response.StatusCode);
}
[Fact]
public async Task ExecuteResultAsync_UsesActionDescriptorName_IfViewNameIsNull()
{

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ViewComponentWebSite
{
public class ViewComponentResultController : Controller
{
public IActionResult Invoke(int number)
{
return ViewComponent("Integer", number);
}
}
}