.NET開発者中心 厳選ブログ記事

MVVMパターンの常識 ― 「M」「V」「VM」の役割とは?

尾上 雅則
2011/05/18

ViewModel

 一般的にViewModelは、C#などの汎用プログラミング言語で記述され、プレゼンテーション・ロジックとステート(=状態)を持ちます。ドメイン・エンティティをViewに表示できるように整形したり、ドメイン・ロジックが公開するメソッドを操作として公開したりする責務を持ちます。

 Modelの公開するステートをいちいちラップしてViewに公開するのもViewModelの役目です。

 また、Viewへアクションの指示を行うためのメッセージ・イベントを発行します(詳しくは後述するMessengerで説明)。

 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インターフェイス(=INotifyDataErrorInfoインターフェイスはWPF 4にはないのでSilverlight専用)の実装として行います。

 ViewModelはViewにバインドされ、そのプロパティをViewに公開しています。ViewModelで、プロパティ値の変更があった場合、INotifyPropertyChangedインターフェイスの実装を通じて、Viewに値の変更を通知してやる必要があります。

 コレクションのバインドは、コレクション・コントロールのItemsSourceプロパティに、INotifyCollectionChangedインターフェイスの実装であるコレクションを設定することで行います。通常、ObservableCollection<T>オブジェクトを使用します。

 ビジネス・ドメインに属するステートをViewModelに置くのはやめましょう。それはModelの責務です。Webシステムにおける3層構造に慣れた方にありがちなことですが、ViewModelにビジネス・ドメインに属するステートを置くことは、MVVMパターンの本来の目的である、「ドメイン・ロジックとプレゼンテーション・ロジックの分離」という考え方に反します。

 通常、最低1つの画面に1つのViewModelが必要で、コレクション・ビュー(=ListBoxやTreeViewなど)の項目ごとに操作があるなら、それの1項目ごと用のViewModelも必要です(操作がなくても普通は作ります)。大抵、コレクション・ビューの各項目は操作を持つし、表示方式をModelのものから変えて表示したい場合が多いからです。

