Re-design TagHelperOutput and runtime dependencies to allow all content modes.

- Added PreContent, PostContent and ContentSet properties to TagHelperOutput.
- Added GeneratePreContent, GeneratePostContent and SupressOutput methods to TagHelperOutput.
- Added multile ExecuteChildContentAsync and GetChildContentAsync to the rendering phase, ultimately only exposing GetChildContentAsync to a TagHelper author.
- Added more knowledge of StartWritingScope and EndWritingScope to the TagHelper runtime components. This is to enable the runtime components to utilize the RazorPage's infrastructure to render a delegate to a writer and retrieve its value to ultimately expose it to the user.

#221
This commit is contained in:
N. Taylor Mullen 2014-12-01 15:39:29 -08:00 committed by NTaylorMullen
parent c38761f504
commit 1ef8c088d1
6 changed files with 214 additions and 63 deletions

View File

@ -1,7 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Threading.Tasks;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
@ -10,25 +12,42 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
public class TagHelperContext
{
private readonly Func<Task<string>> _getChildContentAsync;
/// <summary>
/// Instantiates a new <see cref="TagHelperContext"/>.
/// </summary>
/// <param name="allAttributes">Every attribute associated with the current HTML element.</param>
/// <param name="uniqueId">The unique identifier for the source element this <see cref="TagHelperContext" /> applies to.</param>
public TagHelperContext([NotNull] IDictionary<string, object> allAttributes, [NotNull] string uniqueId)
/// <param name="uniqueId">The unique identifier for the source element this <see cref="TagHelperContext" />
/// applies to.</param>
/// <param name="getChildContentAsync">A delegate used to execute and retrieve the rendered child content
/// asynchronously.</param>
public TagHelperContext([NotNull] IDictionary<string, object> allAttributes,
[NotNull] string uniqueId,
[NotNull] Func<Task<string>> getChildContentAsync)
{
AllAttributes = allAttributes;
UniqueId = uniqueId;
_getChildContentAsync = getChildContentAsync;
}
/// <summary>
/// Every attribute associated with the current HTML element.
/// </summary>
public IDictionary<string, object> AllAttributes { get; private set; }
public IDictionary<string, object> AllAttributes { get; }
/// <summary>
/// An identifier unique to the HTML element this context is for.
/// </summary>
public string UniqueId { get; private set; }
public string UniqueId { get; }
/// <summary>
/// A delegate used to execute and retrieve the rendered child content asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> that when executed returns content rendered by children.</returns>
public Task<string> GetChildContentAsync()
{
return _getChildContentAsync();
}
}
}

View File

