Add `Init` method to `TagHelper`s.

- The init method allows multiple `TagHelper`s to inject data into the `context.Items` bag to properly function when running in unison with other `TagHelper`s that need to communicate with children.
- Transition `GetChildContentAsync` from `TagHelperContext` to `TagHelperOutput`.
- Move `TagHelperContext.GetChildContentAsync` tests to `TagHelperOutputTest`.
- Added `Init` test to ensure `TagHelperRunner` calls it in the correct order.

#571
This commit is contained in:
N. Taylor Mullen 2015-10-16 15:55:53 -07:00
parent b1ad14fd46
commit 6d0b268440
12 changed files with 193 additions and 71 deletions

View File

@ -30,15 +30,21 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
var tagHelperContext = new TagHelperContext( var tagHelperContext = new TagHelperContext(
executionContext.AllAttributes, executionContext.AllAttributes,
executionContext.Items, executionContext.Items,
executionContext.UniqueId, executionContext.UniqueId);
executionContext.GetChildContentAsync); var orderedTagHelpers = executionContext.TagHelpers.OrderBy(tagHelper => tagHelper.Order);
foreach (var tagHelper in orderedTagHelpers)
{
tagHelper.Init(tagHelperContext);
}
var tagHelperOutput = new TagHelperOutput( var tagHelperOutput = new TagHelperOutput(
executionContext.TagName, executionContext.TagName,
executionContext.HTMLAttributes) executionContext.HTMLAttributes,
executionContext.GetChildContentAsync)
{ {
TagMode = executionContext.TagMode, TagMode = executionContext.TagMode,
}; };
var orderedTagHelpers = executionContext.TagHelpers.OrderBy(tagHelper => tagHelper.Order);
foreach (var tagHelper in orderedTagHelpers) foreach (var tagHelper in orderedTagHelpers)
{ {

View File

@ -11,11 +11,24 @@ namespace Microsoft.AspNet.Razor.TagHelpers
public interface ITagHelper public interface ITagHelper
{ {
/// <summary> /// <summary>
/// Gets the execution order of this <see cref= "ITagHelper" /> relative to others targeting the same element. /// When a set of<see cref= "ITagHelper" /> s are executed, their<see cref="Init(TagHelperContext)"/>'s
/// <see cref="ITagHelper"/> instances with lower values are executed first. /// are first invoked in the specified <see cref="Order"/>; then their
/// <see cref="ProcessAsync(TagHelperContext, TagHelperOutput)"/>'s are invoked in the specified
/// <see cref="Order"/>. Lower values are executed first.
/// </summary> /// </summary>
int Order { get; } int Order { get; }
/// <summary>
/// Initializes the <see cref="ITagHelper"/> with the given <paramref name="context"/>. Additions to
/// <see cref="TagHelperContext.Items"/> should be done within this method to ensure they're added prior to
/// executing the children.
/// </summary>
/// <param name="context">Contains information associated with the current HTML tag.</param>
/// <remarks>When more than one <see cref="ITagHelper"/> runs on the same element,
/// <see cref="TagHelperOutput.GetChildContentAsync"/> may be invoked prior to <see cref="ProcessAsync"/>.
/// </remarks>
void Init(TagHelperContext context);
/// <summary> /// <summary>
/// Asynchronously executes the <see cref="ITagHelper"/> with the given <paramref name="context"/> and /// Asynchronously executes the <see cref="ITagHelper"/> with the given <paramref name="context"/> and
/// <paramref name="output"/>. /// <paramref name="output"/>.

View File

@ -14,6 +14,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <remarks>Default order is <c>0</c>.</remarks> /// <remarks>Default order is <c>0</c>.</remarks>
public virtual int Order { get; } = 0; public virtual int Order { get; } = 0;
/// <inheritdoc />
public virtual void Init(TagHelperContext context)
{
}
/// <summary> /// <summary>
/// Synchronously executes the <see cref="TagHelper"/> with the given <paramref name="context"/> and /// Synchronously executes the <see cref="TagHelper"/> with the given <paramref name="context"/> and
/// <paramref name="output"/>. /// <paramref name="output"/>.

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Razor.TagHelpers namespace Microsoft.AspNet.Razor.TagHelpers
{ {
@ -13,8 +12,6 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary> /// </summary>
public class TagHelperContext public class TagHelperContext
{ {
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
/// <summary> /// <summary>
/// Instantiates a new <see cref="TagHelperContext"/>. /// Instantiates a new <see cref="TagHelperContext"/>.
/// </summary> /// </summary>
@ -22,13 +19,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <param name="items">Collection of items used to communicate with other <see cref="ITagHelper"/>s.</param> /// <param name="items">Collection of items used to communicate with other <see cref="ITagHelper"/>s.</param>
/// <param name="uniqueId">The unique identifier for the source element this <see cref="TagHelperContext" /> /// <param name="uniqueId">The unique identifier for the source element this <see cref="TagHelperContext" />
/// applies to.</param> /// applies to.</param>
/// <param name="getChildContentAsync">A delegate used to execute and retrieve the rendered child content
/// asynchronously.</param>
public TagHelperContext( public TagHelperContext(
IEnumerable<IReadOnlyTagHelperAttribute> allAttributes, IEnumerable<IReadOnlyTagHelperAttribute> allAttributes,
IDictionary<object, object> items, IDictionary<object, object> items,
string uniqueId, string uniqueId)
Func<bool, Task<TagHelperContent>> getChildContentAsync)
{ {
if (allAttributes == null) if (allAttributes == null)
{ {
@ -45,16 +39,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
throw new ArgumentNullException(nameof(uniqueId)); throw new ArgumentNullException(nameof(uniqueId));
} }
if (getChildContentAsync == null)
{
throw new ArgumentNullException(nameof(getChildContentAsync));
}
AllAttributes = new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>( AllAttributes = new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>(
allAttributes.Select(attribute => new TagHelperAttribute(attribute.Name, attribute.Value))); allAttributes.Select(attribute => new TagHelperAttribute(attribute.Name, attribute.Value)));
Items = items; Items = items;
UniqueId = uniqueId; UniqueId = uniqueId;
_getChildContentAsync = getChildContentAsync;
} }
/// <summary> /// <summary>
@ -75,26 +63,5 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// An identifier unique to the HTML element this context is for. /// An identifier unique to the HTML element this context is for.
/// </summary> /// </summary>
public string UniqueId { get; } 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>
/// <remarks>This method is memoized.</remarks>
public Task<TagHelperContent> GetChildContentAsync()
{
return GetChildContentAsync(useCachedResult: true);
}
/// <summary>
/// A delegate used to execute and retrieve the rendered child content asynchronously.
/// </summary>
/// <param name="useCachedResult">If <c>true</c> multiple calls to this method will not cause re-execution
/// of child content; cached content will be returned.</param>
/// <returns>A <see cref="Task"/> that when executed returns content rendered by children.</returns>
public Task<TagHelperContent> GetChildContentAsync(bool useCachedResult)
{
return _getChildContentAsync(useCachedResult);
}
} }
} }

View File

@ -2,6 +2,7 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.AspNet.Razor.TagHelpers namespace Microsoft.AspNet.Razor.TagHelpers
@ -11,9 +12,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary> /// </summary>
public class TagHelperOutput public class TagHelperOutput
{ {
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
// Internal for testing // Internal for testing
internal TagHelperOutput(string tagName) internal TagHelperOutput(string tagName)
: this(tagName, new TagHelperAttributeList()) : this(
tagName,
new TagHelperAttributeList(),
(cachedResult) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()))
{ {
} }
@ -22,17 +28,26 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary> /// </summary>
/// <param name="tagName">The HTML element's tag name.</param> /// <param name="tagName">The HTML element's tag name.</param>
/// <param name="attributes">The HTML attributes.</param> /// <param name="attributes">The HTML attributes.</param>
/// <param name="getChildContentAsync">A delegate used to execute and retrieve the rendered child content
/// asynchronously.</param>
public TagHelperOutput( public TagHelperOutput(
string tagName, string tagName,
TagHelperAttributeList attributes) TagHelperAttributeList attributes,
Func<bool, Task<TagHelperContent>> getChildContentAsync)
{ {
if (attributes == null) if (attributes == null)
{ {
throw new ArgumentNullException(nameof(attributes)); throw new ArgumentNullException(nameof(attributes));
} }
if (getChildContentAsync == null)
{
throw new ArgumentNullException(nameof(getChildContentAsync));
}
TagName = tagName; TagName = tagName;
Attributes = new TagHelperAttributeList(attributes); Attributes = new TagHelperAttributeList(attributes);
_getChildContentAsync = getChildContentAsync;
} }
/// <summary> /// <summary>
@ -116,5 +131,26 @@ namespace Microsoft.AspNet.Razor.TagHelpers
PostContent.Clear(); PostContent.Clear();
PostElement.Clear(); PostElement.Clear();
} }
/// <summary>
/// A delegate used to execute children asynchronously.
/// </summary>
/// <returns>A <see cref="Task"/> that on completion returns content rendered by children.</returns>
/// <remarks>This method is memoized.</remarks>
public Task<TagHelperContent> GetChildContentAsync()
{
return GetChildContentAsync(useCachedResult: true);
}
/// <summary>
/// A delegate used to execute children asynchronously.
/// </summary>
/// <param name="useCachedResult">If <c>true</c> multiple calls to this method will not cause re-execution
/// of child content; cached content will be returned.</param>
/// <returns>A <see cref="Task"/> that on completion returns content rendered by children.</returns>
public Task<TagHelperContent> GetChildContentAsync(bool useCachedResult)
{
return _getChildContentAsync(useCachedResult);
}
} }
} }

