Skip to content

API review: Component rendering to HTML outside Blazor hosting #50079

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
SteveSandersonMS opened this issue Aug 15, 2023 · 1 comment
Closed
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-blazor Includes: Blazor, Razor Components

Comments

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Aug 15, 2023

Background and Motivation

Allows developers to render Blazor components to HTML (as a string or into a TextWriter). This is used for Blazor SSR but making it public satisfies many requests we've had over the years for using .razor components for other custom things.

Proposed API

namespace Microsoft.AspNetCore.Components.Web
{
     // Notice that this does not derive from StaticHtmlRenderer (below). Instead it wraps it, providing a convenient
     // API without exposing the more low-level public members from StaticHtmlRenderer.
+    /// <summary>
+    /// Provides a mechanism for rendering components non-interactively as HTML markup.
+    /// </summary>
+    public sealed class HtmlRenderer : IDisposable, IAsyncDisposable
+    {
+        /// <summary>
+        /// Constructs an instance of <see cref="HtmlRenderer"/>.
+        /// </summary>
+        /// <param name="services">The services to use when rendering components.</param>
+        /// <param name="loggerFactory">The logger factory to use.</param>
+        public HtmlRenderer(IServiceProvider services, ILoggerFactory loggerFactory) {}
+
+        /// <summary>
+        /// Gets the <see cref="Components.Dispatcher" /> associated with this instance. Any calls to
+        /// <see cref="RenderComponentAsync{TComponent}()"/> or <see cref="BeginRenderingComponent{TComponent}()"/>
+        /// must be performed using this <see cref="Components.Dispatcher" />.
+        /// </summary>
+        public Dispatcher Dispatcher { get; }
+
         // The reason for having both RenderComponentAsync and BeginRenderingComponent is:
         // - RenderComponentAsync is a more obvious, simple API if you just want to get the end result (after quiescence)
         //   of rendering the component, and don't need to see any intermediate state
         // - BeginRenderingComponent is relevant if you want to do the above *plus* you want to be able to access its
         //   initial synchronous output. We use this for streaming SSR.
         // In both cases you get the actual HTML using APIs on the returned HtmlRootComponent object.
+
+        /// <summary>
+        /// Adds an instance of the specified component and instructs it to render. The resulting content represents the
+        /// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete
+        /// any asynchronous operations such as loading, await <see cref="HtmlRootComponent.QuiescenceTask"/> before
+        /// reading content from the <see cref="HtmlRootComponent"/>.
+        /// </summary>
+        /// <typeparam name="TComponent">The component type.</typeparam>
+        /// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
+        /// <param name="parameters">Parameters for the component.</param>
+        /// <returns>An <see cref="HtmlRootComponent"/> instance representing the render output.</returns>
+        public HtmlRootComponent BeginRenderingComponent<TComponent>() where TComponent : IComponent {}
+        public HtmlRootComponent BeginRenderingComponent<TComponent>(ParameterView parameters) where TComponent : IComponent {}
+        public HtmlRootComponent BeginRenderingComponent(Type componentType) {}
+        public HtmlRootComponent BeginRenderingComponent(Type componentType, ParameterView parameters) {}
+
+        /// <summary>
+        /// Adds an instance of the specified component and instructs it to render, waiting
+        /// for the component hierarchy to complete asynchronous tasks such as loading.
+        /// </summary>
+        /// <typeparam name="TComponent">The component type.</typeparam>
+        /// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
+        /// <param name="parameters">Parameters for the component.</param>
+        /// <returns>A task that completes with <see cref="HtmlRootComponent"/> once the component hierarchy has completed any asynchronous tasks such as loading.</returns>
+        public Task<HtmlRootComponent> RenderComponentAsync<TComponent>() where TComponent : IComponent {}
+        public Task<HtmlRootComponent> RenderComponentAsync(Type componentType) {}
+        public Task<HtmlRootComponent> RenderComponentAsync<TComponent>(ParameterView parameters) where TComponent : IComponent {}
+        public Task<HtmlRootComponent> RenderComponentAsync(Type componentType, ParameterView parameters) {}
+    }
}

+namespace Microsoft.AspNetCore.Components.Web.HtmlRendering
+{
+    /// <summary>
+    /// Represents the output of rendering a root component as HTML. The content can change if the component instance re-renders.
+    /// </summary>
+    public readonly struct HtmlRootComponent
+    {
+        /// <summary>
+        /// Gets the component ID.
+        /// </summary>
+        public int ComponentId { get; }
+
+        /// <summary>
+        /// Gets a <see cref="Task"/> that completes when the component hierarchy has completed asynchronous tasks such as loading.
+        /// </summary>
+        public Task QuiescenceTask { get; } = Task.CompletedTask;
+
+        /// <summary>
+        /// Returns an HTML string representation of the component's latest output.
+        /// </summary>
+        /// <returns>An HTML string representation of the component's latest output.</returns>
+        public string ToHtmlString() {}
+
+        /// <summary>
+        /// Writes the component's latest output as HTML to the specified writer.
+        /// </summary>
+        /// <param name="output">The output destination.</param>
+        public void WriteHtmlTo(TextWriter output) {}
+    }
+}

