[Fixes #5216] Make generic parameters work with ViewComponent tag helpers
This commit is contained in:
parent
ea701f2cd6
commit
c28ad48e98
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Host;
|
using Microsoft.AspNetCore.Mvc.Razor.Host;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
using Microsoft.AspNetCore.Mvc.ViewComponents;
|
||||||
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
|
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
|
||||||
|
|
@ -56,7 +57,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
StringComparison.Ordinal));
|
StringComparison.Ordinal));
|
||||||
|
|
||||||
var tagHelperDescriptors = viewComponentDescriptors
|
var tagHelperDescriptors = viewComponentDescriptors
|
||||||
.Where(descriptor => !descriptor.Parameters.Any(parameter => parameter.ParameterType.GetTypeInfo().IsGenericType))
|
.Where(d => !d.Parameters.Any(p => p.ParameterType.GetTypeInfo().ContainsGenericParameters))
|
||||||
.Select(viewComponentDescriptor => CreateDescriptor(viewComponentDescriptor));
|
.Select(viewComponentDescriptor => CreateDescriptor(viewComponentDescriptor));
|
||||||
|
|
||||||
return tagHelperDescriptors;
|
return tagHelperDescriptors;
|
||||||
|
|
@ -92,11 +93,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
foreach (var parameter in methodParameters)
|
foreach (var parameter in methodParameters)
|
||||||
{
|
{
|
||||||
var lowerKebabName = TagHelperDescriptorFactory.ToHtmlCase(parameter.Name);
|
var lowerKebabName = TagHelperDescriptorFactory.ToHtmlCase(parameter.Name);
|
||||||
|
var typeName = GetCSharpTypeName(parameter.ParameterType);
|
||||||
var descriptor = new TagHelperAttributeDescriptor
|
var descriptor = new TagHelperAttributeDescriptor
|
||||||
{
|
{
|
||||||
Name = lowerKebabName,
|
Name = lowerKebabName,
|
||||||
PropertyName = parameter.Name,
|
PropertyName = parameter.Name,
|
||||||
TypeName = parameter.ParameterType.FullName
|
TypeName = typeName
|
||||||
};
|
};
|
||||||
|
|
||||||
descriptor.IsEnum = parameter.ParameterType.GetTypeInfo().IsEnum;
|
descriptor.IsEnum = parameter.ParameterType.GetTypeInfo().IsEnum;
|
||||||
|
|
@ -113,7 +115,110 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTagName(ViewComponentDescriptor descriptor) =>
|
private string GetTagName(ViewComponentDescriptor descriptor)
|
||||||
$"vc:{TagHelperDescriptorFactory.ToHtmlCase(descriptor.ShortName)}";
|
{
|
||||||
|
return $"vc:{TagHelperDescriptorFactory.ToHtmlCase(descriptor.ShortName)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal for testing.
|
||||||
|
internal static string GetCSharpTypeName(Type type)
|
||||||
|
{
|
||||||
|
var outputBuilder = new StringBuilder();
|
||||||
|
WriteCSharpTypeName(type, outputBuilder);
|
||||||
|
|
||||||
|
var typeName = outputBuilder.ToString();
|
||||||
|
|
||||||
|
// We don't want to add global:: to the top level type because Razor does that for us.
|
||||||
|
return typeName.Substring("global::".Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteCSharpTypeName(Type type, StringBuilder outputBuilder)
|
||||||
|
{
|
||||||
|
var typeInfo = type.GetTypeInfo();
|
||||||
|
if (typeInfo.IsByRef)
|
||||||
|
{
|
||||||
|
WriteCSharpTypeName(typeInfo.GetElementType(), outputBuilder);
|
||||||
|
}
|
||||||
|
else if (typeInfo.IsNested)
|
||||||
|
{
|
||||||
|
WriteNestedTypes(type, outputBuilder);
|
||||||
|
}
|
||||||
|
else if (typeInfo.IsGenericType)
|
||||||
|
{
|
||||||
|
outputBuilder.Append("global::");
|
||||||
|
var part = type.FullName.Substring(0, type.FullName.IndexOf('`'));
|
||||||
|
outputBuilder.Append(part);
|
||||||
|
|
||||||
|
var genericArguments = type.GenericTypeArguments;
|
||||||
|
WriteGenericArguments(genericArguments, 0, genericArguments.Length, outputBuilder);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputBuilder.Append("global::");
|
||||||
|
outputBuilder.Append(type.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteNestedTypes(Type type, StringBuilder outputBuilder)
|
||||||
|
{
|
||||||
|
var nestedTypes = new List<Type>();
|
||||||
|
var currentType = type;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
nestedTypes.Insert(0, currentType);
|
||||||
|
currentType = currentType.DeclaringType;
|
||||||
|
} while (currentType.IsNested);
|
||||||
|
|
||||||
|
nestedTypes.Insert(0, currentType);
|
||||||
|
|
||||||
|
outputBuilder.Append("global::");
|
||||||
|
outputBuilder.Append(currentType.Namespace);
|
||||||
|
outputBuilder.Append(".");
|
||||||
|
|
||||||
|
var typeArgumentIndex = 0;
|
||||||
|
for (var i = 0; i < nestedTypes.Count; i++)
|
||||||
|
{
|
||||||
|
var nestedType = nestedTypes[i];
|
||||||
|
var arityIndex = nestedType.Name.IndexOf('`');
|
||||||
|
if (arityIndex >= 0)
|
||||||
|
{
|
||||||
|
var part = nestedType.Name.Substring(0, arityIndex);
|
||||||
|
outputBuilder.Append(part);
|
||||||
|
|
||||||
|
var genericArguments = type.GenericTypeArguments;
|
||||||
|
var typeArgumentCount = nestedType.IsConstructedGenericType ?
|
||||||
|
nestedType.GenericTypeArguments.Length : nestedType.GetTypeInfo().GenericTypeParameters.Length;
|
||||||
|
|
||||||
|
WriteGenericArguments(genericArguments, typeArgumentIndex, typeArgumentCount, outputBuilder);
|
||||||
|
|
||||||
|
typeArgumentIndex = typeArgumentCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputBuilder.Append(nestedType.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i + 1 < nestedTypes.Count)
|
||||||
|
{
|
||||||
|
outputBuilder.Append(".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteGenericArguments(Type[] genericArguments, int startIndex, int length, StringBuilder outputBuilder)
|
||||||
|
{
|
||||||
|
outputBuilder.Append("<");
|
||||||
|
|
||||||
|
for (var i = startIndex; i < length; i++)
|
||||||
|
{
|
||||||
|
WriteCSharpTypeName(genericArguments[i], outputBuilder);
|
||||||
|
if (i + 1 < length)
|
||||||
|
{
|
||||||
|
outputBuilder.Append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputBuilder.Append(">");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
@ -27,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
return new TheoryData<string, IEnumerable<TagHelperDescriptor>>
|
return new TheoryData<string, IEnumerable<TagHelperDescriptor>>
|
||||||
{
|
{
|
||||||
{ assemblyOne, new [] { provider.GetTagHelperDescriptorOne() } },
|
{ assemblyOne, new [] { provider.GetTagHelperDescriptorOne() } },
|
||||||
{ assemblyTwo, new [] { provider.GetTagHelperDescriptorTwo() } },
|
{ assemblyTwo, new [] { provider.GetTagHelperDescriptorTwo(), provider.GetTagHelperDescriptorGeneric() } },
|
||||||
{ assemblyNone, Enumerable.Empty<TagHelperDescriptor>() }
|
{ assemblyNone, Enumerable.Empty<TagHelperDescriptor>() }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +51,48 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.Default);
|
Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static TheoryData TypeData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var outParamType = typeof(ViewComponentTagHelperDescriptorFactoryTest).GetMethod("MethodWithOutParam").GetParameters().First().ParameterType;
|
||||||
|
var refParamType = typeof(ViewComponentTagHelperDescriptorFactoryTest).GetMethod("MethodWithRefParam").GetParameters().First().ParameterType;
|
||||||
|
|
||||||
|
return new TheoryData<Type, string>
|
||||||
|
{
|
||||||
|
{ typeof(string), "System.String" },
|
||||||
|
{ typeof(string[,]), "System.String[,]" },
|
||||||
|
{ typeof(List<int*[]>), "System.Collections.Generic.List<global::System.Int32*[]>" },
|
||||||
|
{ typeof(List<string[,,]>), "System.Collections.Generic.List<global::System.String[,,]>" },
|
||||||
|
{ typeof(Dictionary<string[], List<string>>),
|
||||||
|
"System.Collections.Generic.Dictionary<global::System.String[], global::System.Collections.Generic.List<global::System.String>>" },
|
||||||
|
{ typeof(Dictionary<string, List<string[,]>>),
|
||||||
|
"System.Collections.Generic.Dictionary<global::System.String, global::System.Collections.Generic.List<global::System.String[,]>>" },
|
||||||
|
{ outParamType, "System.Collections.Generic.List<global::System.Char*[]>" },
|
||||||
|
{ refParamType, "System.String[]" },
|
||||||
|
{ typeof(NonGeneric.Nested1<bool, string>.Nested2),
|
||||||
|
"Microsoft.AspNetCore.Mvc.Razor.Test.Internal.ViewComponentTagHelperDescriptorFactoryTest.NonGeneric.Nested1<global::System.Boolean, global::System.String>.Nested2" },
|
||||||
|
{ typeof(GenericType<string, int>.GenericNestedType<bool, string>),
|
||||||
|
"Microsoft.AspNetCore.Mvc.Razor.Test.Internal.ViewComponentTagHelperDescriptorFactoryTest.GenericType<global::System.String, global::System.Int32>.GenericNestedType<global::System.Boolean, global::System.String>" },
|
||||||
|
{ typeof(GenericType<string, int>.NonGenericNested.MultiNestedType<bool, string>),
|
||||||
|
"Microsoft.AspNetCore.Mvc.Razor.Test.Internal.ViewComponentTagHelperDescriptorFactoryTest.GenericType<global::System.String, global::System.Int32>.NonGenericNested.MultiNestedType<global::System.Boolean, global::System.String>" },
|
||||||
|
{ typeof(Dictionary<GenericType<string, int>.NonGenericNested.MultiNestedType<bool, string>, List<string[]>>),
|
||||||
|
"System.Collections.Generic.Dictionary<global::Microsoft.AspNetCore.Mvc.Razor.Test.Internal.ViewComponentTagHelperDescriptorFactoryTest.GenericType<global::System.String, global::System.Int32>.NonGenericNested.MultiNestedType<global::System.Boolean, global::System.String>, global::System.Collections.Generic.List<global::System.String[]>>" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(TypeData))]
|
||||||
|
public void GetCSharpTypeName_ReturnsCorrectTypeNames(Type type, string expected)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var typeName = ViewComponentTagHelperDescriptorFactory.GetCSharpTypeName(type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, typeName);
|
||||||
|
}
|
||||||
|
|
||||||
// Test invokes are needed for method creation in TestViewComponentDescriptorProvider.
|
// Test invokes are needed for method creation in TestViewComponentDescriptorProvider.
|
||||||
public enum TestEnum
|
public enum TestEnum
|
||||||
{
|
{
|
||||||
|
|
@ -70,6 +113,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void InvokeWithOpenGeneric<T>(List<T> baz)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private class TestViewComponentDescriptorProvider : IViewComponentDescriptorProvider
|
private class TestViewComponentDescriptorProvider : IViewComponentDescriptorProvider
|
||||||
{
|
{
|
||||||
private readonly ViewComponentDescriptor _viewComponentDescriptorOne = new ViewComponentDescriptor
|
private readonly ViewComponentDescriptor _viewComponentDescriptorOne = new ViewComponentDescriptor
|
||||||
|
|
@ -108,6 +155,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
.GetMethod(nameof(ViewComponentTagHelperDescriptorFactoryTest.InvokeWithGenericParams)).GetParameters()
|
.GetMethod(nameof(ViewComponentTagHelperDescriptorFactoryTest.InvokeWithGenericParams)).GetParameters()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly ViewComponentDescriptor _viewComponentDescriptorOpenGeneric = new ViewComponentDescriptor
|
||||||
|
{
|
||||||
|
DisplayName = "OpenGenericDisplayName",
|
||||||
|
FullName = "OpenGenericViewComponent",
|
||||||
|
ShortName = "OpenGeneric",
|
||||||
|
MethodInfo = typeof(ViewComponentTagHelperDescriptorFactoryTest)
|
||||||
|
.GetMethod(nameof(ViewComponentTagHelperDescriptorFactoryTest.InvokeWithOpenGeneric)),
|
||||||
|
TypeInfo = typeof(ViewComponentTagHelperDescriptorFactoryTest).GetTypeInfo(),
|
||||||
|
Parameters = typeof(ViewComponentTagHelperDescriptorFactoryTest)
|
||||||
|
.GetMethod(nameof(ViewComponentTagHelperDescriptorFactoryTest.InvokeWithOpenGeneric)).GetParameters()
|
||||||
|
};
|
||||||
|
|
||||||
public TagHelperDescriptor GetTagHelperDescriptorOne()
|
public TagHelperDescriptor GetTagHelperDescriptorOne()
|
||||||
{
|
{
|
||||||
var descriptor = new TagHelperDescriptor
|
var descriptor = new TagHelperDescriptor
|
||||||
|
|
@ -201,15 +260,91 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TagHelperDescriptor GetTagHelperDescriptorGeneric()
|
||||||
|
{
|
||||||
|
var descriptor = new TagHelperDescriptor
|
||||||
|
{
|
||||||
|
TagName = "vc:generic",
|
||||||
|
TypeName = "__Generated__GenericViewComponentTagHelper",
|
||||||
|
AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Test",
|
||||||
|
Attributes = new List<TagHelperAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "foo",
|
||||||
|
PropertyName = "Foo",
|
||||||
|
TypeName = "System.Collections.Generic.List<global::System.String>"
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "bar",
|
||||||
|
PropertyName = "Bar",
|
||||||
|
TypeName = "System.Collections.Generic.Dictionary<global::System.String, global::System.Int32>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RequiredAttributes = new List<TagHelperRequiredAttributeDescriptor>
|
||||||
|
{
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "foo"
|
||||||
|
},
|
||||||
|
|
||||||
|
new TagHelperRequiredAttributeDescriptor
|
||||||
|
{
|
||||||
|
Name = "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
descriptor.PropertyBag.Add(ViewComponentTagHelperDescriptorConventions.ViewComponentNameKey, "Generic");
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<ViewComponentDescriptor> GetViewComponents()
|
public IEnumerable<ViewComponentDescriptor> GetViewComponents()
|
||||||
{
|
{
|
||||||
return new List<ViewComponentDescriptor>
|
return new List<ViewComponentDescriptor>
|
||||||
{
|
{
|
||||||
_viewComponentDescriptorOne,
|
_viewComponentDescriptorOne,
|
||||||
_viewComponentDescriptorTwo,
|
_viewComponentDescriptorTwo,
|
||||||
_viewComponentDescriptorGeneric
|
_viewComponentDescriptorGeneric,
|
||||||
|
_viewComponentDescriptorOpenGeneric
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void MethodWithOutParam(out List<char*[]> foo)
|
||||||
|
{
|
||||||
|
foo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MethodWithRefParam(ref string[] bar)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GenericType<T1, T2>
|
||||||
|
{
|
||||||
|
public class GenericNestedType<T3, T4>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NonGenericNested
|
||||||
|
{
|
||||||
|
public class MultiNestedType<T5, T6>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NonGeneric
|
||||||
|
{
|
||||||
|
public class Nested1<T1, T2>
|
||||||
|
{
|
||||||
|
public class Nested2
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace TagHelpersWebSite.Components
|
||||||
|
{
|
||||||
|
public class GenericViewComponent : ViewComponent
|
||||||
|
{
|
||||||
|
public IViewComponentResult Invoke(Dictionary<string, List<string>> items)
|
||||||
|
{
|
||||||
|
return View(items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
@addTagHelper "*, TagHelpersWebSite"
|
@addTagHelper "*, TagHelpersWebSite"
|
||||||
@{
|
@{
|
||||||
var year = 2016;
|
var year = 2016;
|
||||||
|
var dict = new Dictionary<string, List<string>>();
|
||||||
|
var items = new List<string>() { "One", "Two", "Three" };
|
||||||
|
dict.Add("Foo", items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<vc:generic items="dict"></vc:generic>
|
||||||
<vc:dan jacket-color="Green" /><br />
|
<vc:dan jacket-color="Green" /><br />
|
||||||
<div>
|
<div>
|
||||||
<vc:copyright website="example.com" year="year" bold></vc:copyright>
|
<vc:copyright website="example.com" year="year" bold></vc:copyright>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
@model Dictionary<string, List<string>>
|
||||||
|
|
||||||
|
<div>Items: </div>
|
||||||
|
<div>
|
||||||
|
@foreach (var item in Model)
|
||||||
|
{
|
||||||
|
<strong>@item.Key</strong><br/>
|
||||||
|
@foreach (var value in Model[item.Key])
|
||||||
|
{
|
||||||
|
<span>@value</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue