MVVM フレームワーク Prism の全体概観


概要

Prism とは Windows クライアントアプリケーション開発で使える代表的な MVVM フレームワークの1つです。この記事では Prism が何をしてくれて、それによって何ができるのか、そして、結果的に何が嬉しいのか解説します。この記事は Prism の全体を概観するものです。具体的な実装方法などは他に詳しい記事がたくさんありますので、そちらを参考にしてください。

前提

  • Prism.Wpf / Prism.Unity 6.3.0 により得た知識が前提で書かれています。
    • Xamarin.Forms, Windows 10 UWP 版は触っていません。
    • 6.3.0 は執筆時点での Prism.Wpf の最新バージョンです。現時点でバージョン 7 系の pre バージョンが出ていますが触っていません。
    • DI コンテナに Unity を選択しています。その他の DI コンテナは触っていません。
    • Prism を知らなくても大丈夫です。これから知りたいと思っている人向けに書いた記事です。
  • MVVM の概要は知っていることが前提となります。
    • この記事には MVVM 自体の解説は含まれていません。
  • 途中 PropertyChanged.Fody や ReactiveProperty が出てきますが、詳細には触れません。
    • 興味が出てきたら合わせて調べてみるといいと思います。Prism と組み合わせて使えます。

背景

Prism を覚えようとしたが個別の記事はあるものの、全体を概観する記事が少なく理解するのに苦しんでいました。個別の記事では Prism がしてくれることの全貌がわかりません。本家の記事は抽象的過ぎて具体性に欠けていて、何をしてくれて、それによって何ができて、何が嬉しいのかがわかりませんでした。

そうしているうちに秀逸なサンプルが本家により公開されていることがわかりました。Prism 提供の個々の機能にフォーカスを当てたサンプルが 29 もあるのです。

1 つ 1 つのサンプルは非常にシンプルで小さく、個々の機能を簡単に理解することができます。Prism はオープンソースなのでサンプルだけでわからないところは、本体を見ることで理解を深めることもできます。とてもお勧めです!

勉強し、実際に使い始めて Prism の素晴らしさがはっきりとしてきました。

本家のサンプルに出会うまでは Prism の表面的な機能しか理解できていませんでした。ViewModel の実装をサポートしてくれる BindableBase や DelegateCommand 等の機能がそうです。比較的理解しやすいという理由からかこれらの機能に関する記事を多く見かけますが、これは Prism のほんの一部にしか過ぎません。むしろこれらの機能は PropertyChanged.Fody や ReactiveProperty など他のライブラリによって置き換えられており、ほとんど使用しない可能性すらあります。では、Prism は必要ないのかというとそんなことはありません。Prism は他にもっと重要な機能を提供してくれるからです。Prism は一体どんな重要な機能を提供してくれるのでしょうか?

次に View に対応する ViewModel インスタンスの自動生成をする ViewModelLocator の機能が目にとまりました。View と ViewModel は 1 対 1 なのだからそれぐらい自分で new するよというのが正直な最初の感想でした。何が嬉しいのかさっぱりわかりませんでした。同じことを思った人は他にもいるんじゃないでしょうか?しかし、よくよく理解すれば ViewModelLocator はその後に恩恵を受けるための単なる入口であり、その恩恵を受けるための手段だということがわかります。では、ViewModelLocator はいったい何のためにあり、それによってどんな恩恵を受けられるのでしょうか?

こういった疑問に対する答えにたどりつくための情報をこの記事に込めたいと思います。

今の私は Prism を調べ、覚え、実際に使い始めて1ヵ月にも満たないひよっ子です。
だからこその生の感動がまだ内にあります。その感動が熱いうちにこの記事としてまとめます。
この記事は今の私だけに書ける、未来の私には決して書くことのできない記事です。

Prism の機能一覧

Prism が提供してくれる機能を以下にまとめます。主観で勝手にまとめたものという点にだけ注意ください。

No 機能名 機能概要
1 ViewModel の実装補佐 ViewModel の実装を補佐してくれる機能
2 InteractionRequest ViewModel ⇔ View 間の通信機能
3 DI コンテナ アプリケーション全体で使用できる DI コンテナ
4 EventAggregator ViewModel ⇔ ViewModel 間の通信機能
5 Region / Navigation 名前付き表示領域の管理機能
6 Module DLL によるプラグイン実装をサポートする機能

