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