BlazorでJavaScriptから.NETの動的(インスタンス)メソッドを呼び出す

# はじめに
仕事で作成しているBlazorのアプリケーションで、`Handsontable`を使用して一覧表を表示しています。

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

検証作業を実施している同僚から、2台のPCで同じ画面を表示した際に一覧表を表示する処理を実施すると、後から起動したPC側に一覧表が表示され、本来表示されるPC側には何も表示されないとの報告を受けました。

土日の休日を使って解決策を見つけることが出来たので、備忘録として記事に残しておきます。

# 問題1
## 現象
会社では複数PCが使用できますが、自宅ではPC1台です。
そこで、違うブラウザ`Chrome`と`Edge`で同じ現象が出るか試してみました。Visual Studio上で `Chrome`を起動しておき、後から`Edge`を起動して、一覧表を表示する処理を実施すると`Edge`側に一覧表が表示されました。
これなら Visual Studio上から デバッグ も可能で調査が進みます。

## 調査
`Handsontable`を使わず、Table関連タグとforeachを使用して表示させると`Chrome`側に表示されました。

ということは、`Handsontable`を使用して表示する部分が怪しい、デバッグすると`data`変数には正しいデータがセットされています。どうも、`await _grid.InvokeVoidAsync()` の処理で`Edge`側に表示してしまうようです。

```csharp
var settings = new
{
    data = Data ?? new List<ReadDataInfo>(),
    columns = cols,
    rowHeaders = false,
    manualColumnResize = true,
    fillHandle = false,
    height = gridHeight
};

await _grid.InvokeVoidAsync("updateSettings", settings);
```

## 原因
原因は分かってしまえばなんて事は無かったんですが、実際には違う方向で原因を探っていたため原因が分かるまでにかなり時間がかかってしまいました。

静的変数として、`static`キーワードを付けていました。
確かに静的変数なら、後から起動した方に上書きされるのも納得します。

```csharp
// グリッドコンポーネントオブジェクト
private static IJSObjectReference? _jsClass;
private static IJSObjectReference? _jsModule;
private static IJSObjectReference? _grid;
```

## 対応
`static`キーワードを外しました。
これにより、先に起動した`Chrome`側に一覧表が表示されるようになりました。

```csharp
// グリッドコンポーネントオブジェクト
private IJSObjectReference? _jsClass;
private IJSObjectReference? _jsModule;
private IJSObjectReference? _grid;
```
# 問題2
実はやみくもに`static`キーワードを付けたわけではなく静的変数にする理由がありました。
一覧表にはチェックボックス欄があり、ヘッダー部にチェックボックスを表示してクリックしたら全選択/全解除するようにしています。

## 現象
一覧表のヘッダー部のチェックボックス欄にチェックを付けると、`Chrome`側ではなく`Edge`側にチェックが付くようになりました。

## 原因
これも`static`メソッドにしているのが原因なのはすぐ分かったのですが、JavaScript側で動的(インスタンス)メソッドにする方法が分からなかったのです。

```csharp
// OnInitialized()に記載
_assemblyName = Assembly.GetExecutingAssembly().GetName().Name ?? "";

// OnAfterRenderAsync(bool firstRender)に記載
_jsModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./Components/Layout/HistoryList.razor.js");
_jsClass = await _jsModule.InvokeAsync<IJSObjectReference>("SpreadSheet.create", _assemblyName);

/// 全選択オン/オフ
/// </summary>
/// <param name="check">チェック状態</param>
[JSInvokable]
public static async Task SetAllCheck(bool check)
{
    if (_jsClass is null) return;

    // 実行したい処理
    await _jsClass.InvokeVoidAsync("setAllCheck", check);

    if (_grid is null) return;

    List<ReadDataInfo> list = await _grid.InvokeAsync<List<ReadDataInfo>>("getSourceData");
}
```

JavaScript側にて、`DotNet.invokeMethodAsync()`を使用して、.NET側の`SetAllCheck`メソッドを呼び出しています。
この`SetAllCheck`が静的メソッドであるために、問題1の`_jsClass`変数等に`static`キーワードを付けて静的変数にしていたのです。

