WPF Prism episode: 4.5 ~ ReactiveProperty からはじまる MVVM 狂想曲 ~

前回記事「DI だけど Unity さえあれば関係ないよねっ【episode: 4 WPF Prism】」

 

前回記事では Prism Shell で生成したデータオブジェクトを DI コンテナの【Unity】を経由して TreeView を配置した Prism Module へ渡す所まで紹介しました。

2020/9/4 追記
このエントリと同じ範囲の内容を再構成して新規連載中の .NET Core WPF Prism MVVM 入門 2020 step: 6 で公開しました。Prism のデータバインディングサポートクラスで Model ⇔ VM 間を双方向でバインドする方法を紹介しています。

2020/9/13 追記

このエントリを書いている頃より MVVM パターンへが多少理解できたため、現在似た内容の連載を公開中です。【連載】.NET Core WPF Prism MVVM 入門 2020 の step: 7『ReactiveProperty を編む』でこのエントリに近い範囲の内容を書いているので、良ければそちらもご覧ください。

このエントリの内容は元々 episode: 5 として公開していた記事の一部でしたが、加筆・修正箇所が予想以上に増大したため、episode: 4.5 と、episode: 5 の 2 つに分割しました。
この回は Prism から少し離れて、MVVM 入門と ReactiveProperty 入門がメインになります。

尚、この記事は Visual Studio 2017 Community Edition で .NET Framework 4.7.2 以上 と C# + Prism 7.1 + ReactiveProperty を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。

MVVM 入門

ここまで WPF の目玉機能の 1 つと言える『データバインディング』についてはほぼ触れていませんでしたが、今回から少しずつ増えていく予定です。
MVVM パターンについては episode: 1 で下 fig. 1 の図と @IT の記事へのリンク を紹介しただけなので、簡単ですが、もう少しだけ書いてみます。

fig. 1 MVVM パターン

MVVM パターンは上 fig. 1 の通りプログラムを Model – View – ViewModel の 3 つに分割して作成するパターンで、View はユーザの目に触れる画面を指し、ViewModel は View と Model の仲介役として、View の入力を Model に渡したり、Model の情報を View に設定する役目を持ちます。

MVVM パターンは MVC パターン等と同様にデータ自体の入出力については定義されておらず、UI 部の入出力に特化したパターンです。厳密に定義されているのは View と ViewModel のみなので極端に言えば View と ViewModel 以外は全て Model に分類されます

そして、WPF で MVVM パターンを適用する場合、View は XAML で作成し、View ⇔ ViewModel(以降、VM)間はデータバインディングでやり取りします。
まずは Prism の BindableBase を利用した簡単な VM のサンプルコードを紹介します。

Prism の BindableBase で作成する ViewModel

下 fig. 2 のようなユーザ情報の編集画面とバインドする VM のサンプルです。

fig. 2 MVVM サンプル

見出しは ViewModel となっていますが、ごく簡単なサンプルなので VM も Model も 1 つのファイルにまとめて書いています。

Person クラスの内容を表示する View にバインドした ViewModel があり、Model層 のクラスとして Person クラスと UserAgent クラスを定義しています。
UserAgent クラスは Person クラスを new して返すだけですが、本来はデータソース(DB やファイル)から Person クラスを取得・保存するためのクラスと思ってください。
info UserAgent.Load メソッド直下のコメントを外せばデータを設定した Person クラスが読み込まれます。

そして VM は『{Binding ~}』が指定された XAML 上のコントロールとバインドすることでデータや Command をやり取りすることができます
VM に対する View のソースは GitHub リポジトリにアップしている XAML を直接参照してください。

前項でも書いた通り、MVVM パターンで厳密に定義されているのは View と ViewModel のみなので、Model には「Business クラス」でも「DataAccess」でも「Service」、「Agent」、「Controller」等何を置いても構いませんし Model を何層かに分けた設計にしても構いません。と言うより、本来 Model は何層かに分けて設計するものです

「Model 層 は『MVVM』の先頭 1 文字目の M だから Model 以外は作成しちゃダメで、個人データを読み込む Load メソッドは Person クラスに実装しなくてはならない!」的な設計にする必要はないので、ここではあえて Agent クラスから Person を取得するサンプルにしています。
黒歴史 「Load メソッドは Person クラスに!」とか考えてたのは実は管理人だったりします…

