From a6a4b5369af3d1a5a3d351422582dec71dab66f3 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Tue, 23 Aug 2016 21:14:47 -0700 Subject: [PATCH] [Fixes #5166] Support passing instance directly when invoking ViewComponents with single parameter --- .../DefaultViewComponentDescriptorProvider.cs | 4 +- .../DefaultViewComponentHelper.cs | 29 ++- .../ViewComponents/ViewComponentDescriptor.cs | 7 + .../ViewComponentResultTest.cs | 48 +++-- .../DefaultViewComponentHelperTest.cs | 198 ++++++++++++++++++ .../Views/InheritingInherits/Index.cshtml | 2 +- .../ViewEngine/ViewWithRelativePath.cshtml | 2 +- 7 files changed, 266 insertions(+), 24 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentHelperTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs index 8c3ee66855..238bb47e1f 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentDescriptorProvider.cs @@ -53,12 +53,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents private static ViewComponentDescriptor CreateDescriptor(TypeInfo typeInfo) { + var methodInfo = FindMethod(typeInfo.AsType()); var candidate = new ViewComponentDescriptor { FullName = ViewComponentConventions.GetComponentFullName(typeInfo), ShortName = ViewComponentConventions.GetComponentName(typeInfo), TypeInfo = typeInfo, - MethodInfo = FindMethod(typeInfo.AsType()) + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters() }; return candidate; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs index 5d63943239..fff5e3e689 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/DefaultViewComponentHelper.cs @@ -2,7 +2,9 @@ // 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.Reflection; +using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; @@ -130,19 +132,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents componentType.FullName)); } - private async Task InvokeCoreAsync( - ViewComponentDescriptor descriptor, - object arguments) + // Internal for testing + internal IDictionary GetArgumentDictionary(ViewComponentDescriptor descriptor, object arguments) { + if (descriptor.Parameters.Count == 1 && descriptor.Parameters[0].ParameterType.IsAssignableFrom(arguments.GetType())) + { + return new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) + { + { descriptor.Parameters[0].Name, arguments } + }; + } + + return PropertyHelper.ObjectToDictionary(arguments); + } + + private async Task InvokeCoreAsync(ViewComponentDescriptor descriptor, object arguments) + { + var argumentDictionary = GetArgumentDictionary(descriptor, arguments); + var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName, ViewBuffer.ViewComponentPageSize); using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding)) { - var context = new ViewComponentContext( - descriptor, - PropertyHelper.ObjectToDictionary(arguments), - _htmlEncoder, - _viewContext, - writer); + var context = new ViewComponentContext(descriptor, argumentDictionary, _htmlEncoder, _viewContext, writer); var invoker = _invokerFactory.CreateInstance(context); if (invoker == null) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentDescriptor.cs index fff90eefed..097b5cef50 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewComponents/ViewComponentDescriptor.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics; using System.Reflection; @@ -124,5 +126,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents /// Gets or sets the to invoke. /// public MethodInfo MethodInfo { get; set; } + + /// + /// Gets or sets the parameters associated with the method described by . + /// + public IReadOnlyList Parameters { get; set; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs index 728e71daec..9e62b5608d 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponentResultTest.cs @@ -55,12 +55,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteAsync_ViewComponentResult_AllowsNullViewDataAndTempData() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -146,12 +148,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_ExecutesSyncViewComponent() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -175,12 +179,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_UsesDictionaryArguments() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -204,12 +210,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_ExecutesAsyncViewComponent() { // Arrange + var methodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.AsyncText", ShortName = "AsyncText", TypeInfo = typeof(AsyncTextViewComponent).GetTypeInfo(), - MethodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -233,12 +241,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_ExecutesViewComponent_AndWritesDiagnosticSource() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var adapter = new TestDiagnosticListener(); @@ -272,12 +282,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_ExecutesViewComponent_ByShortName() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -301,12 +313,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_ExecutesViewComponent_ByFullName() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -330,12 +344,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_ExecutesViewComponent_ByType() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -359,12 +375,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ExecuteResultAsync_SetsStatusCode() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)) + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -417,12 +435,14 @@ namespace Microsoft.AspNetCore.Mvc string expectedContentType) { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -455,12 +475,14 @@ namespace Microsoft.AspNetCore.Mvc public async Task ViewComponentResult_SetsContentTypeHeader_OverrideResponseContentType() { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); @@ -492,12 +514,14 @@ namespace Microsoft.AspNetCore.Mvc string expectedContentType) { // Arrange + var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)); var descriptor = new ViewComponentDescriptor() { FullName = "Full.Name.Text", ShortName = "Text", TypeInfo = typeof(TextViewComponent).GetTypeInfo(), - MethodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke)), + MethodInfo = methodInfo, + Parameters = methodInfo.GetParameters(), }; var actionContext = CreateActionContext(descriptor); diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentHelperTest.cs new file mode 100644 index 0000000000..8098d6511e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewComponents/DefaultViewComponentHelperTest.cs @@ -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 + { + { "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(); + var selector = Mock.Of(); + var invokerFactory = Mock.Of(); + var viewBufferScope = Mock.Of(); + + 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 types) + { + var manager = new ApplicationPartManager(); + manager.ApplicationParts.Add(new TestApplicationPart(types)); + manager.FeatureProviders.Add(new TestFeatureProvider()); + return manager; + } + + private class TestFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, ViewComponentFeature feature) + { + foreach (var type in parts.OfType().SelectMany(p => p.Types)) + { + feature.ViewComponents.Add(type); + } + } + } + } + } +} diff --git a/test/WebSites/RazorWebSite/Views/InheritingInherits/Index.cshtml b/test/WebSites/RazorWebSite/Views/InheritingInherits/Index.cshtml index 333e22352a..faa9205eb6 100644 --- a/test/WebSites/RazorWebSite/Views/InheritingInherits/Index.cshtml +++ b/test/WebSites/RazorWebSite/Views/InheritingInherits/Index.cshtml @@ -1,3 +1,3 @@ @model Person

@Model.Name

-@await Component.InvokeAsync("InheritingViewComponent", new { address = Model.Address }) \ No newline at end of file +@await Component.InvokeAsync("InheritingViewComponent", Model.Address) \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithRelativePath.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithRelativePath.cshtml index 6390921e2e..f138e7d744 100644 --- a/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithRelativePath.cshtml +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/ViewWithRelativePath.cshtml @@ -12,4 +12,4 @@ } ViewWithRelativePath-content @await Html.PartialAsync("../Shared/_PartialThatSetsTitle.cshtml") -@await Component.InvokeAsync("ComponentWithRelativePath", new { person }) \ No newline at end of file +@await Component.InvokeAsync("ComponentWithRelativePath", person) \ No newline at end of file