diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 3cecf900e4..423f00275b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.TagHelpers; @@ -187,6 +188,9 @@ internal static void AddRazorViewEngineServices(IServiceCollection services) services.TryAddSingleton(); services.TryAddSingleton(); + // TagHelperComponents manager + services.TryAddScoped(); + // Consumed by the Cache tag helper to cache results across the lifetime of the application. services.TryAddSingleton(); } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/TagHelperComponentManager.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/TagHelperComponentManager.cs new file mode 100644 index 0000000000..38dde4f941 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/TagHelperComponentManager.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.AspNetCore.Mvc.Razor.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + /// + /// The default implementation of the . + /// + public class TagHelperComponentManager : ITagHelperComponentManager + { + /// + /// Creates a new . + /// + /// The collection of s. + public TagHelperComponentManager(IEnumerable tagHelperComponents) + { + if (tagHelperComponents == null) + { + throw new ArgumentNullException(nameof(tagHelperComponents)); + } + + Components = new List(tagHelperComponents); + } + + /// + public ICollection Components { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/BodyTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/BodyTagHelper.cs index 74aec75f0c..111c67a0ab 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/BodyTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/BodyTagHelper.cs @@ -1,7 +1,6 @@ // 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.Collections.Generic; using System.ComponentModel; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Logging; @@ -18,10 +17,11 @@ public class BodyTagHelper : TagHelperComponentTagHelper /// /// Creates a new . /// - /// The list of . + /// The which contains the collection + /// of s. /// The . - public BodyTagHelper(IEnumerable components, ILoggerFactory loggerFactory) - : base(components, loggerFactory) + public BodyTagHelper(ITagHelperComponentManager manager, ILoggerFactory loggerFactory) + : base(manager, loggerFactory) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/HeadTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/HeadTagHelper.cs index 5d0995e958..131b89c6ba 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/HeadTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/HeadTagHelper.cs @@ -1,7 +1,6 @@ // 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.Collections.Generic; using System.ComponentModel; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Logging; @@ -18,10 +17,11 @@ public class HeadTagHelper : TagHelperComponentTagHelper /// /// Creates a new . /// - /// The list of . + /// The which contains the collection + /// of s. /// The . - public HeadTagHelper(IEnumerable components, ILoggerFactory loggerFactory) - : base(components, loggerFactory) + public HeadTagHelper(ITagHelperComponentManager manager, ILoggerFactory loggerFactory) + : base(manager, loggerFactory) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/ITagHelperComponentManager.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/ITagHelperComponentManager.cs new file mode 100644 index 0000000000..813372d558 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/ITagHelperComponentManager.cs @@ -0,0 +1,21 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers +{ + /// + /// An implementation of this interface provides the collection of s + /// that will be used by s. + /// + public interface ITagHelperComponentManager + { + /// + /// Gets the collection of s that will be used by + /// s. + /// + ICollection Components { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperComponentTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperComponentTagHelper.cs index d5205564fb..e12404574c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperComponentTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/TagHelpers/TagHelperComponentTagHelper.cs @@ -11,6 +11,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers { + /// + /// Initializes and processes the s added to the + /// in the specified order. + /// public abstract class TagHelperComponentTagHelper : TagHelper { private readonly ILogger _logger; @@ -18,16 +22,21 @@ public abstract class TagHelperComponentTagHelper : TagHelper private IEnumerable _components; /// - /// Creates a new . + /// Creates a new and orders the + /// the collection of s in . /// - /// The list of . + /// The which contains the collection + /// of s. /// The . - public TagHelperComponentTagHelper(IEnumerable components, + /// The are ordered after the + /// creation of the to position the s + /// added from controllers and views correctly. + public TagHelperComponentTagHelper(ITagHelperComponentManager manager, ILoggerFactory loggerFactory) { - if (components == null) + if (manager == null) { - throw new ArgumentNullException(nameof(components)); + throw new ArgumentNullException(nameof(manager)); } if (loggerFactory == null) @@ -35,14 +44,13 @@ public TagHelperComponentTagHelper(IEnumerable components, throw new ArgumentNullException(nameof(loggerFactory)); } - _components = components; + _components = manager.Components.OrderBy(p => p.Order).ToArray(); _logger = loggerFactory.CreateLogger(GetType()); } /// public override void Init(TagHelperContext context) { - _components = _components.OrderBy(p => p.Order); foreach (var component in _components) { component.Init(context); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelperComponentTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelperComponentTagHelperTest.cs index 3376c410ea..35f7d55be9 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelperComponentTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/TagHelperComponentTagHelperTest.cs @@ -61,6 +61,30 @@ public async Task InjectsTestBodyTagHelperComponent() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); +#if GENERATE_BASELINES + ResourceFile.UpdateFile(_resourcesAssembly, outputFile, expectedContent, responseContent); +#else + Assert.Equal(expectedContent, responseContent, ignoreLineEndingDifferences: true); +#endif + } + + [Fact] + public async Task AddTestTagHelperComponent_FromController() + { + // Arrange + var url = "http://localhost/AddTagHelperComponent/AddComponent"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + var outputFile = "compiler/resources/RazorWebSite.AddTagHelperComponent.AddComponent.html"; + var expectedContent = + await ResourceFile.ReadResourceAsync(_resourcesAssembly, outputFile, sourceFile: false); + + // Act + var response = await Client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + #if GENERATE_BASELINES ResourceFile.UpdateFile(_resourcesAssembly, outputFile, expectedContent, responseContent); #else diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/RazorWebSite.AddTagHelperComponent.AddComponent.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/RazorWebSite.AddTagHelperComponent.AddComponent.html new file mode 100644 index 0000000000..bfd9e6a783 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/RazorWebSite.AddTagHelperComponent.AddComponent.html @@ -0,0 +1,6 @@ + +Hello from Head Tag Helper Component + + +Hello from Body Tag Helper Component +Processed TagHelperComponent added from controller.Processed TagHelperComponent added from view. \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperComponentTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperComponentTagHelperTest.cs index ae8d536120..95a580e278 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperComponentTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/TagHelpers/TagHelperComponentTagHelperTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -27,7 +28,7 @@ public void Init_InvokesComponentsInitInCorrectOrder() uniqueId: "test"); var incrementer = 0; - var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(new [] + var testTagHelperComponentManager = new TagHelperComponentManager(new [] { new CallbackTagHelperComponent( order: 2, @@ -53,7 +54,9 @@ public void Init_InvokesComponentsInitInCorrectOrder() incrementer++; }, processAsyncCallback: null), - }, NullLoggerFactory.Instance); + }); + + var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(testTagHelperComponentManager, NullLoggerFactory.Instance); // Act testTagHelperComponentTagHelper.Init(tagHelperContext); @@ -80,7 +83,7 @@ public async void ProcessAsync_InvokesComponentsProcessAsyncInCorrectOrder() new DefaultTagHelperContent())); var incrementer = 0; - var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(new [] + var testTagHelperComponentManager = new TagHelperComponentManager(new [] { new CallbackTagHelperComponent( order: 2, @@ -106,7 +109,9 @@ public async void ProcessAsync_InvokesComponentsProcessAsyncInCorrectOrder() Assert.Equal(0, incrementer); incrementer++; }), - }, NullLoggerFactory.Instance); + }); + + var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(testTagHelperComponentManager, NullLoggerFactory.Instance); // Act testTagHelperComponentTagHelper.Init(tagHelperContext); @@ -133,10 +138,12 @@ public void Init_InvokesTagHelperComponentInit() getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( new DefaultTagHelperContent())); - var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(new [] + var testTagHelperComponentManager = new TagHelperComponentManager(new[] { new TestTagHelperComponent() - }, NullLoggerFactory.Instance); + }); + + var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(testTagHelperComponentManager, NullLoggerFactory.Instance); // Act testTagHelperComponentTagHelper.Init(tagHelperContext); @@ -162,16 +169,51 @@ public async Task ProcessAsync_InvokesTagHelperComponentProcessAsync() getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( new DefaultTagHelperContent())); - var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(new [] + var testTagHelperComponentManager = new TagHelperComponentManager(new [] + { + new TestTagHelperComponent() + }); + + var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(testTagHelperComponentManager, NullLoggerFactory.Instance); + + // Act + await testTagHelperComponentTagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + Assert.Equal("Processed1", output.PostContent.GetContent()); + } + + [Fact] + public async Task ProcessAsync_InvokesTagHelperComponentProcessAsync_WithAddedTagHelperComponents() + { + // Arrange + var tagHelperContext = new TagHelperContext( + "head", + allAttributes: new TagHelperAttributeList( + Enumerable.Empty()), + items: new Dictionary(), + uniqueId: "test"); + + var output = new TagHelperOutput( + "head", + attributes: new TagHelperAttributeList(), + getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( + new DefaultTagHelperContent())); + + var testTagHelperComponentManager = new TagHelperComponentManager(new [] { new TestTagHelperComponent() - }, NullLoggerFactory.Instance); + }); + + testTagHelperComponentManager.Components.Add(new TestAddTagHelperComponent(0)); + testTagHelperComponentManager.Components.Add(new TestAddTagHelperComponent(2)); + var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(testTagHelperComponentManager, NullLoggerFactory.Instance); // Act await testTagHelperComponentTagHelper.ProcessAsync(tagHelperContext, output); // Assert - Assert.Equal("Processed", output.PostContent.GetContent()); + Assert.Equal("Processed0Processed1Processed2", output.PostContent.GetContent()); } [Fact] @@ -193,10 +235,12 @@ public void Init_LogsTagHelperComponentInitialized() getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( new DefaultTagHelperContent())); - var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(new [] + var testTagHelperComponentManager = new TagHelperComponentManager(new [] { new TestTagHelperComponent() - }, loggerFactory); + }); + + var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(testTagHelperComponentManager, loggerFactory); // Act testTagHelperComponentTagHelper.Init(tagHelperContext); @@ -224,10 +268,12 @@ public async Task ProcessAsync_LogsTagHelperComponentProcessed() getChildContentAsync: (useCachedResult, encoder) => Task.FromResult( new DefaultTagHelperContent())); - var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(new [] + var testTagHelperComponentManager = new TagHelperComponentManager(new [] { new TestTagHelperComponent() - }, loggerFactory); + }); + + var testTagHelperComponentTagHelper = new TestTagHelperComponentTagHelper(testTagHelperComponentManager, loggerFactory); // Act await testTagHelperComponentTagHelper.ProcessAsync(tagHelperContext, output); @@ -239,9 +285,9 @@ public async Task ProcessAsync_LogsTagHelperComponentProcessed() private class TestTagHelperComponentTagHelper : TagHelperComponentTagHelper { public TestTagHelperComponentTagHelper( - IEnumerable components, + ITagHelperComponentManager manager, ILoggerFactory loggerFactory) - : base(components, loggerFactory) + : base(manager, loggerFactory) { } } @@ -284,7 +330,29 @@ public void Init(TagHelperContext context) public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - output.PostContent.AppendHtml("Processed"); + output.PostContent.AppendHtml("Processed1"); + return Task.CompletedTask; + } + } + + private class TestAddTagHelperComponent : ITagHelperComponent + { + private int _order; + + public TestAddTagHelperComponent(int order) + { + _order = order; + } + + public int Order => _order; + + public void Init(TagHelperContext context) + { + } + + public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + output.PostContent.AppendHtml("Processed" + Order); return Task.CompletedTask; } } diff --git a/test/WebSites/RazorWebSite/Controllers/AddTagHelperComponentController.cs b/test/WebSites/RazorWebSite/Controllers/AddTagHelperComponentController.cs new file mode 100644 index 0000000000..cd318ac287 --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/AddTagHelperComponentController.cs @@ -0,0 +1,24 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; + +namespace RazorWebSite.Controllers +{ + public class AddTagHelperComponentController : Controller + { + private readonly ITagHelperComponentManager _tagHelperComponentManager; + + public AddTagHelperComponentController(ITagHelperComponentManager tagHelperComponentManager) + { + _tagHelperComponentManager = tagHelperComponentManager; + } + + public IActionResult AddComponent() + { + _tagHelperComponentManager.Components.Add(new TestBodyTagHelperComponent(0, "Processed TagHelperComponent added from controller.")); + return View("AddComponent"); + } + } +} diff --git a/test/WebSites/RazorWebSite/Services/TestBodyTagHelperComponent.cs b/test/WebSites/RazorWebSite/Services/TestBodyTagHelperComponent.cs index cc31c525e1..bed0531dc7 100644 --- a/test/WebSites/RazorWebSite/Services/TestBodyTagHelperComponent.cs +++ b/test/WebSites/RazorWebSite/Services/TestBodyTagHelperComponent.cs @@ -7,15 +7,33 @@ namespace RazorWebSite { - public class TestBodyTagHelperComponent : TagHelperComponent + public class TestBodyTagHelperComponent : ITagHelperComponent { - public override int Order => 1; + private int _order; + private string _html; - public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + public TestBodyTagHelperComponent() : this(1, "") + { + + } + + public TestBodyTagHelperComponent(int order, string html) + { + _order = order; + _html = html; + } + + public int Order => _order; + + public void Init(TagHelperContext context) + { + } + + public Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { if (string.Equals(context.TagName, "body", StringComparison.Ordinal) && output.Attributes.ContainsName("inject")) { - output.PostContent.AppendHtml(""); + output.PostContent.AppendHtml(_html); } return Task.FromResult(0); diff --git a/test/WebSites/RazorWebSite/Views/AddTagHelperComponent/AddComponent.cshtml b/test/WebSites/RazorWebSite/Views/AddTagHelperComponent/AddComponent.cshtml new file mode 100644 index 0000000000..f7f508904f --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/AddTagHelperComponent/AddComponent.cshtml @@ -0,0 +1,10 @@ +@inject Microsoft.AspNetCore.Mvc.Razor.TagHelpers.ITagHelperComponentManager manager + +Hello from Head Tag Helper Component + +@{ + manager.Components.Add(new TestBodyTagHelperComponent(2, "Processed TagHelperComponent added from view.")); +} + +Hello from Body Tag Helper Component + \ No newline at end of file