From 65cac0426de3d902b0b97f7fbd9059405cbc7af1 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 11:32:10 +0000 Subject: [PATCH 01/26] Begin adding HtmlRenderer as public API --- .../Web/src/HtmlRendering/HtmlContent.cs | 33 +++ .../Web/src/HtmlRendering/HtmlRenderer.cs | 43 ++++ .../src/HtmlRendering/HtmlRenderingContext.cs | 224 ++++++++++++++++++ .../src/HtmlRendering/PassiveHtmlRenderer.cs | 57 +++++ .../Web/src/PublicAPI.Unshipped.txt | 6 + .../test/HtmlRendering/HtmlRendererTest.cs | 38 +++ 6 files changed, 401 insertions(+) create mode 100644 src/Components/Web/src/HtmlRendering/HtmlContent.cs create mode 100644 src/Components/Web/src/HtmlRendering/HtmlRenderer.cs create mode 100644 src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs create mode 100644 src/Components/Web/src/HtmlRendering/PassiveHtmlRenderer.cs create mode 100644 src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs new file mode 100644 index 000000000000..dcf1b92902aa --- /dev/null +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components.HtmlRendering; + +namespace Microsoft.AspNetCore.Components.Web; + +/// +/// Represents the output of rendering a component as HTML. The content can change if the component instance re-renders. +/// +public sealed class HtmlContent +{ + private readonly PassiveHtmlRenderer _renderer; + private readonly int _componentId; + + internal HtmlContent(PassiveHtmlRenderer renderer, int componentId) + { + _renderer = renderer; + _componentId = componentId; + } + + /// + /// Returns an HTML string representation of the component's latest output. + /// + /// An HTML string. + public string ToHtmlString() + { + using var writer = new StringWriter(); + new HtmlRenderingContext(_renderer, _componentId, writer, HtmlEncoder.Default).Render(); + return writer.ToString(); + } +} diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs new file mode 100644 index 000000000000..6e87fba19e31 --- /dev/null +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.HtmlRendering; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.Web; + +/// +/// Provides a mechanism for rendering components non-interactively as HTML markup. +/// +public class HtmlRenderer : IAsyncDisposable +{ + private readonly PassiveHtmlRenderer _passiveHtmlRenderer; + + /// + /// Constructs an instance of . + /// + /// The services to use when rendering components. + public HtmlRenderer(IServiceProvider services) + { + var componentActivator = services.GetService() ?? DefaultComponentActivator.Instance; + var loggerFactory = services.GetRequiredService(); + _passiveHtmlRenderer = new PassiveHtmlRenderer(services, loggerFactory, componentActivator); + } + + /// + public ValueTask DisposeAsync() + => _passiveHtmlRenderer.DisposeAsync(); + + /// + /// Adds an instance of the specified component and instructs it to render. + /// + /// The component type. + /// A instance representing the render output. + public async Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent: IComponent + { + return await _passiveHtmlRenderer.Dispatcher.InvokeAsync(() => + _passiveHtmlRenderer.RenderComponentAsync(typeof(TComponent), ParameterView.Empty)); + } +} diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs new file mode 100644 index 000000000000..6300c026f5c6 --- /dev/null +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components.HtmlRendering; +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components.Web; + +internal class HtmlRenderingContext +{ + private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" + }; + + private readonly PassiveHtmlRenderer _renderer; + private readonly int _rootComponentId; + private readonly TextWriter _output; + private readonly HtmlEncoder _htmlEncoder; + private string? _closestSelectValueAsString; + + public HtmlRenderingContext(PassiveHtmlRenderer renderer, int componentId, TextWriter output, HtmlEncoder htmlEncoder) + { + _renderer = renderer; + _rootComponentId = componentId; + _output = output; + _htmlEncoder = htmlEncoder; + } + + public void Render() + { + var frames = _renderer.GetCurrentRenderTreeFrames(_rootComponentId); + RenderFrames(frames, 0, frames.Count); + } + + private int RenderFrames(ArrayRange frames, int position, int maxElements) + { + var nextPosition = position; + var endPosition = position + maxElements; + while (position < endPosition) + { + nextPosition = RenderCore(frames, position); + if (position == nextPosition) + { + throw new InvalidOperationException("We didn't consume any input."); + } + position = nextPosition; + } + + return nextPosition; + } + + private int RenderCore( + ArrayRange frames, + int position) + { + ref var frame = ref frames.Array[position]; + switch (frame.FrameType) + { + case RenderTreeFrameType.Element: + return RenderElement(frames, position); + case RenderTreeFrameType.Attribute: + throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}"); + case RenderTreeFrameType.Text: + _htmlEncoder.Encode(_output, frame.TextContent); + return ++position; + case RenderTreeFrameType.Markup: + _output.Write(frame.MarkupContent); + return ++position; + case RenderTreeFrameType.Component: + return RenderChildComponent(frames, position); + case RenderTreeFrameType.Region: + return RenderFrames(frames, position + 1, frame.RegionSubtreeLength - 1); + case RenderTreeFrameType.ElementReferenceCapture: + case RenderTreeFrameType.ComponentReferenceCapture: + return ++position; + default: + throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'."); + } + } + + private int RenderElement( + ArrayRange frames, + int position) + { + ref var frame = ref frames.Array[position]; + _output.Write('<'); + _output.Write(frame.ElementName); + var afterAttributes = RenderAttributes(frames, position + 1, frame.ElementSubtreeLength - 1, out var capturedValueAttribute); + + // When we see an " + + @"" + + @"" + + "" + + @"" + + "

"; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.OpenElement(1, "select"); + rtb.AddAttribute(2, "unrelated-attribute-before", "a"); + rtb.AddAttribute(3, "value", "b"); + rtb.AddAttribute(4, "unrelated-attribute-after", "c"); + + foreach (var optionValue in new[] { "a", "b", "c" }) + { + rtb.OpenElement(5, "option"); + rtb.AddAttribute(6, "unrelated-attribute", "a"); + rtb.AddAttribute(7, "value", optionValue); + rtb.AddContent(8, $"Pick value {optionValue}"); + rtb.CloseElement(); // option + } + + rtb.CloseElement(); // select + + rtb.OpenElement(9, "option"); // To show other value-matching options don't get marked as selected + rtb.AddAttribute(10, "value", "b"); + rtb.AddContent(11, "unrelated option"); + rtb.CloseElement(); // option + + rtb.CloseElement(); // p + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_RendersValueAttributeAsTextContentOfTextareaElement() + { + // Arrange + var expectedHtml = ""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "textarea"); + rtb.AddAttribute(1, "value", "Hello -encoded content!"); + rtb.AddAttribute(2, "rows", "10"); + rtb.AddAttribute(3, "cols", "20"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttribute() + { + // Arrange + var expectedHtml = ""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "textarea"); + rtb.AddAttribute(1, "rows", "10"); + rtb.AddAttribute(2, "cols", "20"); + rtb.AddContent(3, "Hello -encoded content!"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttributeOrTextContent() + { + // Arrange + var expectedHtml = ""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "textarea"); + rtb.AddAttribute(1, "rows", "10"); + rtb.AddAttribute(2, "cols", "20"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_ValueAttributeOfTextareaElementOverridesTextContent() + { + // Arrange + var expectedHtml = ""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "textarea"); + rtb.AddAttribute(1, "value", "Hello World!"); + rtb.AddContent(3, "Some content"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_RendersSelfClosingElement() + { + // Arrange + var expectedHtml = ""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "input"); + rtb.AddAttribute(1, "value", "Hello -encoded content!"); + rtb.AddAttribute(2, "id", "Test"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_RendersSelfClosingElementWithTextComponentAsNormalElement() + { + // Arrange + var expectedHtml = "Something"; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "meta"); + rtb.AddContent(1, "Something"); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_RendersSelfClosingElementBySkippingElementReferenceCapture() + { + // Arrange + var expectedHtml = ""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "input"); + rtb.AddAttribute(1, "value", "Hello -encoded content!"); + rtb.AddAttribute(2, "id", "Test"); + rtb.AddElementReferenceCapture(3, inputReference => _ = inputReference); + rtb.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_MarksSelectedOptionsAsSelected_WithOptGroups() + { + // Arrange + var expectedHtml = + @""; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "select"); + rtb.AddAttribute(1, "value", "beta"); + + foreach (var optionValue in new[] { "alpha", "beta", "gamma" }) + { + rtb.OpenElement(2, "optgroup"); + rtb.OpenElement(3, "option"); + rtb.AddAttribute(4, "value", optionValue); + rtb.AddContent(5, optionValue); + rtb.CloseElement(); // option + rtb.CloseElement(); // optgroup + } + + rtb.CloseElement(); // select + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "span", ">", "Hello world!", "", "", + "<", "span", ">", "Child content!", "" + }; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.OpenElement(1, "span"); + rtb.AddContent(2, "Hello world!"); + rtb.CloseElement(); + rtb.CloseElement(); + rtb.OpenComponent(3, typeof(ChildComponent)); + rtb.AddAttribute(4, "Value", "Child content!"); + rtb.CloseComponent(); + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_ComponentReferenceNoops() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "span", ">", "Hello world!", "", "", + "<", "span", ">", "Child content!", "" + }; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.OpenElement(1, "span"); + rtb.AddContent(2, "Hello world!"); + rtb.CloseElement(); + rtb.CloseElement(); + rtb.OpenComponent(3, typeof(ChildComponent)); + rtb.AddAttribute(4, "Value", "Child content!"); + rtb.AddComponentReferenceCapture(5, cr => { }); + rtb.CloseComponent(); + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_CanPassParameters() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "input", " ", "value", "=", "\"", "5", "\"", " />", "" }; + + RenderFragment Content(ParameterView pc) => new RenderFragment((RenderTreeBuilder rtb) => + { + rtb.OpenElement(0, "p"); + rtb.OpenElement(1, "input"); + rtb.AddAttribute(2, "change", pc.GetValueOrDefault>("update")); + rtb.AddAttribute(3, "value", pc.GetValueOrDefault("value")); + rtb.CloseElement(); + rtb.CloseElement(); + }); + + var serviceProvider = new ServiceCollection() + .AddSingleton(new Func(Content)) + .BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + Action change = (ChangeEventArgs changeArgs) => throw new InvalidOperationException(); + + // Act + var result = await htmlRenderer.RenderComponentAsync( + ParameterView.FromDictionary(new Dictionary + { + { "update", change }, + { "value", 5 } + })); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_CanRenderComponentAsyncWithRenderFragmentContent() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "<", "span", ">", "Hello world!", "", "" }; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.OpenElement(1, "span"); + rtb.AddContent(2, + // This internally creates a region frame. + rf => rf.AddContent(0, "Hello world!")); + rtb.CloseElement(); + rtb.CloseElement(); + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_ElementRefsNoops() + { + // Arrange + var expectedHtml = new[] + { + "<", "p", ">", "<", "span", ">", "Hello world!", "", "" + }; + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => + { + rtb.OpenElement(0, "p"); + rtb.AddElementReferenceCapture(1, er => { }); + rtb.OpenElement(2, "span"); + rtb.AddContent(3, + // This internally creates a region frame. + rf => rf.AddContent(0, "Hello world!")); + rtb.CloseElement(); + rtb.CloseElement(); + })).BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + private class ComponentWithParameters : IComponent + { + public RenderHandle RenderHandle { get; private set; } + + public void Attach(RenderHandle renderHandle) + { + RenderHandle = renderHandle; + } + + [Inject] + Func CreateRenderFragment { get; set; } + + public Task SetParametersAsync(ParameterView parameters) + { + RenderHandle.Render(CreateRenderFragment(parameters)); + return Task.CompletedTask; + } + } + + [Fact] + public async Task CanRender_AsyncComponent() + { + // Arrange + var expectedHtml = new[] { + "<", "p", ">", "20", "" }; + var serviceProvider = new ServiceCollection().AddSingleton().BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + ["Value"] = 10 + })); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task CanRender_NestedAsyncComponents() + { + // Arrange + var expectedHtml = new[] + { + "<", "p", ">", "20", "", + "<", "p", ">", "80", "" + }; + + var serviceProvider = new ServiceCollection().AddSingleton().BuildServiceProvider(); + + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + ["Nested"] = false, + ["Value"] = 10 + })); + + // Assert + AssertHtmlContentEquals(expectedHtml, result); + } + + [Fact] + public async Task RenderComponentAsync_CanCauseRerenderingOfEarlierComponents() + { + // This scenario is important when there are multiple root components. The default project + // template relies on this - HeadOutlet re-renders when a later PageTitle component is rendered, + // even though they are not within the same root component. + + // Arrange/Act/Assert 1: initially get some empty output + var renderer = GetHtmlRenderer(); + var first = await renderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(SectionOutlet.Name), "testsection" } + })); + + Assert.Empty(first.ToHtmlString()); + + // Act/Assert 2: cause it to be updated + var second = await renderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(SectionContent.Name), "testsection" }, + { nameof(SectionContent.ChildContent), (RenderFragment)(builder => + { + builder.AddContent(0, "Hello from the section content provider"); + }) + } + })); + + Assert.Empty(second.ToHtmlString()); + Assert.Equal("Hello from the section content provider", first.ToHtmlString()); + } - HtmlRenderer CreateTestHtmlRenderer() + void AssertHtmlContentEquals(IEnumerable expected, HtmlContent actual) + => AssertHtmlContentEquals(string.Join(string.Empty, expected), actual); + + void AssertHtmlContentEquals(string expected, HtmlContent actual) { - var services = new ServiceCollection(); - services.AddLogging(); + Assert.Equal(expected, actual.ToHtmlString()); + } + + private class NestedAsyncComponent : ComponentBase + { + [Parameter] public bool Nested { get; set; } + [Parameter] public int Value { get; set; } + + protected override async Task OnInitializedAsync() + { + Value = Value * 2; + await Task.Yield(); + } - var serviceProvider = services.BuildServiceProvider(); - return new HtmlRenderer(serviceProvider); + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "p"); + builder.AddContent(1, Value.ToString(CultureInfo.InvariantCulture)); + builder.CloseElement(); + if (!Nested) + { + builder.OpenComponent(2); + builder.AddAttribute(3, "Nested", true); + builder.AddAttribute(4, "Value", Value * 2); + builder.CloseComponent(); + } + } } - class SimpleComponent : IComponent // Using IComponent directly in at least some tests to show we don't rely on ComponentBase + private class AsyncComponent : ComponentBase + { + public AsyncComponent() + { + } + + [Parameter] + public int Value { get; set; } + + protected override async Task OnInitializedAsync() + { + Value = Value * 2; + await Task.Delay(Value * 100); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "p"); + builder.AddContent(1, Value.ToString(CultureInfo.InvariantCulture)); + builder.CloseElement(); + } + } + + private class ChildComponent : IComponent { private RenderHandle _renderHandle; - [Parameter] public string Name { get; set; } = "world"; + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + public Task SetParametersAsync(ParameterView parameters) + { + var content = parameters.GetValueOrDefault("Value"); + _renderHandle.Render(CreateRenderFragment(content)); + return Task.CompletedTask; + } + + private RenderFragment CreateRenderFragment(string content) + { + return RenderFragment; + + void RenderFragment(RenderTreeBuilder rtb) + { + rtb.OpenElement(1, "span"); + rtb.AddContent(2, content); + rtb.CloseElement(); + } + } + } + + private class TestComponent : IComponent + { + private RenderHandle _renderHandle; + + [Inject] + public RenderFragment Fragment { get; set; } public void Attach(RenderHandle renderHandle) { @@ -53,9 +842,25 @@ public void Attach(RenderHandle renderHandle) public Task SetParametersAsync(ParameterView parameters) { - parameters.SetParameterProperties(this); - _renderHandle.Render(builder => builder.AddContent(0, $"Hello, {Name}!")); + _renderHandle.Render(Fragment); return Task.CompletedTask; } } + + // TODO: Test cases to specify the exact asynchrony/quiescence behaviors of RenderComponentAsync. + // TODO: Test cases showing the exception-handling behaviors. + // TODO: Support output to a some kind of stream or writer. + + HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider = null) + { + if (serviceProvider is null) + { + var services = new ServiceCollection(); + services.AddLogging(); + + serviceProvider = services.BuildServiceProvider(); + } + + return new HtmlRenderer(serviceProvider, NullLoggerFactory.Instance); + } } From ff4ee435d8c703734fcab3d4bc9dc00c5eb6bc8c Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 13:20:19 +0000 Subject: [PATCH 05/26] Tidy --- .../test/HtmlRendering/HtmlRendererTest.cs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index cb87fe11fb6f..22986dc1332a 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -24,7 +24,7 @@ public async Task RenderComponentAsync_CanRenderEmptyElement() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert Assert.Equal("

