イメージ図
説明に入る前に、WPFにおけるMVVMとはどんな感じなのかイメージ図を出しておきます。今回はDBと接続するアプリケーションを想定しています。
AIに頼んだらサクッと作ってくれました。すごいですね😎
MVVM で設計すると嬉しいこと
まず理解して欲しいのが MVVM で設計することでどのようなメリットがあるかです。
MVVMを語る上で大切なことなので先に説明しておきます。
1. UI(画面側の部品) と内部処理を分離できる
UI 側の処理とそこで扱うデータの管理を同じところで実装していると、保守性が高いコードとは言えません。
単純にどこで何をやっているかがわかりにくいからですね。
画面の描画用処理と内部処理を分けておけば、各々の役割が明確になります。
これはクラス設計の話にも繋がりますが、1クラスの役割は単一しておくのがセオリーです。
ファイルやクラスごとに明確に役割を分けておくことで、バグ解析が楽になります。
画面が期待通りに表示されないとき、View 側の実装が問題なのか、それ以外のロジックが問題なのかといった感じに問題の切り分けがしやすくなります。
また、ViewModel の設計が上手いことできていると、UI を変更するときにビジネスロジックをいじらなくても良いという利点もあります。
ここでは View - ViewModel 間に寄せた説明をしましたが、ViewModel - Model 間でも同じことが言えます。
まず、M - VM - M それぞれで役割をしっかり分かれているということが重要です。
2. 再利用性の向上
DB を利用するアプリケーションの場合、いろんな View や ViewModel から同じ DB アクセス処理を利用したいはずですよね?
それなのに各 ViewModel クラスでそれぞれDBアクセス処理を実装していると、メンテナンスがかなり大変になります。
新しい View を作りたいときにまた独自で実装しなければならないですし、そこで実装ミスが起きるとバグを埋め込むことになります。
再利用が可能なクラスとして定義しておくことでアプリケーション全体の堅牢性を高めることができます。一箇所にまとめておけば、使用する DB を変えたいときやテーブルの定義が変わったときも簡単に対応できますね。
実際にどう分けるか?
ここまででメリットは十分伝わりましたよね?
じゃあどのように分けていくべきなのか、実際に実装面の話に触れていきましょう。
View
View に関してはなんとなくイメージできると思います。
WPF でいえば xaml と xaml.cs(コードビハインドと呼ぶ)が View に該当します。
コードビハインドは xaml の部品と依存関係が強く、画面描画に対して色々なことができてしまうので一般的にはここに処理をたくさん書くのは推奨されません。
xaml に UI 部品を実装して、そこに反映させるデータの細かい処理は ViewModel に任せるべき。
コードビハインドでDataContext
に任意のクラスを指定すれば View と ViewModel を紐づけることができます。
例)MainWindowとMainWindowViewModelを紐づけたいとき
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel(); // ViewModelを関連付け
}
}
画面に描画する部品だけの実装と割り切って考えても良いでしょう。
(もちろん複雑な View の場合は例外もありますが...)
ViewModel / Model
ここの棲み分けは人によって認識が異なったりするので単純に定義するのは難しいです。
が、あえてひとことでいうなら
その画面(部品)だけに必要な処理 → ViewModel
アプリケーション全体(もしくは複数の画面)に必要な処理 → Model
でしょうか。
例えば、TODO管理アプリのTODO一覧を表示する画面で完了済みのタスクを分けて表示したいとします。
DB に登録されているTODOリストを一覧で取得する処理はアプリ全体で必要ですが、そこからデータを仕分けする作業はこの画面だけで必要になると思います。
👇 実装するとしたらこんな感じ
public class TodoListViewModel
{
// TODOをDBから取得するためのサービスクラス
private TodoService _todoService;
// 未完了のタスクリスト
public ObservableCollection<TodoItem> IncompleteTodos { get; set; }
// 完了済みのタスクリスト
public ObservableCollection<TodoItem> CompletedTodos { get; set; }
public TodoListViewModel()
{
// DBから一覧取得
// 例外発生の可能性があるので、本当はコンストラクタでDBアクセスしにいくのは×
this._todoService = new TodoService();
var allTodos = this._todoService.GetAllTodo();
// 未完了
this.IncompleteTodos = new ObservableCollection<TodoItem>(
allTodos.Where(item => !(item.IsCompleted)));
// 完了済み
this.CompletedTodos = new ObservableCollection<TodoItem>(
allTodos.Where(item => item.IsCompleted));
}
}
// TODO一個の情報を格納するクラス(Modelに分類したい)
public class TodoItem
{
// 他のプロパティは省略
// 完了済みのタスクか
public bool IsCompleted { get; set; }
}
TODOリストの取得をサポートするTodoService
とその先の DB アクセス処理、TODOの情報を保持しておくTodoItem
はアプリケーションのいろんなところから触りたいはずなので Model として分けておくべきでしょう。
ただ、そこから画面表示したい内容によってデータを加工して保持しておくのは ViewModel の役割です。
未完了のタスクを仕分けするのがこの画面だけであることを前提にしてますが、もし他の画面からも必要な処理ならば Model に存在するTodoService
に未完了か完了済みかで分けてタスクを取得する処理を移動することを検討してもいいかもしれません。(ここが難しいところですね)
ちなみに、データ取得だけでなくデータの追加等の処理も同じ考えで大丈夫です。
画面で入力した値を受け取って追加したい旨をリクエストするところまでが ViewModel 、その先の処理が Model に分類するという感じですね。
もし迷ったときは
「もしこの画面を削除したら、このコードは他で使われるか?」
YES → Model に書く
NO → ViewModel に書く
「もし画面デザインが大幅に変わったら、このコードは変更されるか?」
YES → ViewModel に書く
NO → Model に書く
画面への依存度で分けるのが良いかと🙆
実装時の注意点
「分け方はわかった。よし!いざ実装するか!」
という方、ちょっと待ってください。コードを書くときの注意点もあります。
各レイヤーの参照方向に注意してください。
基本的な参照方向の原則は View → ViewModel → Model です。
NG例1: Model から ViewModel の参照
// Model に存在するサービスクラス
public class TodoService
{
// DBアクセス処理を担うアクセサー
private DbAccessor _accessor
// ❌ ModelがViewModelを参照
private TodoListViewModel _viewModel;
// DBからTODO全リストを取得するメソッド
public void GetAllTodo()
{
// クエリ実行
string query = "SELECT * FROM TODO";
var todos = this._dbAccessor.ExecuteQuery(query);
// ❌ ModelからViewModelを直接操作
_viewModel.RefreshList(todos);
}
}
問題点
-
TodoService
が他の ViewModel から利用できなくなる
Model はアプリケーション全体で利用する処理なので、他のレイヤの影響を受けたくないです。
Model からは ViewModel も View も参照しないように注意です。
NG例2: ViewModel から View の参照
// ViewModel
public class TodoListViewModel
{
// ❌ ViewModelがViewを直接参照
private TodoListView _view;
// エラーメッセージを表示するメソッド
public void ShowError(string message)
{
// ❌ ViewModelからViewを直接操作
_view.ShowMessageBox(message);
}
}
問題点
-
TodoListViewModel
が他の View から使えなくなる(他の View から ViewModelを紐づけたいケースはあまりない気がするが...) - View 変更時に ViewModel の修正が必要になる可能性大
ViewModel から View 側の処理を呼び出さないように注意です。
レイヤごとの参照方向を一方向に保っておけば、各々のレイヤが疎結合になって保守性がUPします。
最後に
ここまで理想論を述べてきましたが、実際はそこまで上手くいかないことも多いです。
プロジェクトの既存コードは UI の変更に伴って ViewModel の変更をすることもありますし、 ViewModel が巨大になりすぎてたくさんの役割を持ってしまっていることも多々あります。
もちろんベストは厳格にMVVMに則って実装すべきですが、既存コードと向き合ってその時々でベターな実装ができるエンジニアになりたいですね💪