diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index f1df7292ed..4a5467b699 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -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 diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs index e33e75dcb0..c34773f95a 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Controller.cs @@ -324,6 +324,46 @@ namespace Microsoft.AspNet.Mvc }; } + /// + /// Creates a by specifying the name of a view component to render. + /// + /// + /// The view component name. Can be a view component + /// or + /// . + /// The arguments to pass to the view component. + /// The created object for the response. + [NonAction] + public virtual ViewComponentResult ViewComponent(string componentName, params object[] arguments) + { + return new ViewComponentResult() + { + ViewComponentName = componentName, + Arguments = arguments, + ViewData = ViewData, + TempData = TempData + }; + } + + /// + /// Creates a by specifying the of a view component to + /// render. + /// + /// The view component . + /// The arguments to pass to the view component. + /// The created object for the response. + [NonAction] + public virtual ViewComponentResult ViewComponent(Type componentType, params object[] arguments) + { + return new ViewComponentResult() + { + ViewComponentType = componentType, + Arguments = arguments, + ViewData = ViewData, + TempData = TempData + }; + } + /// /// Creates a object by specifying a string. /// diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/NullView.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/NullView.cs new file mode 100644 index 0000000000..96fffa7b32 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/NullView.cs @@ -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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs index 4c26418900..d5487e2034 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs @@ -778,6 +778,22 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGenerator_FieldNameCannotBeNullOrEmpty"), p0, p1, p2, p3, p4); } + /// + /// Either the '{0}' or '{1}' property must be set in order to invoke a view component. + /// + internal static string ViewComponentResult_NameOrTypeMustBeSet + { + get { return GetString("ViewComponentResult_NameOrTypeMustBeSet"); } + } + + /// + /// Either the '{0}' or '{1}' property must be set in order to invoke a view component. + /// + 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); diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx b/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx index 7b1e86fddd..52868bec65 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx @@ -262,4 +262,7 @@ 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. + + Either the '{0}' or '{1}' property must be set in order to invoke a view component. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewComponentResult.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewComponentResult.cs new file mode 100644 index 0000000000..92057d9201 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewComponentResult.cs @@ -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 +{ + /// + /// An which renders a view component to the response. + /// + public class ViewComponentResult : ActionResult + { + /// + /// Gets or sets the arguments provided to the view component. + /// + public object[] Arguments { get; set; } + + /// + /// Gets or sets the representing the Content-Type header of the response. + /// + public MediaTypeHeaderValue ContentType { get; set; } + + /// + /// Gets or sets the HTTP status code. + /// + public int? StatusCode { get; set; } + + /// + /// Gets or sets the for this result. + /// + public ITempDataDictionary TempData { get; set; } + + /// + /// Gets or sets the name of the view component to invoke. Will be ignored if + /// is set to a non-null value. + /// + public string ViewComponentName { get; set; } + + /// + /// Gets or sets the type of the view component to invoke. + /// + public Type ViewComponentType { get; set; } + + /// + /// Gets or sets the for this result. + /// + public ViewDataDictionary ViewData { get; set; } + + /// + /// Gets or sets the used to locate views. + /// + /// When null, an instance of from + /// ActionContext.HttpContext.RequestServices is used. + public IViewEngine ViewEngine { get; set; } + + /// + public override async Task ExecuteResultAsync(ActionContext context) + { + var response = context.HttpContext.Response; + var services = context.HttpContext.RequestServices; + + var htmlHelperOptions = services.GetRequiredService>().Options.HtmlHelperOptions; + var viewComponentHelper = services.GetRequiredService(); + + var viewData = ViewData; + if (viewData == null) + { + var modelMetadataProvider = services.GetRequiredService(); + 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); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewExecutor.cs index d69b48d854..68d05f6d8e 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewExecutor.cs @@ -14,11 +14,10 @@ namespace Microsoft.AspNet.Mvc /// 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(); /// /// Asynchronously renders the specified 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); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs index d813a4733d..219c40eb9c 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewComponentTests.cs @@ -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")] diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs index 32341cbf72..2a4f3d02d9 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs @@ -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 TestabilityViewTestData { get @@ -666,5 +712,9 @@ namespace Microsoft.AspNet.Mvc public string Property1 { get; set; } public string Property2 { get; set; } } + + private class TagCloudViewComponent + { + } } } diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs index e2cd1fcbb9..d84d374c80 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs @@ -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(); + var view = Mock.Of(); + + 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() { diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs new file mode 100644 index 0000000000..0e0448922b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs @@ -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( + () => 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( + () => 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(); + + var actionContext = CreateActionContext(); + + var viewComponentResult = new ViewComponentResult + { + ViewComponentType = typeof(TextViewComponent), + }; + + // Act and Assert + var exception = await Assert.ThrowsAsync( + () => 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 ViewComponentResultContentTypeData + { + get + { + return new TheoryData + { + { + 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, TestOptionsManager>(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddInstance(new FixedSetViewComponentDescriptorProvider(descriptors)); + services.AddSingleton(); + + 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 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 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(); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs index 758070c95d..2cdc5b640c 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs @@ -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(); + var view = Mock.Of(); + + 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() { diff --git a/test/WebSites/ViewComponentWebSite/ViewComponentResultController.cs b/test/WebSites/ViewComponentWebSite/ViewComponentResultController.cs new file mode 100644 index 0000000000..656060385b --- /dev/null +++ b/test/WebSites/ViewComponentWebSite/ViewComponentResultController.cs @@ -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); + } + } +}