diff --git a/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml b/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml index 7818ea3afd3a..4fe357cf81f1 100644 --- a/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml +++ b/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml @@ -13,9 +13,7 @@ - - @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) - + diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ComponentWithParametersTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ComponentWithParametersTest.cs new file mode 100644 index 000000000000..1aefbe2b8efd --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/ComponentWithParametersTest.cs @@ -0,0 +1,60 @@ +// 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.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests +{ + public class ComponentWithParametersTest : ServerTestBase> + { + public ComponentWithParametersTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + [Fact] + public void PassingParametersToComponentsFromThePageWorks() + { + Navigate("/prerendered/componentwithparameters?QueryValue=testQueryValue"); + + BeginInteractivity(); + + Browser.Exists(By.CssSelector(".interactive")); + + var parameter1 = Browser.FindElement(By.CssSelector(".Param1")); + Assert.Equal(100, parameter1.FindElements(By.CssSelector("li")).Count); + Assert.Equal("99 99", parameter1.FindElement(By.CssSelector("li:last-child")).Text); + + // The assigned value is of a more derived type than the declared model type. This check + // verifies we use the actual model type during round tripping. + var parameter2 = Browser.FindElement(By.CssSelector(".Param2")); + Assert.Equal("Value Derived-Value", parameter2.Text); + + // This check verifies CaptureUnmatchedValues works + var parameter3 = Browser.FindElements(By.CssSelector(".Param3 li")); + Assert.Collection( + parameter3, + p => Assert.Equal("key1 testQueryValue", p.Text), + p => Assert.Equal("key2 43", p.Text)); + } + + private void BeginInteractivity() + { + Browser.FindElement(By.Id("load-boot-script")).Click(); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/ComponentWithParameters.razor b/src/Components/test/testassets/BasicTestApp/ComponentWithParameters.razor new file mode 100644 index 000000000000..8ec03cf1f07e --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/ComponentWithParameters.razor @@ -0,0 +1,48 @@ +

Component With Parameters

+ +
    + @foreach (var value in Param1) + { +
  • @value.StringProperty @value.IntProperty
  • + } +
+ +@* Making sure polymorphism works *@ +
@DerivedParam2.StringProperty @DerivedParam2.DerivedProperty
+ +@* Making sure CaptureUnmatchedValues works *@ + +
    + @foreach (var value in Param3.OrderBy(kvp => kvp.Key)) + { +
  • @value.Key @value.Value
  • + } +
+ +@code +{ + [Parameter] public List Param1 { get; set; } + + [Parameter] public TestModel Param2 { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] public IDictionary Param3 { get; set; } + + private DerivedModel DerivedParam2 => (DerivedModel)Param2; + + public static List TestModelValues => Enumerable.Range(0, 100).Select(c => new TestModel { StringProperty = c.ToString(), IntProperty = c }).ToList(); + + public static DerivedModel DerivedModelValue = new DerivedModel { StringProperty = "Value", DerivedProperty = "Derived-Value" }; + + public class TestModel + { + + public string StringProperty { get; set; } + + public int IntProperty { get; set; } + } + + public class DerivedModel : TestModel + { + public string DerivedProperty { get; set; } + } +} diff --git a/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml b/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml index 77d00063ca6f..9aa4ff85adcb 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml +++ b/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml @@ -12,7 +12,7 @@ - @(await Html.RenderComponentAsync(RenderMode.Server)) + + + +@functions +{ + [BindProperty(SupportsGet = true)] + public string QueryValue { get; set; } +} diff --git a/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml b/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml index 61ce8d6efb35..c38770012977 100644 --- a/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml @@ -12,24 +12,24 @@
@(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) @(await Html.RenderComponentAsync(RenderMode.Server)) - @(await Html.RenderComponentAsync(RenderMode.Static, new { Name = "John" })) - @(await Html.RenderComponentAsync(RenderMode.Server)) + +

Some content before

- @(await Html.RenderComponentAsync(RenderMode.Server)) +

Some content between

- @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) +

Some content after

Some content before

- @(await Html.RenderComponentAsync(RenderMode.Server)) - @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) + +

Some content after

