【Blazor】メッセージダイアログから応答を受け取る


## 概要

メッセージダイアログを自作し、その応答を取得します。

**Razor Componentならではの興味深い書き方になっている**と思います。


## 環境

VS2022のBlazor Appテンプレートからプロジェクトを作成します。

Render modeを`Interactive WebAssembly`とするRazor componentを利用します。

[選定基準](https://qiita.com/Leech/items/bf24fdd01987bd186987#render-mode)に記載のように、デプロイ環境に大きな制約が出るため、基本的には、WebAssemblyが無難と考えています。


## 実装

プロジェクト名は[BlazorAppTips](#リポジトリ)で進めていきます。

メッセージダイアログの作成については、[Modalの書き方](https://qiita.com/Leech/items/d8555dfb01af2b0b8d88#%E5%AE%9F%E8%A3%85)をベースに、JavaScriptを利用する形で実現しています。



- Dialogを呼び出すページを追加

```c#:BlazorAppTips.Client/Pages/Dialog/DialogCaller.razor
@page "/dialog-caller"
@rendermode InteractiveWebAssembly
@inject IJSRuntime JSRuntime

<h3>DialogCaller</h3>
<button id="open-button" @onclick="OpenDialog">Open</button>

<MessageDialog @ref="messageDialog" />

@code {
    private MessageDialog messageDialog = default!;

    private async Task OpenDialog()
    {
        await JSRuntime.InvokeVoidAsync("toggle", true);

        var result = await messageDialog.GetResult();

        if (result == MessageDialog.Result.Yes)
            Console.WriteLine("Yes");
        else
            Console.WriteLine("No");
    }
}
```

Style

```css:BlazorAppTips.Client/Pages/Dialog/DialogCaller.razor.css
#open-button {
    cursor: pointer;
    border: 1px solid #000;
    margin: 10px;
}
```

- Dialogを追加

```c#:BlazorAppTips.Client/Pages/Dialog/MessageDialog.razor
@inject IJSRuntime JSRuntime

<div id="overlay" @onclick="Close" />
<div id="dialog">
    <div id="container">
        <button @onclick="OnYes">Yes</button>
        <button @onclick="OnNo">No</button>
    </div>
</div>

@code {

    public enum Result
    {
        Yes,
        No
    }

    private TaskCompletionSource<Result>? tsc;

    public Task<Result> GetResult()
    {
        tsc = new TaskCompletionSource<Result>();
        return tsc.Task;
    }

    private async Task OnYes()
    {
        if (tsc is not null && !tsc.Task.IsCompleted)
            tsc.SetResult(Result.Yes);

        await Close();
    }

    private async Task OnNo()
    {
        if (tsc is not null && !tsc.Task.IsCompleted)
            tsc.SetResult(Result.No);

        await Close();
    }

    private async Task Close()
    {
        await JSRuntime.InvokeVoidAsync("toggle", false);
    }
}
```

Style

```css:BlazorAppTips.Client/Pages/Dialog/MessageDialog.razor.css
#dialog {
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease-out;
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 1000;
    transform: translate(-50%, -50%);
    width: 30%;
    height: 30%;
    border: 1px solid black;
    background-color: lightblue;
}

#container {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
}

button {
    width:80px;
    height:30px;
    margin:0 10px;
    border: 1px solid #000;
    text-align: center;
}

#overlay {
    opacity: 0;
    visibility: hidden;
    transition: all 0.3s ease-out;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 999;
    background-color: lightgray;
}
```



- JavaScriptを追加

```html:BlazorAppTips/Components/App.razor
<body>
    @* 以下のscriptを追加 *@
    <script>
        // 表示処理
        function toggle(isOpen) {
            // エレメントを取得
            const dialog = document.getElementById('dialog');
            const overlay = document.getElementById('overlay');

            // Scrollを固定するためのエレメント
            const body = document.body;

            if (isOpen) {
                dialog.style.opacity = 1;
                dialog.style.visibility = 'visible';
                overlay.style.opacity = 1;
                overlay.style.visibility = 'visible';
                body.style.overflow = 'hidden';
            } else {
                dialog.style.opacity = 0;
                dialog.style.visibility = 'hidden';
                overlay.style.opacity = 0;
                overlay.style.visibility = 'hidden';
                body.style.overflow = 'auto';
            }
        }
    </script>
<body>
```

## 確認

`http://localhost:[your-port]/dialog-caller`にアクセスしてみてください。

結果はBrowserのコンソールに出力されます。

## ポイント

### `<MessageDialog @ref="messageDialog" />`

作成したDialogに対して、Element Referenceを追加し、`@code`内で`MessageDialog` インスタンスを自由に扱えるようにします。

このアクセス権限の付与により、Dialog内のボタン押下後のアクションに何かしらの干渉を行うことができます。


今回のサンプルは`await messageDialog.GetResult()`の応答を待ち、`DialogCaller`内で結果を受けて分岐をしていますが、例えば、Callbackを渡して、`MessageDialog`内でボタン押下後のアクションにそのCallbackを実行するという方法も可能です。

### `TaskCompletionSource`

`Task`オブジェクトを任意で生成できます。今回は`MessageDialog`の応答を受けて、呼び元の`DialogCaller`でアクションを実行するという方式にしたかったので利用しています。

`GetResult`が呼ばれたタイミングで、メンバ変数の`tsc`に`TaskCompletionSource`のインスタンスを格納し呼び元に戻します。`SetResult`により値が格納されることで、`tsc`がResolveされ、呼び元の`await`が完了します。

### ScriptとStyle

今回のようにJavaScriptを使う場合は、`<script></script>`が呼ばれるタイミングも考慮してあげる必要があります。`App.razor`に置くことで重複を防いでいます。仮に、`MessageDialog`に置いた場合、このComponentが呼ばれる度に`<script></script>`が追加されてしまいます。

Styleもインラインで各場合、同様の注意が必要ですが、Blazorの[**CSS isolation**](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation?view=aspnetcore-8.0#enable-css-isolation)という仕組みによって回避しています。

## 最後に

なぜ、`TaskCompletionSource`を採用したかというと、直感的で分かりやすいからです。

共通ライブラリ・Componentになり得るものは使う側の技術スタックなども考慮して作成していく必要がありますね。


## リポジトリ

[BlazorAppTips](https://github.com/HIppei/BlazorAppTips)