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:
parent
36c744ff29
commit
bdf869c3d5
|
|
@ -223,15 +223,6 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
||||||
AllAttributes.Add(name, value);
|
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>
|
/// <summary>
|
||||||
/// Execute and retrieve the rendered child content asynchronously.
|
/// Execute and retrieve the rendered child content asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,17 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Html.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Razor.TagHelpers
|
namespace Microsoft.AspNet.Razor.TagHelpers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class used to represent the output of an <see cref="ITagHelper"/>.
|
/// Class used to represent the output of an <see cref="ITagHelper"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TagHelperOutput
|
public class TagHelperOutput : IHtmlContent
|
||||||
{
|
{
|
||||||
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
|
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
|
||||||
|
|
||||||
|
|
@ -151,5 +154,85 @@ namespace Microsoft.AspNet.Razor.TagHelpers
|
||||||
{
|
{
|
||||||
return _getChildContentAsync(useCachedResult);
|
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("\"", """);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
|
||||||
if (!_designTimeMode)
|
if (!_designTimeMode)
|
||||||
{
|
{
|
||||||
RenderRunTagHelpers();
|
RenderRunTagHelpers();
|
||||||
RenderWriteTagHelperMethodCall(chunk);
|
RenderTagHelperOutput(chunk);
|
||||||
RenderEndTagHelpersScope();
|
RenderEndTagHelpersScope();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -533,26 +533,50 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
|
||||||
_tagHelperContext.ScopeManagerEndMethodName);
|
_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
|
_writer
|
||||||
.WriteStartInstrumentationContext(_context, chunk.Association, isLiteral: false)
|
.WriteStartInstrumentationContext(_context, chunk.Association, isLiteral: false);
|
||||||
.Write("await ");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_context.TargetWriterName))
|
if (!string.IsNullOrEmpty(_context.TargetWriterName))
|
||||||
{
|
{
|
||||||
_writer
|
_writer
|
||||||
.WriteStartMethodInvocation(_tagHelperContext.WriteTagHelperToAsyncMethodName)
|
.WriteStartMethodInvocation(_context.Host.GeneratedClassContext.WriteToMethodName)
|
||||||
.Write(_context.TargetWriterName)
|
.Write(_context.TargetWriterName)
|
||||||
.WriteParameterSeparator();
|
.WriteParameterSeparator();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_writer.WriteStartMethodInvocation(_tagHelperContext.WriteTagHelperAsyncMethodName);
|
_writer.WriteStartMethodInvocation(_context.Host.GeneratedClassContext.WriteMethodName);
|
||||||
}
|
}
|
||||||
|
|
||||||
_writer
|
_writer
|
||||||
.Write(ExecutionContextVariableName)
|
.Write(tagHelperOutputAccessor)
|
||||||
.WriteEndMethodInvocation()
|
.WriteEndMethodInvocation()
|
||||||
.WriteEndInstrumentationContext(_context);
|
.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)
|
private static bool IsDynamicAttributeValue(Chunk attributeValueChunk)
|
||||||
{
|
{
|
||||||
var parentChunk = attributeValueChunk as ParentChunk;
|
var parentChunk = attributeValueChunk as ParentChunk;
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,11 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
|
||||||
ScopeManagerTypeName = "Microsoft.AspNet.Razor.Runtime.TagHelperScopeManager";
|
ScopeManagerTypeName = "Microsoft.AspNet.Razor.Runtime.TagHelperScopeManager";
|
||||||
ExecutionContextTypeName = "Microsoft.AspNet.Razor.Runtime.TagHelperExecutionContext";
|
ExecutionContextTypeName = "Microsoft.AspNet.Razor.Runtime.TagHelperExecutionContext";
|
||||||
TagHelperContentTypeName = "Microsoft.AspNet.Razor.TagHelperContent";
|
TagHelperContentTypeName = "Microsoft.AspNet.Razor.TagHelperContent";
|
||||||
WriteTagHelperAsyncMethodName = "WriteTagHelperAsync";
|
|
||||||
WriteTagHelperToAsyncMethodName = "WriteTagHelperToAsync";
|
|
||||||
TagHelperContentGetContentMethodName = "GetContent";
|
TagHelperContentGetContentMethodName = "GetContent";
|
||||||
HtmlEncoderPropertyName = "HtmlEncoder";
|
HtmlEncoderPropertyName = "HtmlEncoder";
|
||||||
|
TagHelperOutputIsContentModifiedPropertyName = "IsContentModified";
|
||||||
|
TagHelperOutputContentPropertyName = "Content";
|
||||||
|
TagHelperOutputGetChildContentAsyncMethodName = "GetChildContentAsync";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -124,7 +125,7 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
|
||||||
public string ExecutionContextAddMethodName { get; set; }
|
public string ExecutionContextAddMethodName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The property accessor for the tag helper's output.
|
/// The property name for the tag helper's output.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExecutionContextOutputPropertyName { get; set; }
|
public string ExecutionContextOutputPropertyName { get; set; }
|
||||||
|
|
||||||
|
|
@ -185,17 +186,6 @@ namespace Microsoft.AspNet.Razor.CodeGenerators
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public string TagHelperContentTypeName { get; set; }
|
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>
|
/// <summary>
|
||||||
/// The name of the property containing the <c>HtmlEncoder</c>.
|
/// The name of the property containing the <c>HtmlEncoder</c>.
|
||||||
/// </summary>
|
/// </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"/>.
|
/// The name of the method used to convert a <c>TagHelperContent</c> into a <see cref="string"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string TagHelperContentGetContentMethodName { get; set; }
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,34 +143,6 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
|
||||||
Assert.Empty(content3.GetContent(new HtmlTestEncoder()));
|
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
|
public static TheoryData<string, string> DictionaryCaseTestingData
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue