5
4

More than 1 year has passed since last update.

JavaScript オブジェクトのメソッドを Blazor (C#) から呼び出す

Last updated at Posted at 2022-12-21

グローバルスコープを使わないとダメ?

先日 Qiita に下記記事が投稿されました。

"Blazor で Excel ライクなグリッド JS ライブラリ「Handsontable」を使ってみる"

自分は Handsontable というライブラリを知っていなかったので、知見を広げるよい機会になりました。良記事ありがとうございます!

さて上記記事中、以下の記載がありました。

Handsontable のインスタンス変数を最初に var hot = new Handsontable としました。
しかし、これだと loadData 関数内で hot 変数を使用すると「Error: Microsoft.JSInterop.JSException: hot is not defined」となってしまいます。
原因は var が関数スコープだからです。
グローバルスコープにする必要があるため、windows.hot = new Handsontable に変更することで動作しました。

確かにこの処置で動作します。しかしこのままですと、以下の二点が気になりました。

  • グローバル名前空間の汚染、もしくは衝突
  • 同じページ内で 2 つ以上の Handsontable グリッドを使えない

幸い、Blazor が備えている JavaScript 相互運用機能では、JavaScript ファイルに掲載されている関数 (function) を呼び出すだけでなく、JavaScript オブジェクトの参照を入手し、その JavaScript オブジェクトのメソッドを Blazor 側 (C# 側) から呼び出すことができます。 この仕組みを使うことで、上記2点の気になる課題を解消できます。どういうことか、やってみましょう。

JavaScript 関数呼び出しの戻り値として、JavaScript オブジェクトの参照を受け取れる!

まず JavaScript コード、とくに createGrid 関数からテコ入れしていきます。現在、Handsontable オブジェクトをインスタンス化し、グローバルスコープである windows.hot に設定しているところを (下記) ...

script.js
function createGrid() {
  ...
  window.hot = new Handsontable(grid, {
    ...
  });
}

下記のとおり、createGrid 関数の戻り値として、Handsontable オブジェクトを返すように変更します (下記)。

script.js
function createGrid() {
  ...
  // 👇 createGrid 関数の戻り値として Handsontable オブジェクトを返すよう、変更!
  return new Handsontable(grid, {
    ...
  });
}

次は、この createGrid JavaScript 関数を呼び出している Blazor 側です。
現状は "InvokeVoidAsync" メソッドで戻り値は取得しない形で createGrid JavaScript 関数を呼び出していますが (下記)...

Index.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
  ...
  await jsRuntime.InvokeVoidAsync("createGrid");
  ...

これを改め、InvokeAsync<T> メソッドを使って呼び出すことで、createGrid JavaScript 関数の戻り値を受け取るようにします。このとき、戻り値の型 <T>IJSObjectReferenceとしてください。また、戻ってきた IJSObjectReference オブジェクトを保持するためのフィールド変数も追加しておき、それで戻り値を受け取ります (下記)。

Index.razor
private IJSObjectReference? _grid;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  ...
  _grid = await jsRuntime.InvokeAsync<IJSObjectReference>("createGrid");
  ...

IJSObjectReference は、数値や文字列などのプリミティブ型ではない、JavaScript オブジェクトへを受け取るときに使えるインターフェース型になります。JavaScript 側と Blazor の .NET 側とではまったく世界が違うので、JavaScript 世界のオブジェクトが生身のまままるごと .NET 世界に出現することはできません。なので、.NET 世界から JavaScript 世界に存在するオブジェクトを指し示す "参照" が IJSObjectReference ということになります。

以上の実装で、Blazor 側の IJSObjectReference _gridフィールド変数に、JavaScript 側の createGrid 関数が返してよこした Handsontable オブジェクトへの "参照" が格納されることになります。

JavaScript オブジェクトのメソッドも呼び出せる!

引き続き Blazor 側、データを Handsontable JavaScript オブジェクトに引き渡すところを見ていきます。
現状は、script.js 内で定義してある loadData 関数を呼び出すことで実装されていますが (下記)...

Index.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
  ...
  await jsRuntime.InvokeVoidAsync("loadData", products);
}

先ほどフィールド変数に格納しておいた、Handsontable JavaScript オブジェクトへの参照である IJSObjectReference オブジェクトを介して、直接、Handsontable JavaScript オブジェクトのメソッドを呼び出せます (下記)。

Index.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
  ...
  // 👇 同じ "loadData" という名前のためわかりにくいが、
  // script.js で定義されている loadData 関数を呼び出しているのではなく、
  // _grid フィールド変数が指している Handsontable オブジェクトの loadData メソッドを呼び出している!
  await _grid.InvokeVoidAsync("loadData", products);
}

