😺

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

2023/03/31に公開

はじめに

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

この記事で解ること

  • カスケーディングパラメータを使ってイベントコールバックを行う方法について言及します。

環境

  • Windows 11
  • VisualStudio 2022
    • 「ASP.NETとWeb開発」導入済み
  • .NET 7.0

やりたいこと

  • Blazor Serverアプリの新規作成を行ったときに導入されるテンプレでは、ページの右上にリンク付きの「About」が表示されます。
    • これは、ページ全体のレイアウトを担うMainLayout.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>
  • この部分に「現在のページの見出し」を表示することを考えました。
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でなく、通常の子要素コンポーネントであれば、以下のようにしてイベントコールバックを使えば済む話です。
MainLayout.razor
    <body OnClickCallback="@((string title) => _sectionTitle = title)" />
  • @Bodyを越えてパラメータを渡すためには、以下のようにカスケーディングパラメータを使うことになります。
MainLayout.razor
<CascadingValue Value="@((string title) => _sectionTitle = title)" Name="Section">
    @Body
</CascadingValue>
  • しかし、カスケーディングパラメータだと、デリゲートを渡すだけでは、コールバックできません。

解決方法

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 {~}に書くことも、レシーバをラムダ式でなくメソッドにすることも、もちろん可能です。

ページ側のコード

  • ページ側では、以下のようにしてパラメータを受け取ってコールバックを行います。
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");
}
  • なお、asyncawaitは必須ではなく、この例のように待機が不要であればOnInitialized ()を使うことも可能です。

おわりに

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

Discussion