 
Xamarin逆引きTips
MvvmCrossで画面遷移するには?
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。
- *2 MvvmCross標準のプレゼンター実装は、GitHubから確認できる。iOSはMvxTouchViewPresenterクラス、AndroidはMvxAndroidViewPresenterクラスを確認しておくとよい。
実装方針
MVVM設計では、Viewはできる限り画面表示に専念し、ビジネスロジックやナビゲーションロジックはModelやViewModelで実装することになる。今回は画面遷移をViewModelに実装し、Viewはバインディングのみ実装する。
 最初に、遷移先となるSecondViewを作成し、FirstViewとSecondViewの間で単純な画面遷移の実装を確認する。次に、遷移元のViewModelから遷移先のViewModelへパラメーターを渡す実装を確認する。
それではこの手順でサンプルを作成してみよう。
画面追加と画面遷移の実装
プロジェクトの作成
「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。ソリューション名は「CrossNavigationSample」と設定する。
Coreプロジェクトの実装
遷移先となる画面のViewModelを用意する。
 [ソリューション]ビューからCrossNavigationSample.CoreプロジェクトのViewModelsフォルダーを右クリック-[追加]-[新しいファイル]を選択し、(それにより表示される)[新しいファイル]ダイアログから[General]-[空のクラス]を選択する。名前を「SecondViewModel」と入力し[新規]ボタンをクリックする。
 作成されたSecondViewModel.csファイルを次のように修正する。
| 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;   } } | 
 MvxViewModelクラスのClose()メソッドは、引数に受け取ったViewModelに対応する画面を終了する。つまり1は、SecondViewを閉じて、遷移元の画面へ戻る処理である。これにより、ViewクラスからBackCommandプロパティをバインディングすることで、画面を閉じることができるようになる。
 次に、FirstViewModelクラスを実装する。CrossNavigationSample.CoreプロジェクトのViewModelsフォルダー内にあるFirstViewModel.csファイルを次のように修正する。
| 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;   } } | 
 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)。
※なお本稿のサンプルコードのまま試すには、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。念のため、いったんここで保存しておこう。
- *3 Xamarin StudioとXcodeの連携やXcode Interface Builderの利用はXamarin.iOSの機能であるため、本稿での説明は割愛する(※使い方がよく分からない場合は、「Tips:Xamarin.iOSで画面をレイアウトするには?(Xcode利用/ビルトインiOS用UIデザイナー) - Build Insider」を参考にされたい)。Xcodeを利用できない環境では、Xibファイルを用いずにC#のコードによってレイアウトを作成しても構わない。
 FirstViewと同様の手順でSecondView.xibファイルおよびSecondView.csファイルを作成する。SecondView.xibをダブルクリックし、図3のようにレイアウトを作成する。
 次にバインディングを定義する。ViewsフォルダーのFirstView.csを次のように修正する。
| 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();     }   } } | 
 1では、継承元のクラスをMvxViewControllerへ変更していることに注意してほしい。MvxViewControllerクラスは、バインディングに対応したUIViewControllerである。
2でバインディングを設定している。Viewクラスの実装はバインディングのみとなる。
 同様に、ViewsフォルダーのSecondView.csを次のように修正する。
| 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();     }   } } | 
Droidプロジェクトの実装
Droidプロジェクトについてもレイアウトを作成し、バインディングを定義する。
 まずはFirstViewを編集する。[ソリューションビュー]からCrossNavigationSample.DroidプロジェクトのResources-layoutフォルダーにあるFirstView.axmlファイルを次のように編集する。
| <?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> | 
 次に、同じくlayoutフォルダーを右クリックし、[追加]-[新しいファイル]-[Android]-[Layout]を選択し、名前欄に「SecondView」と設定して[新規]ボタンをクリックする(図4)。
 layoutフォルダーへ作成されたSecondView.axmlファイルを次のように編集する。
| <?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> | 
 Androidでは、バインディングは.axmlファイルへ定義できるため、クラスファイルの実装は必要最小限のものとなる。CrossNavigationSample.DroidプロジェクトのViewsフォルダーを右クリックし[追加]-[新しいファイル]-[Android]-[Activity]と選択し、「SecondView」と設定して[新規]ボタンをクリックする(図5)。
 作成されたSecondView.csファイルを次のように編集する。
| 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);     }   } } | 
 1では継承元クラスをMvxActivityに変更している。MvxActivityはバインディングに対応したActivityである。
 なお、CrossNavigationSample.Droidプロジェクトに存在しているFirstView.csファイルは修正する必要はない。
アプリケーションの実行
ここまでで、単純な画面遷移実装が完了した。それぞれのアプリケーションを実行すると、まずFirstView画面が表示され、画面内の[Go to SecondView]ボタンをタップするとSecondView画面へ遷移する。その後、SecondView画面で[Back to FirstView]ボタンをタップするとSecondView画面を閉じ、FirstView画面が表示される(図6)。
パラメーターを用いた画面遷移の実装
MvvmCrossのプロジェクトでは、画面は任意のパラメーターを受け取ることができる。これを試すために、SecondViewを文字列と数値の2つのパラメーターを受け取るように拡張し、FirstViewからSecondViewをパラメーター付きで呼び出すように修正してみよう。
Coreプロジェクトの修正
 CrossNavigationSample.CoreプロジェクトのSecondViewModel.csファイルを次のように修正する。
