Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Xamarin逆引きTips

Xamarin逆引きTips

MvvmCrossでプロパティバインディングをするには?

2015年3月18日

MvvmCrossでの画面表示に必要なプロパティバインディングについて、iOS/Androidの基本的な実装を説明する。

  • このエントリーをはてなブックマークに追加

 MvvmCrossのiOS/Androidアプリ開発では、プロパティバインディングによって画面を作成する。

 今回はプロパティバインディングの種類と、iOS/Android各プラットフォームでの実装方法を解説する*1

  • *1 なお、本TipsはMac上のXamarin Studio(5.8.0)とMvvmCross(3.5.0)で動作を確認している。

バインディングについて

ViewとViewModelの関連付け

 MVVM設計によるアプリ開発では画面は、ViewViewModelのペアで実現する。MvvmCrossでは、CoreプロジェクトにViewModelとなるクラスを実装し、TouchプロジェクトDroidプロジェクトにViewとなるクラスを実装する。図1に示す通り、バインディングによって1つのViewModelに対して複数のViewを対応させることができる。

ViewModelバインディング
図1 ViewModelとViewの対応関係

 このとき、表1に示す命名規則に従ってクラスを作成することで、ViewとViewModelが自動的に結び付けられ、バインディングが可能となる。例えば、MvvmCrossセットアップ時に用意されている画面はFirstViewModelクラスとFirstViewクラスである。

プロジェクト役割命名規則継承元クラス
Core ViewModel ○○ViewModel MvxViewModelなど
Touch View ○○View MvxViewControllerなど
Droid View ○○View MvxActivityなど
表1 MvvmCrossにおけるViewModelとViewの命名規則

バインディングモードについて

 バインディングは値が伝搬する方向によって次の4つのモードが存在する(図2、表2)。バインディング時にこれらのモードを指定することで、動作を切り替えることができる。

バインディングモード
図2 4つのバインディングモード
モード動作
Two-Way ViewModelとViewの変更が双方向に反映する。デフォルトのモード
One-Way ViewModelの変更のみViewへ反映する
One-Way-To-Source Viewの変更のみViewModelへ反映する
One-Time ViewModelからViewへ一度だけ値が渡され、その後の変更はお互いに反映されない
表2 バインディングモードの違い

 Two-Wayモードでのバインディングがデフォルトであり、これを使用する場面が最も多いが、機能に応じてそれぞれのモードを使い分ける。この中でOne-Timeモードは最も特殊であり、バインディングではあるがViewModelからViewへ一度きりの反映となり、バインディング後に値が変化しても画面が更新されることはない。用途は限られるが、ローカライズなどの一度画面を表示してしまえばめったに変更されない表示要素に使用する。

ViewModelプロパティをバインディングする

 プロパティバインディングは次の手順で実装する。

  1. CoreプロジェクトのViewModelとなるクラスに、バインディングしたいプロパティを実装する
  2. TouchプロジェクトのViewとなるクラスに、バインディングを定義する
  3. DroidプロジェクトのViewとなるクラスに、バインディングを定義する

 3つのプロジェクトにまたがって実装する必要がある。画面の共通化ができていれば、複数のプラットフォーム向けの開発であってもViewModelは共通にできる。

 それでは実際に、この手順でサンプルを作成してみよう。

MvvmCrossプロジェクトの作成

 「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。ソリューション名は「CrossBindingSample」と設定する。

 プロジェクトを作成すると、図3の構成となっている。

プロジェクト構成
図3 プロジェクト構成

Coreプロジェクトの実装

 まずは、CoreプロジェクトのViewModelを実装する。

 CrossBindingSample.CoreプロジェクトのViewModelsフォルダー内にあるFirstViewModel.csファイルを次のように修正する。

