File: JSCallbackFactory.cs
Web Access
Project: Microsoft.AspNetCore.Components.Platform.csproj (Microsoft.AspNetCore.Components.Platform)
// 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.JSInterop;
 
namespace Microsoft.AspNetCore.Components.Platform;
 
/// <summary>
/// Creates JavaScript functions that call a <see cref="JSCallback{T1}"/> through the JavaScript module of this package.
/// </summary>
internal sealed class JSCallbackFactory : IInternalAsyncDisposal
{
    private const string ModulePath = "./_content/Microsoft.AspNetCore.Components.Platform/Microsoft.AspNetCore.Components.Platform.js";
 
    private readonly IJSRuntime _jsRuntime;
    private Task<IJSObjectReference>? _moduleTask;
    private bool _disposed;
 
    public JSCallbackFactory(IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
    }
 
    /// <summary>
    /// Creates a JavaScript function that calls <paramref name="callback"/>.
    /// </summary>
    /// <param name="callback">The <see cref="JSCallback{T1}"/> to call.</param>
    /// <param name="shapesJson">
    /// A JSON array with one <c>ArgumentShape</c> (see <c>js/Platform.ts</c>) per parameter of <paramref name="callback"/>.
    /// </param>
    /// <returns>
    /// The function, and the reference that lets JavaScript reach <paramref name="callback"/>.
    /// The caller owns both and releases them when the function is no longer used.
    /// </returns>
    public async Task<(IJSObjectReference Function, IDisposable CallbackReference)> CreateAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TCallback>(
        TCallback callback,
        string shapesJson)
        where TCallback : class
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
 
        var module = await GetModuleAsync();
        var callbackReference = DotNetObjectReference.Create(callback);
 
        try
        {
            var function = await module.InvokeAsync<IJSObjectReference>("createCallback", callbackReference, shapesJson);
            return (function, callbackReference);
        }
        catch
        {
            callbackReference.Dispose();
            throw;
        }
    }
 
    async ValueTask IInternalAsyncDisposal.InternalDisposeAsync()
    {
        if (_disposed)
        {
            return;
        }
 
        _disposed = true;
 
        // Functions created from the module keep working after the module reference is released.
        if (_moduleTask is { IsCompletedSuccessfully: true } moduleTask)
        {
            await moduleTask.Result.DisposeAsync();
        }
    }
 
    private Task<IJSObjectReference> GetModuleAsync()
    {
        if (_moduleTask is null || _moduleTask.IsFaulted || _moduleTask.IsCanceled)
        {
            _moduleTask = ImportModuleAsync();
        }
 
        return _moduleTask;
    }
 
    private async Task<IJSObjectReference> ImportModuleAsync()
    {
        var module = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", ModulePath);
 
        // Disposal does not wait for the import, so nothing else releases a module that arrives after it.
        if (_disposed)
        {
            await module.DisposeAsync();
            throw new ObjectDisposedException(GetType().Name);
        }
 
        return module;
    }
}