De-dupe TagHelperDescriptors based on Type for rendering.

- This can occur if you have multiple [TargetElement] attributes that overlap. Ultimately the descriptor is the same because its the same type, just the required attributes differ.
- Added tests to validate.

#326
This commit is contained in:
N. Taylor Mullen 2015-03-19 18:39:03 -07:00
parent fa40fe6e46
commit 842549bba9
4 changed files with 138 additions and 6 deletions

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
@ -56,11 +55,15 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
/// <param name="chunk">A <see cref="TagHelperChunk"/> to render.</param>
public void RenderTagHelper(TagHelperChunk chunk)
{
var tagHelperDescriptors = chunk.Descriptors;
// Remove any duplicate TagHelperDescriptors that referrence the same type name. Duplicates can occur when
// multiple TargetElement attributes are on a TagHelper type and matchs overlap for an HTML element.
// Having more than one descriptor with the same TagHelper type results in generated code that runs
// the same TagHelper X many times (instead of once) over a single HTML element.
var tagHelperDescriptors = chunk.Descriptors.Distinct(TypeNameTagHelperDescriptorComparer.Default);
RenderBeginTagHelperScope(chunk.TagName, chunk.SelfClosing, chunk.Children);
RenderTagHelpersCreation(chunk);
RenderTagHelpersCreation(chunk, tagHelperDescriptors);
var attributeDescriptors = tagHelperDescriptors.SelectMany(descriptor => descriptor.Attributes);
var boundHTMLAttributes = attributeDescriptors.Select(descriptor => descriptor.Name);
@ -146,10 +149,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
return Guid.NewGuid().ToString("N");
}
private void RenderTagHelpersCreation(TagHelperChunk chunk)
private void RenderTagHelpersCreation(
TagHelperChunk chunk,
IEnumerable<TagHelperDescriptor> tagHelperDescriptors)
{
var tagHelperDescriptors = chunk.Descriptors;
// This is to maintain value accessors for attributes when creating the TagHelpers.
// Ultimately it enables us to do scenarios like this:
// myTagHelper1.Foo = DateTime.Now;
@ -551,5 +554,25 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
return StringComparer.OrdinalIgnoreCase.GetHashCode(descriptor.Name);
}
}
private class TypeNameTagHelperDescriptorComparer : IEqualityComparer<TagHelperDescriptor>
{
public static readonly TypeNameTagHelperDescriptorComparer Default =
new TypeNameTagHelperDescriptorComparer();
private TypeNameTagHelperDescriptorComparer()
{
}
public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
{
return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal);
}
public int GetHashCode(TagHelperDescriptor descriptor)
{
return StringComparer.Ordinal.GetHashCode(descriptor.TypeName);
}
}
}
}

View File

@ -19,6 +19,56 @@ namespace Microsoft.AspNet.Razor.Test.Generator
private static IEnumerable<TagHelperDescriptor> PrefixedPAndInputTagHelperDescriptors
=> BuildPAndInputTagHelperDescriptors("THS");
private static IEnumerable<TagHelperDescriptor> DuplicateTargetTagHelperDescriptors
{
get
{
var inputTypePropertyInfo = typeof(TestType).GetProperty("Type");
var inputCheckedPropertyInfo = typeof(TestType).GetProperty("Checked");
return new[]
{
new TagHelperDescriptor(
tagName: "*",
typeName: "CatchAllTagHelper",
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[]
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo)
},
requiredAttributes: new[] { "type" }),
new TagHelperDescriptor(
tagName: "*",
typeName: "CatchAllTagHelper",
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[]
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
requiredAttributes: new[] { "type", "checked" }),
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper",
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[]
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo)
},
requiredAttributes: new[] { "type" }),
new TagHelperDescriptor(
tagName: "input",
typeName: "InputTagHelper",
assemblyName: "SomeAssembly",
attributes: new TagHelperAttributeDescriptor[]
{
new TagHelperAttributeDescriptor("type", inputTypePropertyInfo),
new TagHelperAttributeDescriptor("checked", inputCheckedPropertyInfo)
},
requiredAttributes: new[] { "type", "checked" })
};
}
}
private static IEnumerable<TagHelperDescriptor> AttributeTargetingTagHelperDescriptors
{
get
@ -93,6 +143,13 @@ namespace Microsoft.AspNet.Razor.Test.Generator
DefaultPAndInputTagHelperDescriptors,
false
},
{
"DuplicateTargetTagHelper",
"DuplicateTargetTagHelper",
DuplicateTargetTagHelperDescriptors,
DuplicateTargetTagHelperDescriptors,
false
},
{
"BasicTagHelpers",
"BasicTagHelpers.DesignTime",
@ -410,6 +467,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
{ "BasicTagHelpers.RemoveTagHelper", DefaultPAndInputTagHelperDescriptors },
{ "BasicTagHelpers.Prefixed", PrefixedPAndInputTagHelperDescriptors },
{ "ComplexTagHelpers", DefaultPAndInputTagHelperDescriptors },
{ "DuplicateTargetTagHelper", DuplicateTargetTagHelperDescriptors },
{ "EmptyAttributeTagHelpers", DefaultPAndInputTagHelperDescriptors },
{ "EscapedTagHelpers", DefaultPAndInputTagHelperDescriptors },
{ "AttributeTargetingTagHelpers", AttributeTargetingTagHelperDescriptors },

View File

@ -0,0 +1,48 @@
#pragma checksum "DuplicateTargetTagHelper.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "9cd2f5a40be40d26a0756bf6a74cee58bd13927f"
namespace TestOutput
{
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using System;
using System.Threading.Tasks;
public class DuplicateTargetTagHelper
{
#line hidden
#pragma warning disable 0414
private TagHelperContent __tagHelperStringValueBuffer = null;
#pragma warning restore 0414
private TagHelperExecutionContext __tagHelperExecutionContext = null;
private TagHelperRunner __tagHelperRunner = null;
private TagHelperScopeManager __tagHelperScopeManager = new TagHelperScopeManager();
private InputTagHelper __InputTagHelper = null;
private CatchAllTagHelper __CatchAllTagHelper = null;
#line hidden
public DuplicateTargetTagHelper()
{
}
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
__tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner();
Instrumentation.BeginContext(33, 2, true);
WriteLiteral("\r\n");
Instrumentation.EndContext();
__tagHelperExecutionContext = __tagHelperScopeManager.Begin("input", true, "test", async() => {
}
, StartTagHelperWritingScope, EndTagHelperWritingScope);
__InputTagHelper = CreateTagHelper<InputTagHelper>();
__tagHelperExecutionContext.Add(__InputTagHelper);
__InputTagHelper.Type = "checkbox";
__tagHelperExecutionContext.AddTagHelperAttribute("type", __InputTagHelper.Type);
__CatchAllTagHelper = CreateTagHelper<CatchAllTagHelper>();
__tagHelperExecutionContext.Add(__CatchAllTagHelper);
__CatchAllTagHelper.Type = __InputTagHelper.Type;
__tagHelperExecutionContext.AddHtmlAttribute("checked", "true");
__tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
await WriteTagHelperAsync(__tagHelperExecutionContext);
__tagHelperExecutionContext = __tagHelperScopeManager.End();
}
#pragma warning restore 1998
}
}

View File

@ -0,0 +1,3 @@
@addTagHelper "something, nice"
<input type="checkbox" checked="true" />