C#
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
      }
    }
  }
}
バインディングしたいプロパティの実装(FirstViewModel.cs)

 MvvmCrossセットアップ時にすでに実装されていたHelloプロパティの他に、MyCompanyMySwitchValueプロパティを用意している。これらの3つがバインディングされるプロパティである。

 MvvmCrossでは、IMvxNotifyPropertyChangedインターフェースを実装しているクラスのプロパティであればバインディングが可能となる。上記のMvxViewModelクラスでは、このインターフェースを実装したMvxNotifyPropertyChangedクラスを継承している。

 全てのプロパティ内でRaisePropertyChanged()メソッド(以下、()で終わるものはメソッドを表す)を呼び出している(123)。この呼び出しによって、ViewModelのプロパティ値の変更がViewへ反映されることになる。RaisePropertyChanged()へはプロパティ名を文字列として渡してもよいが(引数が異なる2種類の同名メソッドが提供されている)、式木で指定することでXamarin Studioの入力補完機能が使えたり、コンパイルエラーによってタイプミスに気付いたりできる。

Touchプロジェクトの実装

 Touchプロジェクト側でバインディングを定義する。Touchプロジェクトではバインディングをクラス内のコードで定義する必要がある。

 CrossBindingSample.TouchプロジェクトのViewsフォルダー内にあるFirstView.csファイルを以下のように修正する。

C#
……省略……
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();
    }
  }
}
Touchプロジェクト側でのバインディング定義(FirstView.cs)

 1では画面レイアウトを作成している。通常のXamarin.iOS開発と同様に、.xibファイルを用いたり、Storyboardを用いたりしてレイアウト作成をすることも可能だが、今回は簡単のためコード上でレイアウトしている。

 2ではバインディングを定義している。CreateBindingSet()の型引数へFirstView型とFirstViewModel型を渡してMvxFluentBindingDescriptionSetオブジェクト(=set変数)を生成し、これに対してバインディングの定義を追加していく。バインディングを全て設定したら、Apply()を呼び出して設定を反映する。動作を確認するため、textField2コントロールに対してはOneWay()を呼び出し、One-Wayモードを設定した。

 Bind()により、View側のバインド先となるインスタンスを設定し、To()によりバインド元となるプロパティを設定する。ここで、以下の2つの記述は同じ意味である。

C#
set.Bind(label).To(vm => vm.Hello); // A
set.Bind(label).For(v => v.Text).To(vm => vm.Hello); // B
For()の省略について

 ABは全く同じバインディングを表しており、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
MvvmCross 3.5におけるiOSプロジェクトでのデフォルト・バインディング・プロパティ

Droidプロジェクトの実装

 Droidプロジェクト側でバインディングを定義する。Droidプロジェクトでは、バインディングをAndroid Layout XMLファイル(.axml)の中に直接記述できる。

 Viewsフォルダー内にあるFirstView.csファイルはMvvmCrossセットアップ時のまま修正せず、Resourceslayoutフォルダー内にあるFirstView.axmlファイルを以下のように修正する。Xamarin StudioでCrossBindingSample.DroidプロジェクトのFirstView.axmlを開くと、レイアウト・プレビュー・モードで表示されるが、そのモード画面の下部の[ソース]タブを選択し、XMLソースコード編集画面を開いて編集を行う。

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">
  <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>
Droidプロジェクト側でのバインディング定義(FirstView.axml)

 .axmlファイルでレイアウトを作成し、MvxBind属性にバインディング設定を記述する。

local:MvxBind="<バインディング先プロパティ名> <バインディング元プロパティ名>, (オプション)=(値), ..."

の形式で設定する(「,」以降は省略可能)。

 ここでも1EditTextには、local:MvxBind="Text Hello, Mode=OneWay"という記述により、One-Wayモードを設定している。

 2Switchコントロールでは、そのCheckedプロパティへのバインディングを設定している。

 DroidプロジェクトはレイアウトXMLファイルへ直接バインディング設定を記述するのが一般的であるが、iOSで示したCreateBindingSet()によるコード中でのバインディング設定を実装することも可能である。