- @(await Html.RenderComponentAsync(RenderMode.Server, new { Name = "Albert" })) - @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered, new { Name = "Abraham" })) + +
diff --git a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml index 1ba6f2f1c922..2d7eacfb9b9f 100644 --- a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml @@ -1,5 +1,6 @@ @page @using BasicTestApp.RouterTest + @@ -7,7 +8,7 @@ - @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) + @* So that E2E tests can make assertions about both the prerendered and diff --git a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml index 668123d6f91f..af2f28f65854 100644 --- a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml @@ -1,4 +1,5 @@ @page "" +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" @@ -11,7 +12,7 @@ - @(await Html.RenderComponentAsync(RenderMode.Server)) + diff --git a/src/Components/test/testassets/TestServer/Pages/_ViewImports.cshtml b/src/Components/test/testassets/TestServer/Pages/_ViewImports.cshtml new file mode 100644 index 000000000000..59d4a92b6d78 --- /dev/null +++ b/src/Components/test/testassets/TestServer/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@using BasicTestApp + diff --git a/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs b/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs index b940696196e8..ef7a54279007 100644 --- a/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs +++ b/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs @@ -105,6 +105,22 @@ public partial class CacheTagHelperOptions public CacheTagHelperOptions() { } public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type", TagStructure=Microsoft.AspNetCore.Razor.TagHelpers.TagStructure.WithoutEndTag)] + public sealed partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper + { + public ComponentTagHelper() { } + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")] + public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")] + public System.Collections.Generic.IDictionary Parameters { get { throw null; } set { } } + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")] + public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute] + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute] + public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [System.Diagnostics.DebuggerStepThroughAttribute] + public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; } + } [Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")] public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase { diff --git a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs new file mode 100644 index 000000000000..6dc9bfe7224e --- /dev/null +++ b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs @@ -0,0 +1,80 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.TagHelpers +{ + /// + /// A that renders a Razor component. + /// + [HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)] + public sealed class ComponentTagHelper : TagHelper + { + private const string ComponentParameterName = "params"; + private const string ComponentParameterPrefix = "param-"; + private const string ComponentTypeName = "type"; + private const string RenderModeName = "render-mode"; + private IDictionary _parameters; + + /// + /// Gets or sets the for the current request. + /// + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext ViewContext { get; set; } + + /// + /// Gets or sets values for component parameters. + /// + [HtmlAttributeName(ComponentParameterName, DictionaryAttributePrefix = ComponentParameterPrefix)] + public IDictionary Parameters + { + get + { + _parameters ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + return _parameters; + } + set => _parameters = value; + } + + /// + /// Gets or sets the component type. This value is required. + /// + [HtmlAttributeName(ComponentTypeName)] + public Type ComponentType { get; set; } + + /// + /// Gets or sets the + /// + [HtmlAttributeName(RenderModeName)] + public RenderMode RenderMode { get; set; } + + /// + public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (output == null) + { + throw new ArgumentNullException(nameof(output)); + } + + var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService(); + var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters); + + // Reset the TagName. We don't want `component` to render. + output.TagName = null; + output.Content.SetHtmlContent(result); + } + } +} diff --git a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs new file mode 100644 index 000000000000..5c1aff4a7eea --- /dev/null +++ b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs @@ -0,0 +1,75 @@ +// 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.IO.Pipes; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.TagHelpers +{ + public class ComponentTagHelperTest + { + [Fact] + public async Task ProcessAsync_RendersComponent() + { + // Arrange + var tagHelper = new ComponentTagHelper + { + ViewContext = GetViewContext(), + }; + var context = GetTagHelperContext(); + var output = GetTagHelperOutput(); + + // Act + await tagHelper.ProcessAsync(context, output); + + // Assert + var content = HtmlContentUtilities.HtmlContentToString(output.Content); + Assert.Equal("Hello world", content); + Assert.Null(output.TagName); + } + + private static TagHelperContext GetTagHelperContext() + { + return new TagHelperContext( + "component", + new TagHelperAttributeList(), + new Dictionary(), + Guid.NewGuid().ToString("N")); + } + + private static TagHelperOutput GetTagHelperOutput() + { + return new TagHelperOutput( + "component", + new TagHelperAttributeList(), + (_, __) => Task.FromResult(new DefaultTagHelperContent())); + } + + private ViewContext GetViewContext() + { + var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world"); + var renderer = Mock.Of(c => + c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == Task.FromResult(htmlContent)); + + var httpContext = new DefaultHttpContext + { + RequestServices = new ServiceCollection().AddSingleton(renderer).BuildServiceProvider(), + }; + + return new ViewContext + { + HttpContext = httpContext, + }; + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs index e9618a4842d4..dfbd0dec1b04 100644 --- a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs +++ b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs @@ -324,8 +324,8 @@ public enum Html5DateRenderingMode } public static partial class HtmlHelperComponentExtensions { + public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, System.Type componentType, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) { throw null; } public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } - [System.Diagnostics.DebuggerStepThroughAttribute] public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } } public static partial class HtmlHelperDisplayExtensions diff --git a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index 7b17edbe29eb..84ffe50ce1c2 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters; using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; -using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.JSInterop; @@ -206,8 +205,9 @@ internal static void AddViewServices(IServiceCollection services) services.TryAddSingleton(); // - // Component prerendering + // Component rendering // + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperComponentExtensions.cs deleted file mode 100644 index 303c820cc92f..000000000000 --- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperComponentExtensions.cs +++ /dev/null @@ -1,127 +0,0 @@ -// 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.Threading.Tasks; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Mvc.Rendering -{ - /// - /// Extensions for rendering components. - /// - public static class HtmlHelperComponentExtensions - { - private static readonly object ComponentSequenceKey = new object(); - - /// - /// Renders the . - /// - /// The . - /// The for the component. - /// The HTML produced by the rendered . - public static Task RenderComponentAsync(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent - { - if (htmlHelper == null) - { - throw new ArgumentNullException(nameof(htmlHelper)); - } - - return htmlHelper.RenderComponentAsync(renderMode, null); - } - - /// - /// Renders the . - /// - /// The . - /// An containing the parameters to pass - /// to the component. - /// The for the component. - /// The HTML produced by the rendered . - public static async Task RenderComponentAsync( - this IHtmlHelper htmlHelper, - RenderMode renderMode, - object parameters) where TComponent : IComponent - { - if (htmlHelper == null) - { - throw new ArgumentNullException(nameof(htmlHelper)); - } - - var context = htmlHelper.ViewContext.HttpContext; - return renderMode switch - { - RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)), - RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)), - RenderMode.Static => await StaticComponentAsync(context, typeof(TComponent), GetParametersCollection(parameters)), - _ => throw new ArgumentException("Invalid render mode", nameof(renderMode)), - }; - } - - private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext) - { - if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result)) - { - result = new ServerComponentInvocationSequence(); - viewContext.Items[ComponentSequenceKey] = result; - } - - return (ServerComponentInvocationSequence)result; - } - - private static ParameterView GetParametersCollection(object parameters) => parameters == null ? - ParameterView.Empty : - ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters)); - - private static async Task StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) - { - var serviceProvider = context.RequestServices; - var prerenderer = serviceProvider.GetRequiredService(); - - - var result = await prerenderer.PrerenderComponentAsync( - parametersCollection, - context, - type); - - return new ComponentHtmlContent(result); - } - - private static async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) - { - var serviceProvider = context.RequestServices; - var prerenderer = serviceProvider.GetRequiredService(); - var invocationSerializer = serviceProvider.GetRequiredService(); - - var currentInvocation = invocationSerializer.SerializeInvocation( - invocationId, - type, - parametersCollection, - prerendered: true); - - var result = await prerenderer.PrerenderComponentAsync( - parametersCollection, - context, - type); - - return new ComponentHtmlContent( - invocationSerializer.GetPreamble(currentInvocation), - result, - invocationSerializer.GetEpilogue(currentInvocation)); - } - - private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) - { - var serviceProvider = context.RequestServices; - var invocationSerializer = serviceProvider.GetRequiredService(); - var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false); - - return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation)); - } - } -} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs new file mode 100644 index 000000000000..44a618fb04a1 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs @@ -0,0 +1,110 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + internal class ComponentRenderer : IComponentRenderer + { + private static readonly object ComponentSequenceKey = new object(); + private readonly StaticComponentRenderer _staticComponentRenderer; + private readonly ServerComponentSerializer _serverComponentSerializer; + + public ComponentRenderer( + StaticComponentRenderer staticComponentRenderer, + ServerComponentSerializer serverComponentSerializer) + { + _staticComponentRenderer = staticComponentRenderer; + _serverComponentSerializer = serverComponentSerializer; + } + + public async Task RenderComponentAsync( + ViewContext viewContext, + Type componentType, + RenderMode renderMode, + object parameters) + { + if (viewContext is null) + { + throw new ArgumentNullException(nameof(viewContext)); + } + + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + if (!typeof(IComponent).IsAssignableFrom(componentType)) + { + throw new ArgumentException(Resources.FormatTypeMustDeriveFromType(componentType, typeof(IComponent))); + } + + var context = viewContext.HttpContext; + var parameterView = parameters is null ? + ParameterView.Empty : + ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters)); + + return renderMode switch + { + RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView), + RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView), + RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView), + _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)), + }; + } + + private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext) + { + if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result)) + { + result = new ServerComponentInvocationSequence(); + viewContext.Items[ComponentSequenceKey] = result; + } + + return (ServerComponentInvocationSequence)result; + } + + private async Task StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) + { + var result = await _staticComponentRenderer.PrerenderComponentAsync( + parametersCollection, + context, + type); + + return new ComponentHtmlContent(result); + } + + private async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) + { + var currentInvocation = _serverComponentSerializer.SerializeInvocation( + invocationId, + type, + parametersCollection, + prerendered: true); + + var result = await _staticComponentRenderer.PrerenderComponentAsync( + parametersCollection, + context, + type); + + return new ComponentHtmlContent( + _serverComponentSerializer.GetPreamble(currentInvocation), + result, + _serverComponentSerializer.GetEpilogue(currentInvocation)); + } + + private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) + { + var serviceProvider = context.RequestServices; + var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false); + + return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation)); + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IComponentRenderer.cs new file mode 100644 index 000000000000..90df64f9f764 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IComponentRenderer.cs @@ -0,0 +1,19 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures +{ + internal interface IComponentRenderer + { + Task RenderComponentAsync( + ViewContext viewContext, + Type componentType, + RenderMode renderMode, + object parameters); + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs index 72e89faf5fe2..89304873c108 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs @@ -11,11 +11,10 @@ using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents +namespace Microsoft.AspNetCore.Mvc.ViewFeatures { internal class StaticComponentRenderer { diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs index bcb36c37a030..da8402029475 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs @@ -2,7 +2,6 @@ // 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.Threading; using System.Threading.Tasks; using Microsoft.JSInterop; diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs new file mode 100644 index 000000000000..16f021574aff --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs @@ -0,0 +1,70 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.Rendering +{ + /// + /// Extensions for rendering components. + /// + public static class HtmlHelperComponentExtensions + { + /// + /// Renders the . + /// + /// The . + /// The for the component. + /// The HTML produced by the rendered . + public static Task RenderComponentAsync(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent + => RenderComponentAsync(htmlHelper, renderMode, parameters: null); + + /// + /// Renders the . + /// + /// The . + /// An containing the parameters to pass + /// to the component. + /// The for the component. + /// The HTML produced by the rendered . + public static Task RenderComponentAsync( + this IHtmlHelper htmlHelper, + RenderMode renderMode, + object parameters) where TComponent : IComponent + => RenderComponentAsync(htmlHelper, typeof(TComponent), renderMode, parameters); + + /// + /// Renders the specified . + /// + /// The . + /// The component type. + /// An containing the parameters to pass + /// to the component. + /// The for the component. + public static Task RenderComponentAsync( + this IHtmlHelper htmlHelper, + Type componentType, + RenderMode renderMode, + object parameters) + { + if (htmlHelper is null) + { + throw new ArgumentNullException(nameof(htmlHelper)); + } + + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + var viewContext = htmlHelper.ViewContext; + var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService(); + return componentRenderer.RenderComponentAsync(viewContext, componentType, renderMode, parameters); + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Resources.resx b/src/Mvc/Mvc.ViewFeatures/src/Resources.resx index e7362a9bf5ac..ae6a29aa8173 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Resources.resx +++ b/src/Mvc/Mvc.ViewFeatures/src/Resources.resx @@ -295,4 +295,7 @@ Unsupported data type '{0}'. + + Unsupported RenderMode '{0}'. + \ No newline at end of file diff --git a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs similarity index 83% rename from src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs rename to src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs index a7629497a724..1e89472473d8 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -22,24 +22,26 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test +namespace Microsoft.AspNetCore.Mvc.ViewFeatures { - public class HtmlHelperComponentExtensionsTests + public class ComponentRendererTest { private const string PrerenderedServerComponentPattern = "^(?.+?)$"; private const string ServerComponentPattern = "^$"; private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider(); + private readonly ComponentRenderer renderer = GetComponentRenderer(); + [Fact] public async Task CanRender_ParameterlessComponent() { // Arrange - var helper = CreateHelper(); + var viewContext = GetViewContext(); var writer = new StringWriter(); // Act - var result = await helper.RenderComponentAsync(RenderMode.Static); + var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, null); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); @@ -51,15 +53,13 @@ public async Task CanRender_ParameterlessComponent() public async Task CanRender_ParameterlessComponent_ServerMode() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await helper.RenderComponentAsync(RenderMode.Server); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null); + var content = HtmlContentUtilities.HtmlContentToString(result); var match = Regex.Match(content, ServerComponentPattern); // Assert @@ -82,15 +82,13 @@ public async Task CanRender_ParameterlessComponent_ServerMode() public async Task CanPrerender_ParameterlessComponent_ServerMode() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await helper.RenderComponentAsync(RenderMode.ServerPrerendered); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null); + var content = HtmlContentUtilities.HtmlContentToString(result); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); // Assert @@ -125,21 +123,17 @@ public async Task CanPrerender_ParameterlessComponent_ServerMode() public async Task CanRenderMultipleServerComponents() { // Arrange - var helper = CreateHelper(); - var firstWriter = new StringWriter(); - var secondWriter = new StringWriter(); + var viewContext = GetViewContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var firstResult = await helper.RenderComponentAsync(RenderMode.ServerPrerendered); - firstResult.WriteTo(firstWriter, HtmlEncoder.Default); - var firstComponent = firstWriter.ToString(); + var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null); + var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult); var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline); - var secondResult = await helper.RenderComponentAsync(RenderMode.Server); - secondResult.WriteTo(secondWriter, HtmlEncoder.Default); - var secondComponent = secondWriter.ToString(); + var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null); + var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult); var secondMatch = Regex.Match(secondComponent, ServerComponentPattern); // Assert @@ -171,20 +165,13 @@ public async Task CanRenderMultipleServerComponents() public async Task CanRender_ComponentWithParametersObject() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); // Act - var result = await helper.RenderComponentAsync( - RenderMode.Static, - new - { - Name = "Steve" - }); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, new { Name = "Steve" }); // Assert + var content = HtmlContentUtilities.HtmlContentToString(result); Assert.Equal("

Hello Steve!

", content); } @@ -192,20 +179,13 @@ public async Task CanRender_ComponentWithParametersObject() public async Task CanRender_ComponentWithParameters_ServerMode() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await helper.RenderComponentAsync( - RenderMode.Server, - new - { - Name = "Daniel" - }); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" }); + var content = HtmlContentUtilities.HtmlContentToString(result); var match = Regex.Match(content, ServerComponentPattern); // Assert @@ -237,20 +217,14 @@ public async Task CanRender_ComponentWithParameters_ServerMode() public async Task CanRender_ComponentWithNullParameters_ServerMode() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await helper.RenderComponentAsync( - RenderMode.Server, - new - { - Name = (string)null - }); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + + var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null }); + var content = HtmlContentUtilities.HtmlContentToString(result); var match = Regex.Match(content, ServerComponentPattern); // Assert @@ -274,28 +248,22 @@ public async Task CanRender_ComponentWithNullParameters_ServerMode() Assert.Null(parameterDefinition.TypeName); Assert.Null(parameterDefinition.Assembly); - var value = Assert.Single(serverComponent.ParameterValues);; + var value = Assert.Single(serverComponent.ParameterValues); ; Assert.Null(value); } [Fact] - public async Task CanPrerender_ComponentWithParameters_ServerMode() + public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode() { // Arrange - var helper = CreateHelper(); + var viewContext = GetViewContext(); var writer = new StringWriter(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await helper.RenderComponentAsync( - RenderMode.ServerPrerendered, - new - { - Name = "Daniel" - }); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" }); + var content = HtmlContentUtilities.HtmlContentToString(result); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); // Assert @@ -336,23 +304,17 @@ public async Task CanPrerender_ComponentWithParameters_ServerMode() } [Fact] - public async Task CanPrerender_ComponentWithNullParameters_ServerMode() + public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode() { // Arrange - var helper = CreateHelper(); + var viewContext = GetViewContext(); var writer = new StringWriter(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await helper.RenderComponentAsync( - RenderMode.ServerPrerendered, - new - { - Name = (string)null - }); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null }); + var content = HtmlContentUtilities.HtmlContentToString(result); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); // Assert @@ -396,39 +358,28 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerMode() public async Task ComponentWithInvalidRenderMode_Throws() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); // Act & Assert - var result = await Assert.ThrowsAsync(() => helper.RenderComponentAsync( - default, - new - { - Name = "Steve" - })); - Assert.Equal("renderMode", result.ParamName); + var ex = await ExceptionAssert.ThrowsArgumentAsync( + () => renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), default, new { Name = "Daniel" }), + "renderMode", + $"Unsupported RenderMode '{(RenderMode)default}'"); } [Fact] public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); // Act var state = new OnAfterRenderState(); - var result = await helper.RenderComponentAsync( - RenderMode.Static, - new - { - State = state - }); - - result.WriteTo(writer, HtmlEncoder.Default); + var result = await renderer.RenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, new { state }); // Assert - Assert.Equal("

