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 new file mode 100644 index 000000000000..30989642fea9 --- /dev/null +++ b/src/Components/Web/src/HtmlRendering/HtmlComponent.cs @@ -0,0 +1,63 @@ +// 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.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 HtmlComponent +{ + private readonly HtmlRendererCore? _renderer; + private readonly int _componentId; + private readonly 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. + /// + /// 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. + /// + /// 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(); + } + + /// + /// Writes the component's latest output as HTML to the specified writer. + /// + /// The output destination. + public void WriteHtmlTo(TextWriter output) + { + if (_renderer is not null) + { + HtmlComponentWriter.Write(_renderer, _componentId, output); + } + } +} diff --git a/src/Components/Web/src/HtmlRendering/HtmlComponentWriter.cs b/src/Components/Web/src/HtmlRendering/HtmlComponentWriter.cs new file mode 100644 index 000000000000..29ac1f574f98 --- /dev/null +++ b/src/Components/Web/src/HtmlRendering/HtmlComponentWriter.cs @@ -0,0 +1,241 @@ +// 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; + +// 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 HtmlComponentWriter +{ + 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 HtmlRendererCore _renderer; + private readonly TextWriter _output; + private string? _closestSelectValueAsString; + + 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 HtmlComponentWriter(renderer, output); + context.RenderComponent(componentId); + } + + private HtmlComponentWriter(HtmlRendererCore renderer, TextWriter output) + { + _renderer = renderer; + _output = output; + } + + 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); + int afterElement; + var isTextArea = string.Equals(frame.ElementName, "textarea", StringComparison.OrdinalIgnoreCase); + // We don't want to include value attribute of textarea element. + var afterAttributes = RenderAttributes(frames, position + 1, frame.ElementSubtreeLength - 1, !isTextArea, out var capturedValueAttribute); + + // When we see an