MVVMおぼえがき

自分の知ってるMVVMについての知識をまとめるつもりが、なんだか変な方向に行きました。

あと、以下に書いてある内容は正しいとは限りません。あくまで僕の考え方を自分の為にまとめたものです。プログラムの組み方なんて星の数ほどあるじゃないですかという。

WPFを使ってると、どうしても「Model-View-ViewModel パターン」、通称「MVVMパターン」を採用せざるを得なくなります。このMVVMパターン、定義は人によっていろいろなんですが、今回はただ単にViewとViewModelが存在する状態ってことにしといてください。そんなんMVVMじゃねーよって人はぜひ定義を教えてください。

MVVMパターンでプログラムを作成されたことがある人なら分かると思いますが、何も無しでMVVMパターンを実践しようとすると死にます。普通に死にます。大切な事なので二回言いました。それほどまでに正しく実装するのが割と難しいんですが、幸いなことに色々なライブラリやインフラストラクチャがリリースされています。たとえば、MVVM LightやPrismは有名どころですね。あとは最近僕が使ってるやつですが、Livetっていうのもあります。探せばたぶんいっぱい出てくると思います。

まぁインフラは使いましょう。問題はそこから、WinFormsやWPF(Without MVVM)でやってたことをどうやればいいのっていう。

Why MVVM?

なぜ今までのUIモデルを捨て、MVVMパターンを採用するのでしょう。答えは簡単、そのほうが作りやすいからです。少々回りくどくはなりますが、バグが少なくパフォーマンスが高くメンテナンスしやすいプログラムが作成できます。さらに、MVVMはWPFのためのパターンです。WPFの持つ表現力を最大限まで引き出すことができます。

MVVMは、MVPやMVCなどの三層アーキテクチャモデルの親戚です。すでに成熟したMVPやMVCではなく、何故MVVMパターンという新しいモデルを用いるのかと考える方がいるかと思いますが、それは誤解です。MVVMでも基本は変わりません。単純に、実現の方法が違うだけです。BindingというWPFの機能を活用すれば、より自然な三層アーキテクチャモデルを構築可能だということです。三層アーキテクチャ + Binding = MVVMだと考えると良いと思います。僕もそう考えています。

MVVMインフラストラクチャにはWPFという明確なターゲットがあるため、しばしばWPF固有の知識が盛り込まれますが、これはMVVMパターンをより簡単に実装可能にするためのもので、これらを活用しなければMVVMではないということはありません。ただ、車輪の再発明だと思いますので、できれば頼った方が良いとは思います。

要するに、肩肘張ってMVVMパターンを実装する必要はありません、ということです。あなたがMainWindowViewModel.csを作った瞬間、それはMVVMパターンです。(だと思います)

MVVM – Model View ViewModel

Viewは、画面です。勝手にできたり消えたりします。
ユーザーになにかを見せたり、ユーザーから入力を受け取ったりするのが仕事なので、このあたりはとても得意です。ですが、状態に応じて表示を変えたり、ユーザーの入力に応じて何かしたりっていうのは限定的な機能しかありません。

ViewModelは、画面とデータをくっつけるものです。
データをViewに送るために整形したり、ユーザーの操作を解釈したりするのがお仕事です。ちょうどViewに欠けている部分がまとまって固まってるようなものです。

Modelは、データです。なんでもいいです。

WinFormsでプログラムを作成されたことがある方なら、Viewが *.Designer.cs、ViewModelが*.csみたいなものかーって考えられる方もいらっしゃるかと思うんですが、ちょっと違います。Viewはもっと高度なことができます。見た目を司るのがView、全体的な動作を司るのがViewModelって考えるといいかもしれません。

生存期間

ViewとViewModelとModelはそれぞれ生存期間が違います。Modelはそれぞれでしょう。ViewModelは、参照してる間は生きてます。いずれにしても、ModelとViewModelの寿命についてはプログラマがコントロール可能です。ところが、Viewについては勝手にできたり勝手に消えたり、消えたと思ったらまた作られたりを繰り返しています。プログラマはコントロールできません。

たとえば、リストボックスを考えてみてください。10000件のデータがあるけど、そのうちの100件しか描画範囲内に無いなら、その100件だけあればいいですね。このとき、Viewは本当に100件分しか存在しません。(※実際のところはいろいろ変わってくるのですが、今回はそういうことにしといてください。)
リストをスクロールしたら、スクロールして新しく表れてきた分のViewを作って、隠れてしまった分のViewを削除すれば、メモリ領域を無駄に使うことなくリストを表示できますね。(※)
WPFは、この動作をいろんなところで自動的に行います。この一連の動作はプログラマからは隠されていて、いつできたか、いつ消されるかを知ることは基本的にできないと思った方が良いです。

さて、Viewを作るときには当然それの種となるものが必要です。見た目はXAMLのDataTemplateでなんとかなると思いますが、表示するデータはどうするかというと、ここでViewModelを使います。ViewModelが、前のViewの状態を覚えていて、新しく作られるViewにその情報を提供することで、ユーザーにViewが出来たり消えたりしていることを意識させないようにすることができます。←重要

データバインディング

Viewは当然どこかでViewModelへの参照を得ないといけません。また、各コントロールはViewModelのどのデータを参照すればいいかを指定されないとどれを表示すればいいんだか困ります。この時に使われるのが、データバインディングシステムです。

WPFの各コントロールには「DataContext」というプロパティが存在します。このDataContextはobject型のプロパティですから、いろんなデータを渡すことができます。WPFのコントロールの中にはコントロールを内包できるものがあります(WindowやGridなんかは代表例ですね)。そんな親コントロールのDataContextを設定した時、子コントロールのDataContextも自動的にそれを継承します。Windowの中にTextBoxを配置している時に、WindowのDataContextに何かセットすると、TextBoxのDataContextもそれになります。実のところ、これはViewModelへの参照そのものです。DataContextにViewModelのインスタンスをセットしてやればすべて上手く行きます。

