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:
parent
67474d8cbc
commit
e91ce4560f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue