LoginSignup
2
2

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

Posted at

はじめに

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

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

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

問題1

現象

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

調査

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

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

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

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

原因

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

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

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

対応

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

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

問題2

実はやみくもにstaticキーワードを付けたわけではなく静的変数にする理由がありました。
一覧表にはチェックボックス欄があり、ヘッダー部にチェックボックスを表示してクリックしたら全選択/全解除するようにしています。

現象

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

原因

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

// 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キーワードを付けて静的変数にしていたのです。

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を使用したのですが、同じ現象になりました。

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

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

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の変数名は変更してもらって構いません。

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()として動的メソッド呼び出しにします。

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

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

最後に

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

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

2
2
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
2
2