+namespace Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure
+{
     // Low-ish level renderer subclass that deals with producing HTML text output rather than
     // rendering to a browser DOM. App developers aren't expected to use this directly, but it's
     // public so that EndpointHtmlRenderer can derive from it.
+    /// <summary>
+    /// A <see cref="Renderer"/> subclass that is intended for static HTML rendering. Application
+    /// developers should not normally use this class directly. Instead, use
+    /// <see cref="HtmlRenderer"/> for a more convenient API.
+    /// </summary>
+    public class StaticHtmlRenderer : Renderer
+    {
+        public StaticHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) {}
+
+        /// <summary>
+        /// Adds a root component of the specified type and begins rendering it.
+        /// </summary>
+        /// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
+        /// <param name="initialParameters">Parameters for the component.</param>
+        /// <returns>An <see cref="HtmlRootComponent"/> that can be used to obtain the rendered HTML.</returns>
+        public HtmlRootComponent BeginRenderingComponent(Type componentType, ParameterView initialParameters) {}
+
+        /// <summary>
+        /// Adds a root component and begins rendering it.
+        /// </summary>
+        /// <param name="component">The root component instance to be added and rendered. This must not already be associated with any renderer.</param>
+        /// <param name="initialParameters">Parameters for the component.</param>
+        /// <returns>An <see cref="HtmlRootComponent"/> that can be used to obtain the rendered HTML.</returns>
+        public HtmlRootComponent BeginRenderingComponent(IComponent component, ParameterView initialParameters) {}
+
+        /// <summary>
+        /// Renders the specified component as HTML to the output.
+        /// </summary>
+        /// <param name="componentId">The ID of the component whose current HTML state is to be rendered.</param>
+        /// <param name="output">The output destination.</param>
+        protected virtual void WriteComponentHtml(int componentId, TextWriter output) {}
+
+        // Returns false if there's no form mapping context (e.g., you're using this outside Blazor SSR, when there's no use case for event names)
+        /// <summary>
+        /// Creates the fully scope-qualified name for a named event, if the component is within
+        /// a <see cref="FormMappingContext"/> (whether or not that mapping context is named).
+        /// </summary>
+        /// <param name="componentId">The ID of the component that defines a named event.</param>
+        /// <param name="assignedEventName">The name assigned to the named event.</param>
+        /// <param name="scopeQualifiedEventName">The scope-qualified event name.</param>
+        /// <returns>A flag to indicate whether a value could be produced.</returns>
+        protected bool TryCreateScopeQualifiedEventName(int componentId, string assignedEventName, [NotNullWhen(true)] out string? scopeQualifiedEventName) {}
+    }
+}

The following interface enables M.A.Mvc.ViewFeatures' existing <component> tag helper to use the new EndpointHtmlRenderer infrastructure, allowing us to remove all the old implementation.

namespace Microsoft.AspNetCore.Components.Endpoints
{
+    /// <summary>
+    /// A service that can prerender Razor Components as HTML.
+    /// </summary>
+    public interface IComponentPrerenderer
+    {
+        /// <summary>
+        /// Prerenders a Razor Component as HTML.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
+        /// <param name="componentType">The type of component to prerender. This must implement <see cref="IComponent"/>.</param>
+        /// <param name="renderMode">The mode in which to prerender the component.</param>
+        /// <param name="parameters">Parameters for the component.</param>
+        /// <returns>A task that completes with the prerendered content.</returns>
+        ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
+            HttpContext httpContext, Type componentType, IComponentRenderMode renderMode, ParameterView parameters);
+
+        /// <summary>
+        /// Prepares a serialized representation of any component state that is persistible within the current request.
+        /// </summary>
+        /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
+        /// <param name="serializationMode">The <see cref="PersistedStateSerializationMode"/>.</param>
+        /// <returns>A task that completes with the prerendered state content.</returns>
+        ValueTask<IHtmlContent> PrerenderPersistedStateAsync(
+            HttpContext httpContext, PersistedStateSerializationMode serializationMode);
+
+        /// <summary>
+        /// Gets a <see cref="Dispatcher"/> that should be used for calls to <see cref="PrerenderComponentAsync(HttpContext, Type, IComponentRenderMode, ParameterView)"/>.
+        /// </summary>
+        Dispatcher Dispatcher { get; }
+    }
}

namespace Microsoft.AspNetCore.Components
{
+    /// <summary>
+    /// Specifies the mode to use when serializing component persistent state.
+    /// </summary>
+    public enum PersistedStateSerializationMode
+    {
+        /// <summary>
+        /// Indicates that the serialization mode should be inferred from the current request context.
+        /// </summary>
+        Infer = 1,
+
+        /// <summary>
+        /// Indicates that the state should be persisted so that execution may resume on Server.
+        /// </summary>
+        Server = 2,
+
+        /// <summary>
+        /// Indicates that the state should be persisted so that execution may resume on WebAssembly.
+        /// </summary>
+        WebAssembly = 3,
+    }
}
@SteveSandersonMS SteveSandersonMS added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Aug 15, 2023
@ghost ghost added the area-blazor Includes: Blazor, Razor Components label Aug 15, 2023
@SteveSandersonMS
Copy link
Member Author

Closing in favour of #47018, which has more info and examples.

@SteveSandersonMS SteveSandersonMS closed this as not planned Won't fix, can't repro, duplicate, stale Aug 15, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Sep 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

1 participant