WPF MVVMパターン-M・V・VMの役割

MVVMパターンの概要

ビジネス・ロジックとプレゼンテーション・ロジックを分割する」という目的とともに、開発者がUIデザイナーと協業でアプリケーションを作っていく未来を見据えたパターンでもあります。

  • ViewはViewModelに依存し、ViewModelはModelに依存します。逆方向の依存はありません。
  • View
    • UI
    • UI Logic (Code Behind)
    • Viewは、UIの外観と構造を定義し、幾分かのプレゼンテーション・ロジックも含みます。
  • ViewModel
    • Presentation Logic
    • ViewModelは、プレゼンテーション・ロジックとステート(=状態)を含みます。
    • VM → V
      • Notification
    • VM ⇔ V
      • DataBinding
      • Commands
  • Model
    • Business Logic
    • Data
    • Modelは、ドメイン・ロジックそのものです。

View

XAMLで記述され、UIの外観と構造を定義します。基本UIのコードビハインドには、初期のInitializeComponentメソッド以外何も記述されているべきではありません。

  • コードビハインドを書かないメリット
    • パターンの崩壊を防ぎやすい点
    • Viewを宣言的構文であるXAMLコードだけで記述できるという点
    • ビヘイビア、トリガー、アクションを駆使することで、コードビハインド無しでViewを作成することが可能
  • ViewModelとの対話 ― データ・バインディング
    • Viewは、データ・バインディング機構を使ってViewModelの各プロパティの値を表示します。
      • 通常、ViewのDataContextプロパティにViewModelのインスタンスを設定することで、ViewはViewModelとバインドします。
      • ViewからViewModelの情報を取得するのは、内部的にはリフレクションや各種Descriptor(=TypeDescriptionProviderクラスなど)で行われています。
      • ViewModelからの変更通知イベントを受け取ることで、Viewの値は更新されます。
    • ダイアログの表示や、画面遷移の指示などの、ViewModelのプロパティとして表現されるべきでない情報は、ViewにバインドされたViewModelのMessengerからメッセージ・イベントを受け取ることでViewのメッセージ・トリガーが処理します。
    • 大事なことはメッセージ・イベントを受け取ってダイアログや画面遷移を行うことは、データ・バインディング機構の考え方と酷似している、あるいはほとんど同一であることをきちんと認識すること。
    • ViewはViewModelとの対話をすべてデータ・バインディングで処理します。
  • ビヘイビア、トリガー、アクション
    • XAMLだけでは足りない機能は、別途、ビヘイビア、トリガー、アクションを作成して対処します。
    • ビヘイビアというのは、特定のコントロールのコードビハインドをXAMLコードで再利用可能なように外出ししたものです。
      • ViewModelとの通信などは行いません。
      • あるコントロールの宣言に外部からアタッチできるWPFのプロパティ・システムの特徴を使って実現されています。
      • コマンドに対応していないコントロールをコマンドに対応させるビヘイビアや、オブジェクトの移動に自動的にきれいなアニメーションを与えるビヘイビアなど、多種多様なものがあります。
    • トリガーは、任意の条件で起動してアクションを実行するためのものです。
      • ViewModelのプロパティの変化を検知して起動するトリガーや、タイマーなトリガー、イベント・トリガー、ViewModelからメッセージを受け取って起動するトリガーなど、いろいろあります。
    • アクションは、トリガーとセットで使われるアクションです。
      • ダイアログを表示したり、特定のストーリーボードを起動したり、ViewModelのプロパティに値をセットしたり、はたまた画面遷移したり、いろいろできます。
    • トリガーとアクションについては、Expression Blend SDK(WPF版)/同(Siliverlight版)や各種MVVMインフラストラクチャ(例えばLivet、MVVM Light Toolkit, Prism)で用意されているもので対応するのが望ましいです。
    • ビヘイビアもトリガーもアクションも、作成はC#などの汎用プログラミング言語で行いますが、使用する際はXAMLコードの既存のコントロールの適切なプロパティに設定するだけです。
    • ビヘイビアも、トリガーも、アクションも、Expression Blend SDKや各種MVVMインフラを使用しないと、ほとんど標準で用意されていないうえに、標準で用意されているもの利用用途が非常に限定的で実用できるようなものではないので注意が必要です。

ViewModel

一般的にViewModelは、C#などの汎用プログラミング言語で記述され、プレゼンテーション・ロジックとステート(=状態)を持ちます。

