File: Rendering\WebAssemblyRenderer.cs
Web Access
Project: src\src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj (Microsoft.AspNetCore.Components.WebAssembly)
// 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 System.Runtime.CompilerServices;
using System.Runtime.InteropServices.JavaScript;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Infrastructure;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
 
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering;
 
/// <summary>
/// Provides mechanisms for rendering <see cref="IComponent"/> instances in a
/// web browser, dispatching events to them, and refreshing the UI as required.
/// </summary>
internal sealed partial class WebAssemblyRenderer : WebRenderer
{
    private readonly ILogger _logger;
    private readonly Dispatcher _dispatcher;
    private readonly ResourceAssetCollection _resourceCollection;
    private readonly IInternalJSImportMethods _jsMethods;
    private static readonly RendererInfo _componentPlatform = new("WebAssembly", isInteractive: true);
 
    public WebAssemblyRenderer(IServiceProvider serviceProvider, ResourceAssetCollection resourceCollection, ILoggerFactory loggerFactory, JSComponentInterop jsComponentInterop)
        : base(serviceProvider, loggerFactory, DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions(), jsComponentInterop)
    {
        _logger = loggerFactory.CreateLogger<WebAssemblyRenderer>();
        _jsMethods = serviceProvider.GetRequiredService<IInternalJSImportMethods>();
 
        // if SynchronizationContext.Current is null, it means we are on the single-threaded runtime
        _dispatcher = WebAssemblyDispatcher._mainSynchronizationContext == null
            ? NullDispatcher.Instance
            : new WebAssemblyDispatcher();
 
        _resourceCollection = resourceCollection;
 
        ElementReferenceContext = DefaultWebAssemblyJSRuntime.Instance.ElementReferenceContext;
        DefaultWebAssemblyJSRuntime.Instance.OnUpdateRootComponents += OnUpdateRootComponents;
    }
 
