diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs
index cfb32a9aa7..79e00379f8 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DefaultDocumentWriter.cs
@@ -216,6 +216,11 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
Context.TagHelperWriter.WriteCreateTagHelper(Context, node);
}
+ public override void VisitAddTagHelperHtmlAttribute(AddTagHelperHtmlAttributeIRNode node)
+ {
+ Context.TagHelperWriter.WriteAddTagHelperHtmlAttribute(Context, node);
+ }
+
public override void VisitExecuteTagHelpers(ExecuteTagHelpersIRNode node)
{
Context.TagHelperWriter.WriteExecuteTagHelpers(Context, node);
diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DesignTimeTagHelperWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DesignTimeTagHelperWriter.cs
index 2e6ec655eb..df7070a0b7 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DesignTimeTagHelperWriter.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/DesignTimeTagHelperWriter.cs
@@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
public override void WriteAddTagHelperHtmlAttribute(CSharpRenderingContext context, AddTagHelperHtmlAttributeIRNode node)
{
- throw new NotImplementedException();
+ context.RenderChildren(node);
}
public override void WriteCreateTagHelper(CSharpRenderingContext context, CreateTagHelperIRNode node)
diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeBasicWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeBasicWriter.cs
index 57100c4014..c0314e1283 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeBasicWriter.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeBasicWriter.cs
@@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
public string WriteHtmlContentMethod { get; set; } = "WriteLiteral";
+ public string WriteAttributeValueMethod { get; set; } = "WriteAttributeValue";
+
public override void WriteCSharpExpression(CSharpRenderingContext context, CSharpExpressionIRNode node)
{
if (context == null)
diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeCSharpRenderer.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeCSharpRenderer.cs
index fd5d6ba616..407705bd09 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeCSharpRenderer.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeCSharpRenderer.cs
@@ -278,77 +278,6 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
.WriteEndMethodInvocation();
}
- public override void VisitAddTagHelperHtmlAttribute(AddTagHelperHtmlAttributeIRNode node)
- {
- var attributeValueStyleParameter = $"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.ValueStyle}";
- var isConditionalAttributeValue = node.Children.Any(child => child is CSharpAttributeValueIRNode);
-
- // All simple text and minimized attributes will be pre-allocated.
- if (isConditionalAttributeValue)
- {
- // Dynamic attribute value should be run through the conditional attribute removal system. It's
- // unbound and contains C#.
-
- // TagHelper attribute rendering is buffered by default. We do not want to write to the current
- // writer.
- var valuePieceCount = node.Children.Count(
- child => child is HtmlAttributeValueIRNode || child is CSharpAttributeValueIRNode);
-
- Context.Writer
- .WriteStartMethodInvocation("BeginAddHtmlAttributeValues" /* ORIGINAL: BeginAddHtmlAttributeValuesMethodName */)
- .Write("__tagHelperExecutionContext" /* ORIGINAL: ExecutionContextVariableName */)
- .WriteParameterSeparator()
- .WriteStringLiteral(node.Name)
- .WriteParameterSeparator()
- .Write(valuePieceCount.ToString(CultureInfo.InvariantCulture))
- .WriteParameterSeparator()
- .Write(attributeValueStyleParameter)
- .WriteEndMethodInvocation();
-
- var initialRenderingConventions = Context.RenderingConventions;
- Context.RenderingConventions = new TagHelperHtmlAttributeRenderingConventions(Context.Writer);
- VisitDefault(node);
- Context.RenderingConventions = initialRenderingConventions;
-
- Context.Writer
- .WriteMethodInvocation(
- "EndAddHtmlAttributeValues" /* ORIGINAL: EndAddHtmlAttributeValuesMethodName */,
- "__tagHelperExecutionContext" /* ORIGINAL: ExecutionContextVariableName */);
- }
- else
- {
- // This is a data-* attribute which includes C#. Do not perform the conditional attribute removal or
- // other special cases used when IsDynamicAttributeValue(). But the attribute must still be buffered to
- // determine its final value.
-
- // Attribute value is not plain text, must be buffered to determine its final value.
- Context.Writer.WriteMethodInvocation("BeginWriteTagHelperAttribute" /* ORIGINAL: BeginWriteTagHelperAttributeMethodName */);
-
- // We're building a writing scope around the provided chunks which captures everything written from the
- // page. Therefore, we do not want to write to any other buffer since we're using the pages buffer to
- // ensure we capture all content that's written, directly or indirectly.
- var initialRenderingConventions = Context.RenderingConventions;
- Context.RenderingConventions = new CSharpRenderingConventions(Context.Writer);
- VisitDefault(node);
- Context.RenderingConventions = initialRenderingConventions;
-
- Context.Writer
- .WriteStartAssignment("__tagHelperStringValueBuffer" /* ORIGINAL: StringValueBufferVariableName */)
- .WriteMethodInvocation("EndWriteTagHelperAttribute" /* ORIGINAL: EndWriteTagHelperAttributeMethodName */)
- .WriteStartInstanceMethodInvocation(
- "__tagHelperExecutionContext" /* ORIGINAL: ExecutionContextVariableName */,
- "AddHtmlAttribute" /* ORIGINAL: ExecutionContextAddHtmlAttributeMethodName */)
- .WriteStringLiteral(node.Name)
- .WriteParameterSeparator()
- .WriteStartMethodInvocation("Html.Raw" /* ORIGINAL: MarkAsHtmlEncodedMethodName */)
- .Write("__tagHelperStringValueBuffer" /* ORIGINAL: StringValueBufferVariableName */)
- .WriteEndMethodInvocation(endLine: false)
- .WriteParameterSeparator()
- .Write(attributeValueStyleParameter)
- .WriteEndMethodInvocation();
- }
- }
-
public override void VisitSetPreallocatedTagHelperProperty(SetPreallocatedTagHelperPropertyIRNode node)
{
var tagHelperVariableName = GetTagHelperVariableName(node.TagHelperTypeName);
diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeTagHelperWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeTagHelperWriter.cs
index 569c0e5567..c9803ac704 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeTagHelperWriter.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/RuntimeTagHelperWriter.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.Globalization;
+using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
@@ -20,6 +22,8 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
public string ExecutionContextSetOutputContentAsyncMethodName { get; set; } = "SetOutputContentAsync";
+ public string ExecutionContextAddHtmlAttributeMethodName { get; set; } = "AddHtmlAttribute";
+
public string RunnerTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner";
public string RunnerVariableName { get; set; } = "__tagHelperRunner";
@@ -44,6 +48,16 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
public string TagHelperOutputIsContentModifiedPropertyName { get; set; } = "IsContentModified";
+ public string BeginAddHtmlAttributeValuesMethodName { get; set; } = "BeginAddHtmlAttributeValues";
+
+ public string EndAddHtmlAttributeValuesMethodName { get; set; } = "EndAddHtmlAttributeValues";
+
+ public string BeginWriteTagHelperAttributeMethodName { get; set; } = "BeginWriteTagHelperAttribute";
+
+ public string EndWriteTagHelperAttributeMethodName { get; set; } = "EndWriteTagHelperAttribute";
+
+ public string MarkAsHtmlEncodedMethodName { get; set; } = "Html.Raw";
+
public string WriteTagHelperOutputMethod { get; set; } = "Write";
public override void WriteDeclareTagHelperFields(CSharpRenderingContext context, DeclareTagHelperFieldsIRNode node)
@@ -127,7 +141,82 @@ namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
public override void WriteAddTagHelperHtmlAttribute(CSharpRenderingContext context, AddTagHelperHtmlAttributeIRNode node)
{
- throw new NotImplementedException();
+ var attributeValueStyleParameter = $"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.ValueStyle}";
+ var isConditionalAttributeValue = node.Children.Any(child => child is CSharpAttributeValueIRNode);
+
+ // All simple text and minimized attributes will be pre-allocated.
+ if (isConditionalAttributeValue)
+ {
+ // Dynamic attribute value should be run through the conditional attribute removal system. It's
+ // unbound and contains C#.
+
+ // TagHelper attribute rendering is buffered by default. We do not want to write to the current
+ // writer.
+ var valuePieceCount = node.Children.Count(
+ child => child is HtmlAttributeValueIRNode || child is CSharpAttributeValueIRNode);
+
+ context.Writer
+ .WriteStartMethodInvocation(BeginAddHtmlAttributeValuesMethodName)
+ .Write(ExecutionContextVariableName)
+ .WriteParameterSeparator()
+ .WriteStringLiteral(node.Name)
+ .WriteParameterSeparator()
+ .Write(valuePieceCount.ToString(CultureInfo.InvariantCulture))
+ .WriteParameterSeparator()
+ .Write(attributeValueStyleParameter)
+ .WriteEndMethodInvocation();
+
+ // This can be removed once all the tag helper nodes are moved out of the renderers.
+ var initialRenderingConventions = context.RenderingConventions;
+ context.RenderingConventions = new TagHelperHtmlAttributeRenderingConventions(context.Writer);
+ using (context.Push(new TagHelperHtmlAttributeRuntimeBasicWriter()))
+ {
+ context.RenderChildren(node);
+ }
+ context.RenderingConventions = initialRenderingConventions;
+
+ context.Writer
+ .WriteMethodInvocation(
+ EndAddHtmlAttributeValuesMethodName,
+ ExecutionContextVariableName);
+ }
+ else
+ {
+ // This is a data-* attribute which includes C#. Do not perform the conditional attribute removal or
+ // other special cases used when IsDynamicAttributeValue(). But the attribute must still be buffered to
+ // determine its final value.
+
+ // Attribute value is not plain text, must be buffered to determine its final value.
+ context.Writer.WriteMethodInvocation(BeginWriteTagHelperAttributeMethodName);
+
+ // We're building a writing scope around the provided chunks which captures everything written from the
+ // page. Therefore, we do not want to write to any other buffer since we're using the pages buffer to
+ // ensure we capture all content that's written, directly or indirectly.
+ // This can be removed once all the tag helper nodes are moved out of the renderers.
+ var initialRenderingConventions = context.RenderingConventions;
+ context.RenderingConventions = new CSharpRenderingConventions(context.Writer);
+ using (context.Push(new RuntimeBasicWriter()))
+ using (context.Push(new RuntimeTagHelperWriter()))
+ {
+ context.RenderChildren(node);
+ }
+ context.RenderingConventions = initialRenderingConventions;
+
+ context.Writer
+ .WriteStartAssignment(StringValueBufferVariableName)
+ .WriteMethodInvocation(EndWriteTagHelperAttributeMethodName)
+ .WriteStartInstanceMethodInvocation(
+ ExecutionContextVariableName,
+ ExecutionContextAddHtmlAttributeMethodName)
+ .WriteStringLiteral(node.Name)
+ .WriteParameterSeparator()
+ .WriteStartMethodInvocation(MarkAsHtmlEncodedMethodName)
+ .Write(StringValueBufferVariableName)
+ .WriteEndMethodInvocation(endLine: false)
+ .WriteParameterSeparator()
+ .Write(attributeValueStyleParameter)
+ .WriteEndMethodInvocation();
+ }
}
public override void WriteCreateTagHelper(CSharpRenderingContext context, CreateTagHelperIRNode node)
diff --git a/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/TagHelperHtmlAttributeRuntimeBasicWriter.cs b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/TagHelperHtmlAttributeRuntimeBasicWriter.cs
new file mode 100644
index 0000000000..9fe135433e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/TagHelperHtmlAttributeRuntimeBasicWriter.cs
@@ -0,0 +1,11 @@
+// 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.
+
+namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
+{
+ internal class TagHelperHtmlAttributeRuntimeBasicWriter : RuntimeBasicWriter
+ {
+ // This will be used when HtmlAttributeValueIRNode and CSharpAttributeValueIRNode are moved to writers.
+ public new string WriteAttributeValueMethod { get; set; } = "AddHtmlAttributeValue";
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs
index fced9de87e..c93d6a2a3a 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs
@@ -1,6 +1,9 @@
// 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 Microsoft.AspNetCore.Razor.Language.Intermediate;
using Xunit;
@@ -180,5 +183,203 @@ __tagHelperExecutionContext = __tagHelperScopeManager.End();
csharp,
ignoreLineEndingDifferences: true);
}
+
+ [Fact]
+ public void WriteAddTagHelperHtmlAttribute_RendersCorrectly()
+ {
+ // Arrange
+ var writer = new RuntimeTagHelperWriter();
+ var options = RazorParserOptions.CreateDefaultOptions();
+ var codeWriter = new Legacy.CSharpCodeWriter();
+ var context = new CSharpRenderingContext()
+ {
+ Writer = codeWriter,
+ Options = options,
+ BasicWriter = new RuntimeBasicWriter(),
+ TagHelperWriter = writer,
+ RenderChildren = n =>
+ {
+ codeWriter.WriteLine("Render Children");
+ }
+ };
+
+ var descriptors = new[]
+ {
+ CreateTagHelperDescriptor(
+ tagName: "input",
+ typeName: "InputTagHelper",
+ assemblyName: "TestAssembly")
+ };
+ var engine = RazorEngine.Create(builder => builder.AddTagHelpers(descriptors));
+ var content = @"
+@addTagHelper *, TestAssembly
+";
+ var sourceDocument = TestRazorSourceDocument.Create(content);
+ var codeDocument = RazorCodeDocument.Create(sourceDocument);
+ var irDocument = Lower(codeDocument, engine);
+ var node = irDocument.Children.Last().Children[2] as AddTagHelperHtmlAttributeIRNode;
+
+ // Act
+ writer.WriteAddTagHelperHtmlAttribute(context, node);
+
+ // Assert
+ var csharp = context.Writer.Builder.ToString();
+ Assert.Equal(
+@"BeginWriteTagHelperAttribute();
+Render Children
+__tagHelperStringValueBuffer = EndWriteTagHelperAttribute();
+__tagHelperExecutionContext.AddHtmlAttribute(""name"", Html.Raw(__tagHelperStringValueBuffer), global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes);
+",
+ csharp,
+ ignoreLineEndingDifferences: true);
+ }
+
+ [Fact]
+ public void WriteAddTagHelperHtmlAttribute_DataAttribute_RendersCorrectly()
+ {
+ // Arrange
+ var writer = new RuntimeTagHelperWriter();
+ var options = RazorParserOptions.CreateDefaultOptions();
+ var codeWriter = new Legacy.CSharpCodeWriter();
+ var context = new CSharpRenderingContext()
+ {
+ Writer = codeWriter,
+ Options = options,
+ BasicWriter = new RuntimeBasicWriter(),
+ TagHelperWriter = writer,
+ RenderChildren = n =>
+ {
+ codeWriter.WriteLine("Render Children");
+ }
+ };
+
+ var descriptors = new[]
+ {
+ CreateTagHelperDescriptor(
+ tagName: "input",
+ typeName: "InputTagHelper",
+ assemblyName: "TestAssembly")
+ };
+ var engine = RazorEngine.Create(builder => builder.AddTagHelpers(descriptors));
+ var content = @"
+@addTagHelper *, TestAssembly
+";
+ var sourceDocument = TestRazorSourceDocument.Create(content);
+ var codeDocument = RazorCodeDocument.Create(sourceDocument);
+ var irDocument = Lower(codeDocument, engine);
+ var node = irDocument.Children.Last().Children[2] as AddTagHelperHtmlAttributeIRNode;
+
+ // Act
+ writer.WriteAddTagHelperHtmlAttribute(context, node);
+
+ // Assert
+ var csharp = context.Writer.Builder.ToString();
+ Assert.Equal(
+@"BeginWriteTagHelperAttribute();
+Render Children
+__tagHelperStringValueBuffer = EndWriteTagHelperAttribute();
+__tagHelperExecutionContext.AddHtmlAttribute(""data-test"", Html.Raw(__tagHelperStringValueBuffer), global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes);
+",
+ csharp,
+ ignoreLineEndingDifferences: true);
+ }
+
+ [Fact]
+ public void WriteAddTagHelperHtmlAttribute_DynamicAttribute_RendersCorrectly()
+ {
+ // Arrange
+ var writer = new RuntimeTagHelperWriter();
+ var options = RazorParserOptions.CreateDefaultOptions();
+ var codeWriter = new Legacy.CSharpCodeWriter();
+ var context = new CSharpRenderingContext()
+ {
+ Writer = codeWriter,
+ Options = options,
+ BasicWriter = new RuntimeBasicWriter(),
+ TagHelperWriter = writer,
+ RenderChildren = n =>
+ {
+ codeWriter.WriteLine("Render Children");
+ }
+ };
+
+ var descriptors = new[]
+ {
+ CreateTagHelperDescriptor(
+ tagName: "input",
+ typeName: "InputTagHelper",
+ assemblyName: "TestAssembly")
+ };
+ var engine = RazorEngine.Create(builder => builder.AddTagHelpers(descriptors));
+ var content = @"
+@addTagHelper *, TestAssembly
+";
+ var sourceDocument = TestRazorSourceDocument.Create(content);
+ var codeDocument = RazorCodeDocument.Create(sourceDocument);
+ var irDocument = Lower(codeDocument, engine);
+ var node = irDocument.Children.Last().Children[2] as AddTagHelperHtmlAttributeIRNode;
+
+ // Act
+ writer.WriteAddTagHelperHtmlAttribute(context, node);
+
+ // Assert
+ var csharp = context.Writer.Builder.ToString();
+ Assert.Equal(
+@"BeginAddHtmlAttributeValues(__tagHelperExecutionContext, ""test"", 2, global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.DoubleQuotes);
+Render Children
+EndAddHtmlAttributeValues(__tagHelperExecutionContext);
+",
+ csharp,
+ ignoreLineEndingDifferences: true);
+ }
+
+ private static DocumentIRNode Lower(RazorCodeDocument codeDocument)
+ {
+ var engine = RazorEngine.Create();
+
+ return Lower(codeDocument, engine);
+ }
+
+ private static DocumentIRNode Lower(RazorCodeDocument codeDocument, RazorEngine engine)
+ {
+ for (var i = 0; i < engine.Phases.Count; i++)
+ {
+ var phase = engine.Phases[i];
+ phase.Execute(codeDocument);
+
+ if (phase is IRazorIRLoweringPhase)
+ {
+ break;
+ }
+ }
+
+ var irDocument = codeDocument.GetIRDocument();
+ Assert.NotNull(irDocument);
+
+ return irDocument;
+ }
+
+ private static TagHelperDescriptor CreateTagHelperDescriptor(
+ string tagName,
+ string typeName,
+ string assemblyName,
+ IEnumerable> attributes = null)
+ {
+ var builder = TagHelperDescriptorBuilder.Create(typeName, assemblyName);
+
+ if (attributes != null)
+ {
+ foreach (var attributeBuilder in attributes)
+ {
+ builder.BindAttribute(attributeBuilder);
+ }
+ }
+
+ builder.TagMatchingRule(ruleBuilder => ruleBuilder.RequireTagName(tagName));
+
+ var descriptor = builder.Build();
+
+ return descriptor;
+ }
}
}