[Fixes #5216] Make generic parameters work with ViewComponent tag helpers

This commit is contained in:
Ajay Bhargav Baaskaran 2016-11-01 13:50:16 -07:00
parent ea701f2cd6
commit c28ad48e98
6 changed files with 286 additions and 6 deletions

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.AspNetCore.Mvc.Razor.Host;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
@ -56,7 +57,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
StringComparison.Ordinal));
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));
return tagHelperDescriptors;
@ -92,11 +93,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
foreach (var parameter in methodParameters)
{
var lowerKebabName = TagHelperDescriptorFactory.ToHtmlCase(parameter.Name);
var typeName = GetCSharpTypeName(parameter.ParameterType);
var descriptor = new TagHelperAttributeDescriptor
{
Name = lowerKebabName,
PropertyName = parameter.Name,
TypeName = parameter.ParameterType.FullName
TypeName = typeName
};
descriptor.IsEnum = parameter.ParameterType.GetTypeInfo().IsEnum;
@ -113,7 +115,110 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
});
}
private string GetTagName(ViewComponentDescriptor descriptor) =>
$"vc:{TagHelperDescriptorFactory.ToHtmlCase(descriptor.ShortName)}";
private string GetTagName(ViewComponentDescriptor descriptor)
{
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(">");
}
}
}

View File

@ -1,6 +1,7 @@
// 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;
@ -27,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
return new TheoryData<string, IEnumerable<TagHelperDescriptor>>
{
{ assemblyOne, new [] { provider.GetTagHelperDescriptorOne() } },
{ assemblyTwo, new [] { provider.GetTagHelperDescriptorTwo() } },
{ assemblyTwo, new [] { provider.GetTagHelperDescriptorTwo(), provider.GetTagHelperDescriptorGeneric() } },
{ assemblyNone, Enumerable.Empty<TagHelperDescriptor>() }
};
}
@ -50,6 +51,48 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
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.
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 readonly ViewComponentDescriptor _viewComponentDescriptorOne = new ViewComponentDescriptor
@ -108,6 +155,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
.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()
{
var descriptor = new TagHelperDescriptor
@ -201,15 +260,91 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal
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()
{
return new List<ViewComponentDescriptor>
{
_viewComponentDescriptorOne,
_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
{
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -1,8 +1,12 @@
@addTagHelper "*, TagHelpersWebSite"
@{
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 />
<div>
<vc:copyright website="example.com" year="year" bold></vc:copyright>

View File

@ -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>