@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
@ -12,43 +14,61 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
public class TagHelperExecutionContext
{
private readonly List<ITagHelper> _tagHelpers;
private readonly Func<Task> _executeChildContentAsync;
private readonly Action _startWritingScope;
private readonly Func<TextWriter> _endWritingScope;
private string _childContent;
/// <summary>
/// Instantiates a new <see cref="TagHelperExecutionContext"/>.
/// </summary>
/// <param name="tagName">The HTML tag name in the Razor source.</param>
public TagHelperExecutionContext([NotNull] string tagName, [NotNull] string uniqueId)
/// <param name="uniqueId">An identifier unique to the HTML element this context is for.</param>
/// <param name="executeChildContentAsync">A delegate used to execute the child content asynchronously.</param>
/// <param name="startWritingScope">A delegate used to start a writing scope in a Razor page.</param>
/// <param name="endWritingScope">A delegate used to end a writing scope in a Razor page.</param>
public TagHelperExecutionContext([NotNull] string tagName,
[NotNull] string uniqueId,
[NotNull] Func<Task> executeChildContentAsync,
[NotNull] Action startWritingScope,
[NotNull] Func<TextWriter> endWritingScope)
{
_tagHelpers = new List<ITagHelper>();
_executeChildContentAsync = executeChildContentAsync;
_startWritingScope = startWritingScope;
_endWritingScope = endWritingScope;
AllAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
HTMLAttributes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
_tagHelpers = new List<ITagHelper>();
TagName = tagName;
UniqueId = uniqueId;
}
/// <summary>
/// Internal for testing purposes only.
/// Indicates if <see cref="GetChildContentAsync"/> has been called.
/// </summary>
internal TagHelperExecutionContext([NotNull] string tagName)
: this(tagName, string.Empty)
public bool ChildContentRetrieved
{
get
{
return _childContent != null;
}
}
/// <summary>
/// HTML attributes.
/// </summary>
public IDictionary<string, string> HTMLAttributes { get; private set; }
public IDictionary<string, string> HTMLAttributes { get; }
/// <summary>
/// <see cref="ITagHelper"/> bound attributes and HTML attributes.
/// </summary>
public IDictionary<string, object> AllAttributes { get; private set; }
public IDictionary<string, object> AllAttributes { get; }
/// <summary>
/// An identifier unique to the HTML element this context is for.
/// </summary>
public string UniqueId { get; private set; }
public string UniqueId { get; }
/// <summary>
/// <see cref="ITagHelper"/>s that should be run.
@ -64,7 +84,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <summary>
/// The HTML tag name in the Razor source.
/// </summary>
public string TagName { get; private set; }
public string TagName { get; }
/// <summary>
/// The <see cref="ITagHelper"/>s' output.
@ -100,5 +120,34 @@ 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>
/// <returns>A <see cref="Task"/> that on completion returns the rendered child content.</returns>
/// <remarks>
/// Child content is only executed once. Successive calls to this method or successive executions of the
/// returned <see cref="Task{string}"/> return a cached result.
/// </remarks>
public async Task<string> GetChildContentAsync()
{
if (_childContent == null)
{
_startWritingScope();
await _executeChildContentAsync();
_childContent = _endWritingScope().ToString();
}
return _childContent;
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
public class TagHelperOutput
{
private string _content;
private string _tagName;
private bool _contentSet;
// Internal for testing
internal TagHelperOutput(string tagName)
@ -35,37 +35,34 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
/// <param name="tagName">The HTML element's tag name.</param>
/// <param name="attributes">The HTML attributes.</param>
/// <param name="content">The HTML element's content.</param>
public TagHelperOutput(string tagName,
[NotNull] IDictionary<string, string> attributes,
string content)
public TagHelperOutput(string tagName, [NotNull] IDictionary<string, string> attributes)
{
TagName = tagName;
Content = content;
Attributes = new Dictionary<string, string>(attributes, StringComparer.OrdinalIgnoreCase);
PreContent = string.Empty;
_content = string.Empty;
PostContent = string.Empty;
}
/// <summary>
/// The HTML element's tag name.
/// </summary>
/// <remarks>
/// A whitespace value results in no start or end tag being rendered.
/// A whitespace or <c>null</c> value results in no start or end tag being rendered.
/// </remarks>
public string TagName
{
get
{
return _tagName;
}
set
{
_tagName = value ?? string.Empty;
}
}
public string TagName { get; set; }
/// <summary>
/// The HTML element's content.
/// The HTML element's pre content.
/// </summary>
/// <remarks>Value is prepended to the <see cref="ITagHelper"/>'s final output.</remarks>
public string PreContent { get; set; }
/// <summary>
/// The HTML element's main content.
/// </summary>
/// <remarks>Value occurs in the <see cref="ITagHelper"/>'s final output after <see cref="PreContent"/> and
/// before <see cref="PostContent"/></remarks>
public string Content
{
get
@ -74,7 +71,25 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
}
set
{
_content = value ?? string.Empty;
_contentSet = true;
_content = value;
}
}
/// <summary>
/// The HTML element's post content.
/// </summary>
/// <remarks>Value is appended to the <see cref="ITagHelper"/>'s final output.</remarks>
public string PostContent { get; set; }
/// <summary>
/// <c>true</c> if <see cref="Content"/> has been set, <c>false</c> otherwise.
/// </summary>
public bool ContentSet
{
get
{
return _contentSet;
}
}
@ -86,7 +101,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <summary>
/// The HTML element's attributes.
/// </summary>
public IDictionary<string, string> Attributes { get; private set; }
public IDictionary<string, string> Attributes { get; }
/// <summary>
/// Generates the <see cref="TagHelperOutput"/>'s start tag.
@ -126,6 +141,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
return sb.ToString();
}
/// <summary>
/// Generates the <see cref="TagHelperOutput"/>'s <see cref="PreContent"/>.
/// </summary>
/// <returns><c>string.Empty</c> if <see cref="SelfClosing"/> is <c>true</c>. <see cref="PreContent"/>
/// otherwise.
/// </returns>
public string GeneratePreContent()
{
if (SelfClosing)
{
return string.Empty;
}
return PreContent;
}
/// <summary>
/// Generates the <see cref="TagHelperOutput"/>'s body.
/// </summary>
@ -141,6 +172,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
return Content;
}
/// <summary>
/// Generates the <see cref="TagHelperOutput"/>'s <see cref="PostContent"/>.
/// </summary>
/// <returns><c>string.Empty</c> if <see cref="SelfClosing"/> is <c>true</c>. <see cref="PostContent"/>
/// otherwise.
/// </returns>
public string GeneratePostContent()
{
if (SelfClosing)
{
return string.Empty;
}
return PostContent;
}
/// <summary>
/// Generates the <see cref="TagHelperOutput"/>'s end tag.
/// </summary>
@ -155,5 +202,20 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
return string.Format(CultureInfo.InvariantCulture, "</{0}>", TagName);
}
/// <summary>
/// Changes <see cref="TagHelperOutput"/> to generate nothing.
/// </summary>
/// <remarks>
/// Sets <see cref="TagName"/>, <see cref="PreContent"/>, <see cref="Content"/>, and <see cref="PostContent"/>
/// to <c>null</c> to suppress output.
/// </remarks>
public void SuppressOutput()
{
TagName = null;
PreContent = null;
Content = null;
PostContent = null;
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
@ -14,33 +13,17 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <summary>
/// Calls the <see cref="ITagHelper.ProcessAsync"/> method on <see cref="ITagHelper"/>s.
/// </summary>
/// <param name="context">Contains information associated with running <see cref="ITagHelper"/>s.</param>
/// <param name="executionContext">Contains information associated with running <see cref="ITagHelper"/>s.
/// </param>
/// <returns>Resulting <see cref="TagHelperOutput"/> from processing all of the
/// <paramref name="context"/>'s <see cref="ITagHelper"/>s.</returns>
public async Task<TagHelperOutput> RunAsync([NotNull] TagHelperExecutionContext context)
/// <paramref name="executionContext"/>'s <see cref="ITagHelper"/>s.</returns>
public async Task<TagHelperOutput> RunAsync([NotNull] TagHelperExecutionContext executionContext)
{
return await RunAsyncCore(context, string.Empty);
}
/// <summary>
/// Calls the <see cref="ITagHelper.ProcessAsync"/> method on <see cref="ITagHelper"/>s.
/// </summary>
/// <param name="context">Contains information associated with running <see cref="ITagHelper"/>s.</param>
/// <param name="bufferedBody">Contains the buffered content of the current HTML tag.</param>
/// <returns>Resulting <see cref="TagHelperOutput"/> from processing all of the
/// <paramref name="context"/>'s <see cref="ITagHelper"/>s.</returns>
public async Task<TagHelperOutput> RunAsync([NotNull] TagHelperExecutionContext context,
[NotNull] TextWriter bufferedBody)
{
return await RunAsyncCore(context, bufferedBody.ToString());
}
private async Task<TagHelperOutput> RunAsyncCore(TagHelperExecutionContext executionContext, string outputContent)
{
var tagHelperContext = new TagHelperContext(executionContext.AllAttributes, executionContext.UniqueId);
var tagHelperOutput = new TagHelperOutput(executionContext.TagName,
executionContext.HTMLAttributes,
outputContent);
var tagHelperContext = new TagHelperContext(
executionContext.AllAttributes,
executionContext.UniqueId,
executionContext.GetChildContentAsync);
var tagHelperOutput = new TagHelperOutput(executionContext.TagName, executionContext.HTMLAttributes);
foreach (var tagHelper in executionContext.TagHelpers)
{

View File

@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
@ -26,10 +28,21 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary>
/// <param name="tagName">The HTML tag name that the scope is associated with.</param>
/// <param name="uniqueId">An identifier unique to the HTML element this scope is for.</param>
/// <param name="executeChildContentAsync">A delegate used to execute the child content asynchronously.</param>
/// <param name="startWritingScope">A delegate used to start a writing scope in a Razor page.</param>
/// <param name="endWritingScope">A delegate used to end a writing scope in a Razor page.</param>
/// <returns>A <see cref="TagHelperExecutionContext"/> to use.</returns>
public TagHelperExecutionContext Begin(string tagName, string uniqueId)
public TagHelperExecutionContext Begin([NotNull] string tagName,
[NotNull] string uniqueId,
[NotNull] Func<Task> executeChildContentAsync,
[NotNull] Action startWritingScope,
[NotNull] Func<TextWriter> endWritingScope)
{
var executionContext = new TagHelperExecutionContext(tagName, uniqueId);
var executionContext = new TagHelperExecutionContext(tagName,
uniqueId,
executeChildContentAsync,
startWritingScope,
endWritingScope);
_executionScopes.Push(executionContext);

View File

@ -17,9 +17,13 @@ namespace Microsoft.AspNet.Razor.Generator
RunnerRunAsyncMethodName = "RunAsync";
ScopeManagerBeginMethodName = "Begin";
ScopeManagerEndMethodName = "End";
OutputContentSetPropertyName = "ContentSet";
OutputGenerateStartTagMethodName = "GenerateStartTag";
OutputGeneratePreContentMethodName = "GeneratePreContent";
OutputGenerateContentMethodName = "GenerateContent";
OutputGeneratePostContentMethodName = "GeneratePostContent";
OutputGenerateEndTagMethodName = "GenerateEndTag";
ExecutionContextExecuteChildContentAsyncMethodName = "ExecuteChildContentAsync";
ExecutionContextAddMethodName = "Add";
ExecutionContextAddTagHelperAttributeMethodName = "AddTagHelperAttribute";
ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute";
@ -51,21 +55,42 @@ namespace Microsoft.AspNet.Razor.Generator
/// </summary>
public string ScopeManagerEndMethodName { get; set; }
/// <summary>
/// The name of the property used to determine if a tag helper output's content was set.
/// </summary>
public string OutputContentSetPropertyName { get; set; }
/// <summary>
/// The name of the method used to generate a tag helper output's start tag.
/// </summary>
public string OutputGenerateStartTagMethodName { get; set; }
/// <summary>
/// The name of the method used to generate a tag helper output's pre content.
/// </summary>
public string OutputGeneratePreContentMethodName { get; set; }
/// <summary>
/// The name of the method used to generate a tag helper output's content.
/// </summary>
public string OutputGenerateContentMethodName { get; set; }
/// <summary>
/// The name of the method used to generate a tag helper output's post-content.
/// </summary>
public string OutputGeneratePostContentMethodName { get; set; }
/// <summary>
/// The name of the method used to generate a tag helper output's end tag.
/// </summary>
public string OutputGenerateEndTagMethodName { get; set; }
/// <summary>
/// The name of the <see cref="ExecutionContextTypeName"/> method used to get a
/// <see cref="System.Threading.Tasks.Task"/> that executes tag helper child content.
/// </summary>
public string ExecutionContextExecuteChildContentAsyncMethodName { get; set; }
/// <summary>
/// The name of the <see cref="ExecutionContextTypeName"/> method used to add tag helper attributes.
/// </summary>