Add ability to execute child content more than once.

- Added a boolean overload that specifies whether the user wants to retrieve cached content.
- Added tests to validate `TagHelperExecutionContext` `GetChildContentAsync` and that `TagHelperContext` passes the appropriate values through.

#459
This commit is contained in:
N. Taylor Mullen 2015-07-30 15:08:08 -07:00
parent 6a285ce70a
commit a8fd85db1e
4 changed files with 81 additions and 15 deletions

View File

@ -14,22 +14,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// </summary> /// </summary>
public class TagHelperContext public class TagHelperContext
{ {
private readonly Func<Task<TagHelperContent>> _getChildContentAsync; private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
/// <summary> /// <summary>
/// Instantiates a new <see cref="TagHelperContext"/>. /// Instantiates a new <see cref="TagHelperContext"/>.
/// </summary> /// </summary>
/// <param name="allAttributes">Every attribute associated with the current HTML element.</param> /// <param name="allAttributes">Every attribute associated with the current HTML element.</param>
/// <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 /// <param name="getChildContentAsync">A delegate used to execute and retrieve the rendered child content
/// asynchronously.</param> /// asynchronously.</param>
public TagHelperContext( public TagHelperContext(
[NotNull] IEnumerable<IReadOnlyTagHelperAttribute> allAttributes, [NotNull] IEnumerable<IReadOnlyTagHelperAttribute> allAttributes,
[NotNull] IDictionary<object, object> items, [NotNull] IDictionary<object, object> items,
[NotNull] string uniqueId, [NotNull] string uniqueId,
[NotNull] Func<Task<TagHelperContent>> getChildContentAsync) [NotNull] Func<bool, Task<TagHelperContent>> 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)));
@ -47,7 +47,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// Gets the collection of items used to communicate with other <see cref="ITagHelper"/>s. /// Gets the collection of items used to communicate with other <see cref="ITagHelper"/>s.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This <see cref="IDictionary{object, object}"/> is copy-on-write in order to ensure items added to this /// This <see cref="IDictionary{object, object}"/> is copy-on-write in order to ensure items added to this
/// collection are visible only to other <see cref="ITagHelper"/>s targeting child elements. /// collection are visible only to other <see cref="ITagHelper"/>s targeting child elements.
/// </remarks> /// </remarks>
public IDictionary<object, object> Items { get; } public IDictionary<object, object> Items { get; }
@ -61,9 +61,21 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// A delegate used to execute and retrieve the rendered child content asynchronously. /// A delegate used to execute and retrieve the rendered child content asynchronously.
/// </summary> /// </summary>
/// <returns>A <see cref="Task"/> that when executed returns content rendered by children.</returns> /// <returns>A <see cref="Task"/> that when executed returns content rendered by children.</returns>
/// <remarks>This method is memoized.</remarks>
public Task<TagHelperContent> GetChildContentAsync() public Task<TagHelperContent> GetChildContentAsync()
{ {
return _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

@ -191,9 +191,9 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// Child content is only executed once. Successive calls to this method or successive executions of the /// Child content is only executed once. Successive calls to this method or successive executions of the
/// returned <see cref="Task{TagHelperContent}"/> return a cached result. /// returned <see cref="Task{TagHelperContent}"/> return a cached result.
/// </remarks> /// </remarks>
public async Task<TagHelperContent> GetChildContentAsync() public async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult)
{ {
if (_childContent == null) if (!useCachedResult || _childContent == null)
{ {
_startTagHelperWritingScope(); _startTagHelperWritingScope();
await _executeChildContentAsync(); await _executeChildContentAsync();

View File

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

View File

@ -74,8 +74,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
endTagHelperWritingScope: () => defaultTagHelperContent); endTagHelperWritingScope: () => defaultTagHelperContent);
// Act // Act
var content1 = await executionContext.GetChildContentAsync(); var content1 = await executionContext.GetChildContentAsync(useCachedResult: true);
var content2 = await executionContext.GetChildContentAsync(); var content2 = await executionContext.GetChildContentAsync(useCachedResult: true);
// Assert // Assert
Assert.Equal(expectedContent, content1.GetContent()); Assert.Equal(expectedContent, content1.GetContent());
@ -83,7 +83,36 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
} }
[Fact] [Fact]
public async Task GetChildContentAsync_ReturnsNewObjectEveryTimeItIsCalled() public async Task GetChildContentAsync_CanExecuteChildrenMoreThanOnce()
{
// Arrange
var executionCount = 0;
var executionContext = new TagHelperExecutionContext(
"p",
selfClosing: false,
items: null,
uniqueId: string.Empty,
executeChildContentAsync: () =>
{
executionCount++;
return Task.FromResult(result: true);
},
startTagHelperWritingScope: () => { },
endTagHelperWritingScope: () => new DefaultTagHelperContent());
// Act
await executionContext.GetChildContentAsync(useCachedResult: false);
await executionContext.GetChildContentAsync(useCachedResult: false);
// Assert
Assert.Equal(2, executionCount);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task GetChildContentAsync_ReturnsNewObjectEveryTimeItIsCalled(bool useCachedResult)
{ {
// Arrange // Arrange
var defaultTagHelperContent = new DefaultTagHelperContent(); var defaultTagHelperContent = new DefaultTagHelperContent();
@ -97,14 +126,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
endTagHelperWritingScope: () => defaultTagHelperContent); endTagHelperWritingScope: () => defaultTagHelperContent);
// Act // Act
var content1 = await executionContext.GetChildContentAsync(); var content1 = await executionContext.GetChildContentAsync(useCachedResult);
content1.Append("Hello"); content1.Append("Hello");
var content2 = await executionContext.GetChildContentAsync(); var content2 = await executionContext.GetChildContentAsync(useCachedResult);
content2.Append("World!"); content2.Append("World!");
// Assert // Assert
Assert.NotSame(content1, content2); Assert.NotSame(content1, content2);
Assert.Empty((await executionContext.GetChildContentAsync()).GetContent()); Assert.Empty((await executionContext.GetChildContentAsync(useCachedResult)).GetContent());
} }
[Fact] [Fact]