WPF アプリを MVVM パターンで作成する場合、VM は INotifyPropertyChanged インタフェースを継承する必要があり、View とバインドするプロパティは変更通知プロパティとして定義する必要があります。

上記サンプルコードの UserName、UserBirthDay、UserAge の 3 つが変更通知プロパティで、Prism の BindableBase を利用した書き方ですが、WPF 標準の変更通知プロパティ(INotifyPropertyChanged)は C# の自動実装プロパティと比べると非常に冗長で面倒な記述を強いられます
そのため一般的な MVVM フレームワークでは VM 用に INotifyPropertyChanged インタフェースを継承した基底クラスを用意している場合が多く、Prism でも INotifyPropertyChanged インタフェースを継承した Prism.Mvvm.BindableBase クラスが用意されていて、BindableBase を継承して VM を作成すると WPF 標準と比べてほんの少しだけ記述が楽になります。

Prism の BindableBase を使っても変更通知プロパティを書くのは結構面倒ですが、Prism の『propp』スニペットを利用するともう少し楽ができます
Prism のコードスニペットは Prism Template Pack と同時にインストールされるので、Prism Template Pack をインストールしている場合は試してみてください。
Prism に含まれるコードスニペットは【Prism Template Packについてくるコードスニペット – Qiita】で紹介されています。

WPF で MVVM パターンを実装した場合、データバインディング以外にもボタン Click 等のイベントも受け取る必要があり、その際の View → VM の通知には ICommand インタフェースを通じて行われます。
Prism を使用しない場合は ICommand インタフェースを継承した Command 受け取りクラスも作成する必要がありますが、Prism では ICommand インタフェース を継承した Prism.Commands.DelegateCommand も用意されています。

上記サンプルコードでは SaveCommand で Prism の DelegateCommand を使用していますが、コマンドについては又、別の機会に紹介する予定なので、ここでは View のボタンが Click された場合は SaveCommand に設定したメソッドが実行されるとだけ覚えておいてください。

尚、上で紹介したサンプルコードも GitHub リポジトリ にアップしたソリューションに含めているので、実行する際は、MvvmSample をスタートアッププロジェクトに設定して実行してください。



ViewModel の役割

MVVM パターンで最も特徴的かつ最重要と言える ViewModel。Windows Form での開発では存在しなかった(Reactive UI 等は除きます)クラスなのでイメージが掴みにくいと思います。

VM の役割を文章にすると『VM は基本的に View を意識せず、Model の影として位置する薄いレイヤー』のような抽象的で分かりにくい表現で説明されていることも多いですが、「View を意識しないってメッセージボックスとか表示しないの?」とか「薄いレイヤーってどのくらいが適切な厚みなの?」のような疑問が湧くのは当然だと思います。管理人的には「適材適所だし案件や納期によって変わる」と言うような玉虫色の回答しかできませんが、言えるのは「数をこなして自分なりの落としどころを見つける」しかないと思っています。

完全に自分しか使わないアプリならともかく、『人に使ってもらう』、『誰かに依頼されて作る』ことが多いアプリケーションの開発には、当然いろんな制約が付くのでその中でやりくりするしかないと思います。
とは言え、入門編と銘打って公開するので、管理人的に考える VM の位置付けを簡単に紹介します。

MVVM パターンで作成するクラス間の関係は、何度も紹介している @IT の記事で以下のように書かれています。

ViewはViewModelに依存し、ViewModelはModelに依存します。逆方向の依存はありません

<span class="su-quote-cite"><a href="https://www.atmarkit.co.jp/fdotnet/chushin/greatblogentry_02/greatblogentry_02_01.html" target="_blank">MVVMパターンの常識 ― 「M」「V」「VM」の役割とは?</a></span>

上で引用した通り、VM は Model に依存するため Model に定義されているプロパティと同じプロパティを View に表示する数だけ VM に定義する必要があります

Windows Form や、コードビハインドで作成した WPF の場合、読み込んだデータを TextBox 等のコントロールに設定することで画面に表示されますが、MVVM パターンで作成する場合は VM 自身に追加した変更通知プロパティへ設定した値のみ画面に表示されるという違いがあります。
又、VM へ追加するプロパティは値の表示・入力が必要なものだけではなく、例えば IsEnabled や BackGroundColor のようなプロパティであっても、ユーザが目にする要素であれば VM へバインドプロパティを追加する必要があると考えれば分かりやすいかもしれません。
補足 Enabled や 背景色等はデータバインドではなく TriggerAction で変更する方法もあります。

