本エントリはBlazor Advent Calender 2019の15日目の記事です
本文
BlazorでMVVMな作りを試すためにRazor ComponentにViewModelをInjectする構成でサンプルを組んでみました。その過程でViewModel間通信を試したくなり、WPF, WinForms, Xamarinによるアプリ開発でお馴染みのPrism
のEvent Aggregatorを使用してみました。
本エントリはその備忘録です。
早速プロジェクトを作成しPrism.Core
をNugetから追加して実装を始めましょう。
※本エントリ作成時の.NET Coreのバージョンは.NET Core 3.1 LTSです。
なお、Event Aggregatorについての詳細はPrism公式のドキュメントをご参照ください。
NotificationEvent.cs
Event AggregatorでPub/SubするためのEvent Classを作成します。
引数を指定する場合はPubSubEvent
ではなくPubSubEvent<T>
を継承することでパラメータを渡すことが可能です。
using Prism.Events; namespace BlazorWithPrism.Services { public class NotificationEvent : PubSubEvent { } }
Counter.razor
CounterページにViewModelをinjectします。
表示するPropertyやOnClickイベントを全てViewModel側へ移動させます。
@page "/counter" @inject CounterViewModel ViewModel; <h1>Counter</h1> <p>Current count: @ViewModel.CurrentCount</p> <button class="btn btn-primary" @onclick="ViewModel.IncrementCount">Click me</button>
CounterViewModel.cs
CounterのViewModelです。
IEventAggregatorでNotificationEvent
をSubscribeします。
using Prism.Events; namespace BlazorWithPrism.Services { public class CounterViewModel { public int CurrentCount { get; set; } public CounterViewModel(IEventAggregator ea) { ea.GetEvent<NotificationEvent>().Subscribe(IncrementCount); } public void IncrementCount() { CurrentCount++; } } }
Index.razor
IndexページにCounter
Componentを表示させます。
Count++ from IndexPage!
ボタンを押下すると、ViewModelのOnClick
関数を実行しIEventAggregatorでNotificationEvent
をPublishします。
@page "/" @inject IndexViewModel ViewModel <h1>Hello, world!</h1> Welcome to your new app. <button class="btn" @onclick="ViewModel.OnClick">Count++ from IndexPage!</button> <Counter />
IndexViewModel.cs
割愛します。
using Prism.Events; namespace BlazorWithPrism.Services { public class IndexViewModel { private readonly IEventAggregator _ea; public IndexViewModel(IEventAggregator ea) { _ea = ea; } public void OnClick() { _ea.GetEvent<NotificationEvent>().Publish(); } } }
Startup.cs
IEventAggregator
とViewModelをInjectionします。
+ using Prism.Events; public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); + services.AddScoped<IndexViewModel>(); + services.AddScoped<CounterViewModel>(); + services.AddSingleton<IEventAggregator, EventAggregator>(); }
完成です。実行してみましょう。
ところがどっこい...
Count++ from IndexPage!
ボタンを押下すると、CounterViewModel
でIncrementCount
が実行されCurrentCount
の値は+1されています。
しかし画面は更新されません!
試しにCounterViewModel
のIncrementCount
からIEventAggregatorをPublishするようコードを変更し実行します。ボタンを押下した結果今度は画面が更新されました。
別ComponentからIEventAggregatorでPropertyを更新しても、更新対象のPropertyを使用しているComponent自体は再レンダリングされないようです。
※Vue.jsみたいにProperty監視してるわけじゃありませんし、Blazorつよつよマンからすると、"そらそうだろ"って感じなのかな?
Subscribeした際に再レンダリングする
プロパティの変更を検出して画面を再レンダリングさせる必要がありますね。
CounterViewModel
にINotifyPropertyChanged
を継承させるOld Schoolな手法を試してみましょう。
Counter.razor
コードビハインドを追加しCounterViewModel
のPropertyChanged
にStateHasChanged
を追加します。
UI Thread以外からStateHasChanged
を実行することはできないので、InvokeAsync
ごと渡します。今はとりあえず-=
とwarningは省略します。
@page "/counter" @inject CounterViewModel ViewModel; <h1>Counter</h1> <p>Current count: @ViewModel.CurrentCount</p> <button class="btn btn-primary" @onclick="ViewModel.IncrementCount">Click me</button> + @code { + protected override void OnInitialized() + { + ViewModel.PropertyChanged += async (o, e) => await InvokeAsync(StateHasChanged); + } + }
CounterViewModel.cs
INotifyPropertyChanged
を継承します。
CurrentCount
プロパティのsetterでPropertyChanged
を実行し画面を更新させます。
今はとりあえずIDisposable
を省略します。
using System.ComponentModel; using Prism.Events; namespace BlazorWithPrism.Services { - public class CounterViewModel + public class CounterViewModel : INotifyPropertyChanged { + public event PropertyChangedEventHandler PropertyChanged; + + private int _currentCount; - public int CurrentCount {get; set;} + public int CurrentCount + { + get => _currentCount; + set + { + _currentCount = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentCount))); + } + } // 以下省略 } }
再度実行してみる
IndexのCount++ from IndexPage!
ボタンを押下すると、きちんと画面が更新されました。
StateHasChanged
が効いてるっぽいですね。
おわり
ViewModel作って
Prism.Core
のEvent AggregatorでViewModel間通信を行う実験でした。このような方法を取る場合、
INotifyPropertyChanged
を書くのはちょっと辛いのでRectiveProperty
とかで幸せになりたいですね...と言いつつ、Blazorに特化したEvent Aggregator
,Observable
, 更に言うとFluxパターン
による状態管理ライブラリがあるので車輪の再発明だったり。画面が更新されない件、実は
Counter.razor
側のShouldRenderフラグ
をtrueにすれば更新されたりして...。
※それはそれで制御が破綻しそうではありますが。言語化できていないのですが、そもそもBlazorでMVVMってどうなの?
というモヤモヤを感じます🐥💭☁
以上です。