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>
public class TagHelperContext
{
private readonly Func<Task<TagHelperContent>> _getChildContentAsync;
private readonly Func<bool, Task<TagHelperContent>> _getChildContentAsync;
/// <summary>
/// Instantiates a new <see cref="TagHelperContext"/>.
/// </summary>
/// <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="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>
/// <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>
public TagHelperContext(
[NotNull] IEnumerable<IReadOnlyTagHelperAttribute> allAttributes,
[NotNull] IDictionary<object, object> items,
[NotNull] string uniqueId,
[NotNull] Func<Task<TagHelperContent>> getChildContentAsync)
[NotNull] Func<bool, Task<TagHelperContent>> getChildContentAsync)
{
AllAttributes = new ReadOnlyTagHelperAttributeList<IReadOnlyTagHelperAttribute>(
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.
/// </summary>
/// <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.
/// </remarks>
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.
/// </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();
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
/// returned <see cref="Task{TagHelperContent}"/> return a cached result.
/// </remarks>
public async Task<TagHelperContent> GetChildContentAsync()
public async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult)
{
if (_childContent == null)
if (!useCachedResult || _childContent == null)
{
_startTagHelperWritingScope();
await _executeChildContentAsync();

View File

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

View File

@ -74,8 +74,8 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
endTagHelperWritingScope: () => defaultTagHelperContent);
// Act
var content1 = await executionContext.GetChildContentAsync();
var content2 = await executionContext.GetChildContentAsync();
var content1 = await executionContext.GetChildContentAsync(useCachedResult: true);
var content2 = await executionContext.GetChildContentAsync(useCachedResult: true);
// Assert
Assert.Equal(expectedContent, content1.GetContent());
@ -83,7 +83,36 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
}
[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
var defaultTagHelperContent = new DefaultTagHelperContent();
@ -97,14 +126,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
endTagHelperWritingScope: () => defaultTagHelperContent);
// Act
var content1 = await executionContext.GetChildContentAsync();
var content1 = await executionContext.GetChildContentAsync(useCachedResult);
content1.Append("Hello");
var content2 = await executionContext.GetChildContentAsync();
var content2 = await executionContext.GetChildContentAsync(useCachedResult);
content2.Append("World!");
// Assert
Assert.NotSame(content1, content2);
Assert.Empty((await executionContext.GetChildContentAsync()).GetContent());
Assert.Empty((await executionContext.GetChildContentAsync(useCachedResult)).GetContent());
}
[Fact]