IJSObjectReference インターフェース型は、IJSRuntime インターフェース型と同じように InvokeVoidAsync メソッドや InvokeAsync<T> メソッドを備えています。両者の違いは、IJSRuntime インターフェース型ではグローバルスコープに存在する関数を呼び出すのに対し、IJSObjectReference インターフェース型は、それが指し示しているオブジェクトのメソッド (および JavaScript モジュールがエクスポートしている関数) を呼び出す点です。

ということで、これにて script.js 内に定義されていた loadData 関数は不要となりましたので (下記)、

script.js
function createGrid() {
  ...
}

// 👇 もう使われなくなりました!
function loadData(data) {
    window.hot.loadData(data);
}

削除しておきましょう (下記)。

script.js
function createGrid() {
  ...
}

// loadData 関数を削除!

使わなくなった JavaScript オブジェクトへの参照は破棄しましょう

最後に、Razor コンポーネントの寿命にあわせ、使わなくなった JavaScript オブジェクトへの参照を破棄するようにしましょう。.NET 世界側でいつまでも JavaScript 世界のオブジェクトへの参照を持ち続けていると、その JavaScript 世界のオブジェクトがガベージコレクションで回収できず、メモリリークの一因となります。

そのためにまずは Razor コンポーネントで IAsyncDisposable インターフェースを実装するようにし (下記)、

Index.razor
@implements IAsyncDisposable
...
@code {
  ...
  public async ValueTask DisposeAsync()
  {
     // このあと、ここを実装します
  }
}

Razor コンポーネントの DisposeAsync メソッド内で、先にフィールド変数 _grid に格納された Handsontable JavaScript オブジェクトへの参照を破棄します (下記)。

Index.razor
...
@code {
  ...
  public async ValueTask DisposeAsync()
  {
    // 👇 IJSObjectReference の DisposeAsync を呼び出して、
    // Handsontable JavaScript オブジェクトへの参照を破棄する
    if (_grid != null) {
      try { await _grid.DisposeAsync(); } 
      catch (JSDisconnectedException) { }
    }
  }
}

このように、コンポーネントの破棄時に Handsontable JavaScript オブジェクトへの参照を破棄することが目的で、 Handsontable JavaScript オブジェクトへの参照をフィールド変数 _grid に保持していたのでした。

なお、「createGrid して loadData したら以降、.NET 側からは二度と Handsontable JavaScript オブジェクトをいじらない」ということであれば、コンポーネントのフィールド変数に保持する必要はありません。OnAfterRenderAsync ライフサイクルメソッド内で await using var grid = ... のようにローカル変数で受け、スコープ外に出るときに自動で破棄されるようにすればよいです。

なお、一点補足です。
上記実装例では JSDisconnectedException 例外を catch して握り潰していますが、基本的には例外をこのように握り潰すのは決してやってはいけません (せめて ILogger<T> を DI コンテナから取得してそちらにログ記録する必要がある)。しかしコンポーネントの破棄時における JSDisconnectedException 例外は、とくに Blazor Server の場合、その仕組み上、ブラウザのタブを閉じただけで毎回確定的に発生します。加えて、私が知る限り、JavaScript 世界とのつながりが絶たれてるかどうかを事前にしる術もないことから、この例外発生を回避する方法がありません。いっぽうで、コンポーネントの破棄時に JSDisconnectedException 例外が発生すると何かマズイことがあるかというと、自分の理解の限りでは、そのようなことはありません。そのため、自分ではこれだけは catch してそのままでよいことにしています。

まとめ

以上の実装で、グローバルスコープ上に変数を散らかすこともなくなり、1ページ内に複数の Handsontable グリッドを並べて使うこともできるようになりました。

Blazor 側、.NET の世界だけでは完結せず、どうしても JavaScript 世界の力を借りたい場合、IJSRuntime サービスだけでなく、IJSObjectReference も使いこなせるようになると、グッと柔軟性が増しますね。

Learn, Practice, Share :)

5
4
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
5
4