Blazor WebAssembly で MVVM を実現する(第2回)

はじめに

こんにちは。山本れきです。この記事は「Blazor WebAssemblyでMVVMを実現する」の第2回となります。

過去の記事
Blazor WebAssemblyでMVVMを実現する(第1回)

前回は素直にいくつかのコンポーネントを書いてみましたが、サンプルの枠を超えて実際に使ってみるとほとんどを作り直すこととなりました。
また、本格的にデザインを始めてみると、XAMLとHTMLの勝手の違いやWPFとBlazorの違い、BlazorならではのC#の制約などを理解するようになりました。
それで今回はBlazor WebAssembly上のMVVMとして必須の基盤実装について、前回より具体的に触れます。

この記事のソースコードの所在はこちらです。 https://github.com/cct-blog/Oniqys.Blazor/tree/blog/mvvm2

Blazor WebAssemblyにおけるMVVM基盤実装のポイント

WPF上のMVVM基盤は数多ありますが、Blazor WebAssembly用のMVVM基盤は2021年6月時点ではそれほど多くありません。
わざわざMVVMにしなくとも開発可能ではありますが、アトミックデザインを採用してみると想像以上にMVVMはしっくりときます。
かつてWPF黎明期にMVVM基盤を作ることは、私を含め多くのプログラマ達が試みていました。その際に、WPFを分析して得た知見が今回役立ちます。
加えて、Blazor WebAssembly特有の制限にも留意しなければなりません。

弱い参照を用いた実装が必須となる

ViewModelの変更をViewに通知するためにPropertyChangedイベント をViewModelに実装することは必須です。
しかし、ごく普通の実装をするとViewとの間で深刻なメモリリークが発生します。下図をご覧ください。

画面側の(比較的シンプルな)実装によっては何らかのプロパティの変化の都度、再描画が行われます。再描画タイミングはBlazor WebAssemblyが管理しているため、ユーザーコード上からはそれを判別できません。
そして再描画の際、View側は別のインスタンスが生成されてPropertyChangedイベントに再度イベントハンドラーが追加されるかもしれません。
しかしViewModel側からはそのViewが維持すべきものか破棄すべきものか識別はできません。MVVMとして、1つのViewModelに複数のViewを認める必要があり、且つ不要なViewの破棄はGC(ガーベージコレクション)に委ねる必要があります。
上図の実装の場合は再描画の度にViewが追加され、参照されたオブジェクトはGCによる破棄対象にはならず、蓄積し続けていくのです。

この問題の解決策は実のところ単純でありながら、しかし多少のコストを含むものとなります。次の図をご覧ください。

「弱い参照(WeakReference)」を使用します。ここで大切なのは、ViewModelからViewへの直接的な「強い参照」を排除することです。こうしておけば、Viewが破棄されるべきタイミングで、Viewに「強い参照」をするものはなくなります。WeakEventとBindingはPropertyChangedイベントの際に弱い参照先のViewがまだ有効か判定して、無効であればPropertyChangedイベントを外してやるだけで参照元がなくなり、GCが破棄してくれます。

以下はWeakEventのソースコードです。

https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/Core/WeakEvent.cs から抜粋

Bindingについてはまだデザイン途上のため公開していません。.razor 構文内で思いのほか綺麗にまとまらないというのが実情ですが、@bind を使うことで当座の必要には対応できることも多く、実装を一時保留しています。

.NET 5の.razorではジェネリック型制約を使えない

今回の実装で意外にも悩ましいこととなったのは .razor で記述するコンポーネントをジェネリック型にする場合、型制約(where T)をつけることができないことです。この制限が無ければもっとC#として綺麗なものが作れるのに、と感じることが何度かありました。型制約がないと、比較に == 演算子すら使えないことになります。
これは同時にWeakEventや各種基底クラスにも型制約を付けられないことを意味します。ジェネリックのWeakRefereenceには型制約がついており、基盤実装の際にこれらはかなり難しい制約となり得ます。

以下はその制約下で前回より改善した ViewComponentBase です。

https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/Controls/ViewComponentBase.cs から抜粋

メモ:型制約は .NET 6 で使えるようになります。(参考 : Microsoft 開発者ブログの言及先  https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#generic-type-constraints-in-razor )

最も小さなViewの実装

最もシンプルなViewを実装してみる必要があります。それはこのようになります。

https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/Controls/View.razor を引用

これを使う短い構文は Index.razor にあります。以下に抜粋します。

https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor.Sample/Pages/Index.razor から抜粋

これは非常に短いコードですが、Blazor WebAssemblyならではの記述方法を凝縮したような作りになっています。

  • @ChildContentは Viewタグに囲まれた内部を表示する。その際、引数を渡すことができる
  • 使用する側の @context は@ChildContent に渡された引数を表示する
  • ViewのDataContextのViewComponentBase<TViewModel>を継承しているため動的な切り替えが可能

Commandを含むボタン等の実装

MVVMとしてはぜひともCommandをサポートしなければなりません。CommandはCanExecuteの変更を通知するため、ViewModelの基底クラスである ContentBase を継承します。(WPFのCanExecute()とは異なります)

https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/ViewModel/ContentBase.cs
https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/ViewModel/CommandBase.cs
https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/ViewModel/Command.cs
以上3か所より抜粋

View側であるButtonでは@onclickイベント処理内でCommandを実行するという、Blazor WebAssemblyとして平易な実装となっています。ボタンの基底クラスは以下のリンク先にあります。
https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/Controls/ButtonBase.cs

ItemsControlなどのリスト表示

ItemsControlはWPFではListBoxなどの基底クラスです。Blazor WebAssembly用のMVVMでも同様の基底クラスを用意しました。

https://github.com/cct-blog/Oniqys.Blazor/blob/blog/mvvm2/Oniqys.Blazor/Controls/ItemsControl.cs から抜粋

Itemsに変化があると、WeakEventで再描画通知を発します。これだけの実装で大体十分です。
あとはItemsをどのように並べたいかによって継承先のHTML/CSSを記述することになります。
TItemをContentBaseとして、DataTemplateを使用するとItems内の型に応じた内容が表示されます。DataTemplateはChildContent内に配置する必要があります。複数のDataTemplateを組み合わせてコンポーネントとすることで、ResourceDictionaryのような働きをさせることができ、結果としてView内の冗長な記述を避けることが可能です。

現状と今後について

試行錯誤を経て、ようやくデザインの方向性が固まったと感じています。
Bindingについてはまだ試行錯誤段階ですがこちらも徐々にまとめて行きます。
現在はButtonなどに暫定的な実装が残っています。また、HTMLのStyleやclassなどをどのように扱うかは実際のところまだ迷いがあります。
加えて、今後はコントロールを増やしていく必要があると感じています。コントロールやパネル、ItemsControlの継承クラスなど、作るべきものは多いと感じます。
また前述のとおり .NET 6 がリリースされるまで型制約が使えないために妥協している部分もあり、まだまだやるべきことは多いと言えるでしょう。.NET 6 の正式リリースまでに整えることができるようにするのが現在の目標です。

実装指針は概ね固まったと感じています。次回の記事ではコンポーネントを増やしていくつもりです。

最近の記事

  • 関連記事
  • おすすめ記事
  • 特集記事

アーカイブ

カテゴリー

PAGE TOP