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("");
+ writer.Write(TagName);
+ 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