Xamarin逆引きTips

Xamarin逆引きTips

MvvmCrossで画面遷移するには?

2015年4月28日

MvvmCrossでiOS/Androidアプリの画面遷移をするための基本的な実装方法を説明する。

  • このエントリーをはてなブックマークに追加

 MvvmCrossのiOS/Androidアプリ開発では、ViewModelのロジックで画面遷移を行う。

 今回は、iOS/Android各プラットフォームでの基本的な画面遷移の実装方法を解説する*1

  • *1 なお、本TipsはMac OS X(10.10.3)、Xamarin Studio(5.8.3)、MvvmCross(3.5.0)で動作を確認している(編集部注: Windows上のVisual Studioでも同様の手順で、本稿の内容が実現できることは確認している)。

プラットフォームごとの画面の扱いについて

 MvvmCrossではCoreプロジェクトの実装によるViewModelベースの画面遷移をすることができる。

 1つのViewModelに、1つのUIViewControllerやActivityが対応しているため、ViewModelの遷移はそれぞれiOSではUIViewControllerの遷移であり、AndroidではActivityの遷移となる。

 MvvmCrossの標準動作では、UIViewControllerはUINavigationControllerの子要素となる。iOSでのUINavigationControllerのプッシュ/ポップ操作や、AndroidでのActivity Intentの発行など、プラットフォームごとに扱いは異なるが、MvvmCrossによってその差は吸収されるため、Coreプロジェクトの単一実装で画面制御を実現できる。

 IMvxViewPresenterインターフェースを実装したクラスによりプラットフォームごとの画面制御が行われている。今回はMvvmCross標準のプレゼンターを利用するが、これを独自に実装することでAndroidのIntent操作やActivity Stackの管理など、各プラットフォームで詳細に画面遷移をカスタマイズできる*2

実装方針

 MVVM設計では、Viewはできる限り画面表示に専念し、ビジネスロジックやナビゲーションロジックはModelやViewModelで実装することになる。今回は画面遷移をViewModelに実装し、Viewはバインディングのみ実装する。

 最初に、遷移先となるSecondViewを作成し、FirstViewSecondViewの間で単純な画面遷移の実装を確認する。次に、遷移元のViewModelから遷移先のViewModelへパラメーターを渡す実装を確認する。

 それではこの手順でサンプルを作成してみよう。

画面追加と画面遷移の実装

プロジェクトの作成

 「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。ソリューション名は「CrossNavigationSample」と設定する。

Coreプロジェクトの実装

 遷移先となる画面のViewModelを用意する。

 [ソリューション]ビューからCrossNavigationSample.CoreプロジェクトのViewModelsフォルダーを右クリック-[追加]-[新しいファイル]を選択し、(それにより表示される)[新しいファイル]ダイアログから[General]-[空のクラス]を選択する。名前を「SecondViewModel」と入力し[新規]ボタンをクリックする。

 作成されたSecondViewModel.csファイルを次のように修正する。

C#
using System;
using Cirrious.MvvmCross.ViewModels;

namespace CrossNavigationSample.Core.ViewModels {
  public class SecondViewModel : MvxViewModel {
    public IMvxCommand BackCommand {
      get {
        return _backCommand ?? (_backCommand = new MvxCommand(() => {
          // 1
          Close(this);
        }));
      }
    }
    IMvxCommand _backCommand;
  }
}
BackCommandを実装する(SecondViewModel.cs)

 MvxViewModelクラスのClose()メソッドは、引数に受け取ったViewModelに対応する画面を終了する。つまり1は、SecondViewを閉じて、遷移元の画面へ戻る処理である。これにより、ViewクラスからBackCommandプロパティをバインディングすることで、画面を閉じることができるようになる。

 次に、FirstViewModelクラスを実装する。CrossNavigationSample.CoreプロジェクトのViewModelsフォルダー内にあるFirstViewModel.csファイルを次のように修正する。

C#
using Cirrious.MvvmCross.ViewModels;
using Cirrious.CrossCore;

namespace CrossNavigationSample.Core.ViewModels {
  public class FirstViewModel : MvxViewModel {
    public IMvxCommand GoToSecondCommand {
      get {
        return _goToSecondCommand ?? (_goToSecondCommand = new MvxCommand(() => {
          // 1
          ShowViewModel<SecondViewModel>();
        }));
      }
    }
    private IMvxCommand _goToSecondCommand;
  }
}
画面遷移を実装する(FirstViewModel.cs)

 1はSecondViewへ遷移する処理である。MvxViewModelクラスのShowViewModel()メソッドは、遷移先となるViewModelの型(=IMvxViewModelインターフェースを継承したクラスの型引数)を受け取り、指定された型のViewModelクラスと、それに対応するViewクラスのインスタンスを生成し、それを表示する。

Touchプロジェクトの実装

 Touchプロジェクトではレイアウトを作成し、バインディングを定義する。今回は.xibファイルを用いてFirstViewとSecondViewのレイアウトを作成する。

 FirstViewを作成するにはまず、[ソリューション]ビューからCrossNavigationSample.TouchプロジェクトのViewsフォルダーにあるFirstView.csを右クリック-[削除]-削除確認ダイアログの[削除]ボタンをクリックし、ファイルを削除する。次に、同じくCrossNavigationSample.TouchプロジェクトのViewsフォルダーを右クリック-[追加]-[新しいファイル]-[iOS]-[iPhone View Controller]を選択し、名前欄に「FirstView」と設定して[新規]ボタンをクリックする(図1)。

図1 FirstView.csファイルとFirstView.xibファイルの作成

なお本稿のサンプルコードのまま試すには、FirstView/SecondViewクラスの名前空間が「CrossNavigationSample.Touch」ではなく「CrossNavigationSample.Touch.Views」と、ディレクトリ名に関連付けられた名前空間になっている必要がある(Xamarin Studioのデフォルト設定では関連付いていない)。これには、[設定](Mac)/[オプション](Windows)ダイアログの左側のツリーから[ソースコード]-[.NETの命名ポリシー]を選択し、右側から[名前空間をディレクトリ名に関連付ける]、[既定の名前空間をrootとして使用]というチェックボックスそれぞれにチェックを入れ、[ディレクトリ構造:]欄で[フラット]を選択して[OK]ボタンで保存すればよい。

 ViewsフォルダーへFirstView.xibファイルおよびFirstView.csファイルが作成され、FirstView.xibをダブルクリックするとXcodeが起動する。Xcodeから図2のようなレイアウトを作成し、GoToSecondButtonの名前でUIButton(=iOS標準のボタン)のアウトレットを作成する*3。念のため、いったんここで保存しておこう。

図2 FirstView.xibのレイアウト

 FirstViewと同様の手順でSecondView.xibファイルおよびSecondView.csファイルを作成する。SecondView.xibをダブルクリックし、図3のようにレイアウトを作成する。

図3 SecondView.xibのレイアウト

 次にバインディングを定義する。ViewsフォルダーのFirstView.csを次のように修正する。

C#
using System;

using Foundation;
using UIKit;
using Cirrious.MvvmCross.Touch.Views;
using Cirrious.MvvmCross.Binding.BindingContext;
using CrossNavigationSample.Core.ViewModels;

namespace CrossNavigationSample.Touch.Views {
  public partial class FirstView : MvxViewController {  // 1
    public override void ViewDidLoad() {
      base.ViewDidLoad();
      Title = "FirstView";

      // 2
      var set = this.CreateBindingSet<FirstView, FirstViewModel>();
      set.Bind(GoToSecondButton).To(vm => vm.GoToSecondCommand);
      set.Apply();
    }
  }
}
バインディングの設定(FirstView.cs)

 1では、継承元のクラスをMvxViewControllerへ変更していることに注意してほしい。MvxViewControllerクラスは、バインディングに対応したUIViewControllerである。

 2でバインディングを設定している。Viewクラスの実装はバインディングのみとなる。

 同様に、ViewsフォルダーのSecondView.csを次のように修正する。

C#
using System;

using Foundation;
using UIKit;
using Cirrious.MvvmCross.Touch.Views;
using Cirrious.MvvmCross.Binding.BindingContext;
using CoreImage;
using CrossNavigationSample.Core.ViewModels;

namespace CrossNavigationSample.Touch.Views {
  public partial class SecondView : MvxViewController {
    public override void ViewDidLoad() {
      base.ViewDidLoad();
      Title = "SecondView";

      var set = this.CreateBindingSet<SecondView, SecondViewModel>();
      set.Bind(BackButton).To(vm => vm.BackCommand);
      set.Apply();
    }
  }
}
バインディングの設定(SecondView.cs)

Droidプロジェクトの実装

 Droidプロジェクトについてもレイアウトを作成し、バインディングを定義する。

 まずはFirstViewを編集する。[ソリューションビュー]からCrossNavigationSample.DroidプロジェクトのResourceslayoutフォルダーにあるFirstView.axmlファイルを次のように編集する。

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:local="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <Button
    android:text="Go to SecondView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    local:MvxBind="Click GoToSecondCommand" />
</LinearLayout>
レイアウトの作成(FirstView.axml)

 次に、同じくlayoutフォルダーを右クリックし、[追加]-[新しいファイル]-[Android]-[Layout]を選択し、名前欄に「SecondView」と設定して[新規]ボタンをクリックする(図4)。

図4 SecondView.axmlファイルの作成

 layoutフォルダーへ作成されたSecondView.axmlファイルを次のように編集する。

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:local="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <Button
    android:text="Back to FirstView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    local:MvxBind="Click BackCommand" />
</LinearLayout>
レイアウトの作成(SecondView.axml)

 Androidでは、バインディングは.axmlファイルへ定義できるため、クラスファイルの実装は必要最小限のものとなる。CrossNavigationSample.DroidプロジェクトのViewsフォルダーを右クリックし[追加]-[新しいファイル]-[Android]-[Activity]と選択し、「SecondView」と設定して[新規]ボタンをクリックする(図5)。

図5 SecondView.csファイルの作成

 作成されたSecondView.csファイルを次のように編集する。

C#
using Android.App;
using Android.OS;
using Cirrious.MvvmCross.Droid.Views;

namespace CrossNavigationSample.Droid.Views {
  [Activity (Label = "SecondView")]
  public class SecondView : MvxActivity { // 1
    protected override void OnCreate (Bundle bundle) {
      base.OnCreate (bundle);
      SetContentView(Resource.Layout.SecondView);
    }
  }
}
Viewクラスの実装(SecondView.cs)

 1では継承元クラスをMvxActivityに変更している。MvxActivityはバインディングに対応したActivityである。

 なお、CrossNavigationSample.Droidプロジェクトに存在しているFirstView.csファイルは修正する必要はない。

アプリケーションの実行

 ここまでで、単純な画面遷移実装が完了した。それぞれのアプリケーションを実行すると、まずFirstView画面が表示され、画面内の[Go to SecondView]ボタンをタップするとSecondView画面へ遷移する。その後、SecondView画面で[Back to FirstView]ボタンをタップするとSecondView画面を閉じ、FirstView画面が表示される(図6)。

図6 アプリケーションの実行結果

パラメーターを用いた画面遷移の実装

 MvvmCrossのプロジェクトでは、画面は任意のパラメーターを受け取ることができる。これを試すために、SecondViewを文字列と数値の2つのパラメーターを受け取るように拡張し、FirstViewからSecondViewをパラメーター付きで呼び出すように修正してみよう。

Coreプロジェクトの修正

 CrossNavigationSample.CoreプロジェクトのSecondViewModel.csファイルを次のように修正する。

C#
using System;
using Cirrious.MvvmCross.ViewModels;

namespace CrossNavigationSample.Core.ViewModels {
  public class SecondViewModel : MvxViewModel {
    // -- ▼▼▼▼ ここから追加 ▼▼▼▼ --
    // 1
    public class SecondViewParameter {
      public string Message { get; set; }
      public int Number { get; set; }
    }

    // 2
    public void Init(SecondViewParameter param) {
      if (param != null) {
        Message = "Receive : " + param.Message + ", " + param.Number;
      }
    }

    // 3
    public string Message {
      get {
        return _message;
      }
      set {
        _message = value;
        RaisePropertyChanged (() => Message);
      }
    }
    string _message;
    // --  ▲▲▲▲ ここまで追加 ▲▲▲▲  --