ViewModel の役目と責務
  • ドメイン・エンティティをViewに表示できるように整形したり、ドメイン・ロジックが公開するメソッドを操作として公開したりする責務を持ちます。
  • Modelの公開するステートをいちいちラップしてViewに公開するのもViewModelの役目です。
  • また、Viewへアクションの指示を行うためのメッセージ・イベントを発行します。
  • ViewModelは、Viewへの参照を持ったり、Viewの特定の実装を意識したりしません。
    • しかしViewModelはまったくViewを意識しないというわけでもないので注意が必要です。
    • 「ViewModelはViewを意識しますが、その実装について何も知らなくてもよいし、知るべきではない」という認識が妥当です。
    • ViewModelのプロパティは、特定のコントロールにさえ結合しなければ表示専用のものであってよいのです。
  • ViewとViewModelでプレゼンテーション・ロジックを分担します。
    • ViewModelはUI要素と結合しないプレゼンテーション・ロジックを担当し、ViewはUI要素に結合したプレゼンテーション・ロジックを担当します。
    • 具体的にはSystem.Windows名前空間ではじまる列挙体・構造体以外の型のプロパティはViewModelでは所持しないなどの切り分けが可能です。
  • Modelにビジネス・レベルの入力値検証は含まれますが、それとは別にViewModelは当然、入力値検証機能を持ちます。
    • ViewModelはModelの入力値検証結果をラップすることで、Viewに入力値検証の結果を公開したり、ViewModelで新たに定義した表示専用/入力専用のプロパティの検証結果を公開したりします。
    • 入力値検証機能はDataAnnotations(=System.ComponentModel.DataAnnotations名前空間)やIDataErrorInfoインターフェイス(System.ComponentModel名前空間)、あるいはINotifyDataErrorInfoインターフェイスの実装として行います。
  • ViewModelはViewにバインドされ、そのプロパティをViewに公開しています。
    • ViewModelで、プロパティ値の変更があった場合、INotifyPropertyChangedインターフェイスの実装を通じて、Viewに値の変更を通知してやる必要があります。
    • コレクションのバインドは、コレクション・コントロールのItemsSourceプロパティに、INotifyCollectionChangedインターフェイスの実装であるコレクションを設定することで行います。
    • 通常、ObservableCollectionオブジェクトを使用します。
  • ビジネス・ドメインに属するステートをViewModelに置くのはやめましょう。
    • それはModelの責務です。
    • ViewModelにビジネス・ドメインに属するステートを置くことは、MVVMパターンの本来の目的である、「ドメイン・ロジックとプレゼンテーション・ロジックの分離」という考え方に反します。
  • 通常、最低1つの画面に1つのViewModelが必要で、コレクション・ビュー(=ListBoxやTreeViewなど)の項目ごとに操作があるなら、それの1項目ごと用のViewModelも必要です。
    • 操作がなくても普通は作ります。
    • 大抵、コレクション・ビューの各項目は操作を持つし、表示方式をModelのものから変えて表示したい場合が多いからです。
コマンドを使用する理由

コマンドとは、Viewに公開する操作のことです。ICommandインターフェイスの実装であるコマンドは、ViewModelのプロパティとして公開されます。データ・バインディング経由でViewから操作されます。

  • 通常、コマンドは実行したいメソッドと、その操作が実行可能かを判断するメソッドを含めて公開されます。
    • 実行可否状態を判断するメソッドが(戻り値として)「false」を返している場合、そのコマンドにバインドされているボタンなどは操作できなくなります。
    • コマンドが上記の2つのメソッドをラップすることは、ViewModelの責務を考えるうえで重要なことです。
    • 標準ライブラリにMVVM用コマンドの実装は含まれていないため、MVVM補助ライブラリ(=Livet、MVVM Light Toolkit, Prismなど)を使用することになると思いますが、「DelegateCommand」か「RelayCommand」という名前で提供されていることがほとんどです。
    • Buttonコントロールなど、最初からCommandプロパティが存在するもの以外でコマンドを使用したい場合は、Expression Blend SDKのEventTrigger(トリガ-)とInvokeCommandAction(アクション)を使用するのが普通です。
  • コマンドは「メソッド」の公開ではなく、まさに「操作」の公開なのです。
    • 複数のメソッドをカプセル化して意味を与え、そしてViewに対して操作を公開するという責務を明確に表すコマンドは非常に重要な実装です。
    • 「コマンドを作るのが面倒だ」という話は聞きますが、面倒な実装は、簡略化したり、意識させないようなインフラストラクチャを整えたりなど、そういった方向に努力されるべきです。
Messengerを使用する理由

Messengerとは、データ・バインディング機構の応用によってViewModelからViewを操作する機構のことです。

  • ViewModelからViewにダイアログの表示や画面遷移の指示を出したい場合に、ViewModelはMessengerからメッセージ・イベントを発行します。
  • iewはViewModelのMessengerをあらかじめ監視していて、発行されたメッセージに対応したアクションを行えるという仕組みです。
  • Messengerの仕組みが本質にデータ・バインディング機構と同じものです。
    • データ・バインディング機構はViewModelからViewへの値の更新通知をイベントで行います。
      • Viewに恒常的に表示している情報であれば、ViewModelにすでにステート(=プロパティ値)として値が存在するので、データ・バインディングの仕組みを利用してViewに変更通知を行うわけです。
    • ダイアログや画面遷移は本質的に揮発性の現象で、にViewModelにステートとして持っているのは、自然ではありません。
      • ViewModel上に、Viewへの揮発性の現象をイベントによって通知する専用のオブジェクト-Messengerで対応します。
    • MessengerはViewへイベントを発行するだけのオブジェクトで、ViewModel上にインスタンスとして存在します。
      • ViewModelからViewへ通知を行いたい場合、ViewModelは自インスタンス上のMessengerを通してイベント発行という形でViewへ通知を行います。
  • Messenger方式ではViewは、ViewModelからの通知を監視するための専用のトリガーを用意します。
    • トリガーはアクションとセットなので、Viewはコードビハインドを使用せずとも、ViewModelから発行されたイベントに対応したアクションを実行できるというわけです。