個々の機能について個別に説明していきます。

各機能の個別説明

1. ViewModel の実装補佐

INotifyPropertyChanged / ICommand / INotifyDataErrorInfo インターフェイス群の実装を補助するクラス群を提供してくれます。BindableBase / DelegateCommand / ErrorsContainer クラスなどがそれにあたります。

ViewModel の実装はボイラープレート・コード(繰り返し使用する定型コード)が大量に発生するため素の状態で書こうとするととても大変です。Prism なしで実装するよりも Prism ありの方が実装するコードは少なくなり楽になるのは間違いありません。

しかし、Prism が提供するこれらの機能は限定的です。PropertyChanged.Fody や ReactiveProperty など特化した手段を使うことが昨今は多いのではないかと思います。これらを使うと実装に必要なコードがもっと少なくなるだけではなく、柔軟性も増します。
(※ これらを使う場合でも ViewModel は INotifyPropertyChanged を実装しなければならないので、その標準実装として BindableBase を使ったりはしている)。

そのため ViewModel の実装補佐の機能は、(少なくともこの記事を書いた時点においては)Prism の重要機能ではありません。

2. InteractionRequest

Messenger パターンを実装した機能です。

ViewModel にダイアログの表示処理を直接書きたくない(書いてしまうと ViewModel をテスト等で実行しづらくなるため)ので、ViewModel から View(Window や UserControl のこと)に中継役の Messenger を用意するという方法を実現するものです。Messenger は ViewModel から受け取ったメッセージを元に View 側に定義されたダイアログ表示のロジックを呼び出します。

InteractionRequest は ViewModel ⇒ View の通信だけではなく、表示した Yes-No ダイアログでの Yes / No 選択など View ⇒ ViewModel への対話型の通信もサポートしてくれます。
この機能は多くの MVVM アプリケーションで必要になるとても有用な機能です。

3. DI コンテナ

Prism を調べ始めた当初、なぜ DI コンテナの機能が提供されているのか不思議で仕方ありませんでした。しかし、調べて使っているうちに、この DI コンテナは Prism の全体を支えているのみならず、アプリケーション開発にとても大きな恩恵をもたらしてくれることがわかりました。DI コンテナが Prism の最重要機能を担っているといっても過言ではないと思っています。DI コンテナがアプリケーション開発になぜ必要になるのか、一体何が嬉しいのか語弊を恐れずに説明したいと思います。

Prism / MVVM アプリケーション開発における DI コンテナの役割を一言でいうと「アプリケーション全体で使用できるグローバルのインスタンス保管庫」です。

画面間やアプリケーション/モジュール間でインスタンスを共有し、相互通信するための基盤となるのがこの DI コンテナの機能です。

インスタンスの注入の起点となるのは ViewModel です。Prism では ViewModelLocator という機能により View 生成時に対応する ViewModel を DI コンテナで自動生成し、View の DataContext に自動設定してくれるという機能があります。ViewModel に情報の受け口となるコンストラクタやセッターの定義をしておくと、DI コンテナが自動的に各種インスタンスの注入を行ってくれます。ViewModelLocator の目的は ViewModel に DI コンテナの恩恵を届けることです。ViewModelLocator の必要性を理解するポイントは ViewModel を生成することでも、View の DataContext に ViewModel を設定してくれることでもありません(もちろん、それは必要なことではあるが、それが理解するポイントではない)。ViewModel に「アプリケーション全体で使用できるグローバルのインスタンス保管庫」が持つ情報を自動的に設定することがしたいのです。ViewModelLocator に ViewModel のインスタンス生成を任せているからこそ ViewModel への各種インスタンスの注入ができるのです。

これにより受けることのできる恩恵を具体的な例で説明します。

例えば Visual Studio のような統合開発環境を開発する場合を考えましょう(Visual Studio ならこの記事を読んでいる人はどんなものか想像できるでしょう)。その場合、「開いているソリューションやそのソリューションが持つプロジェクト群」の情報を Model / ViewModel として持つことになると思います。それらの Model / ViewModel は画面を構成する色々な View で参照するグローバル情報です。
アプリケーション起動時に DI コンテナにシングルトンとして「開いているソリューションやそのソリューションが持つプロジェクト群」を登録しておくことで、各種 ViewModel にその受け口となるコンストラクタまたはセッターを定義するだけで、必要な情報にアクセスできるようになるのです。もちろん、ViewModelLocator により ViewModel は DI コンテナ経由でインスタンス化されるようにする必要があります。しかし、それだけで実現できます。

