【Blazor Serverアプリ】ページからレイアウトへのイベントコールバック ～ @bodyを越えて

## はじめに
- この記事では、以下のような読者像を想定しています。
    - Blazor Serverアプリのチュートリアルを済ませた
- この記事ではツール類の使用方法には言及しません。

### この記事で解ること
- カスケーディングパラメータを使ってイベントコールバックを行う方法について言及します。

### 環境
- Windows 11
- VisualStudio 2022
    - 「ASP.NETとWeb開発」導入済み
- .NET 7.0

## やりたいこと
![スクリーンショット 2023-03-30 042525.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365845/e9d5c458-e658-f059-d535-386d58a014c7.jpeg)

- Blazor Serverアプリの新規作成を行ったときに導入されるテンプレでは、ページの右上にリンク付きの「About」が表示されます。
    - これは、ページ全体のレイアウトを担う`MainLayout.razor`に記述されています。

```razor:MainLayout.razor
@inherits LayoutComponentBase
<PageTitle>BlazorApp</PageTitle>
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>
        <article class="content px-4">
            @Body
        </article>
    </main>
</div>
```

- この部分に「現在のページの見出し」を表示することを考えました。

```razor:MainLayout.razor
@inherits LayoutComponentBase
<PageTitle>BlazorApp</PageTitle>
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <main>
        <div class="top-row px-4">
            <h1>_sectionTitle</h1>
        </div>
        <article class="content px-4">
            @Body
        </article>
    </main>
</div>
@code {
    protected string _sectionTitle = string.Empty;
}
```

- この見出しは、`@Body`の中身である`@page`コンポーネントから更新される必要があります。
- これが`@Body`でなく、通常の子要素コンポーネントであれば、以下のようにしてイベントコールバックを使えば済む話です。


```razor:MainLayout.razor
    <body OnClickCallback="@((string title) => _sectionTitle = title)" />
```

- `@Body`を越えてパラメータを渡すためには、以下のようにカスケーディングパラメータを使うことになります。

```razor:MainLayout.razor
<CascadingValue Value="@((string title) => _sectionTitle = title)" Name="Section">
    @Body
</CascadingValue>
```

- しかし、カスケーディングパラメータだと、デリゲートを渡すだけでは、コールバックできません。

## 解決方法
- [`EventCallbackFactory`](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.components.eventcallbackfactory?view=aspnetcore-7.0)クラスで[`EventCallback`](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.components.eventcallback?view=aspnetcore-7.0)構造体を生成して渡します。

```razor:MainLayout.razor
@inherits LayoutComponentBase
<PageTitle>BlazorApp</PageTitle>
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <main>
        <div class="top-row px-4">
            <h1>@_sectionTitle</h1>
        </div>
        <article class="content px-4">
            <CascadingValue Value="@(EventCallback.Factory.Create<string> (this, (string title) => _sectionTitle = title))" Name="Section">
                @Body
            </CascadingValue>
        </article>
    </main>
</div>
@code {
    protected string _sectionTitle = string.Empty;
}
```

- `EventCallback.Factory`は、`EventCallbackFactory`のインスタンスを保持している静的な読み出し専用フィールドです。
    - `Create<TValue>(Object, Action)`によって、レシーバとコールバックが生成されます。
- ここでは、双方とも`CascadingValue`のパラメータ`Value`に埋め込みましたが、コールバックを`@code {~}`に書くことも、レシーバをラムダ式でなくメソッドにすることも、もちろん可能です。

### ページ側のコード
- ページ側では、以下のようにしてパラメータを受け取ってコールバックを行います。

```razor:Index.razor
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
@code {
    [CascadingParameter(Name = "Section")] protected EventCallback<string> SetSectionTitle { get; set; }
    protected override async Task OnInitializedAsync () => await SetSectionTitle.InvokeAsync ("Home");
}
```

- なお、`async`、`await`は必須ではなく、この例のように待機が不要であれば`OnInitialized ()`を使うことも可能です。

## おわりに
![スクリーンショット 2023-03-31 041332.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365845/61320309-e852-4cb6-0ecf-4702d2a4f6be.jpeg)

- この結論に至るまでに、`StateHasChanged ()`を使って強制的に再描画させたり、DIコンテナに`Scoped`なサービスを登録したりなどと、試行錯誤を重ねました。
    - 同様の課題を抱えた方のお役に立てば幸いです。
- 執筆者は、Blazor、ASP.NETともに初学者ですので、誤りもあるかと思います。
    - お気づきの際は、是非コメントや編集リクエストにてご指摘ください。
    - あるいは、「それでも解らない」、「自分はこう捉えている」などといった、ご意見、ご感想も歓迎いたします。