Blazor WebAssembly のJS相互運用で、引数にコールバック関数を受け取る関数を実行する方法について

本記事は、Blazorで使えるLIFF(LINE Front-end Framework)のC#ライブラリを実装した際の実装メモです pierre3.hatenablog.com

JavaScriptの関数をC#から実行する

Microsoft.JSInterop.IJSRuntime インターフェースを利用します。
基本的には、JSRuntime.InvokeAsync メソッドに実行したいJSの関数名を渡すだけでOKです。
関数名はグローバルスコープ(window)を基準とした名前です(window.liff.init() の場合 liff.initを渡す)

 public async Task CloseWindowAsync()
            => await JSRuntime.InvokeAsync<object>("liff.closeWindow").ConfigureAwait(false);

引数がある場合

引数がある場合、引数と同じ名前のプロパティを持ったオブジェクトを渡します。 匿名オブジェクトで渡すのが簡単です。

public async Task OpenWindowAsync(string url, bool external)
            => await JSRuntime.InvokeAsync<object>("liff.openWindow", new { url, external }).ConfigureAwait(false);

戻り値がある場合

戻り値がある場合、ジェネリクスの型引数に指定すればその型で受け取ることができます。(デシリアライズしてくれる)

[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class Profile
{
    public string UserId { get; set; }
    public string DisplayName { get; set; }
    public string PictureUrl { get; set; }
    public string StatusMessage { get; set; }
}
public async Task LoadProfileAsync()
            => Profile = await JSRuntime.InvokeAsync<Profile>("liff.getProfile").ConfigureAwait(false);

非同期関数の場合

戻り値にPromiseを返してくれるメソッドの場合、そのままInvokeAsyncで実行するだけで良いです。

  • 実行結果(Promise.then() で受け取れるオブジェクト)を戻り値に返してくれる
  • JS側でエラーとなった場合、C#では例外が投げられ、Exception.Messageプロパティでエラー内容( Promise.catch()で受け取れるオブジェクト)が確認できる
public async Task LoadProfileAsync()
            => Profile = await JSRuntime.InvokeAsync<Profile>("liff.getProfile").ConfigureAwait(false);

コールバック関数を受け取る関数の場合

liff.init()関数が、非同期実行の結果を引数に渡したコールバック関数で受け取るようになっています。

当初は以下の様に、C#側に記述したコールバック関数をJS側で呼ぶようにしていました。

JavaScript側からC#で記述したコールバック処理を実行する(却下)
  • liff.init()に渡したコールバック関数内でC#側で定義した関数を呼ぶJavaScriptの中継コードを用意
window.liffInterop = {
    init: function (dotNet) {
        liff.init(
            function (data) {
                dotNet.invokeMethod('OnInitSuccess', JSON.stringify(data));
            },
            function (error) {
                dotNet.invokeMethod('OnInitError', JSON.stringify({
                    code: error.code,
                    message: error.message,
                    stack: error.stack
                }));
            }
        );
    }
};
  • C#側では、コールバックで実行したい処理をイベントに登録
  • イベントを実行するメソッドOnInitSuccessOnInitError[JSInvokable]でマーク
  • JSInvokableなメソッドを持つオブジェクト(LiffClient)のインスタンスDotNetObjectRefにラップしてJSRuntime.InvokeAsyncの引数に渡す
class LiffClient : ILiffClient
{
    public event EventHandler<InitSuccessEventArgs> InitSuccess;
    public event EventHandler<LiffClientErrorEventArgs> InitError;

    public async Task InitializeAsync(IJSRuntime jSRuntime)
    {
        await JSRuntime.InvokeAsync<object>("liffInterop.init", DotNetObjectRef.Create(this));
    }

    [JSInvokable]
    protected void OnInitSuccess(string data)
    { 
        Data = JsonConvert.DeserializeObject<LiffData>(data);
        InitSuccess?.Invoke(this, new InitSuccessEventArgs(Data));
    }

    [JSInvokable]
    protected void OnInitError(string error)
    {
        Error = error;
        InitError?.Invoke(this, new LiffClientErrorEventArgs(Error));
    }
}

これでも動くことは確認したのですが、大袈裟すぎてとても面倒です。そこで以下の様に対処することとしました。

コールバックを取るJavaScriptの関数をPromise に変換する

中間コードを以下の様に変更します。

window.liffInterop = {
    init: function () {
        return new Promise(function (resolve, reject) {
            liff.init(
                function (data) {
                    resolve(JSON.stringify(data));
                },
                function (error) {
                    reject(error);
                });
        });
    }
};

C#側ではPromise化した関数を呼ぶだけでよくなります。かなりシンプルなコードになりました。 (戻り値 はLiffDataオブジェクトとしてそのまま受け取れるはずなのですが、 デシリアライズが失敗するようなので一旦文字列で受けて自前でデシリアライズしています。)

public async Task InitializeAsync(IJSRuntime jsRuntime)
{
        var json = await jSRuntime.InvokeAsync<string>("liffInterop.init").ConfigureAwait(false);
        Data = JsonConvert.DeserializeObject<LiffData>(json);
}

※ JS相互運用については以下のドキュメントをご確認ください docs.microsoft.com