From a8fd85db1e25444b5175e682852653f87cf02592 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Thu, 30 Jul 2015 15:08:08 -0700 Subject: [PATCH] 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 --- .../TagHelpers/TagHelperContext.cs | 24 ++++++++--- .../TagHelpers/TagHelperExecutionContext.cs | 4 +- .../TagHelpers/TagHelperContextTest.cs | 27 +++++++++++- .../TagHelperExecutionContextTest.cs | 41 ++++++++++++++++--- 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs index 38e18a0e1b..3ee7ba85fc 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs @@ -14,22 +14,22 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// public class TagHelperContext { - private readonly Func> _getChildContentAsync; + private readonly Func> _getChildContentAsync; /// /// Instantiates a new . /// /// Every attribute associated with the current HTML element. /// Collection of items used to communicate with other s. - /// The unique identifier for the source element this + /// The unique identifier for the source element this /// applies to. - /// A delegate used to execute and retrieve the rendered child content + /// A delegate used to execute and retrieve the rendered child content /// asynchronously. public TagHelperContext( [NotNull] IEnumerable allAttributes, [NotNull] IDictionary items, [NotNull] string uniqueId, - [NotNull] Func> getChildContentAsync) + [NotNull] Func> getChildContentAsync) { AllAttributes = new ReadOnlyTagHelperAttributeList( 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 s. /// /// - /// This is copy-on-write in order to ensure items added to this + /// This is copy-on-write in order to ensure items added to this /// collection are visible only to other s targeting child elements. /// public IDictionary 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 that when executed returns content rendered by children. + /// This method is memoized. public Task GetChildContentAsync() { - return _getChildContentAsync(); + return GetChildContentAsync(useCachedResult: true); + } + + /// + /// A delegate used to execute and retrieve the rendered child content asynchronously. + /// + /// If true multiple calls to this method will not cause re-execution + /// of child content; cached content will be returned. + /// A that when executed returns content rendered by children. + public Task GetChildContentAsync(bool useCachedResult) + { + return _getChildContentAsync(useCachedResult); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs index 2fc571a4af..114ed5885e 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -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 return a cached result. /// - public async Task GetChildContentAsync() + public async Task GetChildContentAsync(bool useCachedResult) { - if (_childContent == null) + if (!useCachedResult || _childContent == null) { _startTagHelperWritingScope(); await _executeChildContentAsync(); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs index 239107fdf2..378789b33d 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs @@ -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(), + items: new Dictionary(), + uniqueId: string.Empty, + getChildContentAsync: useCachedResult => + { + useCachedResultValue = useCachedResult; + return Task.FromResult(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(), items: expectedItems, uniqueId: string.Empty, - getChildContentAsync: () => Task.FromResult(new DefaultTagHelperContent())); + getChildContentAsync: useCachedResult => + Task.FromResult(new DefaultTagHelperContent())); // Assert Assert.NotNull(context.Items); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs index 2d5e012c56..d6599dda07 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperExecutionContextTest.cs @@ -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]