View File

@ -394,6 +394,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get; } = 0; public int Order { get; } = 0;
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// 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.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -11,6 +12,35 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public class TagHelperRunnerTest public class TagHelperRunnerTest
{ {
[Fact]
public async Task RunAsync_CallsInitPriorToProcessAsync()
{
// Arrange
var runner = new TagHelperRunner();
var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag);
var incrementer = 0;
var callbackTagHelper = new CallbackTagHelper(
initCallback: () =>
{
Assert.Equal(0, incrementer);
incrementer++;
},
processAsyncCallback: () =>
{
Assert.Equal(1, incrementer);
incrementer++;
});
executionContext.Add(callbackTagHelper);
// Act
await runner.RunAsync(executionContext);
// Assert
Assert.Equal(2, incrementer);
}
public static TheoryData TagHelperOrderData public static TheoryData TagHelperOrderData
{ {
get get
@ -234,5 +264,29 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
ProcessOrderTracker.Add(Order); ProcessOrderTracker.Add(Order);
} }
} }
private class CallbackTagHelper : TagHelper
{
private readonly Action _initCallback;
private readonly Action _processAsyncCallback;
public CallbackTagHelper(Action initCallback, Action processAsyncCallback)
{
_initCallback = initCallback;
_processAsyncCallback = processAsyncCallback;
}
public override void Init(TagHelperContext context)
{
_initCallback();
}
public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
_processAsyncCallback();
return base.ProcessAsync(context, output);
}
}
} }
} }