    public IMvxCommand BackCommand {
      get {
        return _backCommand ?? (_backCommand = new MvxCommand(() => {
          Close(this);
        }));
      }
    }
    IMvxCommand _backCommand;
  }
}
パラメーター受け取り部分の実装(SecondViewModel.cs)

 MvvmCrossではInit()という名前のメソッドを定義すると、パラメーターを渡されてViewが起動したときに、その引数が渡された上で、Init()メソッドが実行される。

 1により、引数となるSecondViewParameterクラスを定義している。SecondViewParameterはstring型のMessageプロパティとint型のNumberプロパティをメンバーとして保持する。

 2により、SecondViewParameter型のオブジェクトを引数として受け取るInit()メソッドを定義している。これによりSecondViewModelクラスは、そのインスタンス生成時にSecondViewParameterオブジェクトおよび、そのメンバーのMessageNumberを受け取ることになる。Init()メソッド内では、その引数を3Messageプロパティへ設定する。3Messageプロパティは画面表示用であり、Viewからバインディングされる。

 Init()メソッドは、継承元であるMvxViewModelクラスでは定義されておらず、オーバーライドもしていない。これはMvvmCrossがInit()メソッドをリフレクションによって検出しているためである。そのため、メソッド名のスペルミスには注意する必要がある。

 次に、CrossNavigationSample.CoreプロジェクトのFirstViewModel.csを以下の通り編集する。

C#
using Cirrious.MvvmCross.ViewModels;
using Cirrious.CrossCore;

namespace CrossNavigationSample.Core.ViewModels {
  public class FirstViewModel : MvxViewModel {
    // 1
    public string Message {
      get { return _message; }
      set {
        _message = value;
        RaisePropertyChanged (() => Message);
      }
    }
    string _message;

    public IMvxCommand GoToSecondCommand {
      get {
        return _goToSecondCommand ?? (_goToSecondCommand = new MvxCommand(() => {
          // 2
          var param = new SecondViewModel.SecondViewParameter{
            Message = this.Message,
            Number = 42
          };
          ShowViewModel<SecondViewModel>(param);
        }));
      }
    }
    private IMvxCommand _goToSecondCommand;
  }
}
パラメーター付き画面呼び出し(FirstViewModel.cs)

 1ではバインディング用のMessageプロパティを定義している。これはViewからバインディングし、入力プロパティとして用いる。

 2では引数となるSecondViewParameterクラスのインスタンスを生成し、ShowViewModel()メソッドによりSecondViewへ引数として渡している。

Touchプロジェクトの修正

 CrossNavigationSample.TouchプロジェクトのFirstView.xibファイルをダブルクリックし、Xcodeから図7のようにレイアウトを修正する。新しく追加したUITextFieldクラス(=iOS標準のテキストフィールド)に対してMessageTextという名前でアウトレットを定義する。

図7 FirstViewのレイアウト

 同様にSecondView.xibファイルを図8のようにレイアウトを修正する。新しく追加したUILabelクラス(=iOS標準のラベル)に対してMessageTextという名前でアウトレットを定義する。

図8 SecondViewのレイアウト

 CrossNavigationSample.TouchプロジェクトのFirstView.csファイルを次のように修正する。

C#
using System;

using Foundation;
using UIKit;
using Cirrious.MvvmCross.Touch.Views;
using Cirrious.MvvmCross.Binding.BindingContext;
using CrossNavigationSample.Core.ViewModels;
using Accelerate;

namespace CrossNavigationSample.Touch.Views {
  public partial class FirstView : MvxViewController {
    public override void ViewDidLoad() {
      base.ViewDidLoad();
      Title = "FirstView";

      var set = this.CreateBindingSet<FirstView, FirstViewModel>();
      set.Bind(MessageText).To(vm => vm.Message);  // この一行を追加する
      set.Bind(GoToSecondButton).To(vm => vm.GoToSecondCommand);
      set.Apply();
    }
  }
}
MessageTextバインディングの追加(FirstView.cs)

 同様にSecondView.csファイルを次のように修正する。

C#
using System;

using Foundation;
using UIKit;
using Cirrious.MvvmCross.Touch.Views;
using Cirrious.MvvmCross.Binding.BindingContext;
using CoreImage;
using CrossNavigationSample.Core.ViewModels;

namespace CrossNavigationSample.Touch.Views {
  public partial class SecondView : MvxViewController {
    public override void ViewDidLoad() {
      base.ViewDidLoad();
      Title = "SecondView";

      var set = this.CreateBindingSet<SecondView, SecondViewModel>();
      set.Bind(MessageText).To(vm => vm.Message);  // この一行を追加する
      set.Bind(BackButton).To(vm => vm.BackCommand);
      set.Apply();
    }
  }
}
MessageTextバインディングの追加(SecondView.cs)