このようにユーザの目に触れる要素を VM の public プロパティとして定義することで、データ処理と画面上の動作が VM の中で完結するようになるため、VM 単体でテストが実行できるようになったり、表示形式が違う View へ簡単に差し替えることもできるようになるのが MVVM パターンのメリットです。

MVVM パターンには確かに上記のようなメリットはありますが、作成する画面によっては 30 ~ 50 個又はそれ以上の変更通知プロパティを作成することになるのは考えただけで面倒になりませんか?
管理人は本当に面倒だと思いますw

前項のサンプルコードは Prism の BindableBase を利用して書いていますが、WPF 標準と比べて記述量が大幅に減る訳でもなく冗長なのは大して変わらないので、ズボラな管理人はデータバインディングのインタフェースに【ReactiveProperty】を使用することにします。

ReactiveProperty を使用しても、この連載で紹介する内容は ReactiveProperty に依存する部分は多くなさそうですし、ReactiveProperty で記述した部分を WPF 標準又は Prism の BindableBase に置き換えるだけで意味が通じるよう書くつもりなので、できれば見捨てず読んでもらえるとありがたいです。

ReactiveProperty で始める MVVM 入門

どこかで見たことがある人も多いと思いますが、ReactiveProperty で変更通知プロパティを書くと、どの程度簡単に書けるか下のサンプルコードを見てください。
src. 2 は同じプロパティを WPF 標準、Prism の BindableBase、ReactiveProperty の 3 パターンで記述した例です。

3 つを比べると、C# の自動実装プロパティで記述できる ReactiveProperty が最も少ないのは一目で分かると思います。

ReactiveProperty のインストール

ReactiveProperty は変更通知プロパティをシンプルに書けるだけでなく ReactiveExtension(Rx)でのコーディングスタイルを初めとして他にも多く機能を含んでいるので、何ができるかを一通り把握したい場合は ReactiveProperty のメンテナーである【かずきさん】が書かれた【MVVMをリアクティブプログラミングで快適にReactivePropertyオーバービュー – かずきのBlog@hatena】に目を通しておくと良いと思います。

ReactiveProperty をインストールするには NuGet で『reactiveproperty』を検索すると見つかるので episode: 4 で作成した TreeView を含む Prism Module(NavigationTreeプロジェクト)へ ReactiveProperty をインストールしてください。

fig.3 Nuget で検索する ReactiveProperty

info ReactiveProperty をインストールすると Reactive Extensions の各ライブラリも併せてインストールされます。(fig. 3 の【依存関係】に見えている System.Reactive 等も一緒にインストールされます)



TreeView の HierarchicalDataTemplate

前回エントリ(episode: 4)公開時点では Prism Module 内の UserControl 上に TreeView コントロールを配置しただけだった NavigationTree XAML を src. 3 のように編集します。

TreeView は ItemsSource プロパティに設定したコレクションを TreeViewItem として描画するので、ItemsSourceプロパティに {Binding TreeNodes} を設定します。

ItemTemplate に TreeViewItem 自体のレイアウトを設定するのは他の List 系コントロールと同じですが、DataTemplate に【HierarchicalDataTemplate】を指定するのが他の List 系コントロールとの違いです。
HierarchicalDataTemplate を設定した項目は Windows Explorer のフォルダツリーやメニューのような階層構造で表示されるようになります。

そして HierarchicalDataTemplate 内に TreeViewItem として描画したいコントロールを配置すると、配置したコントロールの通り TreeViewItem が描画されます。
ここでは TextBlock のみを配置しているので実行すると fig.4 のような画面になります。

fig.4 DataTemplate に TextBlock のみを配置した TreeView

TextBlock の代わりに、StackPanel や Grid を置いてその中へ CheckBox や Button 等のコントロールを配置すると、全ての TreeViewItem が配置したレイアウト通りに描画されるので Windows Form と比べると TreeViewItem の見た目を簡単に変更できます
7 行目の名前空間のエイリアス宣言と、14 行目 ~ 16 行目は次回紹介します。

はじめての ReactiveProperty(ReactiveCollection)

そして、src. 3 の XAML とバインドする VM です。

TreeNodes プロパティに使用している ReadOnlyReactiveCollection<T> 型は ReactiveProperty に含まれる List 型の値をバインドするために使用する型で、ReadOnly のため View 側から書込むことはできません。
TreeView はユーザの操作で項目の追加・削除はできないので双方向の同期は必要ない