", result.ToHtmlString()); @@ -44,7 +44,7 @@ public async Task RenderComponentAsync_CanRenderSimpleComponent() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -64,7 +64,7 @@ public async Task RenderComponentAsync_HtmlEncodesContent() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -84,7 +84,7 @@ public async Task RenderComponentAsync_DoesNotEncodeMarkup() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -106,7 +106,7 @@ public async Task RenderComponentAsync_CanRenderWithAttributes() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -140,7 +140,7 @@ public async Task RenderComponentAsync_SkipsDuplicatedAttribute() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -162,7 +162,7 @@ public async Task RenderComponentAsync_HtmlEncodesAttributeValues() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -183,7 +183,7 @@ public async Task RenderComponentAsync_CanRenderBooleanAttributes() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -204,7 +204,7 @@ public async Task RenderComponentAsync_DoesNotRenderBooleanAttributesWhenValueIs var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -227,7 +227,7 @@ public async Task RenderComponentAsync_CanRenderWithChildren() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -257,7 +257,7 @@ public async Task RenderComponentAsync_CanRenderWithMultipleChildren() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -305,7 +305,7 @@ public async Task RenderComponentAsync_MarksSelectedOptionsAsSelected() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -327,7 +327,7 @@ public async Task RenderComponentAsync_RendersValueAttributeAsTextContentOfTexta var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -349,7 +349,7 @@ public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttribu var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -370,7 +370,7 @@ public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttribu var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -391,7 +391,7 @@ public async Task RenderComponentAsync_ValueAttributeOfTextareaElementOverridesT var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -412,7 +412,7 @@ public async Task RenderComponentAsync_RendersSelfClosingElement() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -432,7 +432,7 @@ public async Task RenderComponentAsync_RendersSelfClosingElementWithTextComponen var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -454,7 +454,7 @@ public async Task RenderComponentAsync_RendersSelfClosingElementBySkippingElemen var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -491,7 +491,7 @@ public async Task RenderComponentAsync_MarksSelectedOptionsAsSelected_WithOptGro var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -520,7 +520,7 @@ public async Task RenderComponentAsync_CanRenderComponentAsyncWithChildrenCompon var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -550,7 +550,7 @@ public async Task RenderComponentAsync_ComponentReferenceNoops() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -612,7 +612,7 @@ public async Task RenderComponentAsync_CanRenderComponentAsyncWithRenderFragment var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); @@ -641,7 +641,7 @@ public async Task RenderComponentAsync_ElementRefsNoops() var htmlRenderer = GetHtmlRenderer(serviceProvider); // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.Empty); + var result = await htmlRenderer.RenderComponentAsync(); // Assert AssertHtmlContentEquals(expectedHtml, result); From 56ea411cbd95e7c24659f17722dfa60ff6331c3f Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 13:48:20 +0000 Subject: [PATCH 06/26] Output to TextWriter --- .../Web/src/HtmlRendering/HtmlContent.cs | 10 +++++-- .../src/HtmlRendering/HtmlRenderingContext.cs | 30 +++++++++++-------- .../Web/src/PublicAPI.Unshipped.txt | 1 + .../test/HtmlRendering/HtmlRendererTest.cs | 26 +++++++++++++++- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs index dcf1b92902aa..c29a1a5fdc75 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components.HtmlRendering; namespace Microsoft.AspNetCore.Components.Web; @@ -27,7 +26,14 @@ internal HtmlContent(PassiveHtmlRenderer renderer, int componentId) public string ToHtmlString() { using var writer = new StringWriter(); - new HtmlRenderingContext(_renderer, _componentId, writer, HtmlEncoder.Default).Render(); + WriteTo(writer); return writer.ToString(); } + + /// + /// Writes the component's latest output as HTML to the specified writer. + /// + /// The output destination. + public void WriteTo(TextWriter output) + => HtmlRenderingContext.Render(_renderer, _componentId, output); } diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs index 8fb1c1ae7101..4cec124fef69 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs @@ -8,31 +8,30 @@ namespace Microsoft.AspNetCore.Components.Web; -internal class HtmlRenderingContext +// This is OK to be a struct because it never gets passed around anywhere. External code can't even get an instance +// of it. It just keeps track of some contextual information during a single synchronous HTML output operation. +internal ref struct HtmlRenderingContext { private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase) { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" }; + private static readonly HtmlEncoder _htmlEncoder = HtmlEncoder.Default; private readonly PassiveHtmlRenderer _renderer; - private readonly int _rootComponentId; private readonly TextWriter _output; - private readonly HtmlEncoder _htmlEncoder; private string? _closestSelectValueAsString; - public HtmlRenderingContext(PassiveHtmlRenderer renderer, int componentId, TextWriter output, HtmlEncoder htmlEncoder) + public static void Render(PassiveHtmlRenderer renderer, int componentId, TextWriter output) { - _renderer = renderer; - _rootComponentId = componentId; - _output = output; - _htmlEncoder = htmlEncoder; + var context = new HtmlRenderingContext(renderer, output); + context.RenderComponent(componentId); } - public void Render() + private HtmlRenderingContext(PassiveHtmlRenderer renderer, TextWriter output) { - var frames = _renderer.GetCurrentRenderTreeFrames(_rootComponentId); - RenderFrames(frames, 0, frames.Count); + _renderer = renderer; + _output = output; } private int RenderFrames(ArrayRange frames, int position, int maxElements) @@ -221,12 +220,17 @@ private int RenderChildren(ArrayRange frames, int position, int return RenderFrames(frames, position, maxElements); } + private void RenderComponent(int componentId) + { + var frames = _renderer.GetCurrentRenderTreeFrames(componentId); + RenderFrames(frames, 0, frames.Count); + } + private int RenderChildComponent(ArrayRange frames, int position) { ref var frame = ref frames.Array[position]; - var childFrames = _renderer.GetCurrentRenderTreeFrames(frame.ComponentId); - RenderFrames(childFrames, 0, childFrames.Count); + RenderComponent(frame.ComponentId); return position + frame.ComponentSubtreeLength; } diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index d5e25ebe3466..5b048778c814 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ #nullable enable Microsoft.AspNetCore.Components.Web.HtmlContent Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlString() -> string! +Microsoft.AspNetCore.Components.Web.HtmlContent.WriteTo(System.IO.TextWriter! output) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Web.HtmlRenderer.HtmlRenderer(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index 22986dc1332a..81bec56685a2 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Text; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Sections; using Microsoft.AspNetCore.Components.Web; @@ -742,6 +743,30 @@ public async Task RenderComponentAsync_CanCauseRerenderingOfEarlierComponents() Assert.Equal("Hello from the section content provider", first.ToHtmlString()); } + [Fact] + public async Task RenderComponentAsync_CanOutputToTextWriter() + { + // Arrange + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(builder => + { + builder.OpenElement(0, "p"); + builder.AddContent(1, "Hey!"); + builder.CloseElement(); + })).BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + using var ms = new MemoryStream(); + using var writer = new StreamWriter(ms, new UTF8Encoding(false)); + + // Act + var result = await htmlRenderer.RenderComponentAsync(); + result.WriteTo(writer); + writer.Flush(); + + // Assert + var actual = Encoding.UTF8.GetString(ms.ToArray()); + Assert.Equal("

Hey!

", actual); + } + void AssertHtmlContentEquals(IEnumerable expected, HtmlContent actual) => AssertHtmlContentEquals(string.Join(string.Empty, expected), actual); @@ -849,7 +874,6 @@ public Task SetParametersAsync(ParameterView parameters) // TODO: Test cases to specify the exact asynchrony/quiescence behaviors of RenderComponentAsync. // TODO: Test cases showing the exception-handling behaviors. - // TODO: Support output to a some kind of stream or writer. HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider = null) { From 3e8e5106c1a79b106932005f7178294415bc3817 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 13:57:25 +0000 Subject: [PATCH 07/26] Renames for clarity --- src/Components/Web/src/HtmlRendering/HtmlContent.cs | 2 +- .../{HtmlRenderingContext.cs => HtmlContentWriter.cs} | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/Components/Web/src/HtmlRendering/{HtmlRenderingContext.cs => HtmlContentWriter.cs} (96%) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs index c29a1a5fdc75..274d2ae11c4a 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -35,5 +35,5 @@ public string ToHtmlString() /// /// The output destination. public void WriteTo(TextWriter output) - => HtmlRenderingContext.Render(_renderer, _componentId, output); + => HtmlContentWriter.Write(_renderer, _componentId, output); } diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs b/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs similarity index 96% rename from src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs rename to src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs index 4cec124fef69..62692175df1c 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderingContext.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs @@ -8,9 +8,9 @@ namespace Microsoft.AspNetCore.Components.Web; -// This is OK to be a struct because it never gets passed around anywhere. External code can't even get an instance +// This is OK to be a struct because it never gets passed around anywhere. Other code can't even get an instance // of it. It just keeps track of some contextual information during a single synchronous HTML output operation. -internal ref struct HtmlRenderingContext +internal ref struct HtmlContentWriter { private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -22,13 +22,13 @@ internal ref struct HtmlRenderingContext private readonly TextWriter _output; private string? _closestSelectValueAsString; - public static void Render(PassiveHtmlRenderer renderer, int componentId, TextWriter output) + public static void Write(PassiveHtmlRenderer renderer, int componentId, TextWriter output) { - var context = new HtmlRenderingContext(renderer, output); + var context = new HtmlContentWriter(renderer, output); context.RenderComponent(componentId); } - private HtmlRenderingContext(PassiveHtmlRenderer renderer, TextWriter output) + private HtmlContentWriter(PassiveHtmlRenderer renderer, TextWriter output) { _renderer = renderer; _output = output; From 203dd8dee691a9ef4c6d9a2d9abb1b1376dc4903 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 14:00:01 +0000 Subject: [PATCH 08/26] Another rename --- src/Components/Web/src/HtmlRendering/HtmlContent.cs | 4 ++-- src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs | 6 +++--- src/Components/Web/src/HtmlRendering/HtmlRenderer.cs | 4 ++-- .../{PassiveHtmlRenderer.cs => HtmlRendererCore.cs} | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/Components/Web/src/HtmlRendering/{PassiveHtmlRenderer.cs => HtmlRendererCore.cs} (92%) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs index 274d2ae11c4a..020d8e4cbcb2 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -10,10 +10,10 @@ namespace Microsoft.AspNetCore.Components.Web; /// public sealed class HtmlContent { - private readonly PassiveHtmlRenderer _renderer; + private readonly HtmlRendererCore _renderer; private readonly int _componentId; - internal HtmlContent(PassiveHtmlRenderer renderer, int componentId) + internal HtmlContent(HtmlRendererCore renderer, int componentId) { _renderer = renderer; _componentId = componentId; diff --git a/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs b/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs index 62692175df1c..6b87df484570 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs @@ -18,17 +18,17 @@ internal ref struct HtmlContentWriter }; private static readonly HtmlEncoder _htmlEncoder = HtmlEncoder.Default; - private readonly PassiveHtmlRenderer _renderer; + private readonly HtmlRendererCore _renderer; private readonly TextWriter _output; private string? _closestSelectValueAsString; - public static void Write(PassiveHtmlRenderer renderer, int componentId, TextWriter output) + public static void Write(HtmlRendererCore renderer, int componentId, TextWriter output) { var context = new HtmlContentWriter(renderer, output); context.RenderComponent(componentId); } - private HtmlContentWriter(PassiveHtmlRenderer renderer, TextWriter output) + private HtmlContentWriter(HtmlRendererCore renderer, TextWriter output) { _renderer = renderer; _output = output; diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index cec1300dbd68..365d1b2f2fef 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.Web; /// public class HtmlRenderer : IAsyncDisposable { - private readonly PassiveHtmlRenderer _passiveHtmlRenderer; + private readonly HtmlRendererCore _passiveHtmlRenderer; /// /// Constructs an instance of . @@ -23,7 +23,7 @@ public class HtmlRenderer : IAsyncDisposable public HtmlRenderer(IServiceProvider services, ILoggerFactory loggerFactory) { var componentActivator = services.GetService() ?? DefaultComponentActivator.Instance; - _passiveHtmlRenderer = new PassiveHtmlRenderer(services, loggerFactory, componentActivator); + _passiveHtmlRenderer = new HtmlRendererCore(services, loggerFactory, componentActivator); } /// diff --git a/src/Components/Web/src/HtmlRendering/PassiveHtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs similarity index 92% rename from src/Components/Web/src/HtmlRendering/PassiveHtmlRenderer.cs rename to src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs index bc4369fb106c..7bfd0f4ba762 100644 --- a/src/Components/Web/src/HtmlRendering/PassiveHtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs @@ -9,11 +9,11 @@ namespace Microsoft.AspNetCore.Components.HtmlRendering; -internal sealed class PassiveHtmlRenderer : Renderer +internal sealed class HtmlRendererCore : Renderer { private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true)); - public PassiveHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IComponentActivator componentActivator) + public HtmlRendererCore(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IComponentActivator componentActivator) : base(serviceProvider, loggerFactory, componentActivator) { } From d60b942533e2bfee5b0703c322be57353888b247 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 14:31:36 +0000 Subject: [PATCH 09/26] Ensure we're on renderer sync context when HTMLifying --- .../Web/src/HtmlRendering/HtmlContent.cs | 15 ++-- .../src/HtmlRendering/HtmlContentWriter.cs | 4 ++ .../Web/src/PublicAPI.Unshipped.txt | 4 +- .../test/HtmlRendering/HtmlRendererTest.cs | 71 ++++++++++--------- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs index 020d8e4cbcb2..a9b1f402dff0 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -22,11 +22,11 @@ internal HtmlContent(HtmlRendererCore renderer, int componentId) /// /// Returns an HTML string representation of the component's latest output. /// - /// An HTML string. - public string ToHtmlString() + /// A task that completes with the HTML string. + public async Task ToHtmlStringAsync() { using var writer = new StringWriter(); - WriteTo(writer); + await WriteToAsync(writer); return writer.ToString(); } @@ -34,6 +34,11 @@ public string ToHtmlString() /// Writes the component's latest output as HTML to the specified writer. /// /// The output destination. - public void WriteTo(TextWriter output) - => HtmlContentWriter.Write(_renderer, _componentId, output); + /// A task representing the completion of the operation. + public Task WriteToAsync(TextWriter output) => _renderer.Dispatcher.InvokeAsync(() => + { + // The HTML-stringification process itself is synchronous, but WriteToAsync needs to be + // async because we have to dispatch to the renderer's sync context. + HtmlContentWriter.Write(_renderer, _componentId, output); + }); } diff --git a/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs b/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs index 6b87df484570..4b7066830b23 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs @@ -24,6 +24,10 @@ internal ref struct HtmlContentWriter public static void Write(HtmlRendererCore renderer, int componentId, TextWriter output) { + // We're about to walk over some buffers inside the renderer that can be mutated during rendering. + // So, we require exclusive access to the renderer during this synchronous process. + renderer.Dispatcher.AssertAccess(); + var context = new HtmlContentWriter(renderer, output); context.RenderComponent(componentId); } diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 5b048778c814..84e38e1c6ccb 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ #nullable enable Microsoft.AspNetCore.Components.Web.HtmlContent -Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlString() -> string! -Microsoft.AspNetCore.Components.Web.HtmlContent.WriteTo(System.IO.TextWriter! output) -> void +Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlStringAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlContent.WriteToAsync(System.IO.TextWriter! output) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Web.HtmlRenderer.HtmlRenderer(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index 81bec56685a2..ea6d86a1b7c9 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -28,7 +28,7 @@ public async Task RenderComponentAsync_CanRenderEmptyElement() var result = await htmlRenderer.RenderComponentAsync(); // Assert - Assert.Equal("

", result.ToHtmlString()); + Assert.Equal("

", await result.ToHtmlStringAsync()); } [Fact] @@ -48,7 +48,7 @@ public async Task RenderComponentAsync_CanRenderSimpleComponent() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -68,7 +68,7 @@ public async Task RenderComponentAsync_HtmlEncodesContent() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -88,7 +88,7 @@ public async Task RenderComponentAsync_DoesNotEncodeMarkup() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -110,7 +110,7 @@ public async Task RenderComponentAsync_CanRenderWithAttributes() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -144,7 +144,7 @@ public async Task RenderComponentAsync_SkipsDuplicatedAttribute() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -166,7 +166,7 @@ public async Task RenderComponentAsync_HtmlEncodesAttributeValues() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -187,7 +187,7 @@ public async Task RenderComponentAsync_CanRenderBooleanAttributes() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -208,7 +208,7 @@ public async Task RenderComponentAsync_DoesNotRenderBooleanAttributesWhenValueIs var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -231,7 +231,7 @@ public async Task RenderComponentAsync_CanRenderWithChildren() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -261,7 +261,7 @@ public async Task RenderComponentAsync_CanRenderWithMultipleChildren() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -309,7 +309,7 @@ public async Task RenderComponentAsync_MarksSelectedOptionsAsSelected() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -331,7 +331,7 @@ public async Task RenderComponentAsync_RendersValueAttributeAsTextContentOfTexta var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -353,7 +353,7 @@ public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttribu var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -374,7 +374,7 @@ public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttribu var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -395,7 +395,7 @@ public async Task RenderComponentAsync_ValueAttributeOfTextareaElementOverridesT var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -416,7 +416,7 @@ public async Task RenderComponentAsync_RendersSelfClosingElement() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -436,7 +436,7 @@ public async Task RenderComponentAsync_RendersSelfClosingElementWithTextComponen var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -458,7 +458,7 @@ public async Task RenderComponentAsync_RendersSelfClosingElementBySkippingElemen var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -495,7 +495,7 @@ public async Task RenderComponentAsync_MarksSelectedOptionsAsSelected_WithOptGro var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -524,7 +524,7 @@ public async Task RenderComponentAsync_CanRenderComponentAsyncWithChildrenCompon var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -554,7 +554,7 @@ public async Task RenderComponentAsync_ComponentReferenceNoops() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -590,7 +590,7 @@ public async Task RenderComponentAsync_CanPassParameters() })); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -616,7 +616,7 @@ public async Task RenderComponentAsync_CanRenderComponentAsyncWithRenderFragment var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -645,7 +645,7 @@ public async Task RenderComponentAsync_ElementRefsNoops() var result = await htmlRenderer.RenderComponentAsync(); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } private class ComponentWithParameters : IComponent @@ -684,7 +684,7 @@ public async Task CanRender_AsyncComponent() })); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -709,7 +709,7 @@ public async Task CanRender_NestedAsyncComponents() })); // Assert - AssertHtmlContentEquals(expectedHtml, result); + await AssertHtmlContentEqualsAsync(expectedHtml, result); } [Fact] @@ -726,7 +726,7 @@ public async Task RenderComponentAsync_CanCauseRerenderingOfEarlierComponents() { nameof(SectionOutlet.Name), "testsection" } })); - Assert.Empty(first.ToHtmlString()); + Assert.Empty(await first.ToHtmlStringAsync()); // Act/Assert 2: cause it to be updated var second = await renderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary @@ -739,8 +739,8 @@ public async Task RenderComponentAsync_CanCauseRerenderingOfEarlierComponents() } })); - Assert.Empty(second.ToHtmlString()); - Assert.Equal("Hello from the section content provider", first.ToHtmlString()); + Assert.Empty(await second.ToHtmlStringAsync()); + Assert.Equal("Hello from the section content provider", await first.ToHtmlStringAsync()); } [Fact] @@ -759,7 +759,7 @@ public async Task RenderComponentAsync_CanOutputToTextWriter() // Act var result = await htmlRenderer.RenderComponentAsync(); - result.WriteTo(writer); + await result.WriteToAsync(writer); writer.Flush(); // Assert @@ -767,12 +767,13 @@ public async Task RenderComponentAsync_CanOutputToTextWriter() Assert.Equal("

Hey!

", actual); } - void AssertHtmlContentEquals(IEnumerable expected, HtmlContent actual) - => AssertHtmlContentEquals(string.Join(string.Empty, expected), actual); + Task AssertHtmlContentEqualsAsync(IEnumerable expected, HtmlContent actual) + => AssertHtmlContentEqualsAsync(string.Join(string.Empty, expected), actual); - void AssertHtmlContentEquals(string expected, HtmlContent actual) + async Task AssertHtmlContentEqualsAsync(string expected, HtmlContent actual) { - Assert.Equal(expected, actual.ToHtmlString()); + var actualHtml = await actual.ToHtmlStringAsync(); + Assert.Equal(expected, actualHtml); } private class NestedAsyncComponent : ComponentBase From b6881f76215e6db99e955d6eaa80e5744d208e0b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 14:54:15 +0000 Subject: [PATCH 10/26] Allow the caller optionally to render the state before and after quiescence. --- .../Web/src/HtmlRendering/HtmlContent.cs | 8 +++- .../Web/src/HtmlRendering/HtmlRenderer.cs | 31 +++++++++++--- .../Web/src/HtmlRendering/HtmlRendererCore.cs | 13 ++++-- .../Web/src/PublicAPI.Unshipped.txt | 3 ++ .../test/HtmlRendering/HtmlRendererTest.cs | 40 +++++++++++++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs index a9b1f402dff0..ae2e8bddb80a 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -13,12 +13,18 @@ public sealed class HtmlContent private readonly HtmlRendererCore _renderer; private readonly int _componentId; - internal HtmlContent(HtmlRendererCore renderer, int componentId) + internal HtmlContent(HtmlRendererCore renderer, int componentId, Task quiescenceTask) { _renderer = renderer; _componentId = componentId; + QuiescenceTask = quiescenceTask; } + /// + /// A that completes when the component hierarchy has completed asynchronous tasks such as loading. + /// + public Task QuiescenceTask { get; } + /// /// Returns an HTML string representation of the component's latest output. /// diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index 365d1b2f2fef..ffa541fd8dcf 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -34,19 +34,40 @@ public ValueTask DisposeAsync() /// Adds an instance of the specified component and instructs it to render. /// /// The component type. - /// A instance representing the render output. + /// A task that completes with instance representing the render output. public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent - => RenderComponentAsync(ParameterView.Empty); + => RenderComponentAsync(ParameterView.Empty, awaitQuiescence: true); /// /// Adds an instance of the specified component and instructs it to render. /// /// The component type. - /// A instance representing the render output. - public async Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( + /// An flag indicating whether or not to wait for the component hierarchy to complete asynchronous tasks such as loading. + /// A task that completes with instance representing the render output. + public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(bool awaitQuiescence) where TComponent : IComponent + => RenderComponentAsync(ParameterView.Empty, awaitQuiescence); + + /// + /// Adds an instance of the specified component and instructs it to render. + /// + /// The component type. + /// Parameters for the component. + /// A task that completes with instance representing the render output. + public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( ParameterView parameters) where TComponent : IComponent + => RenderComponentAsync(parameters, awaitQuiescence: true); + + /// + /// Adds an instance of the specified component and instructs it to render. + /// + /// The component type. + /// Parameters for the component. + /// An flag indicating whether or not to wait for the component hierarchy to complete asynchronous tasks such as loading. Defaults to true. + /// A task that completes with instance representing the render output. + public async Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( + ParameterView parameters, bool awaitQuiescence) where TComponent : IComponent { return await _passiveHtmlRenderer.Dispatcher.InvokeAsync(() => - _passiveHtmlRenderer.RenderComponentAsync(typeof(TComponent), parameters)); + _passiveHtmlRenderer.RenderComponentAsync(typeof(TComponent), parameters, awaitQuiescence)); } } diff --git a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs index 7bfd0f4ba762..335f5a388765 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs @@ -22,13 +22,20 @@ public HtmlRendererCore(IServiceProvider serviceProvider, ILoggerFactory loggerF public async Task RenderComponentAsync( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, - ParameterView initialParameters) + ParameterView initialParameters, + bool awaitQuiescence) { var component = InstantiateComponent(componentType); var componentId = AssignRootComponentId(component); - await RenderRootComponentAsync(componentId, initialParameters); - return new HtmlContent(this, componentId); + var quiescenceTask = RenderRootComponentAsync(componentId, initialParameters); + + if (awaitQuiescence) + { + await quiescenceTask; + } + + return new HtmlContent(this, componentId, quiescenceTask); } protected override void HandleException(Exception exception) diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 84e38e1c6ccb..39aca9fdd94b 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,9 +1,12 @@ #nullable enable Microsoft.AspNetCore.Components.Web.HtmlContent +Microsoft.AspNetCore.Components.Web.HtmlContent.QuiescenceTask.get -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlStringAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlContent.WriteToAsync(System.IO.TextWriter! output) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Web.HtmlRenderer.HtmlRenderer(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(bool awaitQuiescence) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters, bool awaitQuiescence) -> System.Threading.Tasks.Task! diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index ea6d86a1b7c9..30a7e9655fb9 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -767,6 +767,22 @@ public async Task RenderComponentAsync_CanOutputToTextWriter() Assert.Equal("

Hey!

", actual); } + [Fact] + public async Task RenderComponentAsync_CanObserveStateBeforeAndAfterQuiescence() + { + var completionTcs = new TaskCompletionSource(); + var services = new ServiceCollection(); + services.AddSingleton(new AsyncLoadingComponentCompletion { Task = completionTcs.Task }); + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + + var result = await htmlRenderer.RenderComponentAsync(awaitQuiescence: false); + Assert.Equal("Loading...", await result.ToHtmlStringAsync()); + + completionTcs.SetResult(); + await result.QuiescenceTask; + Assert.Equal("Finished loading", await result.ToHtmlStringAsync()); + } + Task AssertHtmlContentEqualsAsync(IEnumerable expected, HtmlContent actual) => AssertHtmlContentEqualsAsync(string.Join(string.Empty, expected), actual); @@ -873,6 +889,30 @@ public Task SetParametersAsync(ParameterView parameters) } } + private class AsyncLoadingComponent : ComponentBase + { + string status; + + [Inject] + public AsyncLoadingComponentCompletion Completion { get; set; } + + protected override async Task OnInitializedAsync() + { + status = "Loading..."; + await Completion.Task; + await Task.Yield(); // So that the test has to await the quiescence task to observe the final outcome + status = "Finished loading"; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + => builder.AddContent(0, status); + } + + private class AsyncLoadingComponentCompletion + { + public Task Task { get; init; } + } + // TODO: Test cases to specify the exact asynchrony/quiescence behaviors of RenderComponentAsync. // TODO: Test cases showing the exception-handling behaviors. From c2c3b6efc4226b0eaaec4fad0a90db0de26d83f0 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 14:57:40 +0000 Subject: [PATCH 11/26] Make the API a bit more idiomatic --- src/Components/Web/src/HtmlRendering/HtmlContent.cs | 9 ++++++--- src/Components/Web/src/PublicAPI.Unshipped.txt | 2 +- .../Web/test/HtmlRendering/HtmlRendererTest.cs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs index ae2e8bddb80a..a010210e7d16 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -12,18 +12,21 @@ public sealed class HtmlContent { private readonly HtmlRendererCore _renderer; private readonly int _componentId; + private readonly Task _quiescenceTask; internal HtmlContent(HtmlRendererCore renderer, int componentId, Task quiescenceTask) { _renderer = renderer; _componentId = componentId; - QuiescenceTask = quiescenceTask; + _quiescenceTask = quiescenceTask; } /// - /// A that completes when the component hierarchy has completed asynchronous tasks such as loading. + /// Obtains a that completes when the component hierarchy has completed asynchronous tasks such as loading. /// - public Task QuiescenceTask { get; } + /// A that completes when the component hierarchy has completed asynchronous tasks such as loading. + public Task WaitForQuiescenceAsync() + => _quiescenceTask; /// /// Returns an HTML string representation of the component's latest output. diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 39aca9fdd94b..967f3a0e5a6a 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ #nullable enable Microsoft.AspNetCore.Components.Web.HtmlContent -Microsoft.AspNetCore.Components.Web.HtmlContent.QuiescenceTask.get -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlStringAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlContent.WaitForQuiescenceAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlContent.WriteToAsync(System.IO.TextWriter! output) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index 30a7e9655fb9..949471f9bf03 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -779,7 +779,7 @@ public async Task RenderComponentAsync_CanObserveStateBeforeAndAfterQuiescence() Assert.Equal("Loading...", await result.ToHtmlStringAsync()); completionTcs.SetResult(); - await result.QuiescenceTask; + await result.WaitForQuiescenceAsync(); Assert.Equal("Finished loading", await result.ToHtmlStringAsync()); } From e13d5ff4a5995cde91b1b76cf11391579ce177fd Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 15:04:22 +0000 Subject: [PATCH 12/26] Update HtmlRendererTest.cs --- src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index 949471f9bf03..df52e52df711 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -913,7 +913,6 @@ private class AsyncLoadingComponentCompletion public Task Task { get; init; } } - // TODO: Test cases to specify the exact asynchrony/quiescence behaviors of RenderComponentAsync. // TODO: Test cases showing the exception-handling behaviors. HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider = null) From 7fe67118b5a01bcc0b4cc2759301bf04c9bfa3ee Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 27 Feb 2023 15:06:24 +0000 Subject: [PATCH 13/26] Make the quiescence test more explicit --- src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index df52e52df711..b7aaa51a819c 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -776,10 +776,12 @@ public async Task RenderComponentAsync_CanObserveStateBeforeAndAfterQuiescence() var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); var result = await htmlRenderer.RenderComponentAsync(awaitQuiescence: false); + var quiescenceTask = result.WaitForQuiescenceAsync(); + Assert.False(quiescenceTask.IsCompleted); Assert.Equal("Loading...", await result.ToHtmlStringAsync()); completionTcs.SetResult(); - await result.WaitForQuiescenceAsync(); + await quiescenceTask; Assert.Equal("Finished loading", await result.ToHtmlStringAsync()); } From 9d90149c73ea9130f561c12c89c097c3734f2d00 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 28 Feb 2023 13:04:29 +0000 Subject: [PATCH 14/26] Tests related to exception handling --- .../test/HtmlRendering/HtmlRendererTest.cs | 119 +++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index b7aaa51a819c..e0b0a3c7053e 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -770,21 +770,106 @@ public async Task RenderComponentAsync_CanOutputToTextWriter() [Fact] public async Task RenderComponentAsync_CanObserveStateBeforeAndAfterQuiescence() { + // Arrange var completionTcs = new TaskCompletionSource(); var services = new ServiceCollection(); services.AddSingleton(new AsyncLoadingComponentCompletion { Task = completionTcs.Task }); var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + // Act/Assert: state before quiescence var result = await htmlRenderer.RenderComponentAsync(awaitQuiescence: false); var quiescenceTask = result.WaitForQuiescenceAsync(); Assert.False(quiescenceTask.IsCompleted); Assert.Equal("Loading...", await result.ToHtmlStringAsync()); + // Act/Assert: state after quiescence completionTcs.SetResult(); await quiescenceTask; Assert.Equal("Finished loading", await result.ToHtmlStringAsync()); } + [Fact] + public async Task RenderComponentAsync_ThrowsSync() + { + // This test is for when the component throws synchronously from the point of view of its own + // rendering flow. The external observer still sees it as async because it has to wait for the + // operation to be dispatched onto the sync context. + + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(new AsyncLoadingComponentCompletion { Task = new TaskCompletionSource().Task }); + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + + // Act/Assert + var ex = await Assert.ThrowsAsync(async () => + { + await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowSync), true } + })); + }); + Assert.Equal("sync", ex.Message); + } + + [Fact] + public async Task RenderComponentAsync_ThrowsAsync() + { + // Arrange + var completionTcs = new TaskCompletionSource(); + var services = new ServiceCollection(); + services.AddSingleton(new AsyncLoadingComponentCompletion { Task = Task.Delay(0) }); + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + + // Act/Assert + var ex = await Assert.ThrowsAsync(() => + htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowAsync), true } + }))); + Assert.Equal("async", ex.Message); + } + + [Fact] + public async Task RenderComponentAsync_ThrowsSyncDuringWaitForQuiescenceAsync() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(new AsyncLoadingComponentCompletion { Task = new TaskCompletionSource().Task }); + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + + // Act/Assert + var content = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowSync), true } + }), awaitQuiescence: false); + + var ex = await Assert.ThrowsAsync(content.WaitForQuiescenceAsync); + Assert.Equal("sync", ex.Message); + } + + [Fact] + public async Task RenderComponentAsync_ThrowsAsyncDuringWaitForQuiescenceAsync() + { + // Arrange + var completionTcs = new TaskCompletionSource(); + var services = new ServiceCollection(); + services.AddSingleton(new AsyncLoadingComponentCompletion { Task = completionTcs.Task }); + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + + // Act/Assert + var content = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowAsync), true } + }), awaitQuiescence: false); + + var ex = await Assert.ThrowsAsync(() => + { + completionTcs.SetResult(); + return content.WaitForQuiescenceAsync(); + }); + Assert.Equal("async", ex.Message); + } + Task AssertHtmlContentEqualsAsync(IEnumerable expected, HtmlContent actual) => AssertHtmlContentEqualsAsync(string.Join(string.Empty, expected), actual); @@ -910,13 +995,43 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) => builder.AddContent(0, status); } + private class ErrorThrowingComponent : ComponentBase + { + [Parameter] public bool ThrowSync { get; set; } + [Parameter] public bool ThrowAsync { get; set; } + + [Inject] + public AsyncLoadingComponentCompletion Completion { get; set; } + + protected override async Task OnParametersSetAsync() + { + await Completion.Task; + await Task.Yield(); + + if (ThrowAsync) + { + throw new InvalidTimeZoneException("async"); + } + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "Hello"); + + if (ThrowSync) + { + throw new InvalidTimeZoneException("sync"); + } + + builder.AddContent(1, "Goodbye"); + } + } + private class AsyncLoadingComponentCompletion { public Task Task { get; init; } } - // TODO: Test cases showing the exception-handling behaviors. - HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider = null) { if (serviceProvider is null) From e3fe72d53f2dd384ce9700b02e421eb356ee7377 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 28 Feb 2023 13:58:16 +0000 Subject: [PATCH 15/26] Change APIs around asynchrony so the caller is responsible for dispatching. Allows the caller more control over fine-grained sync logic. --- .../Web/src/HtmlRendering/HtmlContent.cs | 13 +- .../Web/src/HtmlRendering/HtmlRenderer.cs | 50 +- .../Web/src/HtmlRendering/HtmlRendererCore.cs | 11 +- .../Web/src/PublicAPI.Unshipped.txt | 9 +- .../test/HtmlRendering/HtmlRendererTest.cs | 580 +++++++++++------- 5 files changed, 393 insertions(+), 270 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlContent.cs index a010210e7d16..bc5be397f6b0 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlContent.cs @@ -31,11 +31,11 @@ public Task WaitForQuiescenceAsync() /// /// Returns an HTML string representation of the component's latest output. /// - /// A task that completes with the HTML string. - public async Task ToHtmlStringAsync() + /// An HTML string representation of the component's latest output. + public string ToHtmlString() { using var writer = new StringWriter(); - await WriteToAsync(writer); + WriteTo(writer); return writer.ToString(); } @@ -43,11 +43,8 @@ public async Task ToHtmlStringAsync() /// Writes the component's latest output as HTML to the specified writer. /// /// The output destination. - /// A task representing the completion of the operation. - public Task WriteToAsync(TextWriter output) => _renderer.Dispatcher.InvokeAsync(() => + public void WriteTo(TextWriter output) { - // The HTML-stringification process itself is synchronous, but WriteToAsync needs to be - // async because we have to dispatch to the renderer's sync context. HtmlContentWriter.Write(_renderer, _componentId, output); - }); + } } diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index ffa541fd8dcf..d519d9ab1f69 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -31,43 +31,57 @@ public ValueTask DisposeAsync() => _passiveHtmlRenderer.DisposeAsync(); /// - /// Adds an instance of the specified component and instructs it to render. + /// Gets the associated with this instance. Any calls to + /// or + /// must be performed using this . /// - /// The component type. - /// A task that completes with instance representing the render output. - public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent - => RenderComponentAsync(ParameterView.Empty, awaitQuiescence: true); + public Dispatcher Dispatcher => _passiveHtmlRenderer.Dispatcher; /// - /// Adds an instance of the specified component and instructs it to render. + /// Adds an instance of the specified component and instructs it to render. The resulting content represents the + /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// any asynchronous operations such as loading, use before + /// reading content from the . /// /// The component type. - /// An flag indicating whether or not to wait for the component hierarchy to complete asynchronous tasks such as loading. /// A task that completes with instance representing the render output. - public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(bool awaitQuiescence) where TComponent : IComponent - => RenderComponentAsync(ParameterView.Empty, awaitQuiescence); + public HtmlContent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent + => BeginRenderingComponent(ParameterView.Empty); /// - /// Adds an instance of the specified component and instructs it to render. + /// Adds an instance of the specified component and instructs it to render. The resulting content represents the + /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// any asynchronous operations such as loading, use before + /// reading content from the . /// /// The component type. /// Parameters for the component. /// A task that completes with instance representing the render output. - public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( + public HtmlContent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( ParameterView parameters) where TComponent : IComponent - => RenderComponentAsync(parameters, awaitQuiescence: true); + => _passiveHtmlRenderer.BeginRenderingComponentAsync(typeof(TComponent), parameters); + + /// + /// Adds an instance of the specified component and instructs it to render, waiting + /// for the component hierarchy to complete asynchronous tasks such as loading. + /// + /// The component type. + /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. + public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent + => RenderComponentAsync(ParameterView.Empty); /// - /// Adds an instance of the specified component and instructs it to render. + /// Adds an instance of the specified component and instructs it to render, waiting + /// for the component hierarchy to complete asynchronous tasks such as loading. /// /// The component type. /// Parameters for the component. - /// An flag indicating whether or not to wait for the component hierarchy to complete asynchronous tasks such as loading. Defaults to true. - /// A task that completes with instance representing the render output. + /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. public async Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( - ParameterView parameters, bool awaitQuiescence) where TComponent : IComponent + ParameterView parameters) where TComponent : IComponent { - return await _passiveHtmlRenderer.Dispatcher.InvokeAsync(() => - _passiveHtmlRenderer.RenderComponentAsync(typeof(TComponent), parameters, awaitQuiescence)); + var content = BeginRenderingComponent(parameters); + await content.WaitForQuiescenceAsync(); + return content; } } diff --git a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs index 335f5a388765..bcba022cec4b 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Components.RenderTree; @@ -20,19 +21,17 @@ public HtmlRendererCore(IServiceProvider serviceProvider, ILoggerFactory loggerF public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); - public async Task RenderComponentAsync( + public HtmlContent BeginRenderingComponentAsync( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, - ParameterView initialParameters, - bool awaitQuiescence) + ParameterView initialParameters) { var component = InstantiateComponent(componentType); var componentId = AssignRootComponentId(component); - var quiescenceTask = RenderRootComponentAsync(componentId, initialParameters); - if (awaitQuiescence) + if (quiescenceTask.IsFaulted) { - await quiescenceTask; + ExceptionDispatchInfo.Capture(quiescenceTask.Exception.InnerException ?? quiescenceTask.Exception).Throw(); } return new HtmlContent(this, componentId, quiescenceTask); diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 967f3a0e5a6a..98365d479a74 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,12 +1,13 @@ #nullable enable Microsoft.AspNetCore.Components.Web.HtmlContent -Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlStringAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlString() -> string! Microsoft.AspNetCore.Components.Web.HtmlContent.WaitForQuiescenceAsync() -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.Web.HtmlContent.WriteToAsync(System.IO.TextWriter! output) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlContent.WriteTo(System.IO.TextWriter! output) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer +Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent() -> Microsoft.AspNetCore.Components.Web.HtmlContent! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlContent! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Web.HtmlRenderer.HtmlRenderer(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync() -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(bool awaitQuiescence) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters, bool awaitQuiescence) -> System.Threading.Tasks.Task! diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index e0b0a3c7053e..7858dc6377aa 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -13,6 +13,34 @@ namespace Microsoft.AspNetCore.Components.HtmlRendering; public class HtmlRendererTest { + [Fact] + public async Task RenderComponentAsync_ThrowsIfNotOnSyncContext() + { + // Arrange + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(_ => { })) + .BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + + // Act + var resultTask = htmlRenderer.RenderComponentAsync(); + var ex = await Assert.ThrowsAsync(() => resultTask); + Assert.Contains("The current thread is not associated with the Dispatcher", ex.Message); + } + + [Fact] + public async Task HtmlContent_Write_ThrowsIfNotOnSyncContext() + { + // Arrange + var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(_ => { })) + .BuildServiceProvider(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + var htmlContent = await htmlRenderer.Dispatcher.InvokeAsync(htmlRenderer.BeginRenderingComponent); + + // Act + var ex = Assert.Throws(() => htmlContent.WriteTo(new StringWriter())); + Assert.Contains("The current thread is not associated with the Dispatcher", ex.Message); + } + [Fact] public async Task RenderComponentAsync_CanRenderEmptyElement() { @@ -22,13 +50,16 @@ public async Task RenderComponentAsync_CanRenderEmptyElement() rtb.OpenElement(0, "p"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - Assert.Equal("

", await result.ToHtmlStringAsync()); + // Assert + Assert.Equal("

", result.ToHtmlString()); + }); } [Fact] @@ -42,13 +73,16 @@ public async Task RenderComponentAsync_CanRenderSimpleComponent() rtb.AddContent(1, "Hello world!"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -62,13 +96,16 @@ public async Task RenderComponentAsync_HtmlEncodesContent() rtb.AddContent(1, ""); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -82,13 +119,16 @@ public async Task RenderComponentAsync_DoesNotEncodeMarkup() rtb.AddMarkupContent(1, "Hello world!"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -105,12 +145,14 @@ public async Task RenderComponentAsync_CanRenderWithAttributes() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -119,12 +161,12 @@ public async Task RenderComponentAsync_SkipsDuplicatedAttribute() // Arrange var expectedHtml = new[] { - "<", "p", " ", - "another", "=", "\"", "another-value", "\"", " ", - "Class", "=", "\"", "test2", "\"", ">", - "Hello world!", - "" - }; + "<", "p", " ", + "another", "=", "\"", "another-value", "\"", " ", + "Class", "=", "\"", "test2", "\"", ">", + "Hello world!", + "" + }; var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => { rtb.OpenElement(0, "p"); @@ -139,14 +181,16 @@ public async Task RenderComponentAsync_SkipsDuplicatedAttribute() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } - + [Fact] public async Task RenderComponentAsync_HtmlEncodesAttributeValues() { @@ -161,12 +205,15 @@ public async Task RenderComponentAsync_HtmlEncodesAttributeValues() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { - // Act - var result = await htmlRenderer.RenderComponentAsync(); + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -182,12 +229,14 @@ public async Task RenderComponentAsync_CanRenderBooleanAttributes() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -203,12 +252,14 @@ public async Task RenderComponentAsync_DoesNotRenderBooleanAttributesWhenValueIs })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -226,12 +277,14 @@ public async Task RenderComponentAsync_CanRenderWithChildren() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -239,10 +292,10 @@ public async Task RenderComponentAsync_CanRenderWithMultipleChildren() { // Arrange var expectedHtml = new[] { "<", "p", ">", - "<", "span", ">", "Hello world!", "", - "<", "span", ">", "Bye Bye world!", "", - "" - }; + "<", "span", ">", "Hello world!", "", + "<", "span", ">", "Bye Bye world!", "", + "" + }; var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => { rtb.OpenElement(0, "p"); @@ -256,12 +309,14 @@ public async Task RenderComponentAsync_CanRenderWithMultipleChildren() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -304,12 +359,14 @@ public async Task RenderComponentAsync_MarksSelectedOptionsAsSelected() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -325,13 +382,16 @@ public async Task RenderComponentAsync_RendersValueAttributeAsTextContentOfTexta rtb.AddAttribute(3, "cols", "20"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -347,13 +407,16 @@ public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttribu rtb.AddContent(3, "Hello -encoded content!"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -368,13 +431,16 @@ public async Task RenderComponentAsync_RendersTextareaElementWithoutValueAttribu rtb.AddAttribute(2, "cols", "20"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -389,13 +455,16 @@ public async Task RenderComponentAsync_ValueAttributeOfTextareaElementOverridesT rtb.AddContent(3, "Some content"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -410,13 +479,16 @@ public async Task RenderComponentAsync_RendersSelfClosingElement() rtb.AddAttribute(2, "id", "Test"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -430,13 +502,16 @@ public async Task RenderComponentAsync_RendersSelfClosingElementWithTextComponen rtb.AddContent(1, "Something"); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -452,13 +527,16 @@ public async Task RenderComponentAsync_RendersSelfClosingElementBySkippingElemen rtb.AddElementReferenceCapture(3, inputReference => _ = inputReference); rtb.CloseElement(); })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - // Act - var result = await htmlRenderer.RenderComponentAsync(); + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -490,12 +568,14 @@ public async Task RenderComponentAsync_MarksSelectedOptionsAsSelected_WithOptGro })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -519,12 +599,14 @@ public async Task RenderComponentAsync_CanRenderComponentAsyncWithChildrenCompon })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -549,12 +631,14 @@ public async Task RenderComponentAsync_ComponentReferenceNoops() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -577,20 +661,22 @@ public async Task RenderComponentAsync_CanPassParameters() var serviceProvider = new ServiceCollection() .AddSingleton(new Func(Content)) .BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); Action change = (ChangeEventArgs changeArgs) => throw new InvalidOperationException(); - // Act - var result = await htmlRenderer.RenderComponentAsync( - ParameterView.FromDictionary(new Dictionary - { + var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync( + ParameterView.FromDictionary(new Dictionary + { { "update", change }, { "value", 5 } - })); + })); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -611,12 +697,14 @@ public async Task RenderComponentAsync_CanRenderComponentAsyncWithRenderFragment })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -640,12 +728,14 @@ public async Task RenderComponentAsync_ElementRefsNoops() })).BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } private class ComponentWithParameters : IComponent @@ -676,15 +766,17 @@ public async Task CanRender_AsyncComponent() var serviceProvider = new ServiceCollection().AddSingleton().BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + await htmlRenderer.Dispatcher.InvokeAsync(async () => { - ["Value"] = 10 - })); + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + ["Value"] = 10 + })); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -700,16 +792,18 @@ public async Task CanRender_NestedAsyncComponents() var serviceProvider = new ServiceCollection().AddSingleton().BuildServiceProvider(); var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + await htmlRenderer.Dispatcher.InvokeAsync(async () => { - ["Nested"] = false, - ["Value"] = 10 - })); + // Act + var result = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + ["Nested"] = false, + ["Value"] = 10 + })); - // Assert - await AssertHtmlContentEqualsAsync(expectedHtml, result); + // Assert + AssertHtmlContentEquals(expectedHtml, result); + }); } [Fact] @@ -719,28 +813,31 @@ public async Task RenderComponentAsync_CanCauseRerenderingOfEarlierComponents() // template relies on this - HeadOutlet re-renders when a later PageTitle component is rendered, // even though they are not within the same root component. - // Arrange/Act/Assert 1: initially get some empty output - var renderer = GetHtmlRenderer(); - var first = await renderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + var htmlRenderer = GetHtmlRenderer(); + await htmlRenderer.Dispatcher.InvokeAsync(async () => { - { nameof(SectionOutlet.Name), "testsection" } - })); + // Arrange/Act/Assert 1: initially get some empty output + var first = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(SectionOutlet.Name), "testsection" } + })); - Assert.Empty(await first.ToHtmlStringAsync()); + Assert.Empty(first.ToHtmlString()); - // Act/Assert 2: cause it to be updated - var second = await renderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(SectionContent.Name), "testsection" }, - { nameof(SectionContent.ChildContent), (RenderFragment)(builder => - { - builder.AddContent(0, "Hello from the section content provider"); - }) - } - })); + // Act/Assert 2: cause it to be updated + var second = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(SectionContent.Name), "testsection" }, + { nameof(SectionContent.ChildContent), (RenderFragment)(builder => + { + builder.AddContent(0, "Hello from the section content provider"); + }) + } + })); - Assert.Empty(await second.ToHtmlStringAsync()); - Assert.Equal("Hello from the section content provider", await first.ToHtmlStringAsync()); + Assert.Empty(second.ToHtmlString()); + Assert.Equal("Hello from the section content provider", first.ToHtmlString()); + }); } [Fact] @@ -757,58 +854,63 @@ public async Task RenderComponentAsync_CanOutputToTextWriter() using var ms = new MemoryStream(); using var writer = new StreamWriter(ms, new UTF8Encoding(false)); - // Act - var result = await htmlRenderer.RenderComponentAsync(); - await result.WriteToAsync(writer); - writer.Flush(); - - // Assert - var actual = Encoding.UTF8.GetString(ms.ToArray()); - Assert.Equal("

Hey!

", actual); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act + var result = await htmlRenderer.RenderComponentAsync(); + result.WriteTo(writer); + writer.Flush(); + + // Assert + var actual = Encoding.UTF8.GetString(ms.ToArray()); + Assert.Equal("

Hey!

", actual); + }); } [Fact] - public async Task RenderComponentAsync_CanObserveStateBeforeAndAfterQuiescence() + public async Task BeginRenderingComponent_CanObserveStateBeforeAndAfterQuiescence() { // Arrange var completionTcs = new TaskCompletionSource(); var services = new ServiceCollection(); services.AddSingleton(new AsyncLoadingComponentCompletion { Task = completionTcs.Task }); - var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); - // Act/Assert: state before quiescence - var result = await htmlRenderer.RenderComponentAsync(awaitQuiescence: false); - var quiescenceTask = result.WaitForQuiescenceAsync(); - Assert.False(quiescenceTask.IsCompleted); - Assert.Equal("Loading...", await result.ToHtmlStringAsync()); + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act/Assert: state before quiescence + var result = htmlRenderer.BeginRenderingComponent(); + var quiescenceTask = result.WaitForQuiescenceAsync(); + Assert.False(quiescenceTask.IsCompleted); + Assert.Equal("Loading...", result.ToHtmlString()); - // Act/Assert: state after quiescence - completionTcs.SetResult(); - await quiescenceTask; - Assert.Equal("Finished loading", await result.ToHtmlStringAsync()); + // Act/Assert: state after quiescence + completionTcs.SetResult(); + await quiescenceTask; + Assert.Equal("Finished loading", result.ToHtmlString()); + }); } [Fact] public async Task RenderComponentAsync_ThrowsSync() { - // This test is for when the component throws synchronously from the point of view of its own - // rendering flow. The external observer still sees it as async because it has to wait for the - // operation to be dispatched onto the sync context. - // Arrange var services = new ServiceCollection(); services.AddSingleton(new AsyncLoadingComponentCompletion { Task = new TaskCompletionSource().Task }); - var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); - // Act/Assert - var ex = await Assert.ThrowsAsync(async () => + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + await htmlRenderer.Dispatcher.InvokeAsync(async () => { - await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + // Act/Assert + var ex = await Assert.ThrowsAsync(async () => { - { nameof(ErrorThrowingComponent.ThrowSync), true } - })); + await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowSync), true } + })); + }); + Assert.Equal("sync", ex.Message); }); - Assert.Equal("sync", ex.Message); } [Fact] @@ -818,64 +920,74 @@ public async Task RenderComponentAsync_ThrowsAsync() var completionTcs = new TaskCompletionSource(); var services = new ServiceCollection(); services.AddSingleton(new AsyncLoadingComponentCompletion { Task = Task.Delay(0) }); - var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); - // Act/Assert - var ex = await Assert.ThrowsAsync(() => - htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(ErrorThrowingComponent.ThrowAsync), true } - }))); - Assert.Equal("async", ex.Message); + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + await htmlRenderer.Dispatcher.InvokeAsync(async () => + { + // Act/Assert + var ex = await Assert.ThrowsAsync(() => + htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowAsync), true } + }))); + Assert.Equal("async", ex.Message); + }); } [Fact] - public async Task RenderComponentAsync_ThrowsSyncDuringWaitForQuiescenceAsync() + public async Task BeginRenderingComponent_ThrowsSync() { // Arrange var services = new ServiceCollection(); services.AddSingleton(new AsyncLoadingComponentCompletion { Task = new TaskCompletionSource().Task }); - var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); - // Act/Assert - var content = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + await htmlRenderer.Dispatcher.InvokeAsync(() => { - { nameof(ErrorThrowingComponent.ThrowSync), true } - }), awaitQuiescence: false); - - var ex = await Assert.ThrowsAsync(content.WaitForQuiescenceAsync); - Assert.Equal("sync", ex.Message); + // Act/Assert + var ex = Assert.Throws(() => + { + htmlRenderer.BeginRenderingComponent(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowSync), true } + })); + }); + Assert.Equal("sync", ex.Message); + }); } [Fact] - public async Task RenderComponentAsync_ThrowsAsyncDuringWaitForQuiescenceAsync() + public async Task BeginRenderingComponent_ThrowsAsyncDuringWaitForQuiescenceAsync() { // Arrange var completionTcs = new TaskCompletionSource(); var services = new ServiceCollection(); services.AddSingleton(new AsyncLoadingComponentCompletion { Task = completionTcs.Task }); - var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); - // Act/Assert - var content = await htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary + var htmlRenderer = GetHtmlRenderer(services.BuildServiceProvider()); + await htmlRenderer.Dispatcher.InvokeAsync(async () => { - { nameof(ErrorThrowingComponent.ThrowAsync), true } - }), awaitQuiescence: false); + // Act/Assert + var content = htmlRenderer.BeginRenderingComponent(ParameterView.FromDictionary(new Dictionary + { + { nameof(ErrorThrowingComponent.ThrowAsync), true } + })); - var ex = await Assert.ThrowsAsync(() => - { - completionTcs.SetResult(); - return content.WaitForQuiescenceAsync(); + var ex = await Assert.ThrowsAsync(() => + { + completionTcs.SetResult(); + return content.WaitForQuiescenceAsync(); + }); + Assert.Equal("async", ex.Message); }); - Assert.Equal("async", ex.Message); } - Task AssertHtmlContentEqualsAsync(IEnumerable expected, HtmlContent actual) - => AssertHtmlContentEqualsAsync(string.Join(string.Empty, expected), actual); + void AssertHtmlContentEquals(IEnumerable expected, HtmlContent actual) + => AssertHtmlContentEquals(string.Join(string.Empty, expected), actual); - async Task AssertHtmlContentEqualsAsync(string expected, HtmlContent actual) + void AssertHtmlContentEquals(string expected, HtmlContent actual) { - var actualHtml = await actual.ToHtmlStringAsync(); + var actualHtml = actual.ToHtmlString(); Assert.Equal(expected, actualHtml); } From 4bc113ebba306af35c6bd7453526cffaf7a662fc Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 28 Feb 2023 14:30:53 +0000 Subject: [PATCH 16/26] Renames --- .../{HtmlContent.cs => HtmlComponent.cs} | 10 ++++---- ...ontentWriter.cs => HtmlComponentWriter.cs} | 6 ++--- .../Web/src/HtmlRendering/HtmlRenderer.cs | 24 +++++++++---------- .../Web/src/HtmlRendering/HtmlRendererCore.cs | 4 ++-- .../Web/src/PublicAPI.Unshipped.txt | 8 +++---- .../test/HtmlRendering/HtmlRendererTest.cs | 8 +++---- 6 files changed, 30 insertions(+), 30 deletions(-) rename src/Components/Web/src/HtmlRendering/{HtmlContent.cs => HtmlComponent.cs} (85%) rename src/Components/Web/src/HtmlRendering/{HtmlContentWriter.cs => HtmlComponentWriter.cs} (98%) diff --git a/src/Components/Web/src/HtmlRendering/HtmlContent.cs b/src/Components/Web/src/HtmlRendering/HtmlComponent.cs similarity index 85% rename from src/Components/Web/src/HtmlRendering/HtmlContent.cs rename to src/Components/Web/src/HtmlRendering/HtmlComponent.cs index bc5be397f6b0..931d69ffe726 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlComponent.cs @@ -8,13 +8,13 @@ namespace Microsoft.AspNetCore.Components.Web; /// /// Represents the output of rendering a component as HTML. The content can change if the component instance re-renders. /// -public sealed class HtmlContent +public sealed class HtmlComponent { private readonly HtmlRendererCore _renderer; private readonly int _componentId; private readonly Task _quiescenceTask; - internal HtmlContent(HtmlRendererCore renderer, int componentId, Task quiescenceTask) + internal HtmlComponent(HtmlRendererCore renderer, int componentId, Task quiescenceTask) { _renderer = renderer; _componentId = componentId; @@ -35,7 +35,7 @@ public Task WaitForQuiescenceAsync() public string ToHtmlString() { using var writer = new StringWriter(); - WriteTo(writer); + WriteHtmlTo(writer); return writer.ToString(); } @@ -43,8 +43,8 @@ public string ToHtmlString() /// Writes the component's latest output as HTML to the specified writer. /// /// The output destination. - public void WriteTo(TextWriter output) + public void WriteHtmlTo(TextWriter output) { - HtmlContentWriter.Write(_renderer, _componentId, output); + HtmlComponentWriter.Write(_renderer, _componentId, output); } } diff --git a/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs b/src/Components/Web/src/HtmlRendering/HtmlComponentWriter.cs similarity index 98% rename from src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs rename to src/Components/Web/src/HtmlRendering/HtmlComponentWriter.cs index 4b7066830b23..29ac1f574f98 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlContentWriter.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlComponentWriter.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.Web; // This is OK to be a struct because it never gets passed around anywhere. Other code can't even get an instance // of it. It just keeps track of some contextual information during a single synchronous HTML output operation. -internal ref struct HtmlContentWriter +internal ref struct HtmlComponentWriter { private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -28,11 +28,11 @@ public static void Write(HtmlRendererCore renderer, int componentId, TextWriter // So, we require exclusive access to the renderer during this synchronous process. renderer.Dispatcher.AssertAccess(); - var context = new HtmlContentWriter(renderer, output); + var context = new HtmlComponentWriter(renderer, output); context.RenderComponent(componentId); } - private HtmlContentWriter(HtmlRendererCore renderer, TextWriter output) + private HtmlComponentWriter(HtmlRendererCore renderer, TextWriter output) { _renderer = renderer; _output = output; diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index d519d9ab1f69..2dbd712ec4d0 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -40,24 +40,24 @@ public ValueTask DisposeAsync() /// /// Adds an instance of the specified component and instructs it to render. The resulting content represents the /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete - /// any asynchronous operations such as loading, use before - /// reading content from the . + /// any asynchronous operations such as loading, use before + /// reading content from the . /// /// The component type. - /// A task that completes with instance representing the render output. - public HtmlContent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent + /// A task that completes with instance representing the render output. + public HtmlComponent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent => BeginRenderingComponent(ParameterView.Empty); /// /// Adds an instance of the specified component and instructs it to render. The resulting content represents the /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete - /// any asynchronous operations such as loading, use before - /// reading content from the . + /// any asynchronous operations such as loading, use before + /// reading content from the . /// /// The component type. /// Parameters for the component. - /// A task that completes with instance representing the render output. - public HtmlContent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( + /// A task that completes with instance representing the render output. + public HtmlComponent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( ParameterView parameters) where TComponent : IComponent => _passiveHtmlRenderer.BeginRenderingComponentAsync(typeof(TComponent), parameters); @@ -66,8 +66,8 @@ public ValueTask DisposeAsync() /// for the component hierarchy to complete asynchronous tasks such as loading. /// /// The component type. - /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. - public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent + /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. + public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent => RenderComponentAsync(ParameterView.Empty); /// @@ -76,8 +76,8 @@ public ValueTask DisposeAsync() /// /// The component type. /// Parameters for the component. - /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. - public async Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( + /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. + public async Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( ParameterView parameters) where TComponent : IComponent { var content = BeginRenderingComponent(parameters); diff --git a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs index bcba022cec4b..5b71e143162c 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs @@ -21,7 +21,7 @@ public HtmlRendererCore(IServiceProvider serviceProvider, ILoggerFactory loggerF public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); - public HtmlContent BeginRenderingComponentAsync( + public HtmlComponent BeginRenderingComponentAsync( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, ParameterView initialParameters) { @@ -34,7 +34,7 @@ public HtmlContent BeginRenderingComponentAsync( ExceptionDispatchInfo.Capture(quiescenceTask.Exception.InnerException ?? quiescenceTask.Exception).Throw(); } - return new HtmlContent(this, componentId, quiescenceTask); + return new HtmlComponent(this, componentId, quiescenceTask); } protected override void HandleException(Exception exception) diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 98365d479a74..69a2c469eaba 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -1,8 +1,8 @@ #nullable enable -Microsoft.AspNetCore.Components.Web.HtmlContent -Microsoft.AspNetCore.Components.Web.HtmlContent.ToHtmlString() -> string! -Microsoft.AspNetCore.Components.Web.HtmlContent.WaitForQuiescenceAsync() -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.Web.HtmlContent.WriteTo(System.IO.TextWriter! output) -> void +Microsoft.AspNetCore.Components.Web.HtmlComponent +Microsoft.AspNetCore.Components.Web.HtmlComponent.ToHtmlString() -> string! +Microsoft.AspNetCore.Components.Web.HtmlComponent.WaitForQuiescenceAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlComponent.WriteHtmlTo(System.IO.TextWriter! output) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent() -> Microsoft.AspNetCore.Components.Web.HtmlContent! Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlContent! diff --git a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs index 7858dc6377aa..a458475cfa10 100644 --- a/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs +++ b/src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs @@ -37,7 +37,7 @@ public async Task HtmlContent_Write_ThrowsIfNotOnSyncContext() var htmlContent = await htmlRenderer.Dispatcher.InvokeAsync(htmlRenderer.BeginRenderingComponent); // Act - var ex = Assert.Throws(() => htmlContent.WriteTo(new StringWriter())); + var ex = Assert.Throws(() => htmlContent.WriteHtmlTo(new StringWriter())); Assert.Contains("The current thread is not associated with the Dispatcher", ex.Message); } @@ -858,7 +858,7 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () => { // Act var result = await htmlRenderer.RenderComponentAsync(); - result.WriteTo(writer); + result.WriteHtmlTo(writer); writer.Flush(); // Assert @@ -982,10 +982,10 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () => }); } - void AssertHtmlContentEquals(IEnumerable expected, HtmlContent actual) + void AssertHtmlContentEquals(IEnumerable expected, HtmlComponent actual) => AssertHtmlContentEquals(string.Join(string.Empty, expected), actual); - void AssertHtmlContentEquals(string expected, HtmlContent actual) + void AssertHtmlContentEquals(string expected, HtmlComponent actual) { var actualHtml = actual.ToHtmlString(); Assert.Equal(expected, actualHtml); From 7a48877459b960934ab85d43dc2b5f646012e90c Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 1 Mar 2023 09:43:42 +0000 Subject: [PATCH 17/26] Public API annotation fixes. Temporarily fix ambiguity. --- src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs | 1 - src/Components/Web/src/PublicAPI.Unshipped.txt | 8 ++++---- .../MvcViewFeaturesMvcCoreBuilderExtensions.cs | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs index 5b71e143162c..ddad72c20549 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRendererCore.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Components.RenderTree; diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 69a2c469eaba..179e1dc1b8f4 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -4,10 +4,10 @@ Microsoft.AspNetCore.Components.Web.HtmlComponent.ToHtmlString() -> string! Microsoft.AspNetCore.Components.Web.HtmlComponent.WaitForQuiescenceAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlComponent.WriteHtmlTo(System.IO.TextWriter! output) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer -Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent() -> Microsoft.AspNetCore.Components.Web.HtmlContent! -Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlContent! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent() -> Microsoft.AspNetCore.Components.Web.HtmlComponent! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlComponent! Microsoft.AspNetCore.Components.Web.HtmlRenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Web.HtmlRenderer.HtmlRenderer(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void -Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync() -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! diff --git a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index cac20243b187..931a7ceff4a2 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -5,7 +5,6 @@ using System.Linq; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Infrastructure; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Mvc; @@ -207,7 +206,7 @@ internal static void AddViewServices(IServiceCollection services) // services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); From 7ea0f3069b61e2a0d7163f6ac513a8b648c1fd39 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 1 Mar 2023 13:07:22 +0000 Subject: [PATCH 18/26] Add non-generic overloads of RenderComponentAsync / BeginRenderingComponent --- .../Web/src/HtmlRendering/HtmlRenderer.cs | 54 +++++++++++++++++-- .../Web/src/PublicAPI.Unshipped.txt | 4 ++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index 2dbd712ec4d0..727253ff547d 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -46,7 +46,7 @@ public ValueTask DisposeAsync() /// The component type. /// A task that completes with instance representing the render output. public HtmlComponent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent - => BeginRenderingComponent(ParameterView.Empty); + => _passiveHtmlRenderer.BeginRenderingComponentAsync(typeof(TComponent), ParameterView.Empty); /// /// Adds an instance of the specified component and instructs it to render. The resulting content represents the @@ -61,6 +61,32 @@ public ValueTask DisposeAsync() ParameterView parameters) where TComponent : IComponent => _passiveHtmlRenderer.BeginRenderingComponentAsync(typeof(TComponent), parameters); + /// + /// Adds an instance of the specified component and instructs it to render. The resulting content represents the + /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// any asynchronous operations such as loading, use before + /// reading content from the . + /// + /// The component type. This must implement . + /// A task that completes with instance representing the render output. + public HtmlComponent BeginRenderingComponent( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType) + => _passiveHtmlRenderer.BeginRenderingComponentAsync(componentType, ParameterView.Empty); + + /// + /// Adds an instance of the specified component and instructs it to render. The resulting content represents the + /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// any asynchronous operations such as loading, use before + /// reading content from the . + /// + /// The component type. This must implement . + /// Parameters for the component. + /// A task that completes with instance representing the render output. + public HtmlComponent BeginRenderingComponent( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, + ParameterView parameters) + => _passiveHtmlRenderer.BeginRenderingComponentAsync(componentType, parameters); + /// /// Adds an instance of the specified component and instructs it to render, waiting /// for the component hierarchy to complete asynchronous tasks such as loading. @@ -70,6 +96,16 @@ public ValueTask DisposeAsync() public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent => RenderComponentAsync(ParameterView.Empty); + /// + /// Adds an instance of the specified component and instructs it to render, waiting + /// for the component hierarchy to complete asynchronous tasks such as loading. + /// + /// The component type. This must implement . + /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. + public Task RenderComponentAsync( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType) + => RenderComponentAsync(componentType, ParameterView.Empty); + /// /// Adds an instance of the specified component and instructs it to render, waiting /// for the component hierarchy to complete asynchronous tasks such as loading. @@ -77,10 +113,22 @@ public ValueTask DisposeAsync() /// The component type. /// Parameters for the component. /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. - public async Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( + public Task RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( ParameterView parameters) where TComponent : IComponent + => RenderComponentAsync(typeof (TComponent), parameters); + + /// + /// Adds an instance of the specified component and instructs it to render, waiting + /// for the component hierarchy to complete asynchronous tasks such as loading. + /// + /// The component type. This must implement . + /// Parameters for the component. + /// A task that completes with once the component hierarchy has completed any asynchronous tasks such as loading. + public async Task RenderComponentAsync( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, + ParameterView parameters) { - var content = BeginRenderingComponent(parameters); + var content = BeginRenderingComponent(componentType, parameters); await content.WaitForQuiescenceAsync(); return content; } diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 179e1dc1b8f4..f7c19e5cc666 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -4,10 +4,14 @@ Microsoft.AspNetCore.Components.Web.HtmlComponent.ToHtmlString() -> string! Microsoft.AspNetCore.Components.Web.HtmlComponent.WaitForQuiescenceAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlComponent.WriteHtmlTo(System.IO.TextWriter! output) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer +Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(System.Type! componentType) -> Microsoft.AspNetCore.Components.Web.HtmlComponent! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlComponent! Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent() -> Microsoft.AspNetCore.Components.Web.HtmlComponent! Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlComponent! Microsoft.AspNetCore.Components.Web.HtmlRenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Web.HtmlRenderer.HtmlRenderer(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(System.Type! componentType) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! From aaeba5ad3f777faed0f1751f3a697eee29691dfd Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 1 Mar 2023 14:08:32 +0000 Subject: [PATCH 19/26] Use new HtmlRenderer in real prerendering --- .../ComponentStatePersistenceManager.cs | 11 +- .../Components/src/PublicAPI.Unshipped.txt | 1 + .../Web/src/HtmlRendering/HtmlComponent.cs | 19 +- .../Web/src/PublicAPI.Unshipped.txt | 1 + .../Mvc.TagHelpers/src/ComponentTagHelper.cs | 2 +- .../src/PersistComponentStateTagHelper.cs | 3 +- .../src/Buffers/ViewBuffer.cs | 7 + ...MvcViewFeaturesMvcCoreBuilderExtensions.cs | 4 +- .../RazorComponents/ComponentRenderedText.cs | 19 -- .../src/RazorComponents/ComponentRenderer.cs | 104 ++++-- .../src/RazorComponents/HtmlRenderer.cs | 319 ------------------ .../src/RazorComponents/IAsyncHtmlContent.cs | 11 + .../src/RazorComponents/IComponentRenderer.cs | 16 - .../StaticComponentRenderer.cs | 13 +- .../HtmlHelperComponentExtensions.cs | 2 +- .../src/ServerComponentSerializer.cs | 17 +- .../src/WebAssemblyComponentSerializer.cs | 17 +- 17 files changed, 150 insertions(+), 416 deletions(-) delete mode 100644 src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderedText.cs delete mode 100644 src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs create mode 100644 src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IAsyncHtmlContent.cs delete mode 100644 src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IComponentRenderer.cs diff --git a/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs b/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs index d0261aef7dad..6886ea4a4bce 100644 --- a/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs +++ b/src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs @@ -48,6 +48,15 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store) /// The that components are being rendered. /// A that will complete when the state has been restored. public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer) + => PersistStateAsync(store, renderer.Dispatcher); + + /// + /// Persists the component application state into the given . + /// + /// The to restore the application state from. + /// The corresponding to the components' renderer. + /// A that will complete when the state has been restored. + public Task PersistStateAsync(IPersistentComponentStateStore store, Dispatcher dispatcher) { if (_stateIsPersisted) { @@ -56,7 +65,7 @@ public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer ren _stateIsPersisted = true; - return renderer.Dispatcher.InvokeAsync(PauseAndPersistState); + return dispatcher.InvokeAsync(PauseAndPersistState); async Task PauseAndPersistState() { diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 40ea919e8630..4ebf21f11a6e 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,5 +1,6 @@ #nullable enable Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! *REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri! Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri! diff --git a/src/Components/Web/src/HtmlRendering/HtmlComponent.cs b/src/Components/Web/src/HtmlRendering/HtmlComponent.cs index 931d69ffe726..30989642fea9 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlComponent.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlComponent.cs @@ -10,17 +10,22 @@ namespace Microsoft.AspNetCore.Components.Web; /// public sealed class HtmlComponent { - private readonly HtmlRendererCore _renderer; + private readonly HtmlRendererCore? _renderer; private readonly int _componentId; private readonly Task _quiescenceTask; - internal HtmlComponent(HtmlRendererCore renderer, int componentId, Task quiescenceTask) + internal HtmlComponent(HtmlRendererCore? renderer, int componentId, Task quiescenceTask) { _renderer = renderer; _componentId = componentId; _quiescenceTask = quiescenceTask; } + /// + /// Gets an instance of that produces no content. + /// + public static HtmlComponent Empty { get; } = new HtmlComponent(null, 0, Task.CompletedTask); + /// /// Obtains a that completes when the component hierarchy has completed asynchronous tasks such as loading. /// @@ -34,6 +39,11 @@ public Task WaitForQuiescenceAsync() /// An HTML string representation of the component's latest output. public string ToHtmlString() { + if (_renderer is null) + { + return string.Empty; + } + using var writer = new StringWriter(); WriteHtmlTo(writer); return writer.ToString(); @@ -45,6 +55,9 @@ public string ToHtmlString() /// The output destination. public void WriteHtmlTo(TextWriter output) { - HtmlComponentWriter.Write(_renderer, _componentId, output); + if (_renderer is not null) + { + HtmlComponentWriter.Write(_renderer, _componentId, output); + } } } diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index f7c19e5cc666..fdb62745368b 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -15,3 +15,4 @@ Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(System.Typ Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync() -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.Task! +static Microsoft.AspNetCore.Components.Web.HtmlComponent.Empty.get -> Microsoft.AspNetCore.Components.Web.HtmlComponent! diff --git a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs index 6583986e405d..dd6585505dae 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs @@ -91,7 +91,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu } var requestServices = ViewContext.HttpContext.RequestServices; - var componentRenderer = requestServices.GetRequiredService(); + var componentRenderer = requestServices.GetRequiredService(); var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters); // Reset the TagName. We don't want `component` to render. diff --git a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs index bd48227d2179..f4f24fe36301 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; @@ -76,7 +77,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu output.TagName = null; if (store != null) { - await manager.PersistStateAsync(store, renderer); + await manager.PersistStateAsync(store, renderer.Dispatcher); output.Content.SetHtmlContent( new HtmlContentBuilder() .AppendHtml(""); + writer.Write(""); } - internal static void AppendEpilogue(IHtmlContentBuilder htmlContentBuilder, ServerComponentMarker record) + internal static void AppendEpilogue(TextWriter writer, ServerComponentMarker record) { var endRecord = JsonSerializer.Serialize( record.GetEndRecord(), ServerComponentSerializationSettings.JsonSerializationOptions); - htmlContentBuilder.AppendHtml(""); + writer.Write(""); } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/WebAssemblyComponentSerializer.cs b/src/Mvc/Mvc.ViewFeatures/src/WebAssemblyComponentSerializer.cs index cb69a462ed18..7f1c490abf88 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/WebAssemblyComponentSerializer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/WebAssemblyComponentSerializer.cs @@ -3,7 +3,6 @@ using System.Text.Json; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -25,25 +24,25 @@ public static WebAssemblyComponentMarker SerializeInvocation(Type type, Paramete WebAssemblyComponentMarker.NonPrerendered(assembly, typeFullName, serializedDefinitions, serializedValues); } - internal static void AppendPreamble(ViewBuffer viewBuffer, WebAssemblyComponentMarker record) + internal static void AppendPreamble(TextWriter writer, WebAssemblyComponentMarker record) { var serializedStartRecord = JsonSerializer.Serialize( record, WebAssemblyComponentSerializationSettings.JsonSerializationOptions); - viewBuffer.AppendHtml(""); + writer.Write(""); } - internal static void AppendEpilogue(ViewBuffer viewBuffer, WebAssemblyComponentMarker record) + internal static void AppendEpilogue(TextWriter writer, WebAssemblyComponentMarker record) { var endRecord = JsonSerializer.Serialize( record.GetEndRecord(), WebAssemblyComponentSerializationSettings.JsonSerializationOptions); - viewBuffer.AppendHtml(""); + writer.Write(""); } } From 5378880236c96d66c38f5389d7b85d83c835556d Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 2 Mar 2023 12:30:45 +0000 Subject: [PATCH 20/26] Clean up the prerendering classes --- .../Mvc.TagHelpers/src/ComponentTagHelper.cs | 6 +- .../src/PersistComponentStateTagHelper.cs | 2 +- ...MvcViewFeaturesMvcCoreBuilderExtensions.cs | 3 +- ...entRenderer.cs => ComponentPrerenderer.cs} | 141 ++++++++++++++---- .../StaticComponentRenderer.cs | 113 -------------- .../HtmlHelperComponentExtensions.cs | 8 +- 6 files changed, 122 insertions(+), 151 deletions(-) rename src/Mvc/Mvc.ViewFeatures/src/RazorComponents/{ComponentRenderer.cs => ComponentPrerenderer.cs} (54%) delete mode 100644 src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs diff --git a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs index dd6585505dae..448c6d2c9875 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; @@ -91,8 +92,9 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu } var requestServices = ViewContext.HttpContext.RequestServices; - var componentRenderer = requestServices.GetRequiredService(); - var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters); + var componentPrerenderer = requestServices.GetRequiredService(); + var parameters = _parameters is null || _parameters.Count == 0 ? ParameterView.Empty : ParameterView.FromDictionary(_parameters); + var result = await componentPrerenderer.PrerenderComponentAsync(ViewContext, ComponentType, RenderMode, parameters); // Reset the TagName. We don't want `component` to render. output.TagName = null; diff --git a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs index f4f24fe36301..bf69f3f83ba3 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs @@ -53,7 +53,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu var renderer = services.GetRequiredService(); var store = PersistenceMode switch { - null => ComponentRenderer.GetPersistStateRenderMode(ViewContext) switch + null => ComponentPrerenderer.GetPersistStateRenderMode(ViewContext) switch { InvokedRenderModes.Mode.None => null, diff --git a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index 988673a353ac..d3ed50ef8b96 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -204,8 +204,7 @@ internal static void AddViewServices(IServiceCollection services) // // Component rendering // - services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs similarity index 54% rename from src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs rename to src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs index 37d72cc98bec..95cd256bd1f7 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs @@ -3,38 +3,44 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Infrastructure; +using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; -internal sealed class ComponentRenderer +// Wraps the public HtmlRenderer APIs so that the output also gets annotated with prerendering markers. +// This allows the prerendered content to switch later into interactive mode. +// This class also deals with initializing the standard component DI services once per request. +internal sealed class ComponentPrerenderer { private static readonly object ComponentSequenceKey = new object(); private static readonly object InvokedRenderModesKey = new object(); - private readonly StaticComponentRenderer _staticComponentRenderer; + private readonly HtmlRenderer _htmlRenderer; private readonly ServerComponentSerializer _serverComponentSerializer; - private readonly IViewBufferScope _viewBufferScope; + private readonly object _servicesInitializedLock = new(); + private Task _servicesInitializedTask; - public ComponentRenderer( - StaticComponentRenderer staticComponentRenderer, - ServerComponentSerializer serverComponentSerializer, - IViewBufferScope viewBufferScope) + public ComponentPrerenderer( + HtmlRenderer htmlRenderer, + ServerComponentSerializer serverComponentSerializer) { - _staticComponentRenderer = staticComponentRenderer; _serverComponentSerializer = serverComponentSerializer; - _viewBufferScope = viewBufferScope; + _htmlRenderer = htmlRenderer; } - public async ValueTask RenderComponentAsync( + public async ValueTask PrerenderComponentAsync( ViewContext viewContext, Type componentType, - RenderMode renderMode, - object parameters) + RenderMode prerenderMode, + ParameterView parameters) { ArgumentNullException.ThrowIfNull(viewContext); ArgumentNullException.ThrowIfNull(componentType); @@ -44,24 +50,55 @@ public async ValueTask RenderComponentAsync( throw new ArgumentException(Resources.FormatTypeMustDeriveFromType(componentType, typeof(IComponent))); } - var context = viewContext.HttpContext; - var parameterView = parameters is null ? - ParameterView.Empty : - ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters)); + // Make sure we only initialize the services once, but on every call we wait for that process to complete + var httpContext = viewContext.HttpContext; + lock (_servicesInitializedLock) + { + _servicesInitializedTask ??= InitializeStandardComponentServicesAsync(httpContext); + } + await _servicesInitializedTask; - UpdateSaveStateRenderMode(viewContext, renderMode); + UpdateSaveStateRenderMode(viewContext, prerenderMode); - return renderMode switch + return prerenderMode 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), - RenderMode.WebAssembly => NonPrerenderedWebAssemblyComponent(componentType, parameterView), - RenderMode.WebAssemblyPrerendered => await PrerenderedWebAssemblyComponentAsync(context, componentType, parameterView), - _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)), + RenderMode.Server => NonPrerenderedServerComponent(httpContext, GetOrCreateInvocationId(viewContext), componentType, parameters), + RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(httpContext, GetOrCreateInvocationId(viewContext), componentType, parameters), + RenderMode.Static => await StaticComponentAsync(httpContext, componentType, parameters), + RenderMode.WebAssembly => NonPrerenderedWebAssemblyComponent(componentType, parameters), + RenderMode.WebAssemblyPrerendered => await PrerenderedWebAssemblyComponentAsync(httpContext, componentType, parameters), + _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(prerenderMode), nameof(prerenderMode)), }; } + public async ValueTask PrerenderComponentCoreAsync( + ParameterView parameters, + HttpContext httpContext, + Type componentType) + { + try + { + return await _htmlRenderer.Dispatcher.InvokeAsync(() => + _htmlRenderer.RenderComponentAsync(componentType, parameters)); + } + catch (NavigationException navigationException) + { + // Navigation was attempted during prerendering. + if (httpContext.Response.HasStarted) + { + // We can't perform a redirect as the server already started sending the response. + // This is considered an application error as the developer should buffer the response until + // all components have rendered. + throw new InvalidOperationException("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" + + "response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", navigationException); + } + + httpContext.Response.Redirect(navigationException.Location); + return HtmlComponent.Empty; + } + } + private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext) { if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result)) @@ -115,11 +152,11 @@ internal static InvokedRenderModes.Mode GetPersistStateRenderMode(ViewContext vi private async ValueTask StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) { - var htmlComponent = await _staticComponentRenderer.PrerenderComponentAsync( + var htmlComponent = await PrerenderComponentCoreAsync( parametersCollection, context, type); - return new PrerenderedComponentHtmlContent(_staticComponentRenderer.Dispatcher, htmlComponent, null, null); + return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, null, null); } private async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) @@ -135,12 +172,12 @@ private async Task PrerenderedServerComponentAsync(HttpContext con parametersCollection, prerendered: true); - var htmlComponent = await _staticComponentRenderer.PrerenderComponentAsync( + var htmlComponent = await PrerenderComponentCoreAsync( parametersCollection, context, type); - return new PrerenderedComponentHtmlContent(_staticComponentRenderer.Dispatcher, htmlComponent, marker, null); + return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, marker, null); } private async ValueTask PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) @@ -150,12 +187,12 @@ private async ValueTask PrerenderedWebAssemblyComponentAsync(HttpC parametersCollection, prerendered: true); - var htmlComponent = await _staticComponentRenderer.PrerenderComponentAsync( + var htmlComponent = await PrerenderComponentCoreAsync( parametersCollection, context, type); - return new PrerenderedComponentHtmlContent(_staticComponentRenderer.Dispatcher, htmlComponent, null, marker); + return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, null, marker); } private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) @@ -175,6 +212,8 @@ private static IHtmlContent NonPrerenderedWebAssemblyComponent(Type type, Parame return new PrerenderedComponentHtmlContent(null, null, null, marker); } + // An implementation of IHtmlContent that holds a reference to a component until we're ready to emit it as HTML to the response. + // We don't construct the actual HTML until we receive the call to WriteTo. private class PrerenderedComponentHtmlContent : IHtmlContent, IAsyncHtmlContent { private readonly Dispatcher _dispatcher; @@ -194,6 +233,9 @@ public PrerenderedComponentHtmlContent( _webAssemblyMarker = webAssemblyMarker; } + // For back-compat, we have to supply an implemention of IHtmlContent. However this will only work + // if you call it on the HtmlRenderer's sync context. The framework itself will not call this directly + // and will instead use WriteToAsync which deals with dispatching to the sync context. public void WriteTo(TextWriter writer, HtmlEncoder encoder) { if (_serverMarker.HasValue) @@ -232,4 +274,41 @@ public async ValueTask WriteToAsync(TextWriter writer) } } } + + private static async Task InitializeStandardComponentServicesAsync(HttpContext httpContext) + { + var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService(); + navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); + + var authenticationStateProvider = httpContext.RequestServices.GetService() as IHostEnvironmentAuthenticationStateProvider; + if (authenticationStateProvider != null) + { + var authenticationState = new AuthenticationState(httpContext.User); + authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); + } + + // It's important that this is initialized since a component might try to restore state during prerendering + // (which will obviously not work, but should not fail) + var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService(); + await componentApplicationLifetime.RestoreStateAsync(new PrerenderComponentApplicationStore()); + } + + private static string GetFullUri(HttpRequest request) + { + return UriHelper.BuildAbsolute( + request.Scheme, + request.Host, + request.PathBase, + request.Path, + request.QueryString); + } + + private static string GetContextBaseUri(HttpRequest request) + { + var result = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase); + + // PathBase may be "/" or "/some/thing", but to be a well-formed base URI + // it has to end with a trailing slash + return result.EndsWith('/') ? result : result += "/"; + } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs deleted file mode 100644 index e5fe429f3999..000000000000 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Components.Infrastructure; -using Microsoft.AspNetCore.Components.Routing; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Mvc.ViewFeatures; - -internal sealed class StaticComponentRenderer -{ - private Task _initialized; - private readonly HtmlRenderer _renderer; - private readonly object _lock = new(); - - public Dispatcher Dispatcher => _renderer.Dispatcher; - - public StaticComponentRenderer(HtmlRenderer renderer) - { - _renderer = renderer; - } - - public async ValueTask PrerenderComponentAsync( - ParameterView parameters, - HttpContext httpContext, - Type componentType) - { - await InitializeStandardComponentServicesAsync(httpContext); - - HtmlComponent result = default; - try - { - result = await _renderer.Dispatcher.InvokeAsync(() => _renderer.RenderComponentAsync( - componentType, - parameters)); - } - catch (NavigationException navigationException) - { - // Navigation was attempted during prerendering. - if (httpContext.Response.HasStarted) - { - // We can't perform a redirect as the server already started sending the response. - // This is considered an application error as the developer should buffer the response until - // all components have rendered. - throw new InvalidOperationException("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" + - "response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", navigationException); - } - - httpContext.Response.Redirect(navigationException.Location); - return HtmlComponent.Empty; - } - - return result; - } - - private Task InitializeStandardComponentServicesAsync(HttpContext httpContext) - { - // This might not be the first component in the request we are rendering, so - // we need to check if we already initialized the services in this request. - lock (_lock) - { - if (_initialized == null) - { - _initialized = InitializeCore(httpContext); - } - } - - return _initialized; - - static async Task InitializeCore(HttpContext httpContext) - { - var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService(); - navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request)); - - var authenticationStateProvider = httpContext.RequestServices.GetService() as IHostEnvironmentAuthenticationStateProvider; - if (authenticationStateProvider != null) - { - var authenticationState = new AuthenticationState(httpContext.User); - authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); - } - - // It's important that this is initialized since a component might try to restore state during prerendering - // (which will obviously not work, but should not fail) - var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService(); - await componentApplicationLifetime.RestoreStateAsync(new PrerenderComponentApplicationStore()); - } - } - - private static string GetFullUri(HttpRequest request) - { - return UriHelper.BuildAbsolute( - request.Scheme, - request.Host, - request.PathBase, - request.Path, - request.QueryString); - } - - private static string GetContextBaseUri(HttpRequest request) - { - var result = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase); - - // PathBase may be "/" or "/some/thing", but to be a well-formed base URI - // it has to end with a trailing slash - return result.EndsWith('/') ? result : result += "/"; - } -} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs index 855508910aea..a6f3078d2b09 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs @@ -53,8 +53,12 @@ public static async Task RenderComponentAsync( ArgumentNullException.ThrowIfNull(htmlHelper); ArgumentNullException.ThrowIfNull(componentType); + var parameterView = parameters is null ? + ParameterView.Empty : + ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters)); + var viewContext = htmlHelper.ViewContext; - var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService(); - return await componentRenderer.RenderComponentAsync(viewContext, componentType, renderMode, parameters); + var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService(); + return await componentRenderer.PrerenderComponentAsync(viewContext, componentType, renderMode, parameterView); } } From 41e164d91f31633d47dfcd281fb9124c968557a0 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 2 Mar 2023 12:54:52 +0000 Subject: [PATCH 21/26] Remove unused line --- src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs index bf69f3f83ba3..bf832e1aaa0f 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs @@ -4,7 +4,6 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Infrastructure; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Html; From e52ec5d225d8a2077b2698905e2e6a3fa0309383 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 2 Mar 2023 12:58:14 +0000 Subject: [PATCH 22/26] Fix XML docs --- .../Web/src/HtmlRendering/HtmlRenderer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index 727253ff547d..88155084a653 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -39,49 +39,49 @@ public ValueTask DisposeAsync() /// /// Adds an instance of the specified component and instructs it to render. The resulting content represents the - /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete /// any asynchronous operations such as loading, use before /// reading content from the . /// /// The component type. - /// A task that completes with instance representing the render output. + /// An instance representing the render output. public HtmlComponent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent => _passiveHtmlRenderer.BeginRenderingComponentAsync(typeof(TComponent), ParameterView.Empty); /// /// Adds an instance of the specified component and instructs it to render. The resulting content represents the - /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete /// any asynchronous operations such as loading, use before /// reading content from the . /// /// The component type. /// Parameters for the component. - /// A task that completes with instance representing the render output. + /// An instance representing the render output. public HtmlComponent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>( ParameterView parameters) where TComponent : IComponent => _passiveHtmlRenderer.BeginRenderingComponentAsync(typeof(TComponent), parameters); /// /// Adds an instance of the specified component and instructs it to render. The resulting content represents the - /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete /// any asynchronous operations such as loading, use before /// reading content from the . /// /// The component type. This must implement . - /// A task that completes with instance representing the render output. + /// An instance representing the render output. public HtmlComponent BeginRenderingComponent( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType) => _passiveHtmlRenderer.BeginRenderingComponentAsync(componentType, ParameterView.Empty); /// /// Adds an instance of the specified component and instructs it to render. The resulting content represents the - /// initial synchronous rendering state, which may later change. To wait for the component hierarchy to complete + /// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete /// any asynchronous operations such as loading, use before /// reading content from the . /// /// The component type. This must implement . /// Parameters for the component. - /// A task that completes with instance representing the render output. + /// An instance representing the render output. public HtmlComponent BeginRenderingComponent( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, ParameterView parameters) From 60af0f799cf093e36066bd50f89513ec64db45c9 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 2 Mar 2023 12:59:53 +0000 Subject: [PATCH 23/26] =?UTF-8?q?Seal=20=F0=9F=A6=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Components/Web/src/HtmlRendering/HtmlRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index 88155084a653..caa89ef80a1d 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Components.Web; /// /// Provides a mechanism for rendering components non-interactively as HTML markup. /// -public class HtmlRenderer : IAsyncDisposable +public sealed class HtmlRenderer : IAsyncDisposable { private readonly HtmlRendererCore _passiveHtmlRenderer; From 1805ff501bd17799864121d2a4e4ac7c8ed892a2 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 2 Mar 2023 13:05:50 +0000 Subject: [PATCH 24/26] Another thing can be private --- .../src/RazorComponents/ComponentPrerenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs index 95cd256bd1f7..fce236b306a8 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs @@ -71,7 +71,7 @@ public async ValueTask PrerenderComponentAsync( }; } - public async ValueTask PrerenderComponentCoreAsync( + private async ValueTask PrerenderComponentCoreAsync( ParameterView parameters, HttpContext httpContext, Type componentType) From ab919af99c01998bc272b1b0d944ef6bc544078a Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Thu, 2 Mar 2023 16:06:46 +0000 Subject: [PATCH 25/26] Update existing tests --- .../test/ComponentTagHelperTest.cs | 43 +- .../PersistComponentStateTagHelperTest.cs | 32 +- .../RazorComponents/ComponentPrerenderer.cs | 4 +- ...rerTest.cs => ComponentPrerendererTest.cs} | 181 ++-- .../test/RazorComponents/HtmlRendererTest.cs | 866 ------------------ .../HtmlHelperComponentExtensionsTest.cs | 52 -- 6 files changed, 136 insertions(+), 1042 deletions(-) rename src/Mvc/Mvc.ViewFeatures/test/RazorComponents/{ComponentRendererTest.cs => ComponentPrerendererTest.cs} (84%) delete mode 100644 src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs delete mode 100644 src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs diff --git a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs index d4778092498e..53678d8f652b 100644 --- a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs @@ -2,15 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Moq; namespace Microsoft.AspNetCore.Mvc.TagHelpers; @@ -21,10 +23,12 @@ public class ComponentTagHelperTest public async Task ProcessAsync_RendersComponent() { // Arrange + var viewContext = GetViewContext(); var tagHelper = new ComponentTagHelper { - ViewContext = GetViewContext(), + ViewContext = viewContext, RenderMode = RenderMode.Static, + ComponentType = typeof(TestComponent), }; var context = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -33,8 +37,9 @@ public async Task ProcessAsync_RendersComponent() await tagHelper.ProcessAsync(context, output); // Assert - var content = HtmlContentUtilities.HtmlContentToString(output.Content); - Assert.Equal("Hello world", content); + var htmlRenderer = viewContext.HttpContext.RequestServices.GetRequiredService(); + var content = await htmlRenderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(output.Content)); + Assert.Equal("Hello from the component", content); Assert.Null(output.TagName); } @@ -73,23 +78,31 @@ private static TagHelperOutput GetTagHelperOutput() 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()) == new ValueTask(htmlContent)); + var navManager = new Mock(); + navManager.As(); var httpContext = new DefaultHttpContext { RequestServices = new ServiceCollection() - .AddSingleton(renderer) + .AddScoped() + .AddScoped() + .AddScoped(_ => Mock.Of( + x => x.CreateProtector(It.IsAny()) == Mock.Of())) .AddSingleton() - .AddSingleton(NullLoggerFactory.Instance) - .AddSingleton(HtmlEncoder.Default) + .AddLogging() + .AddScoped() + .AddScoped(_ => navManager.Object) .BuildServiceProvider(), }; - return new ViewContext + return new ViewContext { HttpContext = httpContext }; + } + + private class TestComponent : ComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) { - HttpContext = httpContext, - }; + builder.AddContent(0, "Hello from the component"); + } } } diff --git a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs index 2df13c0ceb10..12dcff2797e9 100644 --- a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -77,7 +80,7 @@ public async Task ExecuteAsync_RendersWebAssemblyStateImplicitlyWhenAWebAssembly ViewContext = GetViewContext() }; - ComponentRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.WebAssemblyPrerendered); + ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.WebAssemblyPrerendered); var context = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -125,7 +128,7 @@ public async Task ExecuteAsync_RendersServerStateImplicitlyWhenAServerComponentW ViewContext = GetViewContext() }; - ComponentRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.ServerPrerendered); + ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.ServerPrerendered); var context = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -150,8 +153,8 @@ public async Task ExecuteAsync_ThrowsIfItCantInferThePersistMode() ViewContext = GetViewContext() }; - ComponentRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.ServerPrerendered); - ComponentRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.WebAssemblyPrerendered); + ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.ServerPrerendered); + ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.WebAssemblyPrerendered); var context = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -179,26 +182,17 @@ private static TagHelperOutput GetTagHelperOutput() 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()) == new ValueTask(htmlContent)); - var httpContext = new DefaultHttpContext { RequestServices = new ServiceCollection() - .AddSingleton(renderer) - .AddSingleton(new ComponentStatePersistenceManager(NullLogger.Instance)) - .AddSingleton() - .AddSingleton(_ephemeralProvider) - .AddSingleton(NullLoggerFactory.Instance) - .AddSingleton(HtmlEncoder.Default) - .AddScoped() + .AddScoped(_ => Mock.Of( + x => x.CreateProtector(It.IsAny()) == _protector)) + .AddLogging() + .AddScoped() + .AddScoped() .BuildServiceProvider(), }; - return new ViewContext - { - HttpContext = httpContext, - }; + return new ViewContext { HttpContext = httpContext }; } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs index fce236b306a8..cac3ca8fdf0e 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs @@ -36,6 +36,8 @@ public ComponentPrerenderer( _htmlRenderer = htmlRenderer; } + public Dispatcher Dispatcher => _htmlRenderer.Dispatcher; + public async ValueTask PrerenderComponentAsync( ViewContext viewContext, Type componentType, @@ -257,7 +259,7 @@ public void WriteTo(TextWriter writer, HtmlEncoder encoder) } else if (_webAssemblyMarker.HasValue) { - WebAssemblyComponentSerializer.AppendPreamble(writer, _webAssemblyMarker.Value); + WebAssemblyComponentSerializer.AppendEpilogue(writer, _webAssemblyMarker.Value); } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentPrerendererTest.cs similarity index 84% rename from src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs rename to src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentPrerendererTest.cs index ef8848075e1b..e644578d0260 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentPrerendererTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -19,10 +20,11 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.JSInterop; using Moq; +using static Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable; namespace Microsoft.AspNetCore.Mvc.ViewFeatures; -public class ComponentRendererTest +public class ComponentPrerendererTest { private const string PrerenderedComponentPattern = "^(?.+?)$"; private const string ComponentPattern = "^$"; @@ -30,9 +32,9 @@ public class ComponentRendererTest private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider(); private readonly IServiceProvider _services = CreateDefaultServiceCollection().BuildServiceProvider(); - private readonly ComponentRenderer renderer; + private readonly ComponentPrerenderer renderer; - public ComponentRendererTest() + public ComponentPrerendererTest() { renderer = GetComponentRenderer(); } @@ -45,7 +47,7 @@ public async Task CanRender_ParameterlessComponent_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssembly, null); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssembly, ParameterView.Empty); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); var match = Regex.Match(content, ComponentPattern); @@ -68,8 +70,8 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssemblyPrerendered, null); - result.WriteTo(writer, HtmlEncoder.Default); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssemblyPrerendered, ParameterView.Empty); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -106,12 +108,12 @@ public async Task CanRender_ComponentWithParameters_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.WebAssembly, - new + ParameterView.FromDictionary(new Dictionary { - Name = "Daniel" - }); + { "Name", "Daniel" } + })); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); var match = Regex.Match(content, ComponentPattern); @@ -143,12 +145,12 @@ public async Task CanRender_ComponentWithNullParameters_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.WebAssembly, - new + ParameterView.FromDictionary(new Dictionary { - Name = (string)null - }); + { "Name", null } + })); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); var match = Regex.Match(content, ComponentPattern); @@ -178,13 +180,13 @@ public async Task CanPrerender_ComponentWithParameters_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, - new + ParameterView.FromDictionary(new Dictionary { - Name = "Daniel" - }); - result.WriteTo(writer, HtmlEncoder.Default); + { "Name", "Daniel" } + })); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -227,13 +229,13 @@ public async Task CanPrerender_ComponentWithNullParameters_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, - new + ParameterView.FromDictionary(new Dictionary { - Name = (string)null - }); - result.WriteTo(writer, HtmlEncoder.Default); + { "Name", null } + })); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -275,8 +277,8 @@ public async Task CanRender_ParameterlessComponent() var writer = new StringWriter(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, null); - result.WriteTo(writer, HtmlEncoder.Default); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, ParameterView.Empty); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); // Assert @@ -292,7 +294,7 @@ public async Task CanRender_ParameterlessComponent_ServerMode() .ToTimeLimitedDataProtector(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, ParameterView.Empty); var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); var match = Regex.Match(content, ComponentPattern); @@ -324,8 +326,8 @@ public async Task CanPrerender_ParameterlessComponent_ServerMode() .ToTimeLimitedDataProtector(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null); - var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, ParameterView.Empty); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); // Assert @@ -367,8 +369,9 @@ public async Task Prerender_ServerAndClientComponentUpdatesInvokedPrerenderModes var viewContext = GetViewContext(); // Act - var server = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Steve" }); - var client = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, new { Name = "Steve" }); + var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); + var server = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var client = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, parameters); // Assert var (_, mode) = Assert.Single(viewContext.Items, (kvp) => kvp.Value is InvokedRenderModes); @@ -384,12 +387,12 @@ public async Task CanRenderMultipleServerComponents() .ToTimeLimitedDataProtector(); // Act - var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null); - var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult); + var firstResult = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, ParameterView.Empty); + var firstComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(firstResult)); var firstMatch = Regex.Match(firstComponent, PrerenderedComponentPattern, RegexOptions.Multiline); - var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null); - var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult); + var secondResult = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, ParameterView.Empty); + var secondComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(secondResult)); var secondMatch = Regex.Match(secondComponent, ComponentPattern); // Assert @@ -424,11 +427,12 @@ public async Task CanRender_ComponentWithParametersObject() var viewContext = GetViewContext(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, new { Name = "Steve" }); + var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, parameters); // Assert - var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); - Assert.Equal("

