Blazor 上でメッセージボックスダイアログを実現しよう
確認メッセージと「はい」「いいえ」ボタンを表示するような、いわゆる "MessageBox" コンポーネントを、Blazor 上で実装してみることにします。例えば「削除」ボタンがあったとして、
これをクリックすると、「本当に削除していいですか?」と確認を行なう、「はい」か「いいえ」で応えるダイアログが表示される、そのようなメッセージボックスダイアログを表示するコンポーネントを作る、という話になります。
とにかく作ってみる
何はともあれ、ざっくり作ってみましょう。Blazor アプリケーションプロジェクトを作成したら、MessageBox.razor
をプロジェクトに追加します、そして、ちょっと長いですが、以下のように、HTML 標準の dialo 要素を使って以下のように実装してみました。
@inject IJSRuntime JSRuntime
<dialog @ref="_dialog" @oncancel="OnClickCancel">
<p>@_message</p>
<button @onclick="OnClickOK">はい</button>
<button @onclick="OnClickCancel" autofocus>いいえ</button>
</dialog>
@code {
private ElementReference _dialog;
private string? _message;
[Parameter]
public EventCallback OnOK { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
public async Task ShowAsync(string message)
{
_message = message;
StateHasChanged();
await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./MessageBox.razor.js");
await module.InvokeVoidAsync("showModal", _dialog);
}
public async Task CloseAsync()
{
await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./MessageBox.razor.js");
await module.InvokeVoidAsync("close", _dialog);
}
private async Task OnClickOK()
{
await CloseAsync();
await OnOK.InvokeAsync();
}
private async Task OnClickCancel()
{
await CloseAsync();
await OnCancel.InvokeAsync();
}
}
以下の JavaScript ファイルも追加します。
export const showModal = (dialogRef) => dialogRef.showModal();
export const close = (dialogRef) => dialogRef.close();
これで MessageBox
コンポーネントが使えるようになります。例えば以下のように使います。
<div>
<button @onclick="OnClickDelete">
削除
</button>
</div>
<!--
// 👇 MessageBox コンポーネントを配置、@ref で参照を取得、および、OnOK イベントをハンドルする
-->
<MessageBox @ref="_messageBox" OnOK="OnOK" />
@code {
private MessageBox? _messageBox;
private async Task OnClickDelete()
{
...
// [削除] ボタンがクリックされたら、MessageBox.ShowAsync メソッドを呼び出すことで
// メッセージボックスが表示される
await _messageBox.ShowAsync("削除してもよろしいですか?");
}
private void OnOK()
{
// MessageBox で [はい] がクリックされたイベントハンドラで、
// 実際の削除処理を行なう
}
}
... と、まぁ、これで一丁上がりなんですが、さて。
使い勝手、良くなくない?
実のところ、ちょっと好きじゃないんですよね、こういうコード。今一度、使う側のコードを再掲しますが、
...
// [削除] ボタンがクリックされたら...
private async Task OnClickDelete()
{
...
// メッセージボックスを表示
await _messageBox.ShowAsync("削除してもよろしいですか?");
}
// メッセージボックスで [はい] がクリックされるとここに来る
private void OnOK()
{
// 実際の削除処理を行なう
}
[削除] ボタンがクリックされたときのハンドラ、OnClickDelete()
内では、実際の削除処理は実行されないんですよね。代わりに、別のイベントハンドラで削除処理が実行されます。こういうコードだと、「あれ、"削除していい?" ってメッセージ表示するのはいいんだけど、そのあと、何処行ったの??」 と混乱しがちです。
本当をいうと、以下のように書きたいんです。
...
@code {
private MessageBox? _messageBox;
private async Task OnClickDelete()
{
...
// [削除] ボタンがクリックされたら、MessageBox.ShowAsync メソッドを呼び出すことで
// メッセージボックスが表示され、そこで [はい] がクリックされたら true が返る
var yes = await _messageBox.ShowAsync("削除してもよろしいですか?");
if (yes)
{
// 実際の削除処理を行なう
}
}
}
上記のように書けるのなら、"削除していい?" のメッセージに "はい" で回答されたら、実際に削除処理をするのね、と何の迷いもなく読み解けると思います。
ということで、では、上記のように記述できるよう、MessageBox.razor
を改訂しましょう。
TaskCompletionSource<T>
を使って改善する
この目的のためには、TaskCompletionSource<T>
クラスが使えます。
これは JavaScript でいう Promise に似てて、TaskCompletionSource<T>
オブジェクトの SetResult<T>(T value)
メソッドを呼び出すと、同じ TaskCompletionSource<T>
オブジェクトの Task
プロパティが "Resolve" される、つまり、その Task<T>
オブジェクトの await 待ちから処理継続する、という感じで動作します。
実際に MessageBox.razor
内で使ってみましょう。
まず、TaskCompletionSource<T>
オブジェクトを保持しておくフィールド変数を、MessageBox.razor
内のコードブロックに追加しておきます。なお今回は、メッセージボックスの「はい」ボタンがクリックされたかどうかで bool
値を返すようにしたいので、型引数 T
は、bool
とします。
...
@code {
// 👇 TaskCompletionSource<bool> 型のフィールド変数を追加
private TaskCompletionSource<bool>? _tcs;
...
そうしたら、ShowAsync()
メソッド内では、TaskCompletionSource<bool>
のオブジェクトを新たに作成し、先に用意したフィールド変数にこれを格納して、同オブジェクトの Task
プロパティ (型は Task<bool>
ですね) を ShowAsync()
メソッドの戻り値として返すようにします。ShowAsync()
メソッドの戻り値型も忘れずに Task<bool>
に直しておきます。
...
@code {
private TaskCompletionSource<bool>? _tcs;
public async Task<bool> ShowAsync(string message)
{
...
// 👇TaskCompletionSource<bool> オブジェクトを生成、フィールド変数に押さえておいて、
// その Task プロパティを ShowAsync メソッドの戻り値として返す
_tcs = new();
return await _tcs.Task;
}
...
そして、この MessageBox
コンポーネント内の [はい] や [いいえ] ボタンがクリックされたときのイベントハンドラでは、先にフィールド変数に格納しておいた TaskCompletionSource<bool>
オブジェクトの SetResult<bool>(bool value)
メソッドを呼び出します。
...
<button @onclick="OnClickOK">はい</button>
<button @onclick="OnClickCancel" autofocus>いいえ</button>
...
@code {
private TaskCompletionSource<bool>? _tcs;
public async Task<bool> ShowAsync(string message)
{
...
_tcs = new();
return await _tcs.Task;
}
...
private async Task OnClickOK()
{
await CloseAsync();
// 👇OK がクリックされたら、TaskCompletionSource の SetResult() を
// 呼び出すことで、TaskCompletionSource.Task が完了する
// (JavaScript における Promise のリゾルバ関数を呼ぶのに相当)
_tcs?.SetResult(true);
}
private async Task OnClickCancel()
{
await CloseAsync();
// 👇 キャンセル時も同じ
_tcs?.SetResult(false);
}
こうすることで、先に ShowAsync()
メソッドの戻り値として返しておいた Task<bool>
オブジェクトに対する await から処理再開し、SetResult()
メソッドに渡した true
あるいは false
が結果として取得されます。
改訂後の MessageBox.razor
の全体を再掲します。
@inject IJSRuntime JSRuntime
<dialog @ref="_dialog" @oncancel="OnClickCancel">
<p>@_message</p>
<button @onclick="OnClickOK">はい</button>
<button @onclick="OnClickCancel" autofocus>いいえ</button>
</dialog>
@code {
private ElementReference _dialog;
private string? _message;
private TaskCompletionSource<bool>? _tcs;
public async Task<bool> ShowAsync(string message)
{
_message = message;
StateHasChanged();
await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./MessageBox.razor.js");
await module.InvokeVoidAsync("showModal", _dialog);
_tcs = new();
return await _tcs.Task;
}
public async Task CloseAsync()
{
await using var module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./MessageBox.razor.js");
await module.InvokeVoidAsync("close", _dialog);
}
private async Task OnClickOK()
{
await CloseAsync();
_tcs?.SetResult(true);
}
private async Task OnClickCancel()
{
await CloseAsync();
_tcs?.SetResult(false);
}
}
結果 - 使う側のコードがスッキリ!
以上、MessageBox.razor
を改善後の、使う側のコードを再掲します。
...
// [削除] ボタンがクリックされたら...
private async Task OnClickDelete()
{
...
// 確認のメッセージボックスを表示
var yes = await _messageBox.ShowAsync("削除してもよろしいですか?");
// メッセージボックスで [はい] がクリックされたのなら...
if (yes)
{
// 実際の削除処理を行なう
}
}
}
やっぱりこっちのほうがいいですね!
以上、TaskCompletionSource<T>
クラスを活用することで、メッセージボックスに対するユーザー応答を、 Task
を用いた await 構文の形で待機できるようになり、これを利用する側のコードがすっきり読みやすくなったと思います。
余談
間違った削除操作を防止するための方法として、確認メッセージで責任をユーザーになすりつけるのは、本当は望ましくない設計ですよね。確認メッセージもあってもいいんですが、可能な限り、削除を撤回 (Undo) もできるようにしておくべきですね。