6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Blazor で ShowAsync メソッドを持つメッセージボックスを作り、応答結果は ShowAsync の戻り値で取得する

Last updated at Posted at 2024-12-10

Blazor 上でメッセージボックスダイアログを実現しよう

確認メッセージと「はい」「いいえ」ボタンを表示するような、いわゆる "MessageBox" コンポーネントを、Blazor 上で実装してみることにします。例えば「削除」ボタンがあったとして、

「削除」ボタンがあるページ

これをクリックすると、「本当に削除していいですか?」と確認を行なう、「はい」か「いいえ」で応えるダイアログが表示される、そのようなメッセージボックスダイアログを表示するコンポーネントを作る、という話になります。

「削除してもよろしいですか」メッセージダイアログが表示されている様子

とにかく作ってみる

何はともあれ、ざっくり作ってみましょう。Blazor アプリケーションプロジェクトを作成したら、MessageBox.razor をプロジェクトに追加します、そして、ちょっと長いですが、以下のように、HTML 標準の dialo 要素を使って以下のように実装してみました。

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;

    [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 ファイルも追加します。

MessageBox.razor.js
export const showModal = (dialogRef) => dialogRef.showModal();
export const close = (dialogRef) => dialogRef.close();

これで MessageBox コンポーネントが使えるようになります。例えば以下のように使います。

何か.razor
<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 で [はい] がクリックされたイベントハンドラで、
        // 実際の削除処理を行なう
    }
}

... と、まぁ、これで一丁上がりなんですが、さて。

使い勝手、良くなくない?

実のところ、ちょっと好きじゃないんですよね、こういうコード。今一度、使う側のコードを再掲しますが、

何か.razor
    ...
    // [削除] ボタンがクリックされたら...
    private async Task OnClickDelete()
    {
        ...
        // メッセージボックスを表示
        await _messageBox.ShowAsync("削除してもよろしいですか?");
    }

    // メッセージボックスで [はい] がクリックされるとここに来る
    private void OnOK()
    {
        // 実際の削除処理を行なう
    }

[削除] ボタンがクリックされたときのハンドラ、OnClickDelete() 内では、実際の削除処理は実行されないんですよね。代わりに、別のイベントハンドラで削除処理が実行されます。こういうコードだと、「あれ、"削除していい?" ってメッセージ表示するのはいいんだけど、そのあと、何処行ったの??」 と混乱しがちです。

本当をいうと、以下のように書きたいんです。

何か.razor
...
@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 とします。

MessageBox.razor
...
@code {
    // 👇 TaskCompletionSource<bool> 型のフィールド変数を追加
    private TaskCompletionSource<bool>? _tcs;
    ...

そうしたら、ShowAsync() メソッド内では、TaskCompletionSource<bool> のオブジェクトを新たに作成し、先に用意したフィールド変数にこれを格納して、同オブジェクトの Task プロパティ (型は Task<bool> ですね) を ShowAsync() メソッドの戻り値として返すようにします。ShowAsync() メソッドの戻り値型も忘れずに Task<bool> に直しておきます。

MessageBox.razor
...
@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) メソッドを呼び出します。

MessageBox.razor
...
    <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 の全体を再掲します。

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 を改善後の、使う側のコードを再掲します。

何か.razor
    ...
    // [削除] ボタンがクリックされたら...
    private async Task OnClickDelete()
    {
        ...
        // 確認のメッセージボックスを表示
        var yes = await _messageBox.ShowAsync("削除してもよろしいですか?");
        
        // メッセージボックスで [はい] がクリックされたのなら...
        if (yes)
        {
            // 実際の削除処理を行なう
        }
    }
}

やっぱりこっちのほうがいいですね!

以上、TaskCompletionSource<T> クラスを活用することで、メッセージボックスに対するユーザー応答を、 Task を用いた await 構文の形で待機できるようになり、これを利用する側のコードがすっきり読みやすくなったと思います。

余談

間違った削除操作を防止するための方法として、確認メッセージで責任をユーザーになすりつけるのは、本当は望ましくない設計ですよね。確認メッセージもあってもいいんですが、可能な限り、削除を撤回 (Undo) もできるようにしておくべきですね。

6
1
2

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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?