Hello Steve!

", content); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); + Assert.Equal("

Hello SomeName!

", content); } [Fact] @@ -440,7 +444,8 @@ public async Task CanRender_ComponentWithParameters_ServerMode() .ToTimeLimitedDataProtector(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" }); + var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, parameters); var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); var match = Regex.Match(content, ComponentPattern); @@ -466,7 +471,7 @@ public async Task CanRender_ComponentWithParameters_ServerMode() var value = Assert.Single(serverComponent.ParameterValues); var rawValue = Assert.IsType(value); - Assert.Equal("Daniel", rawValue.GetString()); + Assert.Equal("SomeName", rawValue.GetString()); } [Fact] @@ -478,8 +483,8 @@ public async Task CanRender_ComponentWithNullParameters_ServerMode() .ToTimeLimitedDataProtector(); // Act - - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null }); + var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", null } }); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, parameters); var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); var match = Regex.Match(content, ComponentPattern); @@ -513,13 +518,13 @@ public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode() { // Arrange var viewContext = GetViewContext(); - var writer = new StringWriter(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" }); - var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); + var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); // Assert @@ -546,10 +551,10 @@ public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode() var value = Assert.Single(serverComponent.ParameterValues); var rawValue = Assert.IsType(value); - Assert.Equal("Daniel", rawValue.GetString()); + Assert.Equal("SomeName", rawValue.GetString()); var prerenderedContent = match.Groups["content"].Value; - Assert.Equal("

