WPFアプリのMVVM構造の設計時に留意すべきこと

WPFアプリケーションでMVVMで実装するサンプルはネット上に数多くあるが、その多くは

  • ウィンドウ 1つ(MainWindow.xaml
  • ビューモデルクラス 1つ(MainWindowViewModel.cs)
  • モデルクラス 1つ(MainWindowModel.cs)

といった簡単な構成になっており、こういった情報だけを読んでWPFアプリケーションを書いたからか、ビューモデルクラスとモデルクラスが膨れ上がって巨大になり、さらにはレイヤ化アーキテクチャ何それという感じの、大変残念な状態になった製品コードを仕事で見てきた。今回は、WPFアプリを設計する時に、特にMVVMに関連して留意すべきことを列挙する。

MVVMはビューのデザインパターンである

MVVMはアプリケーションのアーキテクチャではない。ビューのデザインパターンである。ビューの関心事はビュー層とビューモデル層に、それ以外はモデル層に入れる。ビューをMVVMパターンで設計すると決めたとしても、アプリケーションのアーキテクチャはMVVMとは別に考えなければいけない。

例えばアプリケーションをヘキサゴナルアーキテクチャで設計するなら、MVVMのビュー層はUIのポートでビューモデル層はそのアダプタ、モデル層はそれ以外すべてに対応する。誤解しがちなポイントだが、いわゆるアプリケーション層はMVVMではビューモデル層ではなくモデル層に属する。アプリケーション層のアプリケーションサービスが公開している操作を、ビューモデル層がコマンドとしてビューへ公開する形をとる。UI以外にAPIも併せて公開することを想定すれば納得できるはずだ。

参考 cactuaroid.hatenablog.com

DataContextを薄くする

DataContextはビューからのBindingするためにある*1。ビューモデルクラスをビューのDataContextとするが、そのクラスにビューからのBindingに関わらない機能を持たせてはいけない。DataContextは薄く、単純にする。ユニットテストを書きたいと思ったら、その部分をモデル層へ移動できないか検討する。

1つのUserControlに対して1つのDataContextクラスを作ることを検討する

UserControlには2つの使い方がある。

  • 意味のあるまとまりでWindowの領域をUserControlにまとめる
  • 独自コントロールをUserControlとして作る

1つ目の使い方では、1つのUserControlに対して1つのDataContextクラスをビューモデル層に作り、UserControl.DataContextに設定することを検討する。

参考 stackoverflow.com

ただしUserControl.DataContextに設定すると、UserControlの親のXAMLでUserControlのプロパティへBindingする際に以下の記事で述べられている挙動になることを理解しておくこと。

var.blog.jp

1つ目の使い方であればUserControlへ親要素のDataContextから何かをBindingすること自体があまりなく、必要ならビューモデル層でやり取りするほうが良いため、UserControl.DataContextに設定して何も問題ないだろう。

2つ目の使い方では、そのUserControlを使う際にXAMLをどう書きたいかを考えて支障のないように設計する必要がある。例えばCheckBoxを複数束ねてenumで状態を表すUserControlであれば、CheckBoxのようにCheckBox.IsCheckedにboolプロパティをBindingして使うように、enumプロパティをBindingして使うような使い勝手を期待されるはずだ。それなのに内部で勝手にDataContextを書き換えてしまっていると問題になる。

データの流れを設計する

サンプル実装ではTwoWayのBindingが使われていることが多い。単純な要件であればTwoWayは便利だ。しかし、TwoWayは操作と表示を同一コントロールで扱うことになるため、少し込み入った仕様になると途端に扱いづらくなる。

例えば動的に変化する信号のON/OFF状態がToggleButtonで表現されており、ユーザはToggleButtonを押下することで信号を上書きすることができるがシステムの状態によっては上書き失敗することもあるとする。こういう場合には操作と表示をきっちり分けてしまえばシンプルに作れる。CQRSにも通じるが、操作によりビューモデル層のコマンドを呼び、モデル層で処理され、その結果で画面が更新されるようにデータの流れを設計するわけだ。この場合、ToggleButtonの選択状態は画面操作だけで変化しないようにし、OneWayでBindingする。

データの流れを設計せずに行き当たりばったりで実装してはいけない。

Converterで単純な変換以外のことをしない

Bindingする際に変換をかませられるConverterは便利だが、容易に変換以外のロジックを持たせてしまうことが可能なので気を付ける必要がある。ただし変換であってもユニットテストしたくなるようなロジックであればConverterに実装するのは不適切だ。そのロジックのいるべき場所はConverterの中ではない。

モデル層のクラスに「~Model」と名付けない

ビューに関わらないすべてのクラスはモデル層に属する。サンプルで「~Model」と名付けられているものが多いのは、あくまでも説明の便宜のためである。「~Model」という名前だと特に何も意味しないので、色々な責務を持たせてしまって膨れ上がる原因となる。小規模なアプリケーションでモデル層を単純にフラットに設計するとしても、避けた方が良い。

一方、DataContext用のビューモデルクラスの場合、「<Window名またはUserControl名>+ViewModel」という名前は1:1の関係が分かりやすいため悪くないだろう。「~DataContext」の方が良いかもしれない。

レイヤごとにプロジェクト・フォルダを分ける

MVVMというよりレイヤ化アーキテクチャの話である。レイヤ化アーキテクチャでは上から下へ依存する構造にするが、これを確実に守るためにレイヤごとにプロジェクトを分けてしまう手がある。プロジェクト間で相互参照はできないため、レイヤごとに分けることでレイヤ間で一方向の依存になるように強制できる。

そこまで厳密にしたくなければ単一プロジェクト内でフォルダ分け(名前空間分け)してもよい。ビュー層とビューモデル層は密接に関連するため自由度を重視して1プロジェクトにまとめても良いだろうが、少なくともモデル層がビュー層・ビューモデル層に依存する状況を確実に避けるために、これらは別プロジェクトに分離することをお勧めする。

なお、レイヤごとにプロジェクトを分けるとレイヤ間の依存方向が強制されるため、IoC(制御の反転)を適用する際にDI(依存性注入)するにはDIコンテナを使うべきだろう。

*1:

Data context is a concept that allows elements to inherit information from their parent elements about the data source that is used for binding

FrameworkElement.DataContext Property (System.Windows) | Microsoft Docs