コマンドを使用する理由

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

 通常、コマンドは実行したいメソッドと、その操作が実行可能かを判断するメソッドを含めて公開されます。実行可否状態を判断するメソッドが(戻り値として)「false」を返している場合、そのコマンドにバインドされているボタンなどは操作できなくなります。

 標準ライブラリにMVVM用コマンドの実装は含まれていないため、MVVM補助ライブラリ(=LivetMVVM Light Toolkit, Prismなど)を使用することになると思いますが、「DelegateCommand」か「RelayCommand」という名前で提供されていることがほとんどです。下記のコードは、コマンドの実装例です(C#)。

 Buttonコントロールなど、最初からCommandプロパティが存在するもの以外でコマンドを使用したい場合は、Expression Blend SDKのEventTrigger(トリガ−)とInvokeCommandAction(アクション)を使用するのが普通です。

 コマンドが上記の2つのメソッドをラップすることは、ViewModelの責務を考えるうえで重要なことです。もし2つのメソッド(=実行したいメソッドと、実行可否を表すメソッド)を直接、Viewにバインドできたらどうなるでしょうか?

 その場合、下記のコード例のように、操作の最終的な実行可非制御がViewに移動してしまいます。

 コマンドの存在は、ViewとViewModelのどちらに操作が属するのかの考え方を表した実装であるようにも見えます。

 また、コマンドという粒度で操作が存在することは、ViewModelがViewに公開するべき操作の粒度を担保しているようにも思えてなりません。

 コマンドは「メソッド」の公開ではなく、まさに「操作」の公開なのです。

 「コマンドを作るのが面倒だ」という話は聞きますが、複数のメソッドをカプセル化して意味を与え、そしてViewに対して操作を公開するという責務を明確に表すコマンドは非常に重要な実装です。

 面倒なら、コード・スニペットなり何なりで対応しましょう。設計パターンで面倒な実装にぶち当たった場合、それをなくしてしまおうという発想は大抵、間違いです。意味がなければ誰もそんな実装の提案はしないのです。

 面倒な実装は、簡略化したり、意識させないようなインフラストラクチャを整えたりなど、そういった方向に努力されるべきです。

 Silverlight 5ではMVVMパターンのサポートとしてメソッドを直接、バインドできる仕組みが導入されるそうです。そういった機構の導入は、すでにしっかりとMVVMパターンを理解している人にはともかく、そうでない人のパターンの順守を破壊してしまうリスクがあり、基本的に反対しています。以下の記事に詳細に書いてありますのでご確認ください(2011年4月14日に開催されたMIX11で、Silverlight 5ベータが公開されましたが、結局、MVVMパターンのサポートは含まれていません)。

Messengerを使用する理由

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

 ViewModelからViewにダイアログの表示や画面遷移の指示を出したい場合に、ViewModelはMessengerからメッセージ・イベントを発行します。ViewはViewModelのMessengerをあらかじめ監視していて、発行されたメッセージに対応したアクションを行えるという仕組みです。

 Messengerを使用した仕組みがMVVMで採用されるのはなぜでしょうか?

 それは、Messengerの仕組みが本質にデータ・バインディング機構と同じものだからです。

 データ・バインディング機構はViewModelからViewへの値の更新通知をイベントで行います。Viewに恒常的に表示している情報であれば、ViewModelにすでにステート(=プロパティ値)として値が存在するので、データ・バインディングの仕組みを利用してViewに変更通知を行うわけです。

 しかし、ダイアログや画面遷移の場合はどうでしょうか?

 本質的にダイアログや画面遷移は揮発性の現象です(例えばダイアログを閉じると、そのダイアログ上の情報はすべて消失すべきです)。だから、表示するための情報を常にViewModelにステートとして持っているのは、自然で理解しやすい実装だとはいえません。

 一例として、バックグラウンド・スレッドで発生したエラーをViewに通知するシナリオを考えてみましょう。

 バックグラウンド・スレッドでエラーが発生したことをViewに通知したいときに、あらかじめViewModelが対応するプロパティを持っていたとします。エラーが発生した場合は、そのプロパティの更新を通してViewへ通知を行うという形を考えてみます。この形の実装ですと、Viewがエラー発生の通知を受け取った後は、ViewModelの対応するプロパティの値を元に戻さなければなりません。また、複数のバックグラウンド・スレッドが存在していた場合はどうでしょうか? バックグラウンド・スレッドの数に対応するだけのプロパティをViewModel上に用意するのでしょうか? こういった実装では、ViewModelの実装は非常に複雑で開発も保守も困難なものになってしまいます。そもそも極めて不自然です。

 では、ダイアログや画面遷移を行うためのMVVMパターン的な解決策はどうすればよいでしょうか? MVVMパターンではViewModelからViewへの通知は、データ・バインディング機構、つまりイベントによって行われます。しかし、ViewModelにステートを持っている形は上記のとおり、不自然でかつ困難な実装を強いられてしまいます。答えはシンプルです。ViewModel上に、Viewへの揮発性の現象をイベントによって通知する専用のオブジェクトがあればよいのです。Viewはそのオブジェクトをデータ・バインディングして監視する機構を持っていればよいのです。そしてそれこそがMessengerです。

 MessengerはViewへイベントを発行するだけのオブジェクトで、ViewModel上にインスタンスとして存在します。ViewModelからViewへ通知を行いたい場合、ViewModelは自インスタンス上のMessengerを通してイベント発行という形でViewへ通知を行います。このアプローチの優れたところは、ViewModelは何らViewでの実装方法を規定していない点です。上記の例でいえば、バックグラウンド・スレッドでエラーが発生した場合、ViewModelはMessengerを通じてイベント発行を行うだけです。あらかじめViewModelのMessengerを監視しているViewが知り得るのは、あくまでもバックグラウンド・スレッドでエラーが起こったことだけです。Viewは通知を受け取ったうえで、その通知を無視することもできるし(推奨はしません)、ダイアログを表示する実装を使用してエンド・ユーザーにダイアログとしてエラーを知らせることもできるし、あるいはバルーンとして通知することもできるのです。

 この実装が、Viewに定義したインターフェイスをViewModelが呼び出す形より優れているのは明確です。

 まず、Viewに対応する実装が存在する必要がありません。インターフェイスなら、Viewには必ずそのインターフェイスが実装されていなければなりません。

 また、Viewがインターフェイスを実装する際はViewがコードビハインドを使用しなければなりません。Viewの項で説明した、コードビハインドを使用しないメリットが必ず破壊されてしまいます。Messenger方式ではViewは、ViewModelからの通知を監視するための専用のトリガーを用意します。トリガーはアクションとセットなので、Viewはコードビハインドを使用せずとも、ViewModelから発行されたイベントに対応したアクションを実行できるというわけです。

 この一連の流れをまとめたのが次の図です。

 Messenger方式は、インターフェイスによるMVP的な実装よりもスマートな実装方式として誕生したわけではないことに留意しましょう。Messengerの発想はただ、1つの設計パターンとして責務同士の対話方法を統一しただけです。MVVMパターンでは、ViewとViewModelの対話はデータ・バインディング機構によって行われます。つまりはリフレクションと各種Descriptor、そしてイベントによって対話が行われます。Messenger方式は、結局はデータ・バインディング機構の思想とまったく同じものです。そして、付随的にインターフェイス方式より優れたメリットが生まれてきたわけです。

 スマートだから、とかいう適当な理由でMessengerが使用されるわけじゃないんですよ。

 ビヘイビア、トリガー、アクション、Messengerの関係については、以下の記事に詳しく書いています。詳細については以下の記事をぜひご確認ください。

 次にModelを説明します。


 INDEX
  .NET開発者中心 厳選ブログ記事
  MVVMパターンの常識 ― 「M」「V」「VM」の役割とは?
    1.MVVMパターン概要
    2.View
  3.ViewModel
    4.Model
    5.デザイナーと開発者の分業/MVVMパターンを適用・カスタマイズするときの留意点

インデックス・ページヘ  「.NET開発者中心 厳選ブログ記事」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH