ViewModel から画面遷移するいくつかのパターン


無償化に伴い、小さなベンチャーにとっても強力な選択肢の一つとなった Xamarin。
公式ドキュメントを読みながら勉強しています。

ViewModel から画面遷移できない!

MVVM でアプリを作るうえで、画面遷移は ViewModel 主体で(もしその先にアプリ全体を管理するモデルがあるのであればそこでコントロール)したいと考えます。

公式ドキュメントにもMVVM に触れた記載はあるものの、所詮「ViewModel をバインディングコンテキストにセットするとバインドできるようになるぜ」という MVVM の前提技術の一部を紹介するにとどまっています。
画面遷移のために必要な Navigation オブジェクトは View が持っており、ViewModel から遷移しようとすると ViewModel と View の相互依存が発生してしまいます。

...View は ViewModel の影なのだが

... View.Navigation にアクセスする
→ ViewModel が View について知る必要ができてしまう

MVVM (Model-View-ViewModel) については尾上先生の「MVVMのModelにまつわる誤解」、さらにこのリンク先にあるスライドから勉強することで早く楽になれます。

この記事では、今のところ目についた方法を紹介していきます。

  1. MessagingCenter を使う方法 (どうだろ。いいかも?)
  2. ViewModel に View.Navigation を渡す方法 (簡易)
  3. ViewModel に対して View をペアリングする方法 (理想・手間かかる)
  4. Prism.Forms を使う方法 (理想・プレビュー版)

1.MessagingCenter を使う方法

記事:MVVMっぽくXamarin.Formsアプリ作ってみました。その2
by ゆーかさん 2016/04/19公開

MessagingCenter というのは、アプリケーション内に閉じた汎用的なメッセージ発信・受信手段です。余談ですが、Aurelia.js というJavaScript MVVM フレームワークでは「EventAggregator」という名前がついています。
疎結合のための常套手段ですが、汎用性が高すぎるので乱用するとトレーサビリティの低下がやばいかも?プロジェクトに閉じたルールを作るなど対策が必要になるかもしれませんね。

こちらは日本語の記事なので、詳細はリンク先をご覧ください。

2.ViewModel に View.Navigation を渡す方法

参考記事:Navigation from ViewModel using Xamarin.Forms
by Johan Karlsson (Xamarin MVP) 2014/09/03公開

基本的なアプリの作り方については公式ドキュメントと同じです。
ポイントは View のコードビハインドで ViewModel に Navigation を渡す ということです。

参考記事の例では、View のコンストラクタで ViewModel を生成し、その際に Navigation を渡しています。

namespace XfVmFirst.Pages
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            this.BindingContext = new MainPageViewModel(this.Navigation);
        }
    }
}

ViewModel に Navigation を使ってもらえばそれでよい、という方法です。
ViewModel 側は受け取った Navigation をプライベートメンバとして持ち、画面遷移したいときに直接使えばOKです。

namespace XfVmFirst.Pages
{
    class MainPageViewModel
    {

        INavigation navigation;

        public ICommand GoToNextPage { get; private set; }

        public MainPageViewModel(INavigation navigation)
        {
            this.navigation = navigation;
            this.GoToNextPage = new Command(async () => {
                await navigation.PushAsync(new NextPage());
            });
        }

    }
}

上記コードの await navigation.PushAsync(new NextPage()); が画面遷移のコードですが、ここで渡しているのは次のページの ViewModel ではなく View のインスタンスです。その部分が少し理想と違うところですね。
図にすると以下のようになります。

View のコンストラクタで ViewModel を渡すこともできますが、どちらにしても ViewModel のコード内で次ページの View を直接指定することになります。

私の感想ですが、妥協点としてはアリですし小規模なアプリであれば問題ないかなーと思います。

追記: Twitter で見かけたご意見ですが、ViewModel に View を渡してしまう方もいるようです。ViewModel が View の具象クラスで受け取ってしまうと、結局冒頭の「View が ViewModel について知ってしまう」という問題が発生しますが、Page あたりの抽象クラスで受け取っておけば Navigation や DisplayAlert など基本的なものにアクセスできるので手っ取り早いかもしれません。

3. ViewModel に対して View をペアリングする方法

参考記事:Xamarin Forms – View Model First Navigation
by Matthew Soucoup (Xamarin MVP) 2016/02/18公開

こちらはリフレクションを駆使して View が自分の ViewModel を明示できるようにします。MVVMCross という ViewModel ファーストの画面遷移の実装や ReactiveUI などを参考にしたそうです。
図にすると冒頭の理想の通りとなります。ViewModel を指定して画面遷移すると、ペアリングされた View が自動的に生成・表示されます。

View がペアリング対象の ViewModel を明示する方法として、インタフェースを使います。

Viewのコードビハインドの例
using Xamarin.Forms;

namespace PracticeXamarin
{
    // IViewFor インターフェイスによってペアリング対象の ViewModel を明示する
    public partial class TabOnePage : ContentPage, IViewFor<TabOnePageViewModel>
    {
        TabOnePageViewModel _vm;
        public TabOnePageViewModel ViewModel {
            get { return _vm; }
            set {
                _vm = value;
                this.BindingContext = _vm;
            }
        }