View File

@ -134,6 +134,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get { return 0; } } public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
return Task.FromResult(result: true); return Task.FromResult(result: true);
@ -144,6 +148,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get { return 0; } } public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
return Task.FromResult(result: true); return Task.FromResult(result: true);
@ -154,6 +162,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get { return 0; } } public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
return Task.FromResult(result: true); return Task.FromResult(result: true);
@ -164,6 +176,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get { return 0; } } public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
return Task.FromResult(result: true); return Task.FromResult(result: true);
@ -177,6 +193,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get { return 0; } } public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
return Task.FromResult(result: true); return Task.FromResult(result: true);
@ -187,6 +207,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get { return 0; } } public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
return Task.FromResult(result: true); return Task.FromResult(result: true);
@ -197,6 +221,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
public int Order { get { return 0; } } public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
return Task.FromResult(result: true); return Task.FromResult(result: true);

View File

@ -17,6 +17,10 @@ namespace Microsoft.AspNet.Razor.Fake
} }
} }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View File

@ -415,6 +415,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{ {
public int Order { get; } = 0; public int Order { get; } = 0;
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View File

@ -12,30 +12,6 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{ {
public class TagHelperContextTest public class TagHelperContextTest
{ {
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task GetChildContentAsync_PassesUseCachedResultAsExpected(bool expectedUseCachedResultValue)
{
// Arrange
bool? useCachedResultValue = null;
var context = new TagHelperContext(
allAttributes: Enumerable.Empty<IReadOnlyTagHelperAttribute>(),
items: new Dictionary<object, object>(),
uniqueId: string.Empty,
getChildContentAsync: useCachedResult =>
{
useCachedResultValue = useCachedResult;
return Task.FromResult<TagHelperContent>(new DefaultTagHelperContent());
});
// Act
await context.GetChildContentAsync(expectedUseCachedResultValue);
// Assert
Assert.Equal(expectedUseCachedResultValue, useCachedResultValue);
}
[Fact] [Fact]
public void Constructor_SetsProperties_AsExpected() public void Constructor_SetsProperties_AsExpected()
{ {
@ -49,9 +25,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var context = new TagHelperContext( var context = new TagHelperContext(
allAttributes: Enumerable.Empty<IReadOnlyTagHelperAttribute>(), allAttributes: Enumerable.Empty<IReadOnlyTagHelperAttribute>(),
items: expectedItems, items: expectedItems,
uniqueId: string.Empty, uniqueId: string.Empty);
getChildContentAsync: useCachedResult =>
Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Assert // Assert
Assert.NotNull(context.Items); Assert.NotNull(context.Items);

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// 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.Threading.Tasks;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Extensions.WebEncoders.Testing; using Microsoft.Extensions.WebEncoders.Testing;
using Xunit; using Xunit;
@ -8,6 +10,29 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{ {
public class TagHelperOutputTest public class TagHelperOutputTest
{ {
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task GetChildContentAsync_PassesUseCachedResultAsExpected(bool expectedUseCachedResultValue)
{
// Arrange
bool? useCachedResultValue = null;
var output = new TagHelperOutput(
tagName: "p",
attributes: new TagHelperAttributeList(),
getChildContentAsync: useCachedResult =>
{
useCachedResultValue = useCachedResult;
return Task.FromResult<TagHelperContent>(new DefaultTagHelperContent());
});
// Act
await output.GetChildContentAsync(expectedUseCachedResultValue);
// Assert
Assert.Equal(expectedUseCachedResultValue, useCachedResultValue);
}
[Fact] [Fact]
public void PreElement_SetContent_ChangesValue() public void PreElement_SetContent_ChangesValue()
{ {
@ -156,7 +181,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{ {
{ "class", "btn" }, { "class", "btn" },
{ "something", " spaced " } { "something", " spaced " }
}); },
(cachedResult) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
tagHelperOutput.PreContent.Append("Pre Content"); tagHelperOutput.PreContent.Append("Pre Content");
tagHelperOutput.Content.Append("Content"); tagHelperOutput.Content.Append("Content");
tagHelperOutput.PostContent.Append("Post Content"); tagHelperOutput.PostContent.Append("Post Content");
@ -188,7 +214,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeList new TagHelperAttributeList
{ {
{ originalName, "btn" }, { originalName, "btn" },
}); },
(cachedResult) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act // Act
tagHelperOutput.Attributes[updateName] = "super button"; tagHelperOutput.Attributes[updateName] = "super button";