4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BlazorのJS相互運用をサービスとして切り出して使う

Last updated at Posted at 2025-06-11

はじめに

Blazorでは,ブラウザ内で実行可能なC#のコードを書くことができます.しかしながら,DOM要素の操作や凝ったUIを作ろうとするとJavaScriptが必要となってきます.

JavaScriptをBlazorで利用する方法は,JS相互運用(JavaScript Interop)と呼ばれていて,下記ドキュメントに詳細が記されています.
ASP.NET Core Blazor JavaScript の相互運用性 (JS 相互運用)

ただ,むやみにJavaScript呼び出しを散りばめていくとコードの見通しが悪くなってしまうので,どうにかしたいです.

サービスクラスしてうまく切り出す方法があったので,備忘を兼ねてまとめて行きたいと思います.

環境

  • .NET 9
  • Blazor Web App (レンダーモードはページ/コンポーネントごとに指定)

IJSRuntimeを直接使うやり方

これはASP.NET Core Blazor JavaScript の相互運用性 (JS 相互運用)に載っている通常のやり方です.

JavaScriptを用意

例として,ブラウザが何かを調べてみようと思います 1
.jsファイルを作成して,Serverプロジェクトのwwwroot下に置きます.

wwwroot/browserCheck.js
export function getBrowser() {
    var userAgent = navigator.userAgent.toLowerCase();

     if (userAgent.indexOf('msie') != -1 || userAgent.indexOf('trident') != -1) {
         return 'Internet Explorer';
     } else if (userAgent.indexOf('edge') != -1) {
         return 'Edge';
     } else if (userAgent.indexOf('chrome') != -1) {
         return 'Google Chrome';
     } else if (userAgent.indexOf('safari') != -1) {
         return 'Safari';
     } else if (userAgent.indexOf('firefox') != -1) {
         return 'FireFox';
     } else if (userAgent.indexOf('opera') != -1) {
         return 'Opera';
     } else {
         return 'Other';
     }
}

JavaScriptを呼び出す

下記はServer側の実装です.
WebAssemblyの場合は,Clientプロジェクトで実装します.

Home.razor
@page "/"
@rendermode InteractiveServerWithoutPrerendering
@implements IAsyncDisposable

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<p>Current browser: @browser</p>

@code {

    [Inject]
    IJSRuntime JSRuntime { get; set; } = default!

    private string? browser;

    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // JavaScriptを読み込む
            module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./browserCheck.js");

            // JavaScriptを使用する
            browser = await module.InvokeAsync<string>("getBrowser");

            StateHasChanged();
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is { })
        {
            try
            {
                // 読み込んだJavaScriptを破棄
                await module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }
        }
    }
}


※ ややこしいのでプリレンダリングはOFFにしています.プリレンダリングがONの場合,JavaScript呼び出しはOnAfterRenderAsync以降にしないと実行時エラーになります.

Server版
image.png

WASM版
image.png

このままだと

必要なページやコンポーネントごとにIJSRuntimeをインジェクトして,moduleを生成して.Disposeしてとコードがとっ散らかってしまいます.なので,Serviceクラスとして切り出して使い勝手を良くしたいです.

サービスクラス化していく

基底クラス作成

基底クラスはBlazor作成の一人であるSteve Sanderson氏が提案してくれています2ので,参考にします.

JSModule.cs

public abstract class JSModule(IJSRuntime js, string moduleUrl) : IAsyncDisposable
{
    private readonly Task<IJSObjectReference> moduleTask 
        = js.InvokeAsync<IJSObjectReference>("import", moduleUrl).AsTask();

    protected async ValueTask InvokeVoidAsync(string identifier, params object[]? args)
        => await (await moduleTask).InvokeVoidAsync(identifier, args);

    protected async ValueTask<T> InvokeAsync<T>(string identifier, params object[]? args)
        => await (await moduleTask).InvokeAsync<T>(identifier, args);

    public async ValueTask DisposeAsync()
    {
        try
        {
            await (await moduleTask).DisposeAsync();
        }
        catch (JSDisconnectedException)
        {
        }
    }
}

サービスクラス作成

上記のJSModuleを継承します(Interfaceもついでに).

public interface IBrowserCheckService
{
    Task<string> GetBrowserAsync();
}

public class BrowserCheckService(IJSRuntime js) : JSModule(js, _jsUrl), IBrowserCheckService
{
    private const string _jsUrl = "./browserCheck.js";

    public async Task<string> GetBrowserAsync()
        => await InvokeAsync<string>("getBrowser");
}

Program.csでDIコンテナに入れます.

Program.cs
builder.Services.AddScoped<IBrowserCheckService, BrowserCheckService>();

ページで使用する

下記はSever版です.
WebAssembly版の修正もほぼ同じです.