Model

Modelは、C#などの汎用プログラミング言語で記述され、ドメイン・ロジックとデータ、つまりはビジネス・ロジックとビジネス・ドメインのステートを持ちます。

Modelについてよくある誤解 ― ステートレス、サーバ側がModel、永続化層など
  • MVVMパターンでのModelはステートフルです。
  • サーバ側に何があろうと、基本的にリッチ・クライアント側にもビジネス・ドメインの情報は存在するはずです。
  • Modelを何層に分けたって、それはModelには変わりありません。
Modelの持つべき実装
  • Modelはステートフルであるべきです。
    • Modelがステートフルであれば、Model操作の結果はModelのステートのプロパティ変更という結果をもたらす場合が増えます。
  • Modelは変更通知機能を持ちます。
    • ViewModelと同じように、INotifyPropertyChangedインターフェイスの実装として行われます。
  • Modelは入力値検証機能も持ちます。
    • 例えばRDBMSを使用していれば、データベースに格納可能な値は事前に決まっています。UIがなくとも格納可能な値は決まっていることが多いでしょう。そういった入力値の検証はModelで行います。
    • ViewModelは、それをそのままViewに伝えればよいのです。
      • IDataErrorInfoインターフェイスを実装するのですが、素直に実装するとエラー表示用文字列がModelに含まれてしまいます。
      • リソース・ファイルを使用して、リソース・キーだけをModelの実装に含めるのを推奨します。
ドメイン・ロジック・パターン

ドメイン・ロジック・パターンとしては、大きく下記の2つが挙げられます。

  • トランザクションション・スクリプト・パターン

    • ユース・ケース単位、あるいはユース・ケース・グループ単位でエンティティを切り、そのエンティティは表示状態と大きく乖離(かいり)しない
    • 日本のWebシステム系開発では、ほとんどの場合、トランザクション・スクリプト・パターンが用いられる
  • ドメイン・モデル・パターン

    • ビジネス・ドメインと深く密着し、エンティティをビジネス・ドメインの分析から定義し、エンティティに操作の責務を持たせる
    • ビジネスを意味的に分析し、エンティティを定義します。
    • エンティティが操作を持つことが重要です。
    • そして意味的な分析から生まれたModelは、イベントを使ってお互いに通知し合います。
    • また、イベントを使ってドメイン・モデル間が通信し合うことは、自身が状態を持ち、ほかのエンティティとの関連によって自身のステートが変化していく形と非常に相性がよいのです。
    • ドメイン・モデルに属さないと思われる操作は、別途、サービス層を作って対処します。
      • ドメイン・モデルのサービス層は、あくまでもドメイン・モデルに含まれない操作だけを行うところです。
      • ドメイン・モデルに対して行える操作は、極力、ドメイン・モデルで実装します。

MVVMパターンを適用・カスタマイズするときの留意点

  • リッチ・クライアントは基本的にステートフルです。
  • MVVMパターンの責務分割方式上、ViewのオブジェクトよりViewModelのオブジェクトの方が長く生存することが多く、またViewModelのオブジェクトよりModelのオブジェクトの方が長く生存することが多くなってきます。
  • どちらも寿命が長いオブジェクトがイベント発行元となり、寿命が短いオブジェクトがイベント受信側となるということになります。
    • 寿命が短いオブジェクト側がイベント受信側となることは、そのままメモリ・リークのリスクが高いことを指します。
      • ViewModelのイベントをViewが購読する際、Viewがイベントの購読開始と解除に責任を持つことができればよいのですが、コレクション・コントロールの各項目など、WPF/SilverlightではViewのコントロールの生存期間は完全に開発者が制御できるようにはなっていません(不可能というより、面倒なコーディングを強いられます)。
      • Messenger+トリガー・アクションなどを使用してViewModelとViewの間の対話方法をデータ・バインディングのみに制限すれば、データ・バインディング機構の中で隠ぺいされた形でWeakEventパターンが適用されメモリ・リークは発生しません。
      • 同じく、ViewModelとModelのあいだのメモリ・リーク問題もあります。
        • Modelのイベントを単純にViewModelが購読しようとした場合、ViewModelが先に消滅しようとした際にメモリ・リークが発生します。
        • ウィンドウなどが対応するViewModelをDataContextプロパティとして持つことが多いため、ウィンドウを閉じる処理などに伴って、この問題に悩まされることがあります。
        • この場合も、ViewModel自身がModelのイベントの購読開始と自身の破棄のタイミングでの購読解除をしっかり管理すればよい話ですが、やはり非常に面倒なコーディングを強いられます。
        • MVVMインフラストラクチャ(例えばLivetなど)では、こういった場合の対策として、イベント代替の手段がしっかりと用意されています。