コントロールでどのデータを表示するのかを決めるのがいわゆる「データバインディング」です。たとえば、<TextBlock Text=”{Binding SomeValue}“ />としたとき、このテキストブロックはViewModelのSomeValueプロパティの内容を取得して表示します。ViewModelの参照は先述の通り、DataContextを使います。

ところで、DataContextへViewModelのインスタンスを渡す方法も必要になります。考え付くのは、

  • XAMLでインスタンスを生成する
    <Window.DataContext>
        <vm:WindowViewModel />
    </Window.DataContext>
    のような感じですね。
  • コードビハインドでインスタンスを生成する
  • コードビハインドをViewModelにする(!!)
    Window.DataContext={Binding}
    で設定することができますが、これには実は問題があります。後でまた。
  • インフラの機能に頼る
    特に画面遷移の場面で多いのですが、インフラによっては、ViewModelを指定してViewを表示することができるものがあります。

とまぁいろいろあります。

このデータバインディングの機能を利用してViewとViewModelを結びつけますが、参照可能なのはView→ViewModel だけであることに注意してください。ViewModelはViewを直接操作することはできません。

遠回りな、理不尽な実装に見えますが、これは非常に理にかなっています。次で説明しますね。

バインディングとパフォーマンス

Viewが出来たり消えたりするということと、ViewModelとViewはBindingでくっつくということは分かったかと思いますが、Bindingで参照可能なのはView→ViewModelの一方通行だけです。どうしてこんなことになっているのでしょうか?

例えば、ViewModelにBindingControlsプロパティか何かを用意して、Viewがバインドされたらここに参照を追加、バインド解除されたらここから参照を削除という方法を考えてみましょう。(View->ViewModelは多数張ることができますので、必然的にコレクションとなります。)これならViewModelからもバインドされているViewにアクセス可能です。なぜWPFはこのような実装をしなかったのでしょう。

先述した通り、Viewはめまぐるしい勢いで生成され、削除されます。生成されるたびに通知、削除されるたびに通知、しかもバインディングの数だけ。パフォーマンスは期待できません。すべてのWPFアプリケーションの足枷となってしまいます。そもそも、Viewの削除はGCによって行われていると推測できます。ファイナライザスレッドを使ってこの処理を行わなければいけないのは、パフォーマンスに重大な悪影響を与えます。そのため、ただ単に参照を保持するだけで済む View->ViewModelの一方通行となっています。これならパフォーマンスを高く保つことができます。

メモリリーク

View←ViewModelの参照は持つことが基本的にできません。しかし、どうしてもViewに情報を渡さなければいけない局面がいくつか存在します。たとえば、特定のコントロールにフォーカスを遷移する時。たとえば、メッセージボックスを出す時。たとえば、ダイアログを出して画面を遷移する時。これはどのようにすれば解決できるでしょうか。

コードビハインドでDataContextの変更をリッスンし、変更されたらDataContextにViewを渡す。場合によってはうまく行くでしょう。しかし、Viewの寿命は一般的にViewModelより短いです。このとき、どのようなことが起こるでしょう?

Viewが破棄されると、ガベージコレクションの対象となります。しかし、ViewのインスタンスはまだViewModelから参照されている状態になります。これでは、ガベージコレクションは行えません。

この現象は、ViewがViewModelのイベントをリッスンしている時も同じです。イベントはデリゲートのコレクションを保持し、発行された時に通知して回ります。デリゲートはViewのインスタンスを保持していますから、結局Viewのガベージコレクションが行えません。先述した「コードビハインドをViewModelとして使う」という解決策の問題点もまた同じです。コードビハインドは実行時にViewと結合されます。ViewModelへの参照がそのままViewの参照と同一となり、結局メモリリークを引き起こします。

コレクションされるべきViewがいつまでもコレクションされずメモリを圧迫し続ける、これはまさにメモリリークです。この現象の根本的な原因は、寿命の短いViewの参照をViewより寿命が長いViewModelが持っているという点にあります。

知識がある方なら、WeakReferenceによって解決することを思いつくでしょう。しかし、全てのView←ViewModel参照においてWeakReferenceを間違いなく実装するのは骨が折れます。そこで、各MVVMインフラストラクチャの出番となります。多くのMVVMインフラストラクチャは、この機能を実装しています。それも、簡単に実装が行える形で。たとえば、ViewModelの中にメッセンジャーとよばれる機構を組み込んだり。この機能についての詳細は各インフラストラクチャのヘルプを参照してください。

なぜ「メッセンジャー」を用いるのか。それは、View←ViewModelの参照が持てないから、です。

統一された手法を用いる意味

MVVMパターンでインフラストラクチャを用いる利点について説明しましたが、これらは場所が限定的であれば、例示したような解決策でもうまく行きます。たとえば、アプリケーションの開始から終了まで一貫して破棄されないViewであれば、メモリリークの問題は発生しません。

ですが、「このViewは破棄されるからMessenger」「このViewは破棄されないからイベント」というように一々考慮していてはバグの温床となりますし、メンテナンス性も大きく下がります。それならば、どこでも統一された、安全な手法を用いてアプリケーションを構築した方が良いと思いませんか?メンテナンスもしやすく、バグも生み出しにくくなります。

さぁ、WPF With MVVM!

INotifyPropertyChangedの働きだとかまだいろいろありますが、後は使ってみれば分かると思います。というか、別の人が詳しくまとめてると思います!

throw new 筆();

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です