Hello

", writer.ToString()); + var content = HtmlContentUtilities.HtmlContentToString(result); + Assert.Equal("

Hello

", content); Assert.False(state.OnAfterRenderRan); } @@ -436,10 +387,12 @@ public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() public async Task CanCatch_ComponentWithSynchronousException() { // Arrange - var helper = CreateHelper(); + var viewContext = GetViewContext(); // Act & Assert - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync( + viewContext, + typeof(ExceptionComponent), RenderMode.Static, new { @@ -454,10 +407,12 @@ public async Task CanCatch_ComponentWithSynchronousException() public async Task CanCatch_ComponentWithAsynchronousException() { // Arrange - var helper = CreateHelper(); + var viewContext = GetViewContext(); // Act & Assert - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync( + viewContext, + typeof(ExceptionComponent), RenderMode.Static, new { @@ -472,10 +427,12 @@ public async Task CanCatch_ComponentWithAsynchronousException() public async Task Rendering_ComponentWithJsInteropThrows() { // Arrange - var helper = CreateHelper(); + var viewContext = GetViewContext(); // Act & Assert - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync( + viewContext, + typeof(ExceptionComponent), RenderMode.Static, new { @@ -503,11 +460,12 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponse var responseMock = new Mock(); responseMock.Setup(r => r.HasStarted).Returns(true); ctx.Features.Set(responseMock.Object); - var helper = CreateHelper(ctx); - var writer = new StringWriter(); + var viewContext = GetViewContext(ctx); // Act - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync( + viewContext, + typeof(RedirectComponent), RenderMode.Static, new { @@ -515,8 +473,8 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponse })); Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " + - "Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" + - "reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", + "Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" + + "reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", exception.Message); } @@ -530,10 +488,12 @@ public async Task HtmlHelper_Redirects_WhenComponentNavigates() ctx.Request.PathBase = "/base"; ctx.Request.Path = "/path"; ctx.Request.QueryString = new QueryString("?query=value"); - var helper = CreateHelper(ctx); + var viewContext = GetViewContext(ctx); // Act - await helper.RenderComponentAsync( + await renderer.RenderComponentAsync( + viewContext, + typeof(RedirectComponent), RenderMode.Static, new { @@ -549,8 +509,7 @@ await helper.RenderComponentAsync( public async Task CanRender_AsyncComponent() { // Arrange - var helper = CreateHelper(); - var writer = new StringWriter(); + var viewContext = GetViewContext(); var expectedContent = @" @@ -595,29 +554,29 @@ public async Task CanRender_AsyncComponent()
"; // Act - var result = await helper.RenderComponentAsync(RenderMode.Static); - result.WriteTo(writer, HtmlEncoder.Default); - var content = writer.ToString(); + var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null); + var content = HtmlContentUtilities.HtmlContentToString(result); // Assert Assert.Equal(expectedContent.Replace("\r\n", "\n"), content); } - private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action configureServices = null) + private static ComponentRenderer GetComponentRenderer() => + new ComponentRenderer( + new StaticComponentRenderer(HtmlEncoder.Default), + new ServerComponentSerializer(_dataprotectorProvider)); + + private static ViewContext GetViewContext(HttpContext context = null, Action configureServices = null) { var services = new ServiceCollection(); - services.AddSingleton(HtmlEncoder.Default); - services.AddSingleton(); services.AddSingleton(_dataprotectorProvider); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); configureServices?.Invoke(services); - var helper = new Mock(); - var context = ctx ?? new DefaultHttpContext(); + context ??= new DefaultHttpContext(); context.RequestServices = services.BuildServiceProvider(); context.Request.Scheme = "http"; context.Request.Host = new HostString("localhost"); @@ -625,12 +584,7 @@ private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action h.ViewContext) - .Returns(new ViewContext() - { - HttpContext = context - }); - return helper.Object; + return new ViewContext { HttpContext = context }; } private class TestComponent : IComponent diff --git a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs index 7a670696fbe6..67778e416355 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs @@ -6,13 +6,11 @@ using System.Runtime.ExceptionServices; using System.Text.Encodings.Web; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Mvc.RazorComponents +namespace Microsoft.AspNetCore.Components.Rendering { public class HtmlRendererTest { diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs new file mode 100644 index 000000000000..470b4b3ab04b --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs @@ -0,0 +1,56 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Rendering +{ + public class HtmlHelperComponentExtensionsTest + { + [Fact] + public async Task RenderComponentAsync_Works() + { + // Arrange + var viewContext = GetViewContext(); + var htmlHelper = Mock.Of(h => h.ViewContext == viewContext); + + // Act + var result = await HtmlHelperComponentExtensions.RenderComponentAsync(htmlHelper, RenderMode.Static); + + // Assert + Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(result)); + } + + private static ViewContext GetViewContext() + { + var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world"); + var renderer = Mock.Of(c => + c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == Task.FromResult(htmlContent)); + + var httpContext = new DefaultHttpContext + { + RequestServices = new ServiceCollection().AddSingleton(renderer).BuildServiceProvider(), + }; + + var viewContext = new ViewContext { HttpContext = httpContext }; + return viewContext; + } + + private class TestComponent : IComponent + { + public void Attach(RenderHandle renderHandle) + { + } + + public Task SetParametersAsync(ParameterView parameters) => null; + } + } +} diff --git a/src/Mvc/samples/MvcSandbox/Components/App.razor b/src/Mvc/samples/MvcSandbox/Components/App.razor index 2bf3672b80d1..1c9b07ba2087 100644 --- a/src/Mvc/samples/MvcSandbox/Components/App.razor +++ b/src/Mvc/samples/MvcSandbox/Components/App.razor @@ -1,2 +1,13 @@ @using Microsoft.AspNetCore.Components.Routing - \ No newline at end of file +@using MvcSandbox.Components.Shared + + + + + + + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/src/Mvc/samples/MvcSandbox/Components/NotFound.razor b/src/Mvc/samples/MvcSandbox/Components/NotFound.razor deleted file mode 100644 index 369bfb8dde19..000000000000 --- a/src/Mvc/samples/MvcSandbox/Components/NotFound.razor +++ /dev/null @@ -1,4 +0,0 @@ -@using MvcSandbox.Components.Shared -@layout MainLayout -

Not Found

-

Sorry, nothing was found.

\ No newline at end of file diff --git a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml b/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml index cdafd22f6864..89401bf85b33 100644 --- a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml +++ b/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml @@ -1,5 +1,4 @@ @page -@model MvcSandbox.Pages.ComponentsModel @{ Layout = null; } @@ -15,8 +14,7 @@ - @(await Html.RenderComponentAsync(RenderMode.Static)) - - + + diff --git a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml.cs b/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml.cs deleted file mode 100644 index fce8214cb5f3..000000000000 --- a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace MvcSandbox.Pages -{ - public class ComponentsModel : PageModel - { - public void OnGet() - { - } - } -} \ No newline at end of file diff --git a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml index a3da90da304a..1b670c2b6540 100644 --- a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml +++ b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml @@ -19,6 +19,9 @@ + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml index 7a104af51d00..14d8ae4c3eaf 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml @@ -14,7 +14,7 @@ - @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) +