Hello Daniel!

", prerenderedContent); + Assert.Equal("

Hello SomeName!

", prerenderedContent); var epilogue = match.Groups["epilogue"].Value; var epilogueMarker = JsonSerializer.Deserialize(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions); @@ -564,13 +569,13 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode { // Arrange var viewContext = GetViewContext(); - var writer = new StringWriter(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null }); - var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); + var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", null } }); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); // Assert @@ -617,9 +622,10 @@ public async Task ComponentWithInvalidRenderMode_Throws() var viewContext = GetViewContext(); // Act & Assert + var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); var ex = await ExceptionAssert.ThrowsArgumentAsync( - async () => await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), default, new { Name = "Daniel" }), - "renderMode", + async () => await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), default, parameters), + "prerenderMode", $"Unsupported RenderMode '{(RenderMode)default}'"); } @@ -631,10 +637,11 @@ public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() // Act var state = new OnAfterRenderState(); - var result = await renderer.RenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, new { state }); + var parameters = ParameterView.FromDictionary(new Dictionary { { "state", state } }); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, parameters); // Assert - var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); Assert.Equal("

Hello

", content); Assert.False(state.OnAfterRenderRan); } @@ -644,29 +651,28 @@ public async Task DisposableComponents_GetDisposedAfterScopeCompletes() { // Arrange var collection = CreateDefaultServiceCollection(); - collection.TryAddScoped(); - collection.TryAddScoped(); + collection.TryAddScoped(); collection.TryAddScoped(); collection.TryAddSingleton(HtmlEncoder.Default); collection.TryAddSingleton(NullLoggerFactory.Instance); collection.TryAddSingleton(); collection.TryAddSingleton(_dataprotectorProvider); collection.TryAddSingleton(); - collection.TryAddScoped(); var provider = collection.BuildServiceProvider(); var scope = provider.GetRequiredService().CreateScope(); var scopedProvider = scope.ServiceProvider; var context = new DefaultHttpContext() { RequestServices = scopedProvider }; var viewContext = GetViewContext(context); - var renderer = scopedProvider.GetRequiredService(); + var renderer = scopedProvider.GetRequiredService(); // Act var state = new AsyncDisposableState(); - var result = await renderer.RenderComponentAsync(viewContext, typeof(AsyncDisposableComponent), RenderMode.Static, new { state }); + var parameters = ParameterView.FromDictionary(new Dictionary { { "state", state } }); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(AsyncDisposableComponent), RenderMode.Static, parameters); // Assert - var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); Assert.Equal("