このようなグローバル情報は、アプリケーションを実装する際によく出てきます。DI コンテナ以外の代表的な解決方法には以下のようなものがあります。合わせてその方法のデメリットも記載します。

  • シングルトンを独自実装する。
    • この方法はグローバルに共有する情報が増えるたびにシングルトンの定型コードが増えていく。
    • アプリケーションではシングルトンで良くても、単独テストをする場合は個別のインスタンスを使いたいという事情等に答えづらい。例えばシングルトンによってテストの並列実行が阻害されるなどが発生する。
  • ViewModel に情報を漏れなく伝播させる。
    • 情報を必要とする画面がいくつかの画面を経由して表示される場合や、情報を必要としているのが下層の画面部品である場合など、情報を伝播させるのに苦労する場合がある。
    • グローバルに共有する情報が増えた場合に追加で必要になる実装がたくさん出てくることがある。

DI コンテナによる解決方法は、これらのデメリットを持ちません。DI コンテナはグローバルな情報を簡単に共有するための解決策を提供してくれます。これはアプリケーション開発にものすごく役立ちます。
DI コンテナによる解決方法のデメリットは、DI コンテナへの依存が発生することです。

Prism の DI コンテナの特徴を最後に挙げておきます。

Prism は Autofac, DryIoc, MEF, Ninject, StructureMap, Unity という DI コンテナをサポートしていますがその提供方法が特殊です。Prism には DI コンテナ毎の Nuget パッケージがあり、そのうち1つを選んで使用する形式となっています。

Prism は DI コンテナを抽象化しません。そのため選択した DI コンテナに依存する実装が少なからず出てきます。まず最初に Prism の初期化コードにその違いが現れますが、その他にも DI コンテナによりインスタンスを注入される入口となる ViewModel のコンストラクタやセッターの実装方法などにも多少なりとも影響があります。例えば DI コンテナに Unity を使用した場合、セッターインジェクションを受けるには Unity の [Dependency] アノテーションをセッターに設定します。

Prism(ひいては選択した DI コンテナ)の影響範囲を留めるために DI コンテナによるインスタンスの注入は ViewModel に留めるのが良いのではないかと思います。

4. EventAggregator

Pub/Sub パターンを実装した機能です。

InteractionRequest が ViewModel ⇔ View 間の通信をサポートするのに対して、EventAggregator は ViewModel ⇔ ViewModel 間の通信をサポートします。総じて画面間の通信をサポートする機能と読み替えることができます。
(※ 以上のように書きましたが、この機能は汎用で ViewModel ⇔ ViewModel 間に限定したものではありません。しかし、Prism との依存を避けるなら Model での利用は避けるべきと個人的には考えているので ViewModel ⇔ ViewModel 間の利用が妥当と考えています。ViewModel ⇔ View 間は InteractionRequest を使います。)

通信の仲介役として IEventAggregator のインスタンスを使用します。IEventAggregator インスタンス自体は Prism が標準で DI コンテナに用意してくれます。それを DI コンテナを通じて ViewModel で参照して使います。ViewModel にはコンストラクタやセッターインジェクションで IEventAggregator の受け入れ口を作るだけで使えます。

受信側の ViewModel で IEventAggregator インスタンスにメッセージ受信のイベントを登録しておくと、送信側の ViewModel で IEventAggregator インスタンスに送信したメッセージが受信できます。受信と送信は多対多の接続ができるので、多対多の画面(もしくは画面構成部品)間の通信ができます。

具体的に必要になる場面は、思いついていません。

理由は DI コンテナによるグローバルなインスタンス共有機能が非常に強力なためです。ViewModel 間に必要な多くの通信はインスタンス共有により不要になります。ViewModel を共有してしまえば、同じ情報を複数の View に表示することは簡単です。また、インスタンス共有と ReactiveProperty とを組み合わせれば同じことがもっと柔軟にできると思います。

5. Region / Navigation の機能

Region は、名前付き表示領域の管理機能です。この名前付き表示領域を Region と呼びます。ページ遷移のように Region の内容を切り替えたり、タブビューへのページ追加のように Region に内容を追加したりといったことを支援してくれます。これが Navigation 機能です。

