Xamarin逆引きTips
MvvmCrossでコマンドバインディングをするには?
MvvmCrossでは、画面でのイベント発生をViewModelに通知するためにコマンドバインディングを使用する。iOS/Androidにおける、その基本的な実装方法を説明する。
MvvmCrossを使ったiOS/Androidアプリ開発では、「ボタンが押された」「テキストが入力された」といった画面での各種イベントの発生をViewModelに通知するために、コマンドのバインディングを使用する。
今回は、ViewModelにコマンドを定義して、iOS/Android各プラットフォームでのイベント発生をViewModelに伝達する方法を解説する。
1コマンドバインディングについて
MVVMでは、「ボタンがクリックされる」などのユーザーが行った操作をViewModelに伝える手段として、コマンドを使用する。コマンドオブジェクトは、「操作の実行」/「操作の可否判定」を担当する2つのメソッドと、「操作の可否が変更されたことを検知するイベント」を持っている。
これらはWPFでは、ICommandインターフェース(System.Windows.Input名前空間)として定義されており、そのインターフェースを実装したコマンドオブジェクトを、コントロールのCommandプロパティにバインディングして使用する。
MvvmCrossでは、コマンドはMvxCommandクラス(Cirrious.MvvmCross.ViewModels名前空間)を使用する。MvxCommandクラスもICommandインターフェースを実装しているため、WPFやWindowsストアアプリでMvvmCrossを使う場合は、CommandプロパティにMvxCommandクラスをバインディングして使用できる。
また、iOSやAndroidの標準コントロールは(WPFと違って)Commandプロパティを持たないため、そのイベントにバインディングできる仕組みになっている。ただし、MvvmCrossで独自拡張されているコントロールにはイベントではなく、ICommand型のプロパティが実装されている場合がある。例えばAndroidでは、MvvmCrossが提供するリストビューウィジェットであるMvxListViewのItemClickプロパティの型はICommandである。この場合はMvxCommandオブジェクトを直接バインディングして使用する。独自にカスタムコントロールを実装し、ICommand型のプロパティを提供することもできる。
2コマンドを定義する
今回は例として、ボタンをタップすると、テキストの数字が1ずつカウントアップするプログラムを作成する。「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順で「CommandBindingSample」という名前のプロジェクトが用意されている状態で始める。
まずはViewModel側にコマンドと、そのコマンド実行によって値が変化するプロパティを定義する。具体的には、CommandBindingSample.CoreプロジェクトのViewModelsフォルダー内にあるFirstViewModel.csファイルを次のように修正する。
|
using Cirrious.MvvmCross.ViewModels;
namespace CommandBindingSample.Core.ViewModels
{
public class FirstViewModel : MvxViewModel
{
int _count = 0;
public int Count
{
get { return _count; }
set {
_count = value;
RaisePropertyChanged(() => Count);
}
}
MvxCommand _countUpCommand;
public MvxCommand CountUpCommand
{
get
{
return _countUpCommand ?? (_countUpCommand = new MvxCommand(() => // 1
{
Count++;
}));
}
}
}
}
|
コマンドはMvxCommand型のプロパティとして定義する。MvxCommandクラスのコンストラクター第1引数に、コマンドが実行されるときの処理(このコード例ではCount++している)を指定する。1のように、??演算子を使って定義することにより、このプロパティが最初に使用される際に、コマンドオブジェクト(=MvxCommandオブジェクト)が生成される。使用されない場合、オブジェクトは生成されない。
3コマンドをバインディングする
バインディングの基本的な部分や手順は、「Tips:MvvmCrossでプロパティバインディングをするには?」とあまり変わらない。Androidの場合は、Android Layout XMLファイル(.axml)の中で、local:MvxBind属性に記述できるが、iOSでイベントにバインディングする場合は、多少記述に制限がある。
先ほど2で作ったコマンドとプロパティにバインディングするコードを記述してみよう。iOSでは、CommandBindingSample.TouchプロジェクトのViewsフォルダー内にあるFirstView.csファイルを開き、以下のように記述する。
|
using Cirrious.MvvmCross.Binding.BindingContext;
using Cirrious.MvvmCross.Touch.Views;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;
namespace CommandBindingSample.Touch.Views
{
[Register("FirstView")]
public class FirstView : MvxViewController
{
public override void ViewDidLoad()
{
View = new UIView { BackgroundColor = UIColor.White };
base.ViewDidLoad();
// ios7 layout
if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
{
EdgesForExtendedLayout = UIRectEdge.None;
}
// 画面の実装
var label = new UILabel(new CGRect(10, 10, 300, 40));
Add(label);
var countUpButton = new UIButton(UIButtonType.System)
{
Frame = new CGRect(10, 50, 90, 40)
};
Add(countUpButton);
countUpButton.SetTitle("CountUp", UIControlState.Normal); // 1
// バインディングの指定
var set = this.CreateBindingSet<FirstView, Core.ViewModels.FirstViewModel>();
set.Bind(label).To(vm => vm.Count);
set.Bind(countUpButton).For("TouchUpInside").To(vm => vm.CountUpCommand); // 2
set.Apply();
}
}
}
|
全体としては「Tips:MvvmCrossでプロパティバインディングをするには?」と変わらないが、1の部分ではボタンを作ってボタンのキャプションを設定している。
実際にコマンドにバインディングしているのは、2の部分だ。プロパティではなく、イベントにバインディングする場合は、Forメソッドで対象となるイベントの名前(この例ではTouchUpInside)を文字列で指定する必要がある。MvvmCross独自拡張(前述したMvxListViewのItemClickプロパティなど)や自分で実装したICommand型のプロパティの場合は、このForメソッドで式木を使ったバインディングができる。また、先述の記事に記載のある通り、UIButtonやUIBarButtonItemはデフォルトのバインディング先がそれぞれタップ時のイベントとなっているため、Forメソッド自体を省略できる。
また、AndroidはCommandBindingSample.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="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="40dp"
local:MvxBind="Text Count" />
<!-- 1 -->
<Button
android:text="CountUp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Click CountUpCommand" />
</LinearLayout>
|
1のlocal:MvxBind属性でコマンドにバインディングしているが、その前の<TextView>要素のlocal:MvxBind属性と変わりのない記述となっていることが分かる。
このアプリを実行すると、iOS、Android共に数字とボタンが表示されたアプリが起動する(図1)。ボタンを押すたびに表示されている値が1加算される。
4コマンドの実行可否を制御する
WPFなどでは、ICommandインターフェースのCanExecuteメソッドで実行可否の状態を判断し、その判断結果がバインディング先のボタンの有効/無効にまで波及する。その状態変化はCanExecuteChangedイベントで監視されている。MvxCommandクラスにも同じインターフェースの実装メソッドが用意されており、それらのメソッドを簡便に利用するため、コンストラクターの第2引数でCanExecuteメソッドの内容を記述できたり、値変更イベントを発動させるRaiseCanExecuteChangedメソッドが用意されたりしている。
しかしながらiOSとAndroidには、現時点ではボタンの有効/無効にまで波及させるような実装は行われていない。このため、実行可否の状態を表すプロパティ(以下、状態プロパティ)を用意し、MvxCommandクラスのCanExecuteメソッドで、この状態プロパティの値を返すように実装する。iOS/Androidでは状態プロパティを、コントロールのEnabledプロパティにバインディングする(なお、WPFではCanExecuteメソッドが使用されるので、コマンドのバインディングのみでViewの実装が済む)。
実行可否の状態を判断する例として、ボタンに表示されている数字が偶数の場合のみ、表示されている数字を3カウントアップするボタンを、先ほどのプログラムに追加してみよう。これには、CommandBindingSample.CoreプロジェクトのFirstViewModel.csファイルに以下のように追記する。
|
using Cirrious.MvvmCross.ViewModels;
namespace CommandBindingSample.Core.ViewModels
{
public class FirstViewModel
: MvxViewModel
{
int _count = 0;
public int Count
{
get { return _count; }
set {
_count = value;
RaisePropertyChanged(() => Count);
// -- ▼▼▼▼ ここから追記 ▼▼▼▼ --
RaisePropertyChanged(() => ThreeCountUpCanExecute); // 3
ThreeCountUpCommand.RaiseCanExecuteChanged(); // 4
// -- ▲▲▲▲ ここまで追記 ▲▲▲▲ --
}
}
MvxCommand _countUpCommand;
public MvxCommand CountUpCommand
{
get
{
return _countUpCommand ?? (_countUpCommand = new MvxCommand(() =>
{
Count++;
}));
}
}
// -- ▼▼▼▼ ここから追記 ▼▼▼▼ --
MvxCommand _threeCountUpCommand;
public MvxCommand ThreeCountUpCommand // 1
{
get
{
return _threeCountUpCommand ?? (_threeCountUpCommand = new MvxCommand(() =>
{
Count += 3;
}, () => ThreeCountUpCanExecute));
}
}
public bool ThreeCountUpCanExecute // 2
{
get { return (Count % 2) == 0; }
}
// -- ▲▲▲▲ ここまで追記 ▲▲▲▲ --
}
}
|
このコード例では、3カウントアップのコマンドを新たに定義した(1)。MvxCommandクラスのコンストラクター第2引数に、このコマンドが使用できるかを判断してbool値を返すラムダ式が指定され、そこでThreeCountUpCanExecuteプロパティが使われている。このThreeCountUpCanExecuteプロパティ(2)は、Countプロパティが偶数ならtrue、奇数ならfalseを返すようにしている。
ThreeCountUpCanExecuteプロパティの値は、Countプロパティの値が変わったときに変動するので、Countプロパティのセッターで、プロパティ値が変化したことをViewに通知するように実装する。これにはまず、ThreeCountUpCanExecuteプロパティ値が変わったことを通知するためのRaisePropertyChangedメソッドを呼ぶ(3)。これでこのプロパティを直接バインディングするiOS/Androidアプリはこのタイミングで表示が更新される。その次の行では、ThreeCountUpCommandプロパティ(つまりコマンド)のRaiseCanExecuteChangedメソッドを呼び出して(4)、コマンド自体のCanExecuteの変化を通知している。コマンドの機能が全て使えるWPFではこのタイミングで有効状態が反映される。
次にiOSのViewに、このThreeCountUpCanExecuteプロパティの値を反映する。これにはCommandBindingSample.TouchプロジェクトのFirstView.csファイルに以下のように追加する。
|
using Cirrious.MvvmCross.Binding.BindingContext;
using Cirrious.MvvmCross.Touch.Views;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;
namespace CommandBindingSample.Touch.Views
{
[Register("FirstView")]
public class FirstView : MvxViewController
{
public override void ViewDidLoad()
{
View = new UIView { BackgroundColor = UIColor.White };
base.ViewDidLoad();
// ios7 layout
if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
{
EdgesForExtendedLayout = UIRectEdge.None;
}
// 画面の実装
var label = new UILabel(new CGRect(10, 10, 300, 40));
Add(label);
var countUpButton = new UIButton(UIButtonType.System)
{
Frame = new CGRect(10, 50, 90, 40)
};
Add(countUpButton);
countUpButton.SetTitle("CountUp", UIControlState.Normal);
// -- ▼▼▼▼ ここから追加 ▼▼▼▼ --
var threeCountUpButton = new UIButton(UIButtonType.System)
{
Frame = new CGRect(10, 90, 90, 40)
};
Add(threeCountUpButton);
threeCountUpButton.SetTitle("3 CountUp", UIControlState.Normal);
// -- ▲▲▲▲ ここまで追加 ▲▲▲▲ --
// バインディングの指定
var set = this.CreateBindingSet<FirstView, Core.ViewModels.FirstViewModel>();
set.Bind(label).To(vm => vm.Count);
set.Bind(countUpButton).For("TouchUpInside").To(vm => vm.CountUpCommand);
// -- ▼▼▼▼ ここから追加 ▼▼▼▼ --
set.Bind(threeCountUpButton).To(vm => vm.ThreeCountUpCommand); // 1
set.Bind(threeCountUpButton).For(v => v.Enabled).To(vm => vm. ThreeCountUpCanExecute); // 2
// -- ▲▲▲▲ ここまで追加 ▲▲▲▲ --
set.Apply();
}
}
}
|
新しく作った[3 CountUp]ボタンに、先ほど作ったThreeCountUpCommandコマンドをバインディングしている(1)。先述の通り、UIButtonのTouchUpInsideプロパティはデフォルト・バインディング・プロパティなのでForメソッドは省略できる。
また、その次の行(2)では、ボタンの有効状態を制御するEnabledプロパティに、有効状態を表すThreeCountUpCanExecuteプロパティをバインディングした。
一方、AndroidのViewはCommandBindingSample.Droidプロジェクトの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="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="40dp"
local:MvxBind="Text Count" />
<Button
android:text="CountUp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Click CountUpCommand" />
<!-- ▼▼▼▼ ここから追加 ▼▼▼▼ -->
<Button
android:text="3 CountUp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Click ThreeCountUpCommand; Enabled ThreeCountUpCanExecute" />
<!-- ▲▲▲▲ ここまで追加 ▲▲▲▲ -->
</LinearLayout>
|
ここでは、ボタンのClickイベントにViewModelのThreeCountUpCommandコマンドを、また同様に、ボタンのEnabledプロパティにViewModelのThreeCountUpCanExecuteプロパティをバインディングしている。このように、MvxBind属性に複数のバインディングを記載する場合は、 ; (セミコロン)で区切ればよい。
このアプリを実行すると、iOS、Android共に、3で作ったアプリに[3 CountUp]ボタンが追加されたアプリが起動する(図2)。起動したタイミングでは「0」が表示されているため、どちらのボタンもタップできるが、どちらかのボタンをタップして表示されている数字が奇数になると、[3 CountUp]ボタンは無効状態になりタップできなくなる。
5値を持つイベントにバインディングする
コマンドを経由して発生されるイベントの中には、例えばMvxListViewで項目選択時に発生するItemClickイベントなどのように、コマンドパラメーターが指定される場合がある。その場合には、MvxCommand<T>クラスを使用して、渡されるコマンドパラメーターの型を明示的に指定したコマンドを定義できる。例えば次のコードでは、Message型のコマンドパラメーターが指定されるOpenMessageCommandプロパティ(=コマンド)を定義している。
|
using System.Collections.Generic;
……省略……
public List<Message> Messages
{
get;
set;
}
MvxCommand<Message> _openMessageCommand;
public MvxCommand<Message> OpenMessageCommand
{
get
{
return _openMessageCommand ?? (_openMessageCommand = new MvxCommand<Message>(message => {
// -- ここに選択された message を使った処理を記述する
Debug.WriteLine("Open {0}", message);
}));
}
}
|
6コマンドに静的にパラメーターを渡す
状況によっては、「MvxCommandクラスのコンストラクター引数に、実行時に変わらない決まった文字列や列挙体の値を渡したい」という場合がある。
iOSの場合は、Bindメソッドのメソッドチェーンの中に、CommandParameterメソッドを含めることで、そういったコマンドパラメーター値を指定できる(次のコードはその例)。
|
set.Bind(countUpButton).To(vm => vm.ButtonActionCommand).CommandParameter("Test");
|
Androidの場合は、local:MvxBind属性のバインディング式のオプションにCommandParameterプロパティを指定できる(次のコード)。
|
<Button
android:text="button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Click ButtonActionCommand, CommandParameter=Test" />
|
この場合、5で解説した方法で型を明示してパラメーターを受ける他に、MvxCommandクラスのコンストラクター第1引数のラムダ式に引数を取って、コマンドパラメーターをobject型として受け取ることもできる(次のコードはその例)。
|
MvxCommand _buttonActionCommand;
public MvxCommand OpenMessageCommand
{
get
{
return _buttonActionCommand ?? (_buttonActionCommand = new MvxCommand(parameter => {
// parameter に CommandParameter が入っている
if (parameter is string)
{
Debug.Write("Command: {0}", parameter);
}
}));
}
}
|
【コラム】UIViewController(iOS)やActivity(Android)のライフサイクルを監視する
「画面表示時に値を更新したい」とか、「画面を離れる前に値を保存したい」という要求はよくある。しかしMvxViewModelクラスには、StartやInitなどの「ViewModelの初期化」に関するメソッドはあるが、「画面が表示された」などのViewControllerやActivityのライフサイクルイベントを受け取って実行されるメソッドがない。
MvxViewControllerクラスやMvxActivityクラスでは、それぞれの基底クラスであるMvxEventSourceViewControllerクラス/MvxEventSourceActivityクラスで、ライフサイクルイベントに応じたイベントが定義されている。本Tipsの3の要領で、それらのイベントにコマンドをバインディングすることで、ライフサイクルイベント発生時の処理をViewModelのコマンドとして記述するとよいだろう。
ちなみにMvxViewControllerクラスやMvxActivityクラスには、それぞれ、以下のようにライフサイクルイベントと、それに対応するメソッドが定義されている。
| イベント名 | 対応するメソッド |
|---|---|
| ViewDidLoadCalled | ViewDidLoad |
| ViewDidAppearCalled | ViewDidAppear |
| ViewDidDisappearCalled | ViewDidDisappear |
| ViewWillAppearCalled | ViewWillAppear |
| ViewWillDisappearCalled | ViewWillDisappear |
| DisposeCalled | Dispose |
UIViewControllerのライフサイクルについては、AppleのUIViewControllerのリファレンス内の[Explaining State Transitions]という項で説明されている。
| イベント名 | 対応するメソッド |
|---|---|
| CreateWillBeCalled | OnCreate(基底クラスを呼ぶ前) |
| CreateCalled | OnCreate(基底クラスを呼んだ後) |
| StartCalled | OnStart |
| RestartCalled | OnRestart |
| ResumeCalled | OnResume |
| PauseCalled | OnPause |
| SaveInstanceStateCalled | OnSaveInstanceStateCalled |
| StopCalled | OnStop |
| DestroyCalled | OnDestroy |
| DisposeCalled | Dispose |
| ActivityResultCalled | OnActivityResult |
| NewIntentCalled | OnNewIntent |
| StartActivityForResultCalled | StartActivityForResult |
特徴的なのは、OnCreateメソッドでは、基底クラスのOnCreateメソッドが呼ばれる前に発生する「CreateWillBeCalled」というイベントがあること。Activityのライフサイクルについては、Android DevelopersのリファレンスのActivityの項目内の[Activity Lifecycle]という項で説明されている。
※以下では、本稿の前後を合わせて5回分(第41回~第45回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
43. 【現在、表示中】≫ MvvmCrossでコマンドバインディングをするには?
MvvmCrossでは、画面でのイベント発生をViewModelに通知するためにコマンドバインディングを使用する。iOS/Androidにおける、その基本的な実装方法を説明する。
44. Xamarin.FormsでListViewのコンテキストアクションを使用するには?
リストの1つをスライド(iOS)もしくは長押し(Android)されたらメニューを表示する「コンテキストアクション」の基本的な使い方を説明する。