```js
export class SpreadSheet {
    constructor(name) {
        this.name = name;
    }

    createGrid(elementId, self, headerList) {
        self.COL_EDIT = 1;
        self.checked = true;

        this.hot = new Handsontable(elementId, {
            data: [],
            colHeaders: function (col) {
                let header = headerList;
                if (col == self.COL_EDIT) {
                    let checkmark = self.checked ? "bi-square" : "bi-check-square";
                    let command = `DotNet.invokeMethodAsync('${self.name}', 'SetAllCheck', ${self.checked})`;

                    let content = ""
                    content += "<div>";
                    content += `<span style='font-size: 0.8rem' id='allCheck' class='bi ${checkmark}'`;
                    content += ` onclick =\"${command}\"</span>`;
                    content += "</div>";
                    return content;
                }
                else
                    return header[col];
            }
        });

        return this.hot;
    }

    setAllCheck(chk) {
        if (this.hot.countRows() == 0)
            return;

        this.checked = !chk;
        let col = this.COL_EDIT;
        this.hot.populateFromArray(0, col, [[chk]], this.hot.countRows() - 1, col, null, null, 'down');
    }

    // インスタンス作るためのメソッド
    static create(name) {
        return new SpreadSheet(name);
    }
}
```

## 対応
`static`キーワードを外したのですが、JavaScript側で動的(インスタンス)メソッドにする方法が分かるまでに四苦八苦しました。

最初に下記サイトの`Action`を使用したのですが、同じ現象になりました。

https://stackoverflow.com/questions/66385844/call-a-c-sharp-non-static-method-from-a-static-method-in-blazor-invoked-by-javas

最終的に下記サイトが参考になりました。

https://stackoverflow.com/questions/74347062/blazor-jsinvokable-without-static-function

https://qiita.com/Cable08/items/6d81e1cf2df9067123cf

1.`SetAllCheck`メソッドがあるクラスインスタンス(例 `HistoryList`クラス)を`_objectReference`変数にセットします。
2.`_jsClass`の箇所にて、JavaScript側のコンストラクタ用の`SpreadSheet.create`メソッドに`_objectReference`変数を渡します。

```csharp
private DotNetObjectReference<HistoryList> _objectReference { get; set; } = default!;

// OnInitialized()に記載
_objectReference = DotNetObjectReference.Create(this);

// OnAfterRenderAsync(bool firstRender)に記載
_jsModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./Components/Layout/HistoryList.razor.js");
_jsClass = await _jsModule.InvokeAsync<IJSObjectReference>("SpreadSheet.create", _objectReference);

/// 全選択オン/オフ
/// </summary>
/// <param name="check">チェック状態</param>
[JSInvokable]
public async Task SetAllCheck(bool check)
{
    if (_jsClass is null) return;

    // 実行したい処理
    await _jsClass.InvokeVoidAsync("setAllCheck", check);

    if (_grid is null) return;

    List<ReadDataInfo> list = await _grid.InvokeAsync<List<ReadDataInfo>>("getSourceData");
}
```

3.JavaScript側でコンストラクタにてクラスインスタンスを受け取り`window.ComponentReference`として定義します。
※`ComponentReference`の変数名は変更してもらって構いません。

```js
export class SpreadSheet {
    constructor(reference) {
        window.ComponentReference = reference;
    }

    createGrid(elementId, self, headerList) {
        self.COL_EDIT = 1;
        self.checked = true;

        this.hot = new Handsontable(elementId, {
            data: [],
            colHeaders: function (col) {
                let header = headerList;
                if (col == self.COL_EDIT) {
                    let checkmark = self.checked ? "bi-square" : "bi-check-square";
                    let command = `ComponentReference.invokeMethodAsync('SetAllCheck', ${self.checked})`;

                    let content = ""
                    content += "<div>";
                    content += `<span style='font-size: 0.8rem' id='allCheck' class='bi ${checkmark}'`;
                    content += ` onclick =\"${command}\"</span>`;
                    content += "</div>";
                    return content;
                }
                else
                    return header[col];
            }
        });

        return this.hot;
    }

    setAllCheck(chk) {
        if (this.hot.countRows() == 0)
            return;

        this.checked = !chk;
        let col = this.COL_EDIT;
        this.hot.populateFromArray(0, col, [[chk]], this.hot.countRows() - 1, col, null, null, 'down');
    }

    // インスタンス作るためのメソッド
    static create(reference) {
        return new SpreadSheet(reference);
    }
}
```

4.`DotNet.invokeMethodAsync()`を使用すると静的メソッド呼び出しになってしまうので、`ComponentReference.invokeMethodAsync()`として動的メソッド呼び出しにします。

```js
let command = `DotNet.invokeMethodAsync('${self.name}', 'SetAllCheck', ${self.checked})`;
                   ↓
let command = `ComponentReference.invokeMethodAsync('SetAllCheck', ${self.checked})`;
```

これにより、先に起動した`Chrome`側の一覧表にチェックが付くようになりました。

# 最後に
問題を解決した後に改めて参考記事や公式ドキュメントを読み直すと、そういうことか思うんですよね。解決するまで点と点が繋がっていなかったけど、これでようやく線が繋がった感じです。

技術記事なので、要点だけ簡潔に書けばいいんでしょうけど、解決に辿り着くまでに思案して、もがいたりした部分とかをどうしても書きたくなってしまうですよね。