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のライフサイクルメソッドにより行う方法を説明する。