|
// 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.Reflection.Metadata;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.Web.Infrastructure;
using Microsoft.AspNetCore.Components.WebAssembly.HotReload;
using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure;
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
/// <summary>
/// A host object for Blazor running under WebAssembly. Use <see cref="WebAssemblyHostBuilder"/>
/// to initialize a <see cref="WebAssemblyHost"/>.
/// </summary>
public sealed class WebAssemblyHost : IAsyncDisposable
{
private readonly AsyncServiceScope _scope;
private readonly IServiceProvider _services;
private readonly IConfiguration _configuration;
private readonly RootComponentMappingCollection _rootComponents;
private readonly string? _persistedState;
// NOTE: the host is disposable because it OWNs references to disposable things.
//
// The twist is that in general dispose is not going to run even if the user puts it in a using.
// When a user refreshes or navigates away that terminates the app, like a process.exit. So the
// dispose functionality here is basically so that it can be used in unit tests.
//
// Based on the APIs that exist in Blazor today it's not possible for the
// app to get disposed, however if we add something like that in the future, most of the work is
// already done.
private bool _disposed;
private bool _started;
private WebAssemblyRenderer? _renderer;
internal WebAssemblyHost(
WebAssemblyHostBuilder builder,
IServiceProvider services,
AsyncServiceScope scope,
string? persistedState)
{
// To ensure JS-invoked methods don't get linked out, have a reference to their enclosing types
GC.KeepAlive(typeof(JSInteropMethods));
_services = services;
_scope = scope;
_configuration = builder.Configuration;
_rootComponents = builder.RootComponents;
_persistedState = persistedState;
}
/// <summary>
/// Gets the application configuration.
/// </summary>
public IConfiguration Configuration => _configuration;
/// <summary>
/// Gets the service provider associated with the application.
/// </summary>
public IServiceProvider Services => _scope.ServiceProvider;
/// <summary>
/// Disposes the host asynchronously.
/// </summary>
/// <returns>A <see cref="ValueTask"/> which represents the completion of disposal.</returns>
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
_disposed = true;
if (_renderer != null)
{
await _renderer.DisposeAsync();
}
await _scope.DisposeAsync();
if (_services is IAsyncDisposable asyncDisposableServices)
{
await asyncDisposableServices.DisposeAsync();
}
else if (_services is IDisposable disposableServices)
{
disposableServices.Dispose();
}
}
/// <summary>
/// Runs the application associated with this host.
/// </summary>
/// <returns>A <see cref="Task"/> which represents exit of the application.</returns>
/// <remarks>
/// At this time, it's not possible to shut down a Blazor WebAssembly application using imperative code.
/// The application only stops when the hosting page is reloaded or navigated to another page. As a result
/// the task returned from this method does not complete. This method is not suitable for use in unit-testing.
/// </remarks>
public Task RunAsync()
{
// RunAsyncCore will await until the CancellationToken fires. However, we don't fire it
// currently, so the app will "run" forever.
return RunAsyncCore(CancellationToken.None);
}
// Internal for testing.
internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssemblyCultureProvider? cultureProvider = null)
{
if (_started)
{
throw new InvalidOperationException("The host has already started.");
}
_started = true;
cultureProvider ??= WebAssemblyCultureProvider.Instance!;
cultureProvider.ThrowIfCultureChangeIsUnsupported();
// Application developers might have configured the culture based on some ambient state
// such as local storage, url etc as part of their Program.Main(Async).
// This is the earliest opportunity to fetch satellite assemblies for this selection.
await cultureProvider.LoadCurrentCultureResourcesAsync();
var manager = Services.GetRequiredService<ComponentStatePersistenceManager>();
var store = !string.IsNullOrEmpty(_persistedState) ?
new PrerenderComponentApplicationStore(_persistedState) :
new PrerenderComponentApplicationStore();
await manager.RestoreStateAsync(store);
RestoreAntiforgeryToken();
if (MetadataUpdater.IsSupported)
{
await WebAssemblyHotReload.InitializeAsync();
}
var tcs = new TaskCompletionSource();
using (cancellationToken.Register(() => tcs.TrySetResult()))
{
var loggerFactory = Services.GetRequiredService<ILoggerFactory>();
var jsComponentInterop = new JSComponentInterop(_rootComponents.JSComponents);
var collectionProvider = Services.GetRequiredService<ResourceCollectionProvider>();
var collection = await collectionProvider.GetResourceCollection();
_renderer = new WebAssemblyRenderer(Services, collection, loggerFactory, jsComponentInterop);
WebAssemblyNavigationManager.Instance.CreateLogger(loggerFactory);
RootComponentOperationBatch? initialOperationBatch = null;
if (Environment.GetEnvironmentVariable("__BLAZOR_WEBASSEMBLY_WAIT_FOR_ROOT_COMPONENTS") == "true")
{
// In Blazor web, we wait for the JS side to tell us about the components available
// before we render the initial set of components. Any additional update goes through
// UpdateRootComponents.
// We do it this way to ensure that the persistent component state is only used the first time
// the wasm runtime is initialized and is done in the same way for both webassembly and blazor
// web.
initialOperationBatch = await InternalJSImportMethods.GetInitialComponentUpdate();
}
var initializationTcs = new TaskCompletionSource();
WebAssemblyCallQueue.Schedule((_rootComponents, _renderer, initializationTcs), async state =>
{
var (rootComponents, renderer, initializationTcs) = state;
try
{
// Here, we add each root component but don't await the returned tasks so that the
// components can be processed in parallel.
var count = rootComponents.Count;
var initialOperationCount = initialOperationBatch?.Operations.Length ?? 0;
var pendingRenders = new List<Task>(count + initialOperationCount);
for (var i = 0; i < count; i++)
{
var rootComponent = rootComponents[i];
pendingRenders.Add(renderer.AddComponentAsync(
rootComponent.ComponentType,
rootComponent.Parameters,
rootComponent.Selector));
}
if (initialOperationBatch is not null)
{
AddWebRootComponents(renderer, initialOperationBatch, pendingRenders);
}
// Now we wait for all components to finish rendering.
await Task.WhenAll(pendingRenders);
initializationTcs.SetResult();
}
catch (Exception ex)
{
initializationTcs.SetException(ex);
}
});
await initializationTcs.Task;
store.ExistingState.Clear();
await tcs.Task;
}
}
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "These are root components which belong to the user and are in assemblies that don't get trimmed.")]
private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootComponentOperationBatch operationBatch, List<Task> pendingRenders)
{
var webRootComponentManager = renderer.GetOrCreateWebRootComponentManager();
var operations = operationBatch.Operations;
for (var i = 0; i < operations.Length; i++)
{
var operation = operations[i];
if (operation.Type != RootComponentOperationType.Add)
{
throw new InvalidOperationException("All initial operations must be additions.");
}
pendingRenders.Add(webRootComponentManager.AddRootComponentAsync(
operation.SsrComponentId,
operation.Descriptor!.ComponentType,
operation.Marker?.Key,
operation.Descriptor!.Parameters));
}
renderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
}
private void RestoreAntiforgeryToken()
{
// The act of instantiating the DefaultAntiforgeryStateProvider will automatically
// retrieve the antiforgery token from the persistent state
_scope.ServiceProvider.GetRequiredService<AntiforgeryStateProvider>();
}
}
|