Make `TagHelperOutput` an `IHtmlContent`.

- This allows users to write `TagHelperOutput` directly to an `IHtmlContent` accepting `TextWriter`.
- This also enables us to inspect backing fields for all of the various contents to lazily initialize them.

#358
This commit is contained in:
N. Taylor Mullen 2015-11-12 17:09:17 -08:00
parent 36c744ff29
commit bdf869c3d5
5 changed files with 154 additions and 60 deletions

View File

@ -223,15 +223,6 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
AllAttributes.Add(name, value);
}
/// <summary>
/// Executes the child content asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> which on completion executes all child content.</returns>
public Task ExecuteChildContentAsync()
{
return _executeChildContentAsync();
}
/// <summary>
/// Execute and retrieve the rendered child content asynchronously.
/// </summary>

View File

@ -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
{
/// <summary>
/// Class used to represent the output of an <see cref="ITagHelper"/>.
/// </summary>
public class TagHelperOutput
public class TagHelperOutput : IHtmlContent
{
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
@ -151,5 +154,85 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{
return _getChildContentAsync(useCachedResult);
}
/// <inheritdoc />
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:
// <p name='A " is valid in single quotes'></p>
using (var stringWriter = new StringWriter())
{
htmlContent.WriteTo(stringWriter, encoder);
var stringValue = stringWriter.ToString();
stringValue = stringValue.Replace("\"", "&quot;");
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);
}
}
}
}

View File

@ -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<Chunk> 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;

View File

@ -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";
}
/// <summary>
@ -124,7 +125,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
public string ExecutionContextAddMethodName { get; set; }
/// <summary>
/// The property accessor for the tag helper's output.
/// The property name for the tag helper's output.
/// </summary>
public string ExecutionContextOutputPropertyName { get; set; }
@ -185,17 +186,6 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
/// </remarks>
public string TagHelperContentTypeName { get; set; }
/// <summary>
/// The name of the method used to write <see cref="ExecutionContextTypeName"/>.
/// </summary>
public string WriteTagHelperAsyncMethodName { get; set; }
/// <summary>
/// The name of the method used to write <see cref="ExecutionContextTypeName"/> to a specified
/// <see cref="System.IO.TextWriter"/>.
/// </summary>
public string WriteTagHelperToAsyncMethodName { get; set; }
/// <summary>
/// The name of the property containing the <c>HtmlEncoder</c>.
/// </summary>
@ -205,5 +195,21 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
/// The name of the method used to convert a <c>TagHelperContent</c> into a <see cref="string"/>.
/// </summary>
public string TagHelperContentGetContentMethodName { get; set; }
/// <summary>
/// The name of the property used to indicate the tag helper's content has been modified.
/// </summary>
public string TagHelperOutputIsContentModifiedPropertyName { get; set; }
/// <summary>
/// The name of the property for the tag helper's output content.
/// </summary>
public string TagHelperOutputContentPropertyName { get; set; }
/// <summary>
/// The name of the method on the property <see cref="ExecutionContextOutputPropertyName"/> used to retrieve
/// tag helper child content.
/// </summary>
public string TagHelperOutputGetChildContentAsyncMethodName { get; set; }
}
}

View File

@ -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<object, object>(),
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<string, string> DictionaryCaseTestingData
{
get