diff --git a/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs index 1b9068611f..6a053c32cc 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -223,15 +223,6 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers AllAttributes.Add(name, value); } - /// - /// Executes the child content asynchronously. - /// - /// A which on completion executes all child content. - public Task ExecuteChildContentAsync() - { - return _executeChildContentAsync(); - } - /// /// Execute and retrieve the rendered child content asynchronously. /// diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs index af8eca767c..db9614af62 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs @@ -2,14 +2,17 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; +using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.AspNet.Html.Abstractions; namespace Microsoft.AspNet.Razor.TagHelpers { /// /// Class used to represent the output of an . /// - public class TagHelperOutput + public class TagHelperOutput : IHtmlContent { private readonly Func> _getChildContentAsync; @@ -151,5 +154,85 @@ namespace Microsoft.AspNet.Razor.TagHelpers { return _getChildContentAsync(useCachedResult); } + + /// + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + PreElement.WriteTo(writer, encoder); + + var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName); + + if (!isTagNameNullOrWhitespace) + { + writer.Write('<'); + writer.Write(TagName); + + foreach (var attribute in Attributes) + { + writer.Write(' '); + writer.Write(attribute.Name); + + if (attribute.Minimized) + { + continue; + } + + writer.Write("=\""); + var value = attribute.Value; + var htmlContent = value as IHtmlContent; + if (htmlContent != null) + { + // There's no way of tracking the attribute value quotations in the Razor source. Therefore, we + // must escape any IHtmlContent double quote values in the case that a user wrote: + //