Hello

", content); await ((IAsyncDisposable)scope).DisposeAsync(); @@ -680,14 +686,14 @@ public async Task CanCatch_ComponentWithSynchronousException() var viewContext = GetViewContext(); // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await renderer.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( viewContext, typeof(ExceptionComponent), RenderMode.Static, - new + ParameterView.FromDictionary(new Dictionary { - IsAsync = false - })); + { "IsAsync", false } + }))); // Assert Assert.Equal("Threw an exception synchronously", exception.Message); @@ -700,14 +706,14 @@ public async Task CanCatch_ComponentWithAsynchronousException() var viewContext = GetViewContext(); // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await renderer.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( viewContext, typeof(ExceptionComponent), RenderMode.Static, - new + ParameterView.FromDictionary(new Dictionary { - IsAsync = true - })); + { "IsAsync", true } + }))); // Assert Assert.Equal("Threw an exception asynchronously", exception.Message); @@ -720,15 +726,14 @@ public async Task Rendering_ComponentWithJsInteropThrows() var viewContext = GetViewContext(); // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await renderer.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( viewContext, typeof(ExceptionComponent), RenderMode.Static, - new + ParameterView.FromDictionary(new Dictionary { - JsInterop = true - } - )); + { "JsInterop", true } + }))); // Assert Assert.Equal("JavaScript interop calls cannot be issued during server-side prerendering, " + @@ -753,14 +758,14 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponse var viewContext = GetViewContext(ctx); // Act - var exception = await Assert.ThrowsAsync(async () => await renderer.RenderComponentAsync( + var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( viewContext, typeof(RedirectComponent), RenderMode.Static, - new + ParameterView.FromDictionary(new Dictionary { - RedirectUri = "http://localhost/redirect" - })); + { "RedirectUri", "http://localhost/redirect" } + }))); 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" + @@ -781,14 +786,14 @@ public async Task HtmlHelper_Redirects_WhenComponentNavigates() var viewContext = GetViewContext(ctx); // Act - await renderer.RenderComponentAsync( + await renderer.PrerenderComponentAsync( viewContext, typeof(RedirectComponent), RenderMode.Static, - new + ParameterView.FromDictionary(new Dictionary { - RedirectUri = "http://localhost/redirect" - }); + { "RedirectUri", "http://localhost/redirect" } + })); // Assert Assert.Equal(302, ctx.Response.StatusCode); @@ -844,20 +849,18 @@ public async Task CanRender_AsyncComponent() "; // Act - var result = await renderer.RenderComponentAsync(viewContext, typeof(AsyncComponent), RenderMode.Static, null); - var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); + var result = await renderer.PrerenderComponentAsync(viewContext, typeof(AsyncComponent), RenderMode.Static, ParameterView.Empty); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); // Assert Assert.Equal(expectedContent.Replace("\r\n", "\n"), content); } - private ComponentRenderer GetComponentRenderer(IServiceProvider services = null) + private ComponentPrerenderer GetComponentRenderer(IServiceProvider services = null) { - var viewBufferScope = new TestViewBufferScope(); - return new ComponentRenderer( - new StaticComponentRenderer(new HtmlRenderer(services ?? _services, NullLoggerFactory.Instance, viewBufferScope)), - new ServerComponentSerializer(_dataprotectorProvider), - viewBufferScope); + return new ComponentPrerenderer( + new HtmlRenderer(services ?? _services, NullLoggerFactory.Instance), + new ServerComponentSerializer(_dataprotectorProvider)); } private ViewContext GetViewContext(HttpContext context = null) diff --git a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs deleted file mode 100644 index 015f29ec56ce..000000000000 --- a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs +++ /dev/null @@ -1,866 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Globalization; -using System.Runtime.ExceptionServices; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.Components.Rendering; - -public class HtmlRendererTest -{ - protected readonly HtmlEncoder _encoder = HtmlEncoder.Default; - - [Fact] - public void RenderComponentAsync_CanRenderEmptyElement() - { - // Arrange - var expectedHtml = new[] { "<", "p", ">", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanRenderSimpleComponent() - { - // Arrange - var expectedHtml = new[] { "<", "p", ">", "Hello world!", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddContent(1, "Hello world!"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_HtmlEncodesContent() - { - // Arrange - var expectedHtml = new[] { "<", "p", ">", "<Hello world!>", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddContent(1, ""); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_DoesNotEncodeMarkup() - { - // Arrange - var expectedHtml = new[] { "<", "p", ">", "Hello world!", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddMarkupContent(1, "Hello world!"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanRenderWithAttributes() - { - // Arrange - var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "lead", "\"", ">", "Hello world!", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddAttribute(1, "class", "lead"); - rtb.AddContent(2, "Hello world!"); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_SkipsDuplicatedAttribute() - { - // Arrange - var expectedHtml = new[] - { - "<", "p", " ", - "another", "=", "\"", "another-value", "\"", " ", - "Class", "=", "\"", "test2", "\"", ">", - "Hello world!", - "" - }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddAttribute(1, "class", "test1"); - rtb.AddAttribute(2, "another", "another-value"); - rtb.AddMultipleAttributes(3, new Dictionary() - { - { "Class", "test2" }, // Matching is case-insensitive. - }); - rtb.AddContent(4, "Hello world!"); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_HtmlEncodesAttributeValues() - { - // Arrange - var expectedHtml = new[] { "<", "p", " ", "class", "=", "\"", "<lead", "\"", ">", "Hello world!", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddAttribute(1, "class", " htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanRenderBooleanAttributes() - { - // Arrange - var expectedHtml = new[] { "<", "input", " ", "disabled", " />" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "input"); - rtb.AddAttribute(1, "disabled", true); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_DoesNotRenderBooleanAttributesWhenValueIsFalse() - { - // Arrange - var expectedHtml = new[] { "<", "input", " />" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "input"); - rtb.AddAttribute(1, "disabled", false); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanRenderWithChildren() - { - // Arrange - var expectedHtml = new[] { "<", "p", ">", "<", "span", ">", "Hello world!", "", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.OpenElement(1, "span"); - rtb.AddContent(2, "Hello world!"); - rtb.CloseElement(); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanRenderWithMultipleChildren() - { - // Arrange - var expectedHtml = new[] { "<", "p", ">", - "<", "span", ">", "Hello world!", "", - "<", "span", ">", "Bye Bye world!", "", - "" - }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.OpenElement(1, "span"); - rtb.AddContent(2, "Hello world!"); - rtb.CloseElement(); - rtb.OpenElement(3, "span"); - rtb.AddContent(4, "Bye Bye world!"); - rtb.CloseElement(); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_MarksSelectedOptionsAsSelected() - { - // Arrange - var expectedHtml = "

