🐥note.

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

BlazorでPrismのEvent Aggregatorを使用しViewModel間通信を試した備忘録

本エントリは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公式のドキュメントをご参照ください。

prismlibrary.github.io

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ページにCounterComponentを表示させます。

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!ボタンを押下すると、CounterViewModelIncrementCountが実行されCurrentCountの値は+1されています。
しかし画面は更新されません!

試しにCounterViewModelIncrementCountからIEventAggregatorをPublishするようコードを変更し実行します。ボタンを押下した結果今度は画面が更新されました。

別ComponentからIEventAggregatorでPropertyを更新しても、更新対象のPropertyを使用しているComponent自体は再レンダリングされないようです。
※Vue.jsみたいにProperty監視してるわけじゃありませんし、Blazorつよつよマンからすると、"そらそうだろ"って感じなのかな?

Subscribeした際に再レンダリングする

プロパティの変更を検出して画面を再レンダリングさせる必要がありますね。
CounterViewModelINotifyPropertyChangedを継承させるOld Schoolな手法を試してみましょう。

Counter.razor

コードビハインドを追加しCounterViewModelPropertyChangedStateHasChangedを追加します。

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ってどうなの?
    というモヤモヤを感じます🐥💭☁

以上です。