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
を叩いて通知しています。
sender
はNotifyEvent.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側の実装例です。
OnInitialized
とDispose
でイベントハンドラの登録を行います。
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-fluxor
とBlazor-redux
というライブラリがあります。
Blazor-State
という名のMediatR Pipelineを使用した状態管理のライブラリもあるようです。
なおBlazor-fluxor
については@ryuichi111stdさんの下記記事が大変
参考になるかと思います。。
参考:Event Aggregatorについて
先程のサンプルとしてObserverパターンっぽい感じのコードを書きましたが、Blazorには軽量なEvent AggregatorライブラリのBlazor.EventAggregator
があります。手軽にEvent Aggregatorを使いたい場合は上記ライブラリを使用するのも手かもしれません。
※上記ライブラリを使用する場合、気になるIssueがOpenになっているので目を通しておいた方がよいかも。
おわり
Event Aggregatorと言えばPrism
で大変お世話になったなぁ、という思い出があるので今度Blazor + Prism.Core(IEventAggregator)を試してみたいと思います。