[Blazor]コンポーネント間でのステータスのやり取り方法


悩んだこと

  • アラート系のメッセージ処理を各ページでゴリゴリ書くのはイケてない
  • 共通化して使いまわししたい
  • componentを作って使いまわすのにも限度があるし、MainLayout.razorで一括に処理できればいいよね
  • 親コンポーネントと子コンポーネント間のやりとりってどうやるの?

今回の解決策

State Containerを作成し、それを介すことで親子間のStatusのやり取りを行いました。Fluxの考え方に似た方法です。

できたもの

IStateMessageService.cs
interface IStateMessageService
{
    string StateMessages { get; }
    event Action OnChange;
    void ClearStateMessages();
    void SetStateMessages(string statusMessages);
}
StateMessageService.cs
public class StateMessageService : IStateMessageService
{
    public string StateMessages { get; private set; }
    public event Action OnChange;

    public void SetStateMessages(IStatusMessages statusMessages)
    {
        StateMessages = statusMessages;
        NotifyStateChanged();
    }

    public void ClearStateMessages()
    {
        StateMessages = null;
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}
Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 略
        services.AddScoped<IStateMessageService, StateMessageService>(); // 追記
        // 略
    }
}
MainLayout.razor
@inject IStateMessageService StateMessage
@implements IDisposable

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <AlertMessage Message="StateMessage.StateMessages" />
    <div class="content px-4 vh-100">
        @Body
    </div>
</div>

@code
{
    protected override void OnInitialized()
    {
        StateMessage.OnChange += StateHasChanged;
    }

    public void Dispose()
    {
        StateMessage.OnChange -= StateHasChanged;
    }
}
AlertMessage.razor
@inject IStateMessageService StateMessage
@if(!string.IsNullOrEmpty(Message))
{
    <div class="alert alert-success alert-dismissible @(IsVisible ? "show " : "")text-left shadow" role="alert">
        <button type="button" class="close" data-dismiss="alert" aria-label="Close" @onclick="OnClickCloseButton">
            <span aria-hidden="true">&times;</span>
        </button>
        <p class="m-0">
            @Message
        </p>
    </div>
}

@code {
    [Parameter]
    public string Message { get; set; } = string.Empty;
    public bool IsVisible { get; set; } = true;

    private void OnClickCloseButton()
    {
        IsVisible = false;
        Task.Run(() => StateMessage.ClearStateMessages());
    }
}
ChildComponent.razor
@inject IStateMessageService StateMessage
<button type="button" @onclick="OnClickButton">Show</button>

@code {
    public void OnClickButton()
    {
        StateMessage.SetStateMessages("Hello world!");
    }
}

解説

StateMessageService

DIして各コンポーネントで使っていくサービスです。StateMessageService.SetStateMessagesを使い、表示するメッセージを追加します。内部ではメッセージの追加以外に、StateMessageService.OnChangeに登録されているActionを実行しています。

後々出てきますが、このActionStateHasChangedを追加することで、Messageの内容が変わるたびにDOM更新が走るといった算段です。

Razor Component

フロント側では@injectを使いIStateMessageServiceを受けます。MainLayout.razorにメッセージ表示用のAlertMessageを配置することでどの子コンポーネントでStateMessageService.SetStateMessagesを実行してもメッセージが表示されるようにします。
AlertMessage自体はBootstrapのAlertをラップしているコンポーネントです。Messageに表示したいメッセージを渡せばいい感じに表示してくれます。

AlertMessage.razorではクローズボタンが押された時の処理として、OnClickCloseButtonを定義しています。ここでStateMessageService.ClearStateMessagesを実行することで、非表示及びステートの初期化を行っています。

いいところ

ページ遷移をしてもクローズボタンを押下されない限り表示され続けます。使いどころとしてはブラウザ上でのプッシュ通知などでしょうか? 私個人はAPIのレスポンスメッセージを表示させたりしています。
WASMでもServerSideでもどっちでも使えるところも良いですね。
javascriptを1つも書かずにここまでできるBlazorは本当にすごいと思います。革新的ですね。

悪いところ

紹介したコードではメッセージが1つだけしか表示できません。トーストのようにスタックすることができないので、そういった用途には向かないでしょう。

参考

3 Ways to Communicate Between Components in Blazor
https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/

環境

  • Visual Studio 2019 ver 16.4.2
  • TargetFramework > netstandard2.0
  • LangVersion > 7.3
  • RazorLangVersion > 3.0