Region で扱う表示要素の基本単位は、View / ViewModel のペアです。Navigation 機能の主な機能は、Region への View / ViewModel の追加や View / ViewModel のライフサイクルの制御(アプリケーション実行中は永続するシングルトンとして扱う/表示毎に個別のインスタンスとして扱うなど)や表示固有パラメータ渡しのサポートなどです。また、ページ遷移のヒストリ操作(Navigation Journal 機能)のサポートもあります。

これらの機能は、多くのアプリケーションで便利に使える強力な機能です。例えばブラウザを実装する場合などを想像すると有用性を感じられると思います。

Region 機能のポイントは、Region への操作が名前(すなわち文字列)でできるようになっているという点です。Region によって表示領域が特定の画面から独立して操作できるようになります。Region に表示要素を追加する側は、その Region がどこの画面に表示されるのか具体的な画面の View や ViewModel のクラスを知る必要がないのです。逆に Region を表示する側も Region の名前だけを必要とし、そこに表示される View や ViewModel のクラスを知る必要がありません。

この性質は Module 機能と組み合わせることでプラグインの実装を強力にサポートします。例えばメインに拡張用の Region を定義しておいて、外部モジュールからその Region に表示要素を追加するといったことが簡単にできます。

6. Module の機能

Module は、DLL によるプラグインをサポートする機能です。

Module の基本機能はとてもシンプルです。DLL を読み込み、その DLL から IModule の実装クラスを探し、モジュールの初期化のために Initialize メソッドを呼ぶだけです。IModule の実装クラスを指定する方法が標準でいくつか用意されています。

  • App.config の設定を読み込む方法
  • コードで IModule の実装クラスを明示する方法
  • 指定ディレクトリ配下の DLL から自動探索する方法

再度書きますが、Module の基本的な機能は DLL から IModule を探し出して Initialize メソッドを呼ぶだけです。Module の基本的な使い方には以下のようなものが考えられます。

  • DI コンテナ経由でグローバルリソースを参照し、操作する。
  • DI コンテナ経由で特定インターフェイスに対する固有の実装クラスを注入する。
  • Region 経由で固有の View を組み込む。
  • EventAggregator 経由で固有のメッセージ送信/受信処理をする。
  • その他 ...

Module 機能はとてもシンプルでわかりやすいです。それが DI コンテナ / Region / Event との組み合わせで色々なことができるのです。

まとめ

Prism は MVVM アプリケーションの開発を助けてくれるとても強力なフレームワークです。

最初は ViewModel の実装補佐など基本機能などが目につきますが、それはそれほど重要な機能ではありません。

Prism にとって最も重要な機能は DI コンテナです。DI コンテナは「アプリケーション全体で使用できるグローバルのインスタンス保管庫」として働き、アプリケーション開発の多くの場面で必要となるインスタンスの共有が簡単にできるようになります。

InteractionRequest / Region / Navigation は、MVVM アプリケーションで必要となるダイアログ表示/ページ遷移/タブビューへのページ追加を書く標準的な方法を提供してくれます。

Module は DLL によるプラグインをサポートしてくれます。

最後に Bootstrapper について

今回は Prism の機能に着目したために Bootstrapper という単語が記事内に出てきませんでした。この Bootstrapper は Prism を覚えるにあたってよく目にする単語ですが何なのかがつかみにくいものでした。Bootstrapper は何なのかを紹介して終わりにしたいと思います。

Bootstrapper は Prism の標準セットアップを提供してくれる機能です。具体的には DI コンテナの生成や、記事中に出てきた ViewModelLocator や IEventAggregator の準備などがそうです。Bootstrapper は標準セットアップを定めているものであり、概ねそのまま利用できます。

標準設定から動作を変更する場合、対応するメソッドを override して上書きするという使い方をします。一番最初に override して使うのはたぶん CreateShell でしょう。Shell と聞くとコマンドプロンプトが頭に浮かぶと思いますがメインウィンドウのことです。Bootstrapper.CreateShell の説明には「Creates the shell or main window of the application」と書いてあります。標準実装は null を返すだけです。通常は、ここでメインウィンドウのインスタンスを DI コンテナで生成して返します。その他には DI コンテナのインスタンスを決定する CreateContainer や ViewModelLocator の設定を決める ConfigureViewModelLocator 等があります。