ReactiveCollection は WPF 標準の ObservableCollection<T> と同じで、メンバが追加、削除された時、又はリスト全体が更新されたときに変更通知を発行するコレクションクラスで、ReactiveProperty をインストールしない場合は ObservableCollection<T> で書き換えが可能です。

ReactiveProperty に含まれるクラス名に ReadOnly が付いたクラスと『To ~ Reactive ~』拡張メソッドについては次回詳しく紹介します。

そして前回記事で書いた通り、コンストラクタに追加したパラメータ(20 行目)には DI コンテナの Unity からデータオブジェクトが自動でインジェクションされるので、アプリの StartUp メソッドで Unity に RegisterInstance したデータオブジェクトがこの VM 内で操作できるようになります。

ReactiveProperty は INotifyPropertyChanged インタフェースを継承しているため、本来は VM 自体が INotifyPropertyChanged を継承する必要はありませんが、ReactiveProperty を WPF で使用する場合(View とバインドする VM のプロパティに使用する場合)のみ親となるクラス(ここでは VM)が INotifyPropertyChanged インタフェースを継承していないとメモリーリークする可能性があるそうなので、INotifyPropertyChanged を実装した Prism の BindableBase を継承しています。
(参考:MVVMでメモリリークしちゃってました 原因と対策編 – かずきのBlog@hatena

又、ReactiveProperty の各クラスは IDisposable インタフェースを継承しているため、本来は VM の Dispose 時に個々 ReactiveProperty も Dispose する必要がありますが、AddTo 拡張メソッドで System.Reactive.Disposables.CompositeDisposable へインスタンスを追加しておくと CompositeDisposable を Dispose するだけで一括破棄してくれます。
(参考:ReactivePropertyの後始末
memo Window 終了時に VM の Dispose を呼び出す方法は、別の機会に紹介したいと思っています。

2019/10/26 追記
この記事を書いてから永らく放置状態でしたが、Livet が分割導入できるパッケージ がリリースされたことで Window の VM Dispose をスマートに呼び出すことが可能になりました。

又、Prism 7.2 で追加された IDestractible と同時に使用することで View(UserControl)の VM Dispose も簡単にできるようになりました。
Window と View(UserControl)の Dispose をサンプルアプリに組み込む方法を episode :18 で紹介しているので参考にしてみてください。



ObservableCollection を ReactiveCollection のソースに設定する

現時点のサンプルアプリは TreeView があるだけですが、完成すると下 fig.5 のような画面になる予定です。

fig.5 サンプルアプリの完成後イメージ

TreeView は機能メニューのような位置付けで、TreeViewItem を選択すると対応した画面が右側に表示される予定で、TreeView に表示する内容は前回エントリでも紹介した src. 5 のデータクラスから取得します。

但し、上の WpfTestAppData クラスは TreeView に表示したい構造と合っていないので、src. 6 の TreeViewItemCreator を間に挟んで TreeView へ表示する構造に合わせています。

見ての通りベタで木構造を作成して木構造のルートを返しているだけです。
サンプルでは TreeViewItemCreator を別クラスで作成していますが、別クラスにする必要は特に無いので VM へ直接記述しても構いません。

そして src. 7 のように VM のコンストラクタでバインドプロパティを設定しています。
(VM のコンストラクタ部分のみを抜粋)

TreeViewItemCreator で作成した木構造のルートを追加した ObservableCollection を バインドすると、TreeView は ItemsSource 内のメンバを木構造で表示してくれます。

但し、この時点では TreeViewItem の表示要素を何も設定していないので、実行しても TreeView には何も表示されませんが、今回はここまでとして、続きは次回のエントリに持ち越します。
又、いつも通り今回書いたソースも GitHub リポジトリ に上げておきます。

2020/9/4 追記
このエントリと同じ範囲の内容を再構成して新規連載中の .NET Core WPF Prism MVVM 入門 2020 Step: 6 で公開しました。Prism のデータバインディングサポートクラスで Model ⇔ VM 間を双方向でバインドする方法を紹介しています。

2020/9/13 追記
新規連載中の .NET Core WPF Prism MVVM 入門 2020 で ReactiveProperty の入門エントリの step: 7 を公開しました。Model ⇔ VM 間を ReactiveProperty で双方向バインドする方法を紹介しています。

 

次回記事「TreeView の MVVM には ReactiveProperty が埋まっている【episode: 5 WPF Prism】」

 

 

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください