diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs index ad9b586f03..5c698946e1 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs @@ -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 /// A to render. 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 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 + { + 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); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs index 90626b2740..4a0bdfbc21 100644 --- a/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Generator/CSharpTagHelperRenderingTest.cs @@ -19,6 +19,56 @@ namespace Microsoft.AspNet.Razor.Test.Generator private static IEnumerable PrefixedPAndInputTagHelperDescriptors => BuildPAndInputTagHelperDescriptors("THS"); + private static IEnumerable 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 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 }, diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/DuplicateTargetTagHelper.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/DuplicateTargetTagHelper.cs new file mode 100644 index 0000000000..51d2dbb4d7 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Output/DuplicateTargetTagHelper.cs @@ -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(); + __tagHelperExecutionContext.Add(__InputTagHelper); + __InputTagHelper.Type = "checkbox"; + __tagHelperExecutionContext.AddTagHelperAttribute("type", __InputTagHelper.Type); + __CatchAllTagHelper = CreateTagHelper(); + __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 + } +} diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/DuplicateTargetTagHelper.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/DuplicateTargetTagHelper.cshtml new file mode 100644 index 0000000000..d3c68eb0d4 --- /dev/null +++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/CS/Source/DuplicateTargetTagHelper.cshtml @@ -0,0 +1,3 @@ +@addTagHelper "something, nice" + + \ No newline at end of file