    [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "These are root components which belong to the user and are in assemblies that don't get trimmed.")]
    private void OnUpdateRootComponents(RootComponentOperationBatch batch)
    {
        var webRootComponentManager = GetOrCreateWebRootComponentManager();
        for (var i = 0; i < batch.Operations.Length; i++)
        {
            var operation = batch.Operations[i];
            switch (operation.Type)
            {
                case RootComponentOperationType.Add:
                    _ = webRootComponentManager.AddRootComponentAsync(
                        operation.SsrComponentId,
                        operation.Descriptor!.ComponentType,
                        operation.Marker!.Value.Key!,
                        operation.Descriptor!.Parameters);
                    break;
                case RootComponentOperationType.Update:
                    _ = webRootComponentManager.UpdateRootComponentAsync(
                        operation.SsrComponentId,
                        operation.Descriptor!.ComponentType,
                        operation.Marker?.Key,
                        operation.Descriptor!.Parameters);
                    break;
                case RootComponentOperationType.Remove:
                    webRootComponentManager.RemoveRootComponent(operation.SsrComponentId);
                    break;
            }
        }
 
        NotifyEndUpdateRootComponents(batch.BatchId);
    }
 
    protected override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveWebAssembly;
 
    public void NotifyEndUpdateRootComponents(long batchId)
    {
        _jsMethods.EndUpdateRootComponents(batchId);
    }
 
    protected override ResourceAssetCollection Assets => _resourceCollection;
 
    protected override RendererInfo RendererInfo => _componentPlatform;
 
    public override Dispatcher Dispatcher => _dispatcher;
 
    public Task AddComponentAsync([DynamicallyAccessedMembers(Component)] Type componentType, ParameterView parameters, string domElementSelector)
    {
        var componentId = AddRootComponent(componentType, domElementSelector);
        return RenderRootComponentAsync(componentId, parameters);
    }
 
    protected override int GetWebRendererId() => (int)WebRendererId.WebAssembly;
 
    protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector)
    {
        _jsMethods.AttachRootComponentToElement(domElementSelector, componentId, RendererId);
    }
 
    /// <inheritdoc />
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
    }
 
    /// <inheritdoc />
    protected override void ProcessPendingRender()
    {
        // For historical reasons, Blazor WebAssembly doesn't enforce that you use InvokeAsync
        // to dispatch calls that originated from outside the system. Changing that now would be
        // too breaking, at least until we can make it a prerequisite for multithreading.
        // So, we don't have a way to guarantee that calls to here are already on our work queue.
        //
        // We do need rendering to happen on the work queue so that incoming events can be deferred
        // until we've finished this rendering process (and other similar cases where we want
        // execution order to be consistent with Blazor Server, which queues all JS->.NET calls).
        //
        // So, if we find that we're here and are not yet on the work queue, get onto it. Either
        // way, rendering must continue synchronously here and is not deferred until later.
        if (WebAssemblyCallQueue.IsInProgress)
        {
            base.ProcessPendingRender();
        }
        else
        {
            WebAssemblyCallQueue.Schedule(this, static @this => @this.CallBaseProcessPendingRender());
        }
    }
 
    private void CallBaseProcessPendingRender() => base.ProcessPendingRender();
 
    /// <inheritdoc />
    protected override unsafe Task UpdateDisplayAsync(in RenderBatch batch)
    {
        // This is a GC hazard - it would be ideal to pin 'batch' and all its contents to prevent
        // it from getting moved, or pause the GC for the duration of the 'RenderBatch()' call.
        // The key mitigation is that the JS-side code always processes renderbatches synchronously
        // and never calls back into .NET during that process, so GC cannot run (assuming it would
        // only run on the current thread).
        // As an early-warning system in case we accidentally introduce bugs and violate that rule,
        // or for edge cases where user code can be invoked during rendering (e.g., DOM mutation
        // observers) we further enforce it on the JS side using a notion of "locking the heap"
        // during rendering, which prevents any JS-to-.NET calls that go through Blazor APIs such
        // as DotNet.invokeMethod or event handlers.
        var batchCopy = batch;
        RenderBatch(RendererId, Unsafe.AsPointer(ref batchCopy));
 
        if (WebAssemblyCallQueue.HasUnstartedWork)
        {
            // Because further incoming calls from JS to .NET are already queued (e.g., event notifications),
            // we have to delay the renderbatch acknowledgement until it gets to the front of that queue.
            // This is for consistency with Blazor Server which queues all JS-to-.NET calls relative to each
            // other, and because various bits of cleanup logic rely on this ordering.
            var tcs = new TaskCompletionSource();
            WebAssemblyCallQueue.Schedule(tcs, static tcs => tcs.SetResult());
            return tcs.Task;
        }
        else
        {
            // Nothing else is pending, so we can treat the renderbatch as acknowledged synchronously.
            // This lets upstream code skip an expensive code path and avoids some allocations.
            return Task.CompletedTask;
        }
    }
 
    /// <inheritdoc />
    protected override void HandleException(Exception exception)
    {
        if (exception is AggregateException aggregateException)
        {
            foreach (var innerException in aggregateException.Flatten().InnerExceptions)
            {
                Log.UnhandledExceptionRenderingComponent(_logger, innerException.Message, innerException);
            }
        }
        else
        {
            Log.UnhandledExceptionRenderingComponent(_logger, exception.Message, exception);
        }
    }
 
    protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
        => renderMode switch
        {
            InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode => componentActivator.CreateInstance(componentType),
            _ => throw new NotSupportedException($"Cannot create a component of type '{componentType}' because its render mode '{renderMode}' is not supported by WebAssembly rendering."),
        };
 
    private static partial class Log
    {
        [LoggerMessage(100, LogLevel.Critical, "Unhandled exception rendering component: {Message}", EventName = "ExceptionRenderingComponent")]
        public static partial void UnhandledExceptionRenderingComponent(ILogger logger, string message, Exception exception);
    }
 
    [JSImport("Blazor._internal.renderBatch", "blazor-internal")]
    private static unsafe partial void RenderBatch(int id, void* batch);
}