アプリの実行

 以上でTouch/Droid両プロジェクトの実装は完了となる。それぞれを実行して動作を確認する。

 Touchプロジェクトの起動は次の通りである。

  • [ソリューション]ビューの「CrossBindingSample.Touch」を右クリックし、(表示されるコンテキストメニューの)[スタートアップ プロジェクトとして設定]を選択し、Touchプロジェクトを起動できるようにする
  • プロジェクト構成を「Debug」に設定し、実行したいiOSシミュレーターまたはiPhone実機を選択した後、[実行]によりアプリを実行する(図4)

 Droidプロジェクトの起動もほぼ同様になるが次の通りである。

  • [ソリューション]ビューの「CrossBindingSample.Droid」を右クリックし、[スタートアップ プロジェクトとして設定]を選択し、Droidプロジェクトを起動できるようにする
  • プロジェクト構成を「Debug」に設定し、実行したいAndroidエミュレーターまたはAndroid実機を選択した後、[実行]によりアプリを実行する。
図4 Touchプロジェクトの実行

 それぞれ図5/図6の画面となる。上段の入力欄はTwo-Wayバインディングであるため、テキストを編集するとラベルや下段の入力欄へも反映されるが、下段の入力欄はOne-Wayバインディングであるため、テキストを編集してもViewModel側のプロパティ値は更新されず、ラベルや上段の入力欄へは反映されない。

 その下にはViewModel側のMyCompanyプロパティの値や、MySwitchValueプロパティの値が表示されていることが確認できる。

図5 Touchプロジェクトの実行結果
図6 Droidプロジェクトの実行結果

まとめ

 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
Releaseビルド時のバインディングエラー

この場合では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)。

図7 Xamarin StudioのLinkerオプション

 この問題に対してMvvmCrossでは、プロジェクトセットアップ時にプロジェクトルートへ作成されるLinkerPleaseInclude.csファイルで対策している(図8)。

LinkerPleaseInclude.cs
図8 プロジェクト内のLinkerPleaseInclude.cs

 XamarinのLink assemblies機能はプロジェクトのどこかで一度でも対象が出現すればプロパティやメソッドが削除されることはない。LinkerPleaseInclude.cs内には以下のようなコードが実装されており、これによりバインディングしたいプロパティの削除を防ぐことができる。

C#
public class LinkerPleaseInclude
{
  public void Include(Button button)
  {
    button.Click += (s,e) => button.Text = button.Text + "";
  }
……省略……
LinkerPleaseIncludeの実装

このコードではButtonクラスのClickプロパティとTextプロパティを保護しているため、Releaseビルド時にこれらのプロパティが削除されることはない。
なお、LinkerPleaseInclude.csに実装したコードはアプリ実行時中に呼び出されることはないため、記述されているコードの内容に意味はない。

※以下では、本稿の前後を合わせて5回分(第39回~第43回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Xamarin逆引きTips
39. Xamarin.Formsで地図を表示するには?(Xamarin.Forms.Maps使用)

Android/iOSアプリで各プラットフォーム標準の地図を表示するには、Xamarin.Forms.Mapsコントロールを使う。その基本的な使い方を説明する。

Xamarin逆引きTips
40. Xamarin.Formsで地図の現在位置やピンの表示、縮尺や地図タイプの変更を行うには?(Xamarin.Forms.Maps使用)

Xamarin.Forms.Mapsコントロールで利用可能な機能として、「現在位置」や「衛星写真」「ピン立て」「スライダーコントロールによる地図の拡大・縮小」などを解説する。

Xamarin逆引きTips
41. 【現在、表示中】≫ MvvmCrossでプロパティバインディングをするには?

MvvmCrossでの画面表示に必要なプロパティバインディングについて、iOS/Androidの基本的な実装を説明する。

Xamarin逆引きTips
42. Xamarin.Formsでビヘイビアーを使用するには?

サブクラス化することなく、UIコントロールに機能を追加できる「ビヘイビアー」の基本的な使い方を説明する。

Xamarin逆引きTips
43. MvvmCrossでコマンドバインディングをするには?

MvvmCrossでは、画面でのイベント発生をViewModelに通知するためにコマンドバインディングを使用する。iOS/Androidにおける、その基本的な実装方法を説明する。

サイトからのお知らせ

Twitterでつぶやこう!