🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

BlazorのComponent間のデータのやり取りや状態管理ライブラリのメモ

Observer, EventAggregator, Fluxパターンとかそこら辺のメモです。

本文

BlazorのようなComponent指向なフレームワークで開発しているとComponentが持つデータを別のComponentに渡したい(Component間通信したい)ケースがあるかと思います。

Blazorは[Parameter]属性で指定したデータモデル<CascadingParameter>で指定したデータモデルを使いComponent間でデータをやり取り可能です。小さなアプリなら上記方法で事足りるのですが、Componentの階層が深くなった場合や、ちょっと遠い場所にあるComponentにデータを渡したい場合など、途端に辛くなってしまいます。

なんとかならんでしょうか。

温もり溢れる手作りの通知手段

イベントのSubscribe/Unsubscribeでデータを受け渡す方法はどうでしょう。

以下にコードの例を載せておきます。

INotification.cs

Notify関数がトリガーする側で、OnUpdatedはトリガーされる側の関数です。

namespace Sample.Notification
{
    public interface INotification<T1, T2>
    {
         event Func<T1, T2, Task> OnUpdated;
         Task Notify(T1 sender, T2 args);
    }
}

NotifyService.cs

INotification<T1, T2>の実装クラスです。

namespace Sample.Notification
{
    public class NotifyService : INotification<object, object>
    {
        public event Func<object, object, Task> OnUpdated;
        public async Task Notify(object sender, object args)
        {
            await OnUpdated?.Invoke(sender, args);
        }
    }
}

NotifyEvent.cs

INofitication<T1, T2>Notify関数の第1引数であるsenderで指定するEnumを作成します。

namespace ObserverBlazor.Notification
{
    public enum NotifyEvent
    {
        RefreshHogeHogeComponent
    }
}

Startup.cs

INotification<T1, T2>とその実装クラスであるNotifyServiceをSingletonでServiceに登録します。
senderに相当するobject型は予めEnumの種類(今回で言うとこNotifyEvent)を指定しておいた方が良いかもしれませんね。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();

+   services.AddSingleton<INotification<object, object>, NotifyService>();
}

親Component

親Component側の実装例です。
ボタンを押下した際にUpdateを叩いて通知しています。
senderNotifyEvent.csで作成したEnumを指定しています。

@page "/counter"
+ @using Sample.Notification
+ @inject INotification<object, object> notifyService

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

<FooComponent />

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
+       notifyService.Notify(NotifyEvent.RefreshHogeHogeComponent, currentCount);
    }
}

子Component

子Component側の実装例です。
OnInitializedDisposeイベントハンドラの登録を行います。
HandleAsyncで通知されたイベントに応じた処理を記述します。

@using Sample.Notification
@inject INotification<object, object> notifyService

<p>Status is @Status</p>

@code{
    public string Status = "🐥🔜🍖";

    protected override void OnInitialized()
    {
        notifyService.OnUpdated += HandleAsync;
    }

    void Dispose()
    {
        notifyService.OnUpdated -= HandleAsync;
    }

    async Task HandleAsync(object sender, object args )
    {
        if(sender == NotifyEvent.RefreshHogeHogeComponent)
        {
            Status = args as string;
        }
        StateHasChanged();
    }
}

以上で完了です。
Component間でデータのやり取りができました。

いずれ破綻する

イベントの種類を増やしたい場合、senderで指定するEnumの種類を追加したり、INotification<T1, T2>T1を予めEnum指定して実装クラスの種類を増やしていくことである程度対応可能です。しかし、イベントの種類の増加やアプリの規模がスケールするに伴い、沢山のイベントが飛び交い次第に手に負えなくなってきます。

爆発する前に状態管理関連のライブラリの導入を検討しましょう。

巨人の肩に乗る

React/Angular/Vue.jsでお馴染みのFluxパターンを実装したBlazor-fluxorBlazor-reduxというライブラリがあります。

github.com

github.com

Blazor-Stateという名のMediatR Pipelineを使用した状態管理のライブラリもあるようです。

github.com

なおBlazor-fluxorについては@ryuichi111stdさんの下記記事が大変 参考になるかと思います。。

ryuichi111std.hatenablog.com

参考:Event Aggregatorについて

先程のサンプルとしてObserverパターンっぽい感じのコードを書きましたが、Blazorには軽量なEvent AggregatorライブラリのBlazor.EventAggregatorがあります。手軽にEvent Aggregatorを使いたい場合は上記ライブラリを使用するのも手かもしれません。
※上記ライブラリを使用する場合、気になるIssueがOpenになっているので目を通しておいた方がよいかも。

github.com

おわり

Event Aggregatorと言えばPrismで大変お世話になったなぁ、という思い出があるので今度Blazor + Prism.Core(IEventAggregator)を試してみたいと思います。