+ using (var stringWriter = new StringWriter()) + { + htmlContent.WriteTo(stringWriter, encoder); + + var stringValue = stringWriter.ToString(); + stringValue = stringValue.Replace("\"", """); + + writer.Write(stringValue); + } + } + else if (value != null) + { + encoder.Encode(writer, value.ToString()); + } + + writer.Write('"'); + } + + if (TagMode == TagMode.SelfClosing) + { + writer.Write(" /"); + } + + writer.Write('>'); + } + + if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag) + { + PreContent.WriteTo(writer, encoder); + + Content.WriteTo(writer, encoder); + + PostContent.WriteTo(writer, encoder); + } + + if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag) + { + writer.Write(""); + } + + PostElement.WriteTo(writer, encoder); + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs index 30b74de65d..bf46fbb6b7 100644 --- a/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs +++ b/src/Microsoft.AspNet.Razor/CodeGenerators/CSharpTagHelperCodeRenderer.cs @@ -93,7 +93,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators if (!_designTimeMode) { RenderRunTagHelpers(); - RenderWriteTagHelperMethodCall(chunk); + RenderTagHelperOutput(chunk); RenderEndTagHelpersScope(); } } @@ -533,26 +533,50 @@ namespace Microsoft.AspNet.Razor.CodeGenerators _tagHelperContext.ScopeManagerEndMethodName); } - private void RenderWriteTagHelperMethodCall(TagHelperChunk chunk) + private void RenderTagHelperOutput(TagHelperChunk chunk) { + var tagHelperOutputAccessor = + $"{ExecutionContextVariableName}.{_tagHelperContext.ExecutionContextOutputPropertyName}"; + + if (ContainsChildContent(chunk.Children)) + { + _writer + .Write("if (!") + .Write(tagHelperOutputAccessor) + .Write(".") + .Write(_tagHelperContext.TagHelperOutputIsContentModifiedPropertyName) + .WriteLine(")"); + + using (_writer.BuildScope()) + { + _writer + .Write(tagHelperOutputAccessor) + .Write(".") + .WriteStartAssignment(_tagHelperContext.TagHelperOutputContentPropertyName) + .Write("await ") + .WriteInstanceMethodInvocation( + tagHelperOutputAccessor, + _tagHelperContext.TagHelperOutputGetChildContentAsyncMethodName); + } + } + _writer - .WriteStartInstrumentationContext(_context, chunk.Association, isLiteral: false) - .Write("await "); + .WriteStartInstrumentationContext(_context, chunk.Association, isLiteral: false); if (!string.IsNullOrEmpty(_context.TargetWriterName)) { _writer - .WriteStartMethodInvocation(_tagHelperContext.WriteTagHelperToAsyncMethodName) + .WriteStartMethodInvocation(_context.Host.GeneratedClassContext.WriteToMethodName) .Write(_context.TargetWriterName) .WriteParameterSeparator(); } else { - _writer.WriteStartMethodInvocation(_tagHelperContext.WriteTagHelperAsyncMethodName); + _writer.WriteStartMethodInvocation(_context.Host.GeneratedClassContext.WriteMethodName); } _writer - .Write(ExecutionContextVariableName) + .Write(tagHelperOutputAccessor) .WriteEndMethodInvocation() .WriteEndInstrumentationContext(_context); } @@ -688,6 +712,24 @@ namespace Microsoft.AspNet.Razor.CodeGenerators } } + private static bool ContainsChildContent(IList children) + { + // False will be returned if there are no children or if there are only non-TagHelper ParentChunk leaf + // nodes. + foreach (var child in children) + { + var parentChunk = child as ParentChunk; + if (parentChunk == null || + parentChunk is TagHelperChunk || + ContainsChildContent(parentChunk.Children)) + { + return true; + } + } + + return false; + } + private static bool IsDynamicAttributeValue(Chunk attributeValueChunk) { var parentChunk = attributeValueChunk as ParentChunk; diff --git a/src/Microsoft.AspNet.Razor/CodeGenerators/GeneratedTagHelperContext.cs b/src/Microsoft.AspNet.Razor/CodeGenerators/GeneratedTagHelperContext.cs index 70a9ddc9bd..3bdffb843b 100644 --- a/src/Microsoft.AspNet.Razor/CodeGenerators/GeneratedTagHelperContext.cs +++ b/src/Microsoft.AspNet.Razor/CodeGenerators/GeneratedTagHelperContext.cs @@ -33,10 +33,11 @@ namespace Microsoft.AspNet.Razor.CodeGenerators ScopeManagerTypeName = "Microsoft.AspNet.Razor.Runtime.TagHelperScopeManager"; ExecutionContextTypeName = "Microsoft.AspNet.Razor.Runtime.TagHelperExecutionContext"; TagHelperContentTypeName = "Microsoft.AspNet.Razor.TagHelperContent"; - WriteTagHelperAsyncMethodName = "WriteTagHelperAsync"; - WriteTagHelperToAsyncMethodName = "WriteTagHelperToAsync"; TagHelperContentGetContentMethodName = "GetContent"; HtmlEncoderPropertyName = "HtmlEncoder"; + TagHelperOutputIsContentModifiedPropertyName = "IsContentModified"; + TagHelperOutputContentPropertyName = "Content"; + TagHelperOutputGetChildContentAsyncMethodName = "GetChildContentAsync"; } /// @@ -124,7 +125,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators public string ExecutionContextAddMethodName { get; set; } /// - /// The property accessor for the tag helper's output. + /// The property name for the tag helper's output. /// public string ExecutionContextOutputPropertyName { get; set; } @@ -185,17 +186,6 @@ namespace Microsoft.AspNet.Razor.CodeGenerators /// public string TagHelperContentTypeName { get; set; } - /// - /// The name of the method used to write . - /// - public string WriteTagHelperAsyncMethodName { get; set; } - - /// - /// The name of the method used to write to a specified - /// . - /// - public string WriteTagHelperToAsyncMethodName { get; set; } - /// /// The name of the property containing the HtmlEncoder. /// @@ -205,5 +195,21 @@ namespace Microsoft.AspNet.Razor.CodeGenerators /// The name of the method used to convert a TagHelperContent into a . /// public string TagHelperContentGetContentMethodName { get; set; } + + /// + /// The name of the property used to indicate the tag helper's content has been modified. + /// + public string TagHelperOutputIsContentModifiedPropertyName { get; set; } + + /// + /// The name of the property for the tag helper's output content. + /// + public string TagHelperOutputContentPropertyName { get; set; } + + /// + /// The name of the method on the property used to retrieve + /// tag helper child content. + /// + public string TagHelperOutputGetChildContentAsyncMethodName { get; set; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs index a719d07212..056b148dd1 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs @@ -143,34 +143,6 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers Assert.Empty(content3.GetContent(new HtmlTestEncoder())); } - [Fact] - public async Task ExecuteChildContentAsync_IsNotMemoized() - { - // Arrange - var childContentExecutionCount = 0; - var executionContext = new TagHelperExecutionContext( - "p", - tagMode: TagMode.StartTagAndEndTag, - items: new Dictionary(), - uniqueId: string.Empty, - executeChildContentAsync: () => - { - childContentExecutionCount++; - - return Task.FromResult(result: true); - }, - startTagHelperWritingScope: () => { }, - endTagHelperWritingScope: () => new DefaultTagHelperContent()); - - // Act - await executionContext.ExecuteChildContentAsync(); - await executionContext.ExecuteChildContentAsync(); - await executionContext.ExecuteChildContentAsync(); - - // Assert - Assert.Equal(3, childContentExecutionCount); - } - public static TheoryData DictionaryCaseTestingData { get