はじめに
仕事で作成しているBlazorのアプリケーションで、Handsontable
を使用して一覧表を表示しています。
検証作業を実施している同僚から、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
側に表示してしまうようです。
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
側の一覧表にチェックが付くようになりました。
最後に
問題を解決した後に改めて参考記事や公式ドキュメントを読み直すと、そういうことか思うんですよね。解決するまで点と点が繋がっていなかったけど、これでようやく線が繋がった感じです。
技術記事なので、要点だけ簡潔に書けばいいんでしょうけど、解決に辿り着くまでに思案して、もがいたりした部分とかをどうしても書きたくなってしまうですよね。