nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

Prism for Xamarin.Forms入門 ViewModelLocator

さて、今回からいよいよ個々の細かいエッセンスを掘り下げて解説していきたいと思います。
栄えある?第一回目はViewModelLocatorです。
しょぼいと思うかもしれませんが、Prismを利用してアプリケーション開発を進める上で、重要な起点となる要素です。
本エントリーではViewModelLocatorの標準の振る舞いと利用方法以外に、詳細な解説、いくつかのカスタマイズして利用するシナリオをご紹介したいと思います。

なお本エントリーは連載記事「Prism for Xamarin.Forms入門」の一部となっております。
以下に目次がありますので、他のエントリーもご覧いただけると嬉しいです。
【Xamarin】Prism.Forms入門 目次 - nuits.jp blog

Quickstart

ViewModelLocatorの役割は、Viewに対してViewModelをインジェクション(抽入)することです。
ViewのBindignContextに対して、自動でViewModelを特定して紐づける処理を行ってくれます。

このことの重要性は、前回のエントリーで解説しているので、興味がおありでしたら良かったら目を通してみてください。

nuits.hatenadiary.jp

ViewModelLocatorを利用するためには、ViewのXAMLファイルに以下の2行を追加するだけで動作します。

xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
prism:ViewModelLocator.AutowireViewModel="True"

具体的には以下のような感じでXAMLの先頭に記載します。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="HelloPrism.Views.MainPage"
             Title="MainPage">
  <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
    <Label Text="{Binding Title}" />
  </StackLayout>
</ContentPage>

ちなみにViewModelLocatorは、添付プロパティ(Attached Property)という技術を利用して作成されています。
Xamarinだけではなく、XAMLアーキテクチャ全般で利用される技術なので、気が向いたら調べてみることをお勧めします。

ViewModelLocatorはデフォルトでは、いくつかのルールに従ってViewに対してViewModelを決定します。

  • ViewとViewModelは同一のアセンブリ内に含まれていること
  • ViewのnamespaceがXx.Viewsであった場合
    ViewModelのnamespaceはXx.ViewModelsであること
  • ViewModelのクラス名は、「Viewのクラス名+ViewModel」であること
    ただし、Viewの名称が~Viewであった場合、「~ViewModel」であること
    例1) FirstPage > FirstPageViewModel
    例2) SecondView > SecondViewModel

それと、本来はNavigationの項に書くべきことな気もしますが、大切なことなのでこちらにも書いておきます。
ViewModelのインスタンスは、画面遷移の都度生成されます。DIコンテナにキャッシュされないことを認識しておいてください。

このあたりはPrism Templcate Packを利用して実際にプロジェクトを作成して中身を覗いてもらうと分かりやすいかと思います。
Prism Templcate Packの導入方法と、そこから作成されるものの詳細は、以下のエントリーでも解説していますので良ければご覧になってください。

nuits.hatenadiary.jp

Deep Dive

さて、ここからはもう少し掘り下げてViewModelLocatorの仕組みについてお話していきたいと思います。

ViewModelLocatorの役割は、Viewに対してViewModelをインジェクション(抽入)することであると言うのは、先に述べた通りです。
そしてインジェクションするViewModelの生成は自体は、実はViewModelLocationProviderが担っています。
おおよそ以下のような関係になっています。

f:id:nuitsjp:20160814133326p:plain

したがって、ViewModelの生成ロジックをカスタマイズしたい場合、ViewModelLocationProviderの設定を変更する必要があります。
ViewModelLocationProviderの設定変更は、通常はAppクラスから行いますが詳細は後述します。

ちなみにですが、ViewModelLocatorの中身はほとんどスッカラカンで、実体はほぼViewModelLocationProvider側にあります。
一見、ViewModelLocatorとViewModelLocationProviderを統合しちゃった方が、見通しが良くなりそうなものなんですが、次の二つの理由でクラスが分けられているのではないかと想像しています。

  1. ViewModelLocationProviderはCoreライブラリとしてWPFなどと共通化したい
  2. 次の二つの点で、ViewへViewModelをインジェクションする箇所は共通化が図れない
    1. Attached Propertyの実装方法がXamarinとそれ以外で異なる
    2. ViewModelをインジェクションする先が、XamarinだとBindableContextだが、WPFなどではDataContextである

クロスプラットフォームは大変ですね。PrismもXamarinも知れば知るほど神がかってると感じます。

話を戻しましょう。
ViewModelLocationProviderはいくつかのルールに基づいてViewの情報からViewModelを生成します。
大枠の手順は次の流れになります。

  1. Viewの型からViewModelの型を決定する
  2. ViewModelのインスタンスを生成する

この、1.と2.の双方に対して、自由にカスタマイズする仕組みが容易されています。
またこの枠とは別に、例外的に特定のViewに対して、専用のViewModelのFactoryを登録することができます。
専用のFactoryが登録されていた場合、優先的に利用されることになります。

Viewの型からViewModelの型を決定する

ViewModelの方を決定する要素は以下の二つがあります。

要素 説明
TypeFactory Viewの型に対して個別にViewModelの型を割当てる
ViewTypeToViewModelTypeResolver Viewの型からルールをもとViewModelの型にを決定する

デフォルトの状態ではViewTypeToViewModelTypeResolverが設定されており、前述の命名規則によりViewModelを決定します。
TypeFactoryは未設定です。

アプリケーション内でViewModelの型を決定するルールを変更したいような場合、SetDefaultViewTypeToViewModelTypeResolverで独自のResolverに変更することが可能です。
具体的なコードサンプルは後述します。

