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)されたらメニューを表示する「コンテキストアクション」の基本的な使い方を説明する。