[Fixes #5166] Support passing instance directly when invoking ViewComponents with single parameter

This commit is contained in:
Ajay Bhargav Baaskaran 2016-08-23 21:14:47 -07:00
parent 9ed753288f
commit a6a4b5369a
7 changed files with 266 additions and 24 deletions

View File

@ -53,12 +53,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo) private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo)
{ {
var methodInfo = FindMethod(typeInfo.AsType());
var candidate = new ViewComponentDescriptor var candidate = new ViewComponentDescriptor
{ {
FullName = ViewComponentConventions.GetComponentFullName(typeInfo), FullName = ViewComponentConventions.GetComponentFullName(typeInfo),
ShortName = ViewComponentConventions.GetComponentName(typeInfo), ShortName = ViewComponentConventions.GetComponentName(typeInfo),
TypeInfo = typeInfo, TypeInfo = typeInfo,
MethodInfo = FindMethod(typeInfo.AsType()) MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters()
}; };
return candidate; return candidate;

View File

@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
@ -130,19 +132,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
componentType.FullName)); componentType.FullName));
} }
private async Task<IHtmlContent> InvokeCoreAsync( // Internal for testing
ViewComponentDescriptor descriptor, internal IDictionary<string, object> GetArgumentDictionary(ViewComponentDescriptor descriptor, object arguments)
object arguments)
{ {
if (descriptor.Parameters.Count == 1 && descriptor.Parameters[0].ParameterType.IsAssignableFrom(arguments.GetType()))
{
return new Dictionary<string, object>(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase)
{
{ descriptor.Parameters[0].Name, arguments }
};
}
return PropertyHelper.ObjectToDictionary(arguments);
}
private async Task<IHtmlContent> InvokeCoreAsync(ViewComponentDescriptor descriptor, object arguments)
{
var argumentDictionary = GetArgumentDictionary(descriptor, arguments);
var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName, ViewBuffer.ViewComponentPageSize); var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName, ViewBuffer.ViewComponentPageSize);
using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding)) using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding))
{ {
var context = new ViewComponentContext( var context = new ViewComponentContext(descriptor, argumentDictionary, _htmlEncoder, _viewContext, writer);
descriptor,
PropertyHelper.ObjectToDictionary(arguments),
_htmlEncoder,
_viewContext,
writer);
var invoker = _invokerFactory.CreateInstance(context); var invoker = _invokerFactory.CreateInstance(context);
if (invoker == null) if (invoker == null)

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
@ -124,5 +126,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
/// Gets or sets the <see cref="System.Reflection.MethodInfo"/> to invoke. /// Gets or sets the <see cref="System.Reflection.MethodInfo"/> to invoke.
/// </summary> /// </summary>
public MethodInfo MethodInfo { get; set; } public MethodInfo MethodInfo { get; set; }
/// <summary>
/// Gets or sets the parameters associated with the method described by <see cref="MethodInfo"/>.
/// </summary>
public IReadOnlyList<ParameterInfo> Parameters { get; set; }
} }
} }

View File