ViewTypeToViewModelTypeResolverではアプリケーション全体のViewModelの決定方法を示しますが、個別のViewだけルールを逸脱して変更したいというケースがあるかと思います。
そのような場合は、TypeFactoryにView別にViewModelの型を登録することで優先的に適用することができます。
TypeFactoryへの登録にはRegisterメソッドを利用します。
TypeFactoryが設定されているViewに対してはResolverではなく優先的にTypeFactoryが利用されます。

ViewModelのインスタンスを生成する

ViewModelのインスタンスは、前述のシーケンスで決定された型に従って生成されます。
ViewModelをインスタンス化する要素も2種類存在します。

要素 説明
ViewModelFactory ViewModelの型からインスタンスを生成する
ViewModelFactoryWithViewParameter Viewのインスタンスの情報を利用してViewModelの型からインスタンスを生成する

デフォルトではViewModelFactoryが利用され、ViewModelのインスタンスはDIコンテナによって生成されます。
ViewModelFactoryはもちろん独自の実装に変更が可能なのですが、Prismの提供するPage Navigation Serviceとも絡んでくるため注意が必要です。

さて、ViewModelFactoryにはもう一つ、ViewModelFactoryWithViewParameterが存在します。
ViewModelFactoryは、ViewModelの型を引数にインスタンスを生成しますが、ViewModelFactoryWithViewParameterは、それに合わせてViewのインスタンスをパラメータに取ってViewModelを生成します。
Viewの何らかの状態によって、ViewModelの生成をカスタマイズしたい場合に利用します。
標準では設定されておらず、ViewModelFactoryWithViewParameterを設定した場合、こちらが優先され、ViewModelFactoryは利用されません。

ViewModel生成の全体図

ここまで説明した流れを図示したものが以下になります。

f:id:nuitsjp:20160814131439p:plain

Case Study

さて、ここからは少し実践的な話をしたいと思います。
実際のケースでありえそうなシナリオの元に、対応方法を解説していきたいと思います。

なお、注意点が1点あります。
ViewModelLocator(というか、ViewModelLocationProvider)のカスタマイズは、AppクラスでConfigureViewModelLocatorメソッドをオーバーライドして行います。
この際、DefaultViewModelFactoryを変更しない場合は、必ずbase.ConfigureViewModelLocator()を呼び出すのを忘れないようにしてください。
全体的にDIが適用されなくなります。
それでは行きましょう。

01:「~Page」のViewModelの名称を、「~PageViewModel」から「~ViewModel」に変更したい

一番ありそうなのがこれですね。
そんくらい我慢せ~よ!と言いたいような、でも良く分かります。
こういった、ViewModelのTypeを特定するルールそのものを変更したい場合、ViewTypeToViewModelTypeResolverを置き換えることで実装します。

具体的には以下のコードをApp.xaml.csクラスに追加してください。
ConfigureViewModelLocatorをオーバーラードして実装します。

コードはPrismのデフォルトのViewTypeToViewModelTypeResolverを参考にさせていただきました。
くどいようですが、base.ConfigureViewModelLocator()の呼び出しを忘れないようにしてください。

02:特定のViewだけ指定のViewModelを適用したい

異なるViewで、同一のViewModelを利用したいというような時とか?実際に遭遇したことはまだありませんが、ありそうではあります。
その場合、同一のBaseViewModelを作ったりするのとどちらが良いか、ケースバイケースの気もしますが、ViewModelLocationProviderで対応する方法を記載しておきます。

このケースもAppクラスで、ConfigureViewModelLocatorをオーバーラードして実装します。
ViewModelLocationProviderに対して、Viewを指定してViewModelのTypeを登録します。
具体的には以下の2種類のメソッドが提供されています。

  1. void Register<T, VM>()
  2. void Register(string viewTypeName, Type viewModelType)

1.の内部では2.が呼び出されており、結果的に同様に管理されます。
利用例のコードは以下の通りです。

この例ではMainPageクラスに対して、HogeViewModelを利用するよう指定しています。
簡単ですね。

03:特定のViewのViewModelだけ自前で生成したい

01、02の例ではViewに対するViewModelのTypeの決定をカスタマイズする方法でした。
こちらは特定のViewのViewModelの生成を自前でやりたい場合の方法論です。
基本的に、ViewModelの生成はDIコンテナに任せた方が良いとは思いますが、例えばパフォーマンスを特別に重視したい画面でコンテナ任せにしたくない?とか?

このケースもAppクラスで、ConfigureViewModelLocatorをオーバーラードして実装します。
ViewModelLocationProviderに対して、Viewを指定してViewModelを生成するFunc<object>を登録します。
具体的には以下の2種類のメソッドが提供されています。

  1. void Register<T>(Func<object> factory)
  2. void Register(string viewTypeName, Func<object> factory)

こちらでも、1.の内部では2.が呼び出されており結果的に同様に管理されます。
利用例のコードは以下の通りです。

サンプルいんのかよってレベルですが一応。
割り当てるViewModelが標準から変わってないじゃん?的な所がありますが、生成にコンテナを通さないので少し早い。。。かもね?

総括

たかがViewModelLocatorですが、ちゃんと書くと結構大変でした。
てか、ぶっちゃけGitHubのViewModelLocationProvider見た方が早い説もないではないですがw
それと、今回はViewModelFactoryの置き換えは割愛させていただいています。
基本的にはDIコンテナ使った方が良いと思いますし、Viewインスタンスを引数にとってうれしいケースもいまいちピンとこなかったので。。。
何か思うところがあれば、いつか追記するか、別のエントリーを上げるかもしれません。

ということで、今日はここまで。
それではまた!