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(
executionContext.AllAttributes,
executionContext.Items,
executionContext.UniqueId,
executionContext.GetChildContentAsync);
executionContext.UniqueId);
var orderedTagHelpers = executionContext.TagHelpers.OrderBy(tagHelper => tagHelper.Order);
foreach (var tagHelper in orderedTagHelpers)
{
tagHelper.Init(tagHelperContext);
}
var tagHelperOutput = new TagHelperOutput(
executionContext.TagName,
executionContext.HTMLAttributes)
executionContext.HTMLAttributes,
executionContext.GetChildContentAsync)
{
TagMode = executionContext.TagMode,
};
var orderedTagHelpers = executionContext.TagHelpers.OrderBy(tagHelper => tagHelper.Order);
foreach (var tagHelper in orderedTagHelpers)
{

View File

@ -11,11 +11,24 @@ namespace Microsoft.AspNet.Razor.TagHelpers
public interface ITagHelper
{
/// <summary>
/// Gets the execution order of this <see cref= "ITagHelper" /> relative to others targeting the same element.
/// <see cref="ITagHelper"/> instances with lower values are executed first.
/// When a set of<see cref= "ITagHelper" /> s are executed, their<see cref="Init(TagHelperContext)"/>'s
/// 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>
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>
/// Asynchronously executes the <see cref="ITagHelper"/> with the given <paramref name="context"/> and
/// <paramref name="output"/>.

View File

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

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Razor.TagHelpers
{
@ -13,8 +12,6 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary>
public class TagHelperContext
{
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
/// <summary>
/// Instantiates a new <see cref="TagHelperContext"/>.
/// </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="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(
IEnumerable<IReadOnlyTagHelperAttribute> allAttributes,
IDictionary<object, object> items,
string uniqueId,
Func<bool, Task<TagHelperContent>> getChildContentAsync)
string uniqueId)
{
if (allAttributes == null)
{
@ -45,16 +39,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers
throw new ArgumentNullException(nameof(uniqueId));
}
if (getChildContentAsync == null)
{
throw new ArgumentNullException(nameof(getChildContentAsync));
}
AllAttributes = new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>(
allAttributes.Select(attribute => new TagHelperAttribute(attribute.Name, attribute.Value)));
Items = items;
UniqueId = uniqueId;
_getChildContentAsync = getChildContentAsync;
}
/// <summary>
@ -75,26 +63,5 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// An identifier unique to the HTML element this context is for.
/// </summary>
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.
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
namespace Microsoft.AspNet.Razor.TagHelpers
@ -11,9 +12,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// </summary>
public class TagHelperOutput
{
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
// Internal for testing
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>
/// <param name="tagName">The HTML element's tag name.</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(
string tagName,
TagHelperAttributeList attributes)
TagHelperAttributeList attributes,
Func<bool, Task<TagHelperContent>> getChildContentAsync)
{
if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}
if (getChildContentAsync == null)
{
throw new ArgumentNullException(nameof(getChildContentAsync));
}
TagName = tagName;
Attributes = new TagHelperAttributeList(attributes);
_getChildContentAsync = getChildContentAsync;
}
/// <summary>
@ -116,5 +131,26 @@ namespace Microsoft.AspNet.Razor.TagHelpers
PostContent.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 void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
throw new NotImplementedException();

View File

@ -1,6 +1,7 @@
// 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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
@ -11,6 +12,35 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
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
{
get
@ -234,5 +264,29 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
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 void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
return Task.FromResult(result: true);
@ -144,6 +148,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
return Task.FromResult(result: true);
@ -154,6 +162,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
return Task.FromResult(result: true);
@ -164,6 +176,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
return Task.FromResult(result: true);
@ -177,6 +193,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
return Task.FromResult(result: true);
@ -187,6 +207,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
return Task.FromResult(result: true);
@ -197,6 +221,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{
public int Order { get { return 0; } }
public void Init(TagHelperContext context)
{
}
public Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
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)
{
throw new NotImplementedException();

View File

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

View File

@ -12,30 +12,6 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{
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]
public void Constructor_SetsProperties_AsExpected()
{
@ -49,9 +25,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
var context = new TagHelperContext(
allAttributes: Enumerable.Empty<IReadOnlyTagHelperAttribute>(),
items: expectedItems,
uniqueId: string.Empty,
getChildContentAsync: useCachedResult =>
Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
uniqueId: string.Empty);
// Assert
Assert.NotNull(context.Items);

View File

@ -1,6 +1,8 @@
// 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.
using System.Threading.Tasks;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Extensions.WebEncoders.Testing;
using Xunit;
@ -8,6 +10,29 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{
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]
public void PreElement_SetContent_ChangesValue()
{
@ -156,7 +181,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{
{ "class", "btn" },
{ "something", " spaced " }
});
},
(cachedResult) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
tagHelperOutput.PreContent.Append("Pre Content");
tagHelperOutput.Content.Append("Content");
tagHelperOutput.PostContent.Append("Post Content");
@ -188,7 +214,8 @@ namespace Microsoft.AspNet.Razor.TagHelpers
new TagHelperAttributeList
{
{ originalName, "btn" },
});
},
(cachedResult) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
// Act
tagHelperOutput.Attributes[updateName] = "super button";