| 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;   } } | 
 MvvmCrossではInit()という名前のメソッドを定義すると、パラメーターを渡されてViewが起動したときに、その引数が渡された上で、Init()メソッドが実行される。
 1により、引数となるSecondViewParameterクラスを定義している。SecondViewParameterはstring型のMessageプロパティとint型のNumberプロパティをメンバーとして保持する。
 2により、SecondViewParameter型のオブジェクトを引数として受け取るInit()メソッドを定義している。これによりSecondViewModelクラスは、そのインスタンス生成時にSecondViewParameterオブジェクトおよび、そのメンバーのMessageとNumberを受け取ることになる。Init()メソッド内では、その引数を3のMessageプロパティへ設定する。3のMessageプロパティは画面表示用であり、Viewからバインディングされる。
 Init()メソッドは、継承元であるMvxViewModelクラスでは定義されておらず、オーバーライドもしていない。これはMvvmCrossがInit()メソッドをリフレクションによって検出しているためである。そのため、メソッド名のスペルミスには注意する必要がある。
 次に、CrossNavigationSample.CoreプロジェクトのFirstViewModel.csを以下の通り編集する。
| 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;   } } | 
 1ではバインディング用のMessageプロパティを定義している。これはViewからバインディングし、入力プロパティとして用いる。
 2では引数となるSecondViewParameterクラスのインスタンスを生成し、ShowViewModel()メソッドによりSecondViewへ引数として渡している。
Touchプロジェクトの修正
 CrossNavigationSample.TouchプロジェクトのFirstView.xibファイルをダブルクリックし、Xcodeから図7のようにレイアウトを修正する。新しく追加したUITextFieldクラス(=iOS標準のテキストフィールド)に対してMessageTextという名前でアウトレットを定義する。
 同様にSecondView.xibファイルを図8のようにレイアウトを修正する。新しく追加したUILabelクラス(=iOS標準のラベル)に対してMessageTextという名前でアウトレットを定義する。
 CrossNavigationSample.TouchプロジェクトのFirstView.csファイルを次のように修正する。
| 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();     }   } } | 
 同様にSecondView.csファイルを次のように修正する。
| 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();     }   } } | 
Droidプロジェクトの修正
 CrossNavigationSample.DroidプロジェクトのFirstView.axmlファイルを以下のように修正する。ここで追加するEditTextウィジェット(=Android標準のエディットテキスト)は、FirstViewModelクラスのMessageプロパティへバインドしている。
| <?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> | 
 同様に、SecondView.axmlファイルを以下のように修正する。ここで追加するTextViewウィジェット(=Android標準のテキストビュー)はSecondViewModelクラスのMessageプロパティへバインドしている。
| <?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> | 
アプリケーションの実行
ここでアプリケーションを実行すると、それぞれ以下のような画面となる。FirstViewで入力した文字列がSecondViewへパラメーターとして渡されている様子が確認できる(図9)。
パラメーターの型について
 本稿で定義したSecondViewParameterクラスはstring型と、int型のプロパティを定義していた。MvvmCrossではこのViewModelのパラメーターとして用意するクラスは、シリアライズ可能でなくてはならない。具体的には、パラメータークラスのメンバープロパティは次の型に制限される。
- int型
- long型
- double型
- string型
- Guidクラス
- 列挙型
 Androidの画面となるActivityクラスはAndroid OSのライフサイクルに従って管理されるが、このActivityがパラメーターとして受け取ることのできるBundleクラスはシリアライズ可能なクラスである。MvvmCrossはAndroidアプリケーションの画面パラメーターを引き渡すときに、このBundleクラスを利用しているため、上記のような型制限の制約を受けている。
パラメーター引数の匿名クラス化について
 本稿ではSecondViewParameterクラスをパラメータークラスとして定義してそれを用いたが、パラメーターとして匿名クラスを受け渡しすることも可能である。匿名クラスを用いると、パラメータークラスを別途定義する手順を省略でき、パラメーターが簡易なものである場合には便利な表記となる。
 本稿の例では、呼び出しコードとInit()メソッドを次のように実装することで、パラメータークラスを用いた場合と同様の動作となる。匿名クラスのプロパティ名とInit()メソッドの引数名が合致していることに注意してほしい。
| ……省略……   ShowViewModel<SecondViewModel>(     new {       message = this.Message,       number = 42     }   ); ……省略…… | 
| ……省略……   public void Init(string message, int number) {     Message = "Receive : " + message + ", " + number;   } ……省略…… | 
匿名クラスを用いた簡易実装はクラス定義が不要という簡便さのメリットはあるが、呼び出し側のコードの実装時にコンパイル時の型チェックやスペルミスに気付きにくいというデメリットもあるため注意して利用すること。
※以下では、本稿の前後を合わせて5回分(第45回~第49回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
 
46. Xamarin.FormsでWebビューを使用するには?
外部のWebページやローカルに配置されたHTMLコンテンツを簡単に表示できるWebViewコントロールをXamarin.Formsで使う方法を説明する。
 
48. Xamarin.Formsでプラットフォームごとの微調整を行うには?
カスタムレンダラーやDependencyServiceの仕組みを使わず、Deviceクラスを利用してプラットフォーム間で異なる部分を微調整する方法を説明する。
 
49. MvvmCrossでAndroidの画面の再生成に対応するには?
Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。





 
  
  
  
  
  
  
  
  
 