        object IViewFor.ViewModel {
            get { return this.ViewModel; }
            set { this.ViewModel = (TabOnePageViewModel)value; }
        }

        public TabOnePage()
        {
            InitializeComponent();
        }
    }
}

Xamarin に ViewModel ファーストの概念を追加するため、最初は手順が多めとなっています。
以下の手順は参考記事のためのサンプルコードを写経しながら参考にしていただけるといいと思います。

準備1.必要なライブラリの追加

このサンプルではRefractored.MvvmHelpersSplatというライブラリを使っています。
どちらも NuGet でインストールできますので、検索してインストールしてください。
(ツール > NuGet パッケージマネージャ > ソリューションの NuGet パッケージの管理)

Refractored.MvvmHelpers

インストール先は共通部分の PCL だけで、.iOS や .Droid にはインストールしなくて大丈夫です。
MVVM での開発を補助するクラス、基底クラスなどが提供されます。
このサンプルでは BaseViewModel という ViewModel の基底クラスだけ使っています。

Splat

PCL だけでなく、.iOS や .Droid にもインストールしてください。
画像リソースを PCL に置いて各プラットフォームで共通化したりといったサポートが提供されます。
このサンプルではサービスコンテナ機能 Locator.CurrentMutable を利用します。

準備2.ナビゲーションサービスの実装

画面遷移に使う View.Navigation ですが、これをサービスとして ViewModel etc で使えるようにします。
また同時に、View が ViewModel を明示しペアリングする機能も実装します。

XamFormsVMNav/VMFirstNav/VMFirstNav/Navigation/ をそれぞれ写経するか、コピーしてください。

準備3.ナビゲーションサービスの登録

この作業は iOS, Android のプラットフォームごとに必要となります。

iOS は AppDelegate.cs を、Android は MainActivity.cs をそれぞれ参照してください。
やるべきことは using Splat; の追記と、下記3行の挿入だけです。

INavigationService navService = new NavigationService ();
navService.RegisterViewModels (typeof(RootTabPage).Assembly); // アセンブリから IViewFor<*> を読み込ませる
// サービスコンテナにナビゲーションサービスを登録
Locator.CurrentMutable.RegisterConstant (navService, typeof(INavigationService));

あとは上の「Viewのコードビハインドの例」のように View に ViewModel を明示させ、
ViewModel は BaseViewModel を継承しつつ以下のようにナビゲーションサービスを取得して画面遷移します。

Splat.Locatorを使ったナビゲーションサービスの取得
// ViewModel のコンストラクタ等で呼び出す
this.navService = Locator.CurrentMutable.GetService<INavigationService>();
ViewModelを指定して画面遷移
// 画面遷移が発生するコマンド等で使う
await this.navService.PushAsync(new TabOneChildViewModel());

準備さえ整ってしまえば理想的な形が実現できるのですが、いかんせん手順が多く煩雑な面もあるので、できれば MvvmHelpers などに実装されるとありがたいですね。。。
MVVMCross を使うという選択肢もあるのでしょうか。

4.Prism.Forms を使う方法

記事:XamarinでPrismを使ったHello world
by かずきさん (Microsoft MVP for Windows Development) 2016/04/19公開

記事:Xamarin.Forms + Prism.FormsでVとVMを結びつける
※同ブログ 2016/04/21公開

Prism.Forms では基本的に命名規約によって View と ViewModel を紐づけます。僕の好きな Aurelia.js と同じ考え方で、画面遷移に必要な NavigationService も DI によって ViewModel に注入することができます。

Xamarin.Forms + Prism.FormsでVとVMを結びつける」では View と ViewModel の作成と View の登録まで解説されています。さらに ViewModel に NavigationService を注入するには ViewModel に次のように書き加えます。

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;

namespace PrismUnityApp7.ViewModels
{
    public class NextPageViewModel : BindableBase
    {
        public string Text => "Hello MVVM world!!";

        public ICommand GoToMainPage { get; private set; }

        // ViewModel のコンストラクタで INavigationService を受け取るようにするだけ
        // ただし引数の命名規約によって Inject されるため、引数名は「navigationService」にする
        public NextPageViewModel(INavigationService navigationService)
        {
            this.GoToMainPage = new Command(async () => await navigationService.Navigate("MainPage"));
        }
    }
}

ViewModel の生成はコンテナがやってくれるので、自分でサービスを渡すコードを書く必要がありません。
追記:詳細な手順は同ブログ「Xamarin.Forms + Prism.FormsでViewModelから画面遷移をする (2016/04/22公開)」をご覧ください。


以上です。
Prism を使った方法がある程度レールが敷かれていて躓きにくいですね。拡張機能として Prism のテンプレートパックが用意されているのもポイントが高いです。
ほかにも「2.ViewModel に View.Navigation を渡す方法」に近いですが、ViewModel から View への依存を回避するために Behavior を使う方法は WPF でも使われるようです。