From 0b17f14d68ce1e9e8f986670af72ef85c71b2086 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Fri, 7 Apr 2017 18:42:24 -0700 Subject: [PATCH] Moved AddTagHelperHtmlAttribute from renderer to writer --- .../CodeGeneration/DefaultDocumentWriter.cs | 5 + .../DesignTimeTagHelperWriter.cs | 2 +- .../CodeGeneration/RuntimeBasicWriter.cs | 2 + .../CodeGeneration/RuntimeCSharpRenderer.cs | 71 ------- .../CodeGeneration/RuntimeTagHelperWriter.cs | 91 +++++++- ...agHelperHtmlAttributeRuntimeBasicWriter.cs | 11 + .../RuntimeTagHelperWriterTest.cs | 201 ++++++++++++++++++ 7 files changed, 310 insertions(+), 73 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Language/CodeGeneration/TagHelperHtmlAttributeRuntimeBasicWriter.cs 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; + } } }