" + - @"" + - @"" + - "

"; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.OpenElement(1, "select"); - rtb.AddAttribute(2, "unrelated-attribute-before", "a"); - rtb.AddAttribute(3, "value", "b"); - rtb.AddAttribute(4, "unrelated-attribute-after", "c"); - - foreach (var optionValue in new[] { "a", "b", "c" }) - { - rtb.OpenElement(5, "option"); - rtb.AddAttribute(6, "unrelated-attribute", "a"); - rtb.AddAttribute(7, "value", optionValue); - rtb.AddContent(8, $"Pick value {optionValue}"); - rtb.CloseElement(); // option - } - - rtb.CloseElement(); // select - - rtb.OpenElement(9, "option"); // To show other value-matching options don't get marked as selected - rtb.AddAttribute(10, "value", "b"); - rtb.AddContent(11, "unrelated option"); - rtb.CloseElement(); // option - - rtb.CloseElement(); // p - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_RendersValueAttributeAsTextContentOfTextareaElement() - { - // Arrange - var expectedHtml = ""; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "textarea"); - rtb.AddAttribute(1, "value", "Hello -encoded content!"); - rtb.AddAttribute(2, "rows", "10"); - rtb.AddAttribute(3, "cols", "20"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_RendersTextareaElementWithoutValueAttribute() - { - // Arrange - var expectedHtml = ""; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "textarea"); - rtb.AddAttribute(1, "rows", "10"); - rtb.AddAttribute(2, "cols", "20"); - rtb.AddContent(3, "Hello -encoded content!"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_RendersTextareaElementWithoutValueAttributeOrTextContent() - { - // Arrange - var expectedHtml = ""; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "textarea"); - rtb.AddAttribute(1, "rows", "10"); - rtb.AddAttribute(2, "cols", "20"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_ValueAttributeOfTextareaElementOverridesTextContent() - { - // Arrange - var expectedHtml = ""; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "textarea"); - rtb.AddAttribute(1, "value", "Hello World!"); - rtb.AddContent(3, "Some content"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_RendersSelfClosingElement() - { - // Arrange - var expectedHtml = ""; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "input"); - rtb.AddAttribute(1, "value", "Hello -encoded content!"); - rtb.AddAttribute(2, "id", "Test"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_RendersSelfClosingElementWithTextComponentAsNormalElement() - { - // Arrange - var expectedHtml = "Something"; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "meta"); - rtb.AddContent(1, "Something"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_RendersSelfClosingElementBySkippingElementReferenceCapture() - { - // Arrange - var expectedHtml = ""; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "input"); - rtb.AddAttribute(1, "value", "Hello -encoded content!"); - rtb.AddAttribute(2, "id", "Test"); - rtb.AddElementReferenceCapture(3, inputReference => _ = inputReference); - rtb.CloseElement(); - })).BuildServiceProvider(); - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_MarksSelectedOptionsAsSelected_WithOptGroups() - { - // Arrange - var expectedHtml = - @""; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "select"); - rtb.AddAttribute(1, "value", "beta"); - - foreach (var optionValue in new[] { "alpha", "beta", "gamma" }) - { - rtb.OpenElement(2, "optgroup"); - rtb.OpenElement(3, "option"); - rtb.AddAttribute(4, "value", optionValue); - rtb.AddContent(5, optionValue); - rtb.CloseElement(); // option - rtb.CloseElement(); // optgroup - } - - rtb.CloseElement(); // select - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanRenderComponentAsyncWithChildrenComponents() - { - // Arrange - var expectedHtml = new[] { - "<", "p", ">", "<", "span", ">", "Hello world!", "", "", - "<", "span", ">", "Child content!", "" - }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.OpenElement(1, "span"); - rtb.AddContent(2, "Hello world!"); - rtb.CloseElement(); - rtb.CloseElement(); - rtb.OpenComponent(3, typeof(ChildComponent)); - rtb.AddAttribute(4, "Value", "Child content!"); - rtb.CloseComponent(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_ComponentReferenceNoops() - { - // Arrange - var expectedHtml = new[] { - "<", "p", ">", "<", "span", ">", "Hello world!", "", "", - "<", "span", ">", "Child content!", "" - }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.OpenElement(1, "span"); - rtb.AddContent(2, "Hello world!"); - rtb.CloseElement(); - rtb.CloseElement(); - rtb.OpenComponent(3, typeof(ChildComponent)); - rtb.AddAttribute(4, "Value", "Child content!"); - rtb.AddComponentReferenceCapture(5, cr => { }); - rtb.CloseComponent(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanPassParameters() - { - // Arrange - var expectedHtml = new[] { - "<", "p", ">", "<", "input", " ", "value", "=", "\"", "5", "\"", " />", "" }; - - RenderFragment Content(ParameterView pc) => new RenderFragment((RenderTreeBuilder rtb) => - { - rtb.OpenElement(0, "p"); - rtb.OpenElement(1, "input"); - rtb.AddAttribute(2, "change", pc.GetValueOrDefault>("update")); - rtb.AddAttribute(3, "value", pc.GetValueOrDefault("value")); - rtb.CloseElement(); - rtb.CloseElement(); - }); - - var serviceProvider = new ServiceCollection() - .AddSingleton(new Func(Content)) - .BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - Action change = (ChangeEventArgs changeArgs) => throw new InvalidOperationException(); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync( - ParameterView.FromDictionary(new Dictionary - { - { "update", change }, - { "value", 5 } - })))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_CanRenderComponentAsyncWithRenderFragmentContent() - { - // Arrange - var expectedHtml = new[] { - "<", "p", ">", "<", "span", ">", "Hello world!", "", "" }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.OpenElement(1, "span"); - rtb.AddContent(2, - // This internally creates a region frame. - rf => rf.AddContent(0, "Hello world!")); - rtb.CloseElement(); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - [Fact] - public void RenderComponentAsync_ElementRefsNoops() - { - // Arrange - var expectedHtml = new[] - { - "<", "p", ">", "<", "span", ">", "Hello world!", "", "" - }; - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddElementReferenceCapture(1, er => { }); - rtb.OpenElement(2, "span"); - rtb.AddContent(3, - // This internally creates a region frame. - rf => rf.AddContent(0, "Hello world!")); - rtb.CloseElement(); - rtb.CloseElement(); - })).BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = GetResult(htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.Empty))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result); - } - - private IHtmlContent GetResult(Task task) - { - Assert.True(task.IsCompleted); - if (task.IsCompletedSuccessfully) - { - return task.Result.HtmlContent; - } - else - { - ExceptionDispatchInfo.Capture(task.Exception).Throw(); - throw new InvalidOperationException("We will never hit this line"); - } - } - - private void AssertHtmlContentEquals(IEnumerable expected, IHtmlContent actual) - { - var expectedString = string.Concat(expected); - AssertHtmlContentEquals(expectedString, actual); - } - - private void AssertHtmlContentEquals(string expected, IHtmlContent actual) - { - Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(actual, _encoder)); - } - - private class ComponentWithParameters : IComponent - { - public RenderHandle RenderHandle { get; private set; } - - public void Attach(RenderHandle renderHandle) - { - RenderHandle = renderHandle; - } - - [Inject] - Func CreateRenderFragment { get; set; } - - public Task SetParametersAsync(ParameterView parameters) - { - RenderHandle.Render(CreateRenderFragment(parameters)); - return Task.CompletedTask; - } - } - - [Fact] - public async Task CanRender_AsyncComponent() - { - // Arrange - var expectedHtml = new[] { - "<", "p", ">", "20", "" }; - var serviceProvider = new ServiceCollection().AddSingleton().BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = await htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary - { - ["Value"] = 10 - }))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result.HtmlContent); - } - - [Fact] - public async Task CanRender_NestedAsyncComponents() - { - // Arrange - var expectedHtml = new[] - { - "<", "p", ">", "20", "", - "<", "p", ">", "80", "" - }; - - var serviceProvider = new ServiceCollection().AddSingleton().BuildServiceProvider(); - - var htmlRenderer = GetHtmlRenderer(serviceProvider); - - // Act - var result = await htmlRenderer.Dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(ParameterView.FromDictionary(new Dictionary - { - ["Nested"] = false, - ["Value"] = 10 - }))); - - // Assert - AssertHtmlContentEquals(expectedHtml, result.HtmlContent); - } - - [Fact] - public async Task PrerendersMultipleComponentsSuccessfully() - { - // Arrange - var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb => - { - rtb.OpenElement(0, "p"); - rtb.AddMarkupContent(1, "Hello world!"); - rtb.CloseElement(); - })).BuildServiceProvider(); - var renderer = GetHtmlRenderer(serviceProvider); - - // Act - var first = await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync(ParameterView.Empty)); - var second = await renderer.Dispatcher.InvokeAsync(() => renderer.RenderComponentAsync(ParameterView.Empty)); - - // Assert - Assert.Equal(0, first.ComponentId); - Assert.Equal(1, second.ComponentId); - } - - private HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider) - { - return new HtmlRenderer(serviceProvider, NullLoggerFactory.Instance, new TestViewBufferScope()); - } - - private class NestedAsyncComponent : ComponentBase - { - [Parameter] public bool Nested { get; set; } - [Parameter] public int Value { get; set; } - - protected override async Task OnInitializedAsync() - { - Value = Value * 2; - await Task.Yield(); - } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.OpenElement(0, "p"); - builder.AddContent(1, Value.ToString(CultureInfo.InvariantCulture)); - builder.CloseElement(); - if (!Nested) - { - builder.OpenComponent(2); - builder.AddAttribute(3, "Nested", true); - builder.AddAttribute(4, "Value", Value * 2); - builder.CloseComponent(); - } - } - } - - private class AsyncComponent : ComponentBase - { - public AsyncComponent() - { - } - - [Parameter] - public int Value { get; set; } - - protected override async Task OnInitializedAsync() - { - Value = Value * 2; - await Task.Delay(Value * 100); - } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.OpenElement(0, "p"); - builder.AddContent(1, Value.ToString(CultureInfo.InvariantCulture)); - builder.CloseElement(); - } - } - - private class ChildComponent : IComponent - { - private RenderHandle _renderHandle; - - public void Attach(RenderHandle renderHandle) - { - _renderHandle = renderHandle; - } - - public Task SetParametersAsync(ParameterView parameters) - { - var content = parameters.GetValueOrDefault("Value"); - _renderHandle.Render(CreateRenderFragment(content)); - return Task.CompletedTask; - } - - private RenderFragment CreateRenderFragment(string content) - { - return RenderFragment; - - void RenderFragment(RenderTreeBuilder rtb) - { - rtb.OpenElement(1, "span"); - rtb.AddContent(2, content); - rtb.CloseElement(); - } - } - } - - private class TestComponent : IComponent - { - private RenderHandle _renderHandle; - - [Inject] - public RenderFragment Fragment { get; set; } - - public void Attach(RenderHandle renderHandle) - { - _renderHandle = renderHandle; - } - - public Task SetParametersAsync(ParameterView parameters) - { - _renderHandle.Render(Fragment); - return Task.CompletedTask; - } - } -} diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs deleted file mode 100644 index 4e6b65a13b1a..000000000000 --- a/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Html; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.Extensions.DependencyInjection; -using Moq; - -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()) == new ValueTask(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; - } -} From d189018f261b88190977d0615f908265e674824a Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 3 Mar 2023 09:16:13 +0000 Subject: [PATCH 26/26] Restore back-compat by implementing IDisposable --- src/Components/Web/src/HtmlRendering/HtmlRenderer.cs | 6 +++++- src/Components/Web/src/PublicAPI.Unshipped.txt | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs index caa89ef80a1d..3a2811c4ca76 100644 --- a/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/HtmlRenderer.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Components.Web; /// /// Provides a mechanism for rendering components non-interactively as HTML markup. /// -public sealed class HtmlRenderer : IAsyncDisposable +public sealed class HtmlRenderer : IDisposable, IAsyncDisposable { private readonly HtmlRendererCore _passiveHtmlRenderer; @@ -26,6 +26,10 @@ public HtmlRenderer(IServiceProvider services, ILoggerFactory loggerFactory) _passiveHtmlRenderer = new HtmlRendererCore(services, loggerFactory, componentActivator); } + /// + public void Dispose() + => _passiveHtmlRenderer.Dispose(); + /// public ValueTask DisposeAsync() => _passiveHtmlRenderer.DisposeAsync(); diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index fdb62745368b..8c8ed39e008f 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -9,6 +9,7 @@ Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(System. Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent() -> Microsoft.AspNetCore.Components.Web.HtmlComponent! Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlComponent! Microsoft.AspNetCore.Components.Web.HtmlRenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! +Microsoft.AspNetCore.Components.Web.HtmlRenderer.Dispose() -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer.DisposeAsync() -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Web.HtmlRenderer.HtmlRenderer(System.IServiceProvider! services, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Components.Web.HtmlRenderer.RenderComponentAsync(System.Type! componentType) -> System.Threading.Tasks.Task!