diff --git a/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs b/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs index 5ec7453277..c6ebeddef4 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs @@ -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) { diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/ITagHelper.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/ITagHelper.cs index ec0a64f41e..5cb6151f22 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/ITagHelper.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/ITagHelper.cs @@ -11,11 +11,24 @@ namespace Microsoft.AspNet.Razor.TagHelpers public interface ITagHelper { /// - /// Gets the execution order of this relative to others targeting the same element. - /// instances with lower values are executed first. + /// When a set of s are executed, their's + /// are first invoked in the specified ; then their + /// 's are invoked in the specified + /// . Lower values are executed first. /// int Order { get; } + /// + /// Initializes the with the given . Additions to + /// should be done within this method to ensure they're added prior to + /// executing the children. + /// + /// Contains information associated with the current HTML tag. + /// When more than one runs on the same element, + /// may be invoked prior to . + /// + void Init(TagHelperContext context); + /// /// Asynchronously executes the with the given and /// . diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelper.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelper.cs index ef051505b5..267a243deb 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelper.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelper.cs @@ -14,6 +14,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// Default order is 0. public virtual int Order { get; } = 0; + /// + public virtual void Init(TagHelperContext context) + { + } + /// /// Synchronously executes the with the given and /// . diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs index ed6ef0491b..272ec5a48c 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperContext.cs @@ -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 /// public class TagHelperContext { - private readonly Func> _getChildContentAsync; - /// /// Instantiates a new . /// @@ -22,13 +19,10 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// Collection of items used to communicate with other s. /// The unique identifier for the source element this /// applies to. - /// A delegate used to execute and retrieve the rendered child content - /// asynchronously. public TagHelperContext( IEnumerable allAttributes, IDictionary items, - string uniqueId, - Func> 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( allAttributes.Select(attribute => new TagHelperAttribute(attribute.Name, attribute.Value))); Items = items; UniqueId = uniqueId; - _getChildContentAsync = getChildContentAsync; } /// @@ -75,26 +63,5 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// An identifier unique to the HTML element this context is for. /// public string UniqueId { get; } - - /// - /// 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(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/TagHelperOutput.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs index c418fc7fe5..b7816fab02 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperOutput.cs @@ -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 /// public class TagHelperOutput { + private readonly Func> _getChildContentAsync; + // Internal for testing internal TagHelperOutput(string tagName) - : this(tagName, new TagHelperAttributeList()) + : this( + tagName, + new TagHelperAttributeList(), + (cachedResult) => Task.FromResult(new DefaultTagHelperContent())) { } @@ -22,17 +28,26 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// The HTML element's tag name. /// The HTML attributes. + /// A delegate used to execute and retrieve the rendered child content + /// asynchronously. public TagHelperOutput( string tagName, - TagHelperAttributeList attributes) + TagHelperAttributeList attributes, + Func> 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; } /// @@ -116,5 +131,26 @@ namespace Microsoft.AspNet.Razor.TagHelpers PostContent.Clear(); PostElement.Clear(); } + + /// + /// A delegate used to execute children asynchronously. + /// + /// A that on completion returns content rendered by children. + /// This method is memoized. + public Task GetChildContentAsync() + { + return GetChildContentAsync(useCachedResult: true); + } + + /// + /// A delegate used to execute children asynchronously. + /// + /// If true multiple calls to this method will not cause re-execution + /// of child content; cached content will be returned. + /// A that on completion returns content rendered by children. + public Task GetChildContentAsync(bool useCachedResult) + { + return _getChildContentAsync(useCachedResult); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/RuntimeTypeInfoTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/RuntimeTypeInfoTest.cs index c3280fb45b..0c9210d344 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/RuntimeTypeInfoTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/RuntimeTypeInfoTest.cs @@ -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(); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperRunnerTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperRunnerTest.cs index 2500e4c265..3411185438 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperRunnerTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperRunnerTest.cs @@ -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); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperTypeResolverTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperTypeResolverTest.cs index bcdd56662f..10800f6d66 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperTypeResolverTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperTypeResolverTest.cs @@ -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); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/ImplementsRealTagHelper.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/ImplementsRealTagHelper.cs index 15a2f9f345..e1a9536cfc 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/ImplementsRealTagHelper.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/ImplementsRealTagHelper.cs @@ -17,6 +17,10 @@ namespace Microsoft.AspNet.Razor.Fake } } + public void Init(TagHelperContext context) + { + } + public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { throw new NotImplementedException(); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs index ff2901de7b..78b0c6c73a 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/Runtime/TagHelpers/TestTagHelpers/TagHelperDescriptorFactoryTagHelpers.cs @@ -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(); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs index 8ba9269c77..537fc343da 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperContextTest.cs @@ -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(), - 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() { @@ -49,9 +25,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers var context = new TagHelperContext( allAttributes: Enumerable.Empty(), items: expectedItems, - uniqueId: string.Empty, - getChildContentAsync: useCachedResult => - Task.FromResult(new DefaultTagHelperContent())); + uniqueId: string.Empty); // Assert Assert.NotNull(context.Items); diff --git a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs index 3f06c324c7..d7e41d64d4 100644 --- a/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs +++ b/test/Microsoft.AspNet.Razor.Runtime.Test/TagHelpers/TagHelperOutputTest.cs @@ -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(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(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(new DefaultTagHelperContent())); // Act tagHelperOutput.Attributes[updateName] = "super button";