Xamarin逆引きTips
MvvmCrossでプロパティバインディングをするには?
MvvmCrossでの画面表示に必要なプロパティバインディングについて、iOS/Androidの基本的な実装を説明する。
MvvmCrossのiOS/Androidアプリ開発では、プロパティバインディングによって画面を作成する。
今回はプロパティバインディングの種類と、iOS/Android各プラットフォームでの実装方法を解説する*1。
- *1 なお、本TipsはMac上のXamarin Studio(5.8.0)とMvvmCross(3.5.0)で動作を確認している。
バインディングについて
ViewとViewModelの関連付け
MVVM設計によるアプリ開発では画面は、ViewとViewModelのペアで実現する。MvvmCrossでは、CoreプロジェクトにViewModelとなるクラスを実装し、TouchプロジェクトやDroidプロジェクトにViewとなるクラスを実装する。図1に示す通り、バインディングによって1つのViewModelに対して複数のViewを対応させることができる。
このとき、表1に示す命名規則に従ってクラスを作成することで、ViewとViewModelが自動的に結び付けられ、バインディングが可能となる。例えば、MvvmCrossセットアップ時に用意されている画面はFirstViewModel
クラスとFirstView
クラスである。
プロジェクト | 役割 | 命名規則 | 継承元クラス |
---|---|---|---|
Core | ViewModel | ○○ViewModel | MvxViewModelなど |
Touch | View | ○○View | MvxViewControllerなど |
Droid | View | ○○View | MvxActivityなど |
バインディングモードについて
バインディングは値が伝搬する方向によって次の4つのモードが存在する(図2、表2)。バインディング時にこれらのモードを指定することで、動作を切り替えることができる。
モード | 動作 |
---|---|
Two-Way | ViewModelとViewの変更が双方向に反映する。デフォルトのモード |
One-Way | ViewModelの変更のみViewへ反映する |
One-Way-To-Source | Viewの変更のみViewModelへ反映する |
One-Time | ViewModelからViewへ一度だけ値が渡され、その後の変更はお互いに反映されない |
Two-Wayモードでのバインディングがデフォルトであり、これを使用する場面が最も多いが、機能に応じてそれぞれのモードを使い分ける。この中でOne-Timeモードは最も特殊であり、バインディングではあるがViewModelからViewへ一度きりの反映となり、バインディング後に値が変化しても画面が更新されることはない。用途は限られるが、ローカライズなどの一度画面を表示してしまえばめったに変更されない表示要素に使用する。
ViewModelプロパティをバインディングする
プロパティバインディングは次の手順で実装する。
- CoreプロジェクトのViewModelとなるクラスに、バインディングしたいプロパティを実装する
- TouchプロジェクトのViewとなるクラスに、バインディングを定義する
- DroidプロジェクトのViewとなるクラスに、バインディングを定義する
3つのプロジェクトにまたがって実装する必要がある。画面の共通化ができていれば、複数のプラットフォーム向けの開発であってもViewModelは共通にできる。
それでは実際に、この手順でサンプルを作成してみよう。
MvvmCrossプロジェクトの作成
「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。ソリューション名は「CrossBindingSample」と設定する。
プロジェクトを作成すると、図3の構成となっている。
Coreプロジェクトの実装
まずは、CoreプロジェクトのViewModelを実装する。
CrossBindingSample.Core
プロジェクトのViewModels
フォルダー内にあるFirstViewModel.cs
ファイルを次のように修正する。
using Cirrious.MvvmCross.ViewModels;
namespace CrossBindingSample.Core.ViewModels {
public class FirstViewModel : MvxViewModel {
private string _hello = "Hello MvvmCross";
public string Hello {
get { return _hello; }
set { _hello = value; RaisePropertyChanged(() => Hello); } // 1
}
public class Company {
public string Name { get; set; }
public string City { get; set; }
}
private Company _myCompany = new Company {
Name = "フェンリル株式会社", City = "大阪府"
};
public Company MyCompany {
get { return _myCompany; }
set { _myCompany = value; RaisePropertyChanged (() => MyCompany); } // 2
}
private bool _mySwitchValue = true;
public bool MySwitchValue {
get { return _mySwitchValue; }
set {
_mySwitchValue = value;
RaisePropertyChanged (() => MySwitchValue); // 3
}
}
}
}
|
MvvmCrossセットアップ時にすでに実装されていたHello
プロパティの他に、MyCompany
とMySwitchValue
プロパティを用意している。これらの3つがバインディングされるプロパティである。
MvvmCrossでは、IMvxNotifyPropertyChanged
インターフェースを実装しているクラスのプロパティであればバインディングが可能となる。上記のMvxViewModel
クラスでは、このインターフェースを実装したMvxNotifyPropertyChanged
クラスを継承している。
全てのプロパティ内でRaisePropertyChanged()
メソッド(以下、()
で終わるものはメソッドを表す)を呼び出している(123)。この呼び出しによって、ViewModelのプロパティ値の変更がViewへ反映されることになる。RaisePropertyChanged()
へはプロパティ名を文字列として渡してもよいが(※引数が異なる2種類の同名メソッドが提供されている)、式木で指定することでXamarin Studioの入力補完機能が使えたり、コンパイルエラーによってタイプミスに気付いたりできる。
Touchプロジェクトの実装
Touchプロジェクト側でバインディングを定義する。Touchプロジェクトではバインディングをクラス内のコードで定義する必要がある。
CrossBindingSample.Touch
プロジェクトのViews
フォルダー内にあるFirstView.cs
ファイルを以下のように修正する。
……省略……
using CrossBindingSample.Core.ViewModels
namespace CrossBindingSample.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;
}
// 1
var label = new UILabel(new CGRect(10, 10, 300, 40));
Add(label);
var textField = new UITextField(new CGRect(10, 50, 300, 40));
Add(textField);
var textField2 = new UITextField(new CGRect(10, 90, 300, 40));
Add(textField2);
var textCompanyName = new UITextField(new CGRect(10, 130, 300, 40));
Add(textCompanyName);
var textCompanyCity = new UITextField(new CGRect(10, 170, 300, 40));
Add(textCompanyCity);
var switch1 = new UISwitch(new CGRect(10, 210, 100, 40));
Add(switch1);
// 2
var set = this.CreateBindingSet<FirstView, FirstViewModel>();
set.Bind(label).To(vm => vm.Hello);
set.Bind(textField).To(vm => vm.Hello);
set.Bind(textField2).To(vm => vm.Hello).OneWay();
set.Bind(textCompanyName).To(vm => vm.MyCompany.Name);
set.Bind(textCompanyCity).To(vm => vm.MyCompany.City);
set.Bind(switch1).To(vm => vm.MySwitchValue);
set.Apply();
}
}
}
|
1では画面レイアウトを作成している。通常のXamarin.iOS開発と同様に、.xibファイルを用いたり、Storyboardを用いたりしてレイアウト作成をすることも可能だが、今回は簡単のためコード上でレイアウトしている。
2ではバインディングを定義している。CreateBindingSet()
の型引数へFirstView
型とFirstViewModel
型を渡してMvxFluentBindingDescriptionSet
オブジェクト(=set
変数)を生成し、これに対してバインディングの定義を追加していく。バインディングを全て設定したら、Apply()
を呼び出して設定を反映する。動作を確認するため、textField2
コントロールに対してはOneWay()
を呼び出し、One-Wayモードを設定した。
Bind()
により、View側のバインド先となるインスタンスを設定し、To()
によりバインド元となるプロパティを設定する。ここで、以下の2つの記述は同じ意味である。
set.Bind(label).To(vm => vm.Hello); // A
set.Bind(label).For(v => v.Text).To(vm => vm.Hello); // B
|
AとBは全く同じバインディングを表しており、AではFor()
の呼び出しが省略されている。これは、MvvmCrossにはあらかじめUITextField
のデフォルト・バインディング・プロパティとしてText
プロパティが設定されているためである。もし、Text
プロパティではなくTextColor
プロパティなど、別のプロパティをバインディングしたい場合にはそれを明示する必要がある。
【コラム】MvvmCrossのデフォルト・バインディング・プロパティ一覧
MvvmCross 3.5には以下のプロパティがデフォルトバインディング対象として登録されている。以下の内容はGitHub上のMvxTouchBindingBuilder.csファイルの実装内容を確認してしまうと早くて確実である。
クラス | デフォルトのバインディングプロパティ |
---|---|
UIButton | TouchUpInside |
UIBarButtonItem | Clicked |
UISearchBar | Text |
UITextField | Text |
UITextView | Text |
UILabel | Text |
MvxCollectionViewSource | ItemsSource |
MvxTableViewSource | ItemsSource |
MvxImageView | ImageUrl |
UIImageView | Image |
UIDatePicker | Date |
UISlider | Value |
UISwitch | On |
UIProgressView | Progress |
IMvxImageHelper |
ImageUrl |
MvxImageViewLoader | ImageUrl |
UISegmentedControl | SelectedSegment |
Droidプロジェクトの実装
Droidプロジェクト側でバインディングを定義する。Droidプロジェクトでは、バインディングをAndroid Layout XMLファイル(.axml)の中に直接記述できる。
Views
フォルダー内にあるFirstView.cs
ファイルはMvvmCrossセットアップ時のまま修正せず、Resources
-layout
フォルダー内にあるFirstView.axml
ファイルを以下のように修正する。Xamarin StudioでCrossBindingSample.Droid
プロジェクトの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">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
local:MvxBind="Text Hello" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
local:MvxBind="Text Hello" />
<!-- 1 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
local:MvxBind="Text Hello, Mode=OneWay" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
local:MvxBind="Text MyCompany.Name" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
local:MvxBind="Text MyCompany.City" />
<!-- 2 -->
<Switch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
local:MvxBind="Checked MySwitchValue" />
</LinearLayout>
|
.axmlファイルでレイアウトを作成し、MvxBind
属性にバインディング設定を記述する。
local:MvxBind="<バインディング先プロパティ名> <バインディング元プロパティ名>, (オプション)=(値), ..."
の形式で設定する(※「,」以降は省略可能)。
ここでも1のEditText
には、local:MvxBind="Text Hello, Mode=OneWay"
という記述により、One-Wayモードを設定している。
2のSwitch
コントロールでは、そのChecked
プロパティへのバインディングを設定している。
DroidプロジェクトはレイアウトXMLファイルへ直接バインディング設定を記述するのが一般的であるが、iOSで示したCreateBindingSet()
によるコード中でのバインディング設定を実装することも可能である。
アプリの実行
以上でTouch/Droid両プロジェクトの実装は完了となる。それぞれを実行して動作を確認する。
Touchプロジェクトの起動は次の通りである。
- [ソリューション]ビューの「CrossBindingSample.Touch」を右クリックし、(表示されるコンテキストメニューの)[スタートアップ プロジェクトとして設定]を選択し、Touchプロジェクトを起動できるようにする
- プロジェクト構成を「Debug」に設定し、実行したいiOSシミュレーターまたはiPhone実機を選択した後、[実行]によりアプリを実行する(図4)
Droidプロジェクトの起動もほぼ同様になるが次の通りである。
- [ソリューション]ビューの「CrossBindingSample.Droid」を右クリックし、[スタートアップ プロジェクトとして設定]を選択し、Droidプロジェクトを起動できるようにする
- プロジェクト構成を「Debug」に設定し、実行したいAndroidエミュレーターまたはAndroid実機を選択した後、[実行]によりアプリを実行する。
それぞれ図5/図6の画面となる。上段の入力欄はTwo-Wayバインディングであるため、テキストを編集するとラベルや下段の入力欄へも反映されるが、下段の入力欄はOne-Wayバインディングであるため、テキストを編集してもViewModel側のプロパティ値は更新されず、ラベルや上段の入力欄へは反映されない。
その下にはViewModel側のMyCompany
プロパティの値や、MySwitchValue
プロパティの値が表示されていることが確認できる。
まとめ
MvvmCrossでXamarin.iOSとXamarin.Androidアプリ開発をする場合の、画面作成の基本となるプロパティバインディングについて説明した。バインディングには4種類のモードがあり、その中でもよく利用されるTwo-WayモードとOne-Wayモードの動作を確認した。
DroidプロジェクトではレイアウトXMLファイル中にバインディングを記述できるため、Viewクラスに手を入れずにバインディングを実現できるが、TouchプロジェクトではViewクラス内のコードでバインディングを設定する必要がある。
【コラム】Releaseモードでビルドするとバインディングが失敗するとき
MvvmCrossでバインディングしたとき、Debugモードでは正常にバインディングされるが、Releaseモードで実行したときには以下のような実行時エラーとなり、バインディングが失敗することがある。
MvxBind:Warning: 0.92 Failed to create target binding for binding Title for Hello
|
この場合ではTitle
プロパティへのバインディングが失敗している。
Xamarinはアプリバイナリサイズを節約するため、使われていないプロパティやメソッドを削除するLink assemblies機能があり、iOSプロジェクトでもAndroidプロジェクトでも、Releaseモードではその機能が有効となっている。プロパティが削除されていると、MvvmCrossのプロパティバインディングは実行時に失敗する(※なお、今回のサンプルでは、このエラーは発生しない)。
そのLink assemblies機能が有効になっているかを確認するには、次の手順で実行する。
- [ソリューション]ビューのTouchプロジェクトまたはDroidプロジェクトを右クリックし、[オプション]から[プロジェクト オプション」ダイアログを表示する
- iOSの場合: [ビルド]-[iOS Build]-[Generalタブ]-[Linker behavior]の設定値を確認する
- Androidの場合: [Android Build]-[Linkerタブ]-[Linker behavior]の設定値を確認する
これが「Link SDK assemblies only」または「Link all assemblies」に設定されていると、その機能が有効となっている(図7)。
この問題に対してMvvmCrossでは、プロジェクトセットアップ時にプロジェクトルートへ作成されるLinkerPleaseInclude.cs
ファイルで対策している(図8)。
XamarinのLink assemblies機能はプロジェクトのどこかで一度でも対象が出現すればプロパティやメソッドが削除されることはない。LinkerPleaseInclude.cs
内には以下のようなコードが実装されており、これによりバインディングしたいプロパティの削除を防ぐことができる。
public class LinkerPleaseInclude
{
public void Include(Button button)
{
button.Click += (s,e) => button.Text = button.Text + "";
}
……省略……
|
このコードではButton
クラスのClick
プロパティとText
プロパティを保護しているため、Releaseビルド時にこれらのプロパティが削除されることはない。
なお、LinkerPleaseInclude.cs
に実装したコードはアプリ実行時中に呼び出されることはないため、記述されているコードの内容に意味はない。
※以下では、本稿の前後を合わせて5回分(第39回~第43回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
39. Xamarin.Formsで地図を表示するには?(Xamarin.Forms.Maps使用)
Android/iOSアプリで各プラットフォーム標準の地図を表示するには、Xamarin.Forms.Mapsコントロールを使う。その基本的な使い方を説明する。
40. Xamarin.Formsで地図の現在位置やピンの表示、縮尺や地図タイプの変更を行うには?(Xamarin.Forms.Maps使用)
Xamarin.Forms.Mapsコントロールで利用可能な機能として、「現在位置」や「衛星写真」「ピン立て」「スライダーコントロールによる地図の拡大・縮小」などを解説する。
41. 【現在、表示中】≫ MvvmCrossでプロパティバインディングをするには?
MvvmCrossでの画面表示に必要なプロパティバインディングについて、iOS/Androidの基本的な実装を説明する。
43. MvvmCrossでコマンドバインディングをするには?
MvvmCrossでは、画面でのイベント発生をViewModelに通知するためにコマンドバインディングを使用する。iOS/Androidにおける、その基本的な実装方法を説明する。