Droidプロジェクトの修正

 CrossNavigationSample.DroidプロジェクトのFirstView.axmlファイルを以下のように修正する。ここで追加するEditTextウィジェット(=Android標準のエディットテキスト)は、FirstViewModelクラスのMessageプロパティへバインドしている。

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:local="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <!-- ▼▼▼▼ ここから追加 ▼▼▼▼ -->
  <EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    local:MvxBind="Text Message" />
  <!--  ▲▲▲▲ ここまで追加 ▲▲▲▲  -->
  <Button
    android:text="Go to SecondView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    local:MvxBind="Click GoToSecondCommand" />
</LinearLayout>
EditTextの追加(FirstView.axml)

 同様に、SecondView.axmlファイルを以下のように修正する。ここで追加するTextViewウィジェット(=Android標準のテキストビュー)はSecondViewModelクラスのMessageプロパティへバインドしている。

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:local="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <!-- ▼▼▼▼ ここから追加 ▼▼▼▼ -->
  <TextView
    android:text=""
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    local:MvxBind="Text Message" />
  <!--  ▲▲▲▲ ここまで追加 ▲▲▲▲  -->
  <Button
    android:text="Back to FirstView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    local:MvxBind="Click BackCommand" />
</LinearLayout>
TextViewの追加(SecondView.axml)

アプリケーションの実行

 ここでアプリケーションを実行すると、それぞれ以下のような画面となる。FirstViewで入力した文字列がSecondViewへパラメーターとして渡されている様子が確認できる(図9)。

図9 パラメーターありのアプリケーションの実行結果

パラメーターの型について

 本稿で定義したSecondViewParameterクラスはstring型と、int型のプロパティを定義していた。MvvmCrossではこのViewModelのパラメーターとして用意するクラスは、シリアライズ可能でなくてはならない。具体的には、パラメータークラスのメンバープロパティは次の型に制限される。

  • int型
  • long型
  • double型
  • string型
  • Guidクラス
  • 列挙型

 Androidの画面となるActivityクラスはAndroid OSのライフサイクルに従って管理されるが、このActivityがパラメーターとして受け取ることのできるBundleクラスはシリアライズ可能なクラスである。MvvmCrossはAndroidアプリケーションの画面パラメーターを引き渡すときに、このBundleクラスを利用しているため、上記のような型制限の制約を受けている。

パラメーター引数の匿名クラス化について

 本稿ではSecondViewParameterクラスをパラメータークラスとして定義してそれを用いたが、パラメーターとして匿名クラスを受け渡しすることも可能である。匿名クラスを用いると、パラメータークラスを別途定義する手順を省略でき、パラメーターが簡易なものである場合には便利な表記となる。

 本稿の例では、呼び出しコードとInit()メソッドを次のように実装することで、パラメータークラスを用いた場合と同様の動作となる。匿名クラスのプロパティ名とInit()メソッドの引数名が合致していることに注意してほしい。

C#
……省略……
  ShowViewModel<SecondViewModel>(
    new {
      message = this.Message,
      number = 42
    }
  );
……省略……
匿名クラスを用いた呼び出し側のコード(FirstViewModel.cs)
C#
……省略……
  public void Init(string message, int number) {
    Message = "Receive : " + message + ", " + number;
  }
……省略……
匿名クラスを用いたInit()側のコード(SecondViewModel.cs)

 匿名クラスを用いた簡易実装はクラス定義が不要という簡便さのメリットはあるが、呼び出し側のコードの実装時にコンパイル時の型チェックやスペルミスに気付きにくいというデメリットもあるため注意して利用すること。

※以下では、本稿の前後を合わせて5回分(第45回~第49回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

45. Xamarin.Formsでトリガーを使用するには?

イベントやプロパティの変化に応じたコントロールの外観の変更をXAMLだけで実装できるトリガーの基本的な使用方法を解説する。

46. Xamarin.FormsでWebビューを使用するには?

外部のWebページやローカルに配置されたHTMLコンテンツを簡単に表示できるWebViewコントロールをXamarin.Formsで使う方法を説明する。

47. 【現在、表示中】≫ MvvmCrossで画面遷移するには?

MvvmCrossでiOS/Androidアプリの画面遷移をするための基本的な実装方法を説明する。

48. Xamarin.Formsでプラットフォームごとの微調整を行うには?

カスタムレンダラーやDependencyServiceの仕組みを使わず、Deviceクラスを利用してプラットフォーム間で異なる部分を微調整する方法を説明する。

49. MvvmCrossでAndroidの画面の再生成に対応するには?

Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。

サイトからのお知らせ

Twitterでつぶやこう!