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

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

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

![「削除」ボタンがあるページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/0578c25a-87f7-a35d-259e-a3723a6f4ea4.png)

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

![「削除してもよろしいですか」メッセージダイアログが表示されている様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/f7a85ee7-7deb-b63e-f9f8-2bed7c3b2123.png)

## とにかく作ってみる

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

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

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

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

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

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

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

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

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

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

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

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

```cs:何か.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>` クラス**が使えます。

https://learn.microsoft.com/dotnet/api/system.threading.tasks.taskcompletionsource-1

これは JavaScript でいう Promise に似てて、`TaskCompletionSource<T>` オブジェクトの `SetResult<T>(T value)` メソッドを呼び出すと、同じ `TaskCompletionSource<T>` オブジェクトの `Task` プロパティが "Resolve" される、つまり、その `Task<T>` オブジェクトの await 待ちから処理継続する、という感じで動作します。

実際に `MessageBox.razor` 内で使ってみましょう。

まず、`TaskCompletionSource<T>` オブジェクトを保持しておくフィールド変数を、`MessageBox.razor` 内のコードブロックに追加しておきます。なお今回は、メッセージボックスの「はい」ボタンがクリックされたかどうかで `bool` 値を返すようにしたいので、型引数 `T` は、`bool` とします。

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

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

```cs: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)` メソッドを呼び出します。

```cs: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` の全体を再掲します。

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

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

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

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

## 余談

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