@ -55,12 +55,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteAsync_ViewComponentResult_AllowsNullViewDataAndTempData() public async Task ExecuteAsync_ViewComponentResult_AllowsNullViewDataAndTempData()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -146,12 +148,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_ExecutesSyncViewComponent() public async Task ExecuteResultAsync_ExecutesSyncViewComponent()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -175,12 +179,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_UsesDictionaryArguments() public async Task ExecuteResultAsync_UsesDictionaryArguments()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -204,12 +210,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_ExecutesAsyncViewComponent() public async Task ExecuteResultAsync_ExecutesAsyncViewComponent()
{ {
// Arrange // Arrange
var methodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.AsyncText", FullName = "Full.Name.AsyncText",
ShortName = "AsyncText", ShortName = "AsyncText",
TypeInfo = typeof(AsyncTextViewComponent).GetTypeInfo(), TypeInfo = typeof(AsyncTextViewComponent).GetTypeInfo(),
MethodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -233,12 +241,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_ExecutesViewComponent_AndWritesDiagnosticSource() public async Task ExecuteResultAsync_ExecutesViewComponent_AndWritesDiagnosticSource()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var adapter = new TestDiagnosticListener(); var adapter = new TestDiagnosticListener();
@ -272,12 +282,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_ExecutesViewComponent_ByShortName() public async Task ExecuteResultAsync_ExecutesViewComponent_ByShortName()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -301,12 +313,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_ExecutesViewComponent_ByFullName() public async Task ExecuteResultAsync_ExecutesViewComponent_ByFullName()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -330,12 +344,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_ExecutesViewComponent_ByType() public async Task ExecuteResultAsync_ExecutesViewComponent_ByType()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -359,12 +375,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ExecuteResultAsync_SetsStatusCode() public async Task ExecuteResultAsync_SetsStatusCode()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)) MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -417,12 +435,14 @@ namespace Microsoft.AspNetCore.Mvc
string expectedContentType) string expectedContentType)
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -455,12 +475,14 @@ namespace Microsoft.AspNetCore.Mvc
public async Task ViewComponentResult_SetsContentTypeHeader_OverrideResponseContentType() public async Task ViewComponentResult_SetsContentTypeHeader_OverrideResponseContentType()
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);
@ -492,12 +514,14 @@ namespace Microsoft.AspNetCore.Mvc
string expectedContentType) string expectedContentType)
{ {
// Arrange // Arrange
var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
var descriptor = new ViewComponentDescriptor() var descriptor = new ViewComponentDescriptor()
{ {
FullName = "Full.Name.Text", FullName = "Full.Name.Text",
ShortName = "Text", ShortName = "Text",
TypeInfo = typeof(TextViewComponent).GetTypeInfo(), TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), MethodInfo = methodInfo,
Parameters = methodInfo.GetParameters(),
}; };
var actionContext = CreateActionContext(descriptor); var actionContext = CreateActionContext(descriptor);

View File

@ -0,0 +1,198 @@
// 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.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewComponents
{
public class DefaultViewComponentHelperTest
{
[Fact]
public void GetArgumentDictionary_SupportsAnonymouslyTypedArguments()
{
// Arrange
var helper = CreateHelper();
var descriptor = CreateDescriptorForType(typeof(ViewComponentSingleParam));
// Act
var argumentDictionary = helper.GetArgumentDictionary(descriptor, new { a = 0 });
// Assert
Assert.Collection(argumentDictionary,
item =>
{
Assert.Equal("a", item.Key);
Assert.IsType(typeof(int), item.Value);
Assert.Equal(0, item.Value);
});
}
[Fact]
public void GetArgumentDictionary_SingleParameter_DoesNotNeedAnonymouslyTypedArguments()
{
// Arrange
var helper = CreateHelper();
var descriptor = CreateDescriptorForType(typeof(ViewComponentSingleParam));
// Act
var argumentDictionary = helper.GetArgumentDictionary(descriptor, 0);
// Assert
Assert.Collection(argumentDictionary,
item =>
{
Assert.Equal("a", item.Key);
Assert.IsType(typeof(int), item.Value);
Assert.Equal(0, item.Value);
});
}
[Fact]
public void GetArgumentDictionary_MultipleParameters_NeedsAnonymouslyTypedArguments()
{
// Arrange
var helper = CreateHelper();
var descriptor = CreateDescriptorForType(typeof(ViewComponentMultipleParam));
// Act
var argumentDictionary = helper.GetArgumentDictionary(descriptor, new { a = 0, b = "foo" });
// Assert
Assert.Collection(argumentDictionary,
item1 =>
{
Assert.Equal("a", item1.Key);
Assert.IsType(typeof(int), item1.Value);
Assert.Equal(0, item1.Value);
},
item2 =>
{
Assert.Equal("b", item2.Key);
Assert.IsType(typeof(string), item2.Value);
Assert.Equal("foo", item2.Value);
});
}
[Fact]
public void GetArgumentDictionary_SingleObjectParameter_DoesNotNeedAnonymouslyTypedArguments()
{
// Arrange
var helper = CreateHelper();
var descriptor = CreateDescriptorForType(typeof(ViewComponentObjectParam));
var expectedValue = new object();
// Act
var argumentDictionary = helper.GetArgumentDictionary(descriptor, expectedValue);
// Assert
Assert.Collection(argumentDictionary,
item =>
{
Assert.Equal("o", item.Key);
Assert.IsType(typeof(object), item.Value);
Assert.Same(expectedValue, item.Value);
});
}
[Fact]
public void GetArgumentDictionary_SingleParameter_AcceptsDictionaryType()
{
// Arrange
var helper = CreateHelper();
var descriptor = CreateDescriptorForType(typeof(ViewComponentSingleParam));
var arguments = new Dictionary<string, object>
{
{ "a", 10 }
};
// Act
var argumentDictionary = helper.GetArgumentDictionary(descriptor, arguments);
// Assert
Assert.Collection(argumentDictionary,
item =>
{
Assert.Equal("a", item.Key);
Assert.IsType(typeof(int), item.Value);
Assert.Equal(10, item.Value);
});
}
private DefaultViewComponentHelper CreateHelper()
{
var descriptorCollectionProvider = Mock.Of<IViewComponentDescriptorCollectionProvider>();
var selector = Mock.Of<IViewComponentSelector>();
var invokerFactory = Mock.Of<IViewComponentInvokerFactory>();
var viewBufferScope = Mock.Of<IViewBufferScope>();
return new DefaultViewComponentHelper(
descriptorCollectionProvider,
new HtmlTestEncoder(),
selector,
invokerFactory,
viewBufferScope);
}
private ViewComponentDescriptor CreateDescriptorForType(Type componentType)
{
var provider = CreateProvider(componentType);
return provider.GetViewComponents().First();
}
private class ViewComponentSingleParam
{
public IViewComponentResult Invoke(int a) => null;
}
private class ViewComponentMultipleParam
{
public IViewComponentResult Invoke(int a, string b) => null;
}
private class ViewComponentObjectParam
{
public IViewComponentResult Invoke(object o) => null;
}
private DefaultViewComponentDescriptorProvider CreateProvider(Type componentType)
{
return new FilteredViewComponentDescriptorProvider(componentType);
}
// This will only consider types nested inside this class as ViewComponent classes
private class FilteredViewComponentDescriptorProvider : DefaultViewComponentDescriptorProvider
{
public FilteredViewComponentDescriptorProvider(params Type[] allowedTypes)
: base(GetApplicationPartManager(allowedTypes.Select(t => t.GetTypeInfo())))
{
}
private static ApplicationPartManager GetApplicationPartManager(IEnumerable<TypeInfo> types)
{
var manager = new ApplicationPartManager();
manager.ApplicationParts.Add(new TestApplicationPart(types));
manager.FeatureProviders.Add(new TestFeatureProvider());
return manager;
}
private class TestFeatureProvider : IApplicationFeatureProvider<ViewComponentFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewComponentFeature feature)
{
foreach (var type in parts.OfType<IApplicationPartTypeProvider>().SelectMany(p => p.Types))
{
feature.ViewComponents.Add(type);
}
}
}
}
}
}

View File

@ -1,3 +1,3 @@
@model Person @model Person
<h1>@Model.Name</h1> <h1>@Model.Name</h1>
@await Component.InvokeAsync("InheritingViewComponent", new { address = Model.Address }) @await Component.InvokeAsync("InheritingViewComponent", Model.Address)

View File

@ -12,4 +12,4 @@
} }
ViewWithRelativePath-content ViewWithRelativePath-content
<partial>@await Html.PartialAsync("../Shared/_PartialThatSetsTitle.cshtml")</partial> <partial>@await Html.PartialAsync("../Shared/_PartialThatSetsTitle.cshtml")</partial>
@await Component.InvokeAsync("ComponentWithRelativePath", new { person }) @await Component.InvokeAsync("ComponentWithRelativePath", person)