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

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

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

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

https://qiita.com/yaju/items/bcca337af393f15a6795

自分は 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` に設定しているところを (下記) ...

```js:script.js
function createGrid() {
  ...
  window.hot = new Handsontable(grid, {
    ...
  });
}
```
下記のとおり、**`createGrid` 関数の戻り値として、`Handsontable` オブジェクトを返す**ように変更します (下記)。

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

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

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

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

```csharp: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` 関数を呼び出すことで実装されていますが (下記)...

```csharp:Index.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
  ...
  await jsRuntime.InvokeVoidAsync("loadData", products);
}
```
先ほどフィールド変数に格納しておいた、`Handsontable` JavaScript オブジェクトへの参照である `IJSObjectReference` オブジェクトを介して、**直接、`Handsontable` JavaScript オブジェクトのメソッドを呼び出せます** (下記)。
```csharp: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` 関数は不要となりましたので (下記)、

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

// 👇 もう使われなくなりました!
function loadData(data) {
    window.hot.loadData(data);
}
```
削除しておきましょう (下記)。
```js:script.js
function createGrid() {
  ...
}

// loadData 関数を削除!
```

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

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

そのためにまずは Razor コンポーネントで `IAsyncDisposable` インターフェースを実装するようにし (下記)、
```csharp:Index.razor
@implements IAsyncDisposable
...
@code {
  ...
  public async ValueTask DisposeAsync()
  {
     // このあと、ここを実装します
  }
}
```
Razor コンポーネントの `DisposeAsync` メソッド内で、先にフィールド変数 `_grid` に格納された `Handsontable` JavaScript オブジェクトへの参照を破棄します (下記)。
```csharp: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 = ...` のようにローカル変数で受け、スコープ外に出るときに自動で破棄されるようにすればよいです。

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

## まとめ

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

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

_Learn, Practice, Share :)_