Home.razor
@page "/"
@using BlazorApp1.Client.Service
@rendermode InteractiveServerWithoutPrerendering

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<p>Current browser: @browser</p>

@code {

    [Inject]
    IBrowserCheckService BrowserCheckService { get; set; } = default!;

    private string? browser;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            browser = await BrowserCheckService.GetBrowserAsync();

            StateHasChanged();
        }
    }
}

Server版
image.png

WASM版
image.png

とても使いやすくなりました.

余談

上記のJSModuleですが,アップデート版があり,面白いので記載します.
こちらのブログに載っています.
Blazor Bits: Javascript Module Interop Base Class

/// <summary>
/// Helper for loading any JavaScript (ES6) module and calling its exports
/// </summary>
public abstract class JSModule : IAsyncDisposable
{
    private readonly CancellationTokenSource _cancellationTokenSource = new();
    private readonly AsyncLazy<IJSObjectReference> _jsModuleProvider;
    private bool _isDisposed;

    /// <summary>
    /// On construction, we start loading the JS module
    /// </summary>
    /// <param name="js"></param>
    /// <param name="moduleUrl">javascript web uri</param>
    protected JSModule(IJSRuntime js, string moduleUrl)
        => _jsModuleProvider = new AsyncLazy<IJSObjectReference>(async () =>
            await js.InvokeAsync<IJSObjectReference>("import", moduleUrl));

    /// <inheritdoc />
    public virtual async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Perform the asynchronous cleanup the JS module.
    /// </summary>
    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_isDisposed)
            return;

        _cancellationTokenSource.Cancel();
        _cancellationTokenSource.Dispose();

        if (_jsModuleProvider.IsValueCreated)
        {
            var module = await _jsModuleProvider.Value;

            // ここだけ修正.そのままだとServer版でエラーになるので,try-catchしときます
            // 詳細: https://learn.microsoft.com/ja-jp/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-9.0#javascript-interop-calls-without-a-circuit
            try
            {
                await module.DisposeAsync().ConfigureAwait(false);
            }
            catch (JSDisconnectedException)
            {
            }
            
        }
        _isDisposed = true;
    }

    /// <summary>
    /// invoke exports from the module with an expected return type T
    /// </summary>
    /// <param name="identifier"></param>
    /// <param name="args"></param>
    /// <typeparam name="T">Return Type</typeparam>
    /// <returns></returns>
    protected async ValueTask<T> InvokeAsync<T>(string identifier, params object[]? args)
    {
        var jsModule = await _jsModuleProvider.Value;
        return await jsModule.InvokeAsync<T>(identifier, _cancellationTokenSource.Token, args);
    }

    /// <summary>
    /// invoke exports from the module
    /// </summary>
    /// <param name="identifier"></param>
    /// <param name="args"></param>
    protected async ValueTask InvokeVoidAsync(string identifier, params object[]? args)
    {
        var jsModule = await _jsModuleProvider.Value;
        await jsModule.InvokeVoidAsync(identifier, _cancellationTokenSource.Token, args);
    }

    /// <summary>
    /// Asynchronous initialization to delay the creation of a resource until it’s absolutely needed.
    /// </summary>
    /// <remarks>
    /// This naive approach is much quicker than attempting to explain the use of a DotNext
    /// implementation. Alternatively, DotNext threading could be added via NuGet: `dotnet add
    /// package DotNext.Threading`.
    /// </remarks>
    /// <see href="https://devblogs.microsoft.com/pfxteam/asynclazyt/" />
    /// <see href="https://github.com/dotnet//blob/master/src/DotNext.Threading/Threading/AsyncLazy.cs" />
    /// <see href="https://dev.azure.com/vercodev/Venso/_git/Vanguard?path=%2FVanguard%2FVanguard.Core%2FAsyncLazy.cs" />
    /// <typeparam name="T">Resource to be initialized asynchronously</typeparam>
    public class AsyncLazy<T> : Lazy<Task<T>>
    {
        public AsyncLazy(Func<T> valueFactory) :
            base(() => Task.Factory.StartNew(valueFactory))
        { }

        public AsyncLazy(Func<Task<T>> taskFactory) :
            base(() => Task.Factory.StartNew(taskFactory).Unwrap())
        { }
    }
}

AsyncLazyは非同期操作に対応した遅延初期化パターンを実装したクラスです.
これを使うことで,リソースの最適化やUI応答性の向上を図れます.

このクラスは.NEXTというライブラリにいます.
.NEXTはBCL(C#の標準組み込みライブラリ)の発展版で,dotnetの公式リポジトリで管理されています.

Nugetから使用することもできます.
Nuget .NEXT

ブログによると,もしかするとBCLに入ってくるかもとあるので,期待ですね.

  1. 使用してるブラウザを判定したい

  2. JsModule

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?