[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>(); // 追記
        // 略
    }
}
```

```cs: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;
    }
}
```

```cs: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());
    }
}
```

```cs: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`を実行しています。

後々出てきますが、この`Action`に`StateHasChanged`を追加することで、`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