MVVMの目的と要件は当初示されたものから大きく変わっていません。しかし、その実装の方法は、長年にわたり試行錯誤されてきました。実装の方法は、開発者によって考え方が異なります。そのため、初学者にとって、どれが標準的な実装の方法なのか、わからなくなります。そもそも、標準的な実装方法があるのかも怪しい感がします。
この記事では、論点になりがちな事柄や、誤ったMVVMの認識を検討します。
ただ、私も、MVVMについての様々な意見を読む中で、考えが変わることはあります。
まず、MVVMについて確認しておきましょう。
MVVMの要件
MVVMを理解するに書いた次の事柄を、MVVMの要件とします。
- Viewと、Modelとを分ける。
- 単に分けるだけでなく、ViewとModelとをつなぐViewModelを間に配置する。
- Modelは、ViewModelとViewにコード上は依存しない。
- ViewModelは、Viewにコード上は依存しない。
- View, ViewModel, Modelは、それぞれの役割に応じた処理にする。
「それぞれの役割」の捉え方が、開発者によって異なります。そのため実装の方法も異なります。それがMVVMの論点になりがちです。
MVVMの依存関係
View, ViewModel, Modelの依存関係を明確化したプロジェクトの構成は次のようになります。
- Viewが依存するプロジェクトは、ViewModel
- ViewModelが依存するプロジェクトは、Model
この構成により、ModelからView, ViewModelのコードを参照できず、ViewModelから View Model のコードを参照することはできません。
そして、
- Viewプロジェクトに配置するコードは、Viewの役割(責務)のためのコード
- ViewModelプロジェクトに配置するコードは、ViewModelの役割(責務)のためのコード
- Modelプロジェクトに配置するコードは、Modelの役割(責務)のためのコード
になるようにします。
もちろん、MVVMを実現するのに、3つのプロジェクトに必ず分ける必要はありません。
1つのプロジェクト内でも、Modelのコードが、View, ViewModelのコードを参照しないようにする、ViewModelのコードがModelのコードを参照しないようにする、というのを徹底すればOKです。
しかし、この原則を守ろうとしても、どうしても難しかったり、煩雑になりすぎたりする場合があります。そのようなケースでは、DI(依存性注入)でサービス化するなどの手法で解決します。
ViewにViewModel経由でModelのプロパティを利用して良いか?
私の考えではOKです。
私の方法では、ViewModelのクラスに、Modelのインスタンスを配置するのがセオリーです。
public class ViewModel1 : NotifyBase, IDisposable
{
public Model1 Model { get; }
そして、単純なプロパティなら、ViewのXAMLのバインディングで、Binding = Model.SomePropertyとします。
これをAIに確認してみました。AIが何を根拠としているのかわかりませんが、回答は納得のゆくものです。
技術的には問題なし
Viewの DataContext が ViewModel であり、その中に Model プロパティがあるなら、Binding Model.SomeProperty は正しく動作します。
MVVMの原則から見た位置づけ
- Viewは「ViewModelの公開プロパティ」にバインドしているだけで、直接Modelを参照しているわけではありません。
- ただし、ViewModelがModelをそのまま公開しているため、Modelの構造が透けて見える状態になります。
問題になりやすい点
- 疎結合の弱まり ViewがModelのプロパティ名を知ってしまうため、Modelの変更がUIに影響しやすくなる。
- UI変換・検証の難しさ 例えば日付フォーマットやバリデーションを挟みたい場合、ViewModelでラップしていないと制御しづらい。
- テスト性の低下 ViewModelだけをモック化してテストするのが難しく、Modelも必要になる。
つまり「Binding Model.SomeProperty」は MVVMの原則から見るとやや妥協的だが、実務では許容されるケースが多い という位置づけです。
私が補足すると、「ViewとModelとの疎結合が弱まる」ということです。
UIコントロールを、ViewModelで処理して良いか?
UIコントロールはViewなので、直接ViewModel, Modelで処理しない。という考え方があります。
この考え方にもとづく、UIコントロールの処理は、次のパターンがあります。
-
UIコントロールをViewのコードビハインドで処理する
例:ViewにUIコントロールの処理を書き、ViewModelから呼び出す(Actionなどの方法で) - UIコントロールの処理をサービス化(例:DIでサービスにする) して、ViewModel, Modelで利用する。
- ViewからViewModelにUIコントロールをインターフェイスとして渡し、 インターフェイスを通して操作する。
AIに確認すると
実務的推奨
原則:UIコントロールはViewの責務。ViewModelは操作しない。
例外的妥協:どうしてもViewModelからUIを操作したい場合、直接依存よりは「インターフェイス経由」の方がまだ良い。
推奨パターン:基本はコードビハインドやサービス化で責務分離を守る。
とのことです。
原則から考えると以上の方法が良いでしょう。しかし、以上の方法は、コードの量がそれなりに増えます。
私は、UIコントロールの持つプロパティの値をViewModelで取得するだけなら、煩雑にしたくないために、UIコントロール自体を引数にしてViewからViewModelに渡すことがあります。
の「方法1 Behaviorsを利用する」がそれに該当します。
ViewModelで、
(ComboBoxItem)e.OriginalSource
として、ComboBoxコントロール・ComboBoxItemコントロールを取得しています。
「方法2 Behaviorsを利用しない」も、UIコントロールを含むeを直接ViewModelに渡しています。
vm.ComboBox_GotFocus(e);
もっとも、これは、viewのコードビハインドで、eからUIコントロールを含まない値を取得し、その値を引数にして、vmに渡すことができます。こうした時は、ViewModelでUIコントロールを直接処理するわけではなくなります。
- UIコントロールの例:Button、TextBox
- MVVM pattern violation: MediaElement.Play()
- Why we don't use UI Elements in Viewmodel in MVVM Viewmodel Pattern
ViewのコードビハインドはOKか?
OKです。MVVMとして、問題ありません。
Viewに関する処理で、Viewのコードビハインドにするのが素直な構成なら、そうすれば良いです。
Viewのコードビハインドはできるだけしない、という方針もあります。それを否定するわけではありません。
つまり、XAMLのみでViewを構成する。こうするメリットもあり、デザイナーがXAMLのみで処理できるので、デザイナーと開発者との分業がしやすくなるらしいです。
View, ViewModelから、Modelのメソッドを呼び出し、返値を取得するのはOKか?
OKです。MVVMとして、問題ありません。
例えば次のViewのようなコードなら、
public partial class View1 : Window, IDisposable
{
private readonly ViewModel1 vm;
public View1(ViewModel1 viewModel)
{
InitializeComponent();
vm = viewModel;
DataContext = vm;
}
方法1 ViewModelのメソッドを呼び出し、そのViewModelのメソッドがModelのメソッドを呼び出す。
var result = vm.HOGEHOGE();
方法2 Viewから、ViewModelにあるModelプロパティを通してメソッド呼び出し
var result = vm.Model.HOGEHOGE();
のようになるでしょう。
ただ、2の方法は、MVVMとしてOKかどうかについて意見が分かれるかもしれません。
もっとも、Model → ViewModel → Viewへのデータの流れを、NotifyPropertyChangedとデータバインディング の仕組みでのみ行いたい、というのでしたら、返値は利用しません。ただし、返値を利用しないと、かなり回りくどい処理になります。返値を受け取るだけで簡潔な処理になるなら、そうするのが良いと考えます。
ViewとViewModel, ViewModelとModelは、疎結合か?
お互いに依存しないのが、疎結合の定義で良いはずです。
そのため、ViewはViewModelに依存し、ViewModelはModelに依存しているので、疎結合ではないとするのが、私の考えです。
AIによると、
- 疎結合であるという回答もあった
- 疎結合ではないという回答もあった
- View と ViewModelは、疎結合だが、ViewModel と Modelは、部分的疎結合という回答もあった。
と、回答が定まりません。
ViewとModelは、疎結合か?
ViewはViewModelに依存しますが、Modelに依存するのではないので、疎結合と言えるようです。
Viewから、Modelにあるメソッドを呼び出す時、ViewModel経由で呼び出すことを考えます。
「返値を取得するのはOKか」でも書きましたが、
- Viewが、ViewModelのメソッドを呼び出し、そのViewModelのメソッドがModelのメソッドを呼び出す。
なら、疎結合でしょう。ViewはViewModelのメソッドにしか依存していないので。 - しかし、次のようにした場合はどうでしょうか。
Viewから、var result = vm.Model.HOGEHOGE()
これが「ViewがModelに依存する」ものと考えるなら、疎結合にはなっていない、と言えるでしょう。
View, ViewModel, Modelは、1対1対1か?
違います。
1対10対100も、100対10対1にしても、MVVMとしてはOKです。
Model 100, ViewModel 100, View 1 になる例
-
ObservalCollectionで、各項目のModelが100個あり、それぞれをViewModelにする。しかし、表示する項目はViewは1つのリスト。
Model 1, ViewModel 1, View 1~N個 になる例
- Buttonクリックで、Window(View)を表示。Windowはいくつでも表示できる。ViewModelは、共通。
-
ObservalCollectionで、10個のModel, 10個のViewModelを使用。それらをリストのViewで表示する。リストで1つ選択したら、その行のViewModelを使って、詳細のViewで表示する。
複数のModel, 1つのViewModelの例
Model1, Model2を、ViewModelのコンストラクタで挿入
public class SomeViewModel()
{
public SomeModel1 Model1 { get; set; }
public SomeModel2 Model2 { get; set; }
public SomeViewModel(SomeModel1 someModel1, SomeModel2 someModel2)
{
Model1 = someModel1;
Model2 = someModel2;
}
}
もっとも、この例だと、Model1に、Model2を含めて、Model1のみViewModelに渡せば、ModelとViewModelは1対1になります。
WikiPediaのModel View ViewModelの解説には、
「一般には1つのModelを用意し、View全体をViewコンポーネント群へと分割、橋渡しとして各Viewに対してViewModelを設定する(1:N:N)。 」
と書かれています。実際、複数のModelを、1つのViewModelにコンストラクタで挿入する必要はあまりない感がします。
ModelにINotifyPropertyChangedを利用して良いか?
良いです。MVVMとして、問題ありません。
Modelのプロパティが値変更時に通知するのは、MVVMでは素直な方法と考えます。
Modelから通知が最適なケース
次は、Modelから通知するのが最適なケースです。
1つのModel。2つのViewModel(A,B)。 2つのView(A,B)。
AのViewで更新した値が、AのViewModelを経由して、Modelのプロパティの値が変更されたとします。
その値の更新が、BのViewで表示されるには、何らかの方法で、値の変更が、BのViewModelで分からないといけません。その何らかの方法の1つが、INotifyPropertyChangedによって、値の変更があったことをBのViewModelに通知する方法です。
Modelから通知しなくてもすむケース
もっとも、ModelにINotifyPropertyChangedを利用しない方針もあります。
ViewModelで、Modelの値の変更を完全に把握するなら、Modelから通知する必要はないです。
1つのModel。1つのViewModel 。1つのView(あるいは複数のViewで、それぞれのViewのDataContextは、1つのViewModelを共通に利用する)
の構成とします。
この構成で
- Viewのボタンを押した時、ViewModelのメソッドを実行する。
- ViewModelのメソッドは、Modelのメソッドを呼び出し、Model内でModelのプロパティを変更する。
- 続けて、ViewModelのメソッドは、Modelのプロパティの値を、ViewModelのプロパティにセット(同期する)する。
の処理の流れとします。この場合なら、Modelで、INotifyPropertyChangedを利用しなくてすみます。
とはいえ、このケースでも、Modelから通知して差し支えありません。私なら、パターン化しておく事柄だと考えますので、この場合もINotifyPropertyChangedを利用します。
備考
- 論点についてあちこち検索して調べて考えるより、AIの回答が要領よくまとまっていて気づきも多いです。とはいえ、やっかいな点は、AIの回答が完全に間違いとわかるのならまだましですが、ここは違うかもしれないと思ってもAIの回答で合っている可能性がある点です。
- 回答の根拠となる情報を求めると、自分の書いた記事が表示されることもあり、そんなこと言っているつもりはないのだが・・・となります。
- ViewModelの役割(責務)を明確にすると、ViewとModelの役割が自ずと定まります。 ところが、ViewModelの役割を明確にするのが難しいのです。何をして何をしてはいけないか? をコードで具体的に示すつもりで記事のメモを2023年8月から書いているのですが、なかなかまとまりません。
