Xamarin

Xamarin でのデータ バインディングの実装と使用

Laurent Bugnion

コード サンプルのダウンロード

データ バインディングは、現在のプログラミングでよく使われる考え方で、特にクライアント アプリケーションで多く採用されています。そこで今回は、データ バインディングの概要と、データ バインディングをネイティブにサポートするテクノロジ (Windows XAML など) での使い方について考えます。まず、データ バインディングと監視可能なプロパティの基本原理を紹介し、テクノロジ "そのもの" ではデータ バインディングをサポートしない場合の使い方を説明します。次に、MVVM Light Toolkit (mvvmlight.net、英語) を使用して Xamarin.Android と Xamarin.iOS にデータ バインディングのサポートを追加する方法を取り上げます。この簡易ツールキットは、Windows Presentation Foundation (WPF)、Silverlight、Windows Phone、Windows ストア、Xamarin.Android、Xamarin.iOS、Xamarin.Forms などのプラットフォーム向けに最もよく使用される MVVM (Model-View-ViewModel: モデル - ビュー - ビューモデル) フレームワークです。

最後にまとめとして、Xamarin のフレームワークに新たに追加された Xamarin.Forms でのデータ バインディングについて説明します。Xamarin.Forms を使用すると、XAML コードで UI を一度ビルドするだけで、Android、iOS、および Windows Phone で実行できるようになります。

データ バインディングとは

データ バインディングとは、UI とビジネス ロジックを結び付けるプロセスです。データ バインディングを使用して、結び付けられた 2 つのプロパティが同期した状態になります。つまり、オブジェクト (通常はビューモデルというデータ モデル) の値を変更すると、対応する UI のプロパティも自動的に更新されます。"双方向" データ バインディングの場合は、同様に、UI の値を変更すれば、ビューモデルも自動的に更新されます。データ バインディングは、WPF などの XAML ベースのテクノロジが導入される以前から、クライアント アプリケーションに利用されていました。たとえば、ASP.NET Web フォームでは、リスト ボックスのようなデータ コントロールを、画面に表示する項目のコレクションにデータ バインディングすることができます。データ項目を目に見える表示へと "変換" するためには、通常、項目テンプレートを定義します。項目テンプレートとは、データ項目のプロパティに含まれる値を表示するコントロールの一部に含まれる、少量の HTML コードです。

XAML ではこの考え方が拡張され、すべての XAML 開発者は多かれ少なかれデータ バインディングを使用することになります。このメカニズムがサポートされない従来型のテクノロジを使用していた開発者は特に、ソース コードで直接データ モデルの変更の多くを処理しようと考えますが、データ バインディングを習得してしまえば、おそらくソース コードでデータの変更を管理するコードを記述する面倒な作業に戻ろうとは考えないでしょう。

単なるデータ コントロールではない

XAML のデータ バインディングは、"単なる" データ コントロールではありません。実質的に、あらゆるオブジェクトのあらゆるプロパティを他のプロパティにバインドできます (ただし、データ バインディングのターゲットになり得るのは、依存関係プロパティ "DP" と呼ばれる特別な種類のプロパティだけです。DP は Android や iOS には存在しないため、利用可能なデータ バインディング フレームワークではなんらかの回避策が必要になります)。

Web 環境では、KnockoutJS や AngularJS などのフレームワークの導入により、データ バインディングへの関心が戻ってきています。これらのフレームワークには、バックグラウンドで実行される JavaScript コードに対するデータ バインディング拡張機能があります。このようなデータ バインディング フレームワークは、XAML フレームワークの影響を多く受けているため、多くの開発者の関心が XAML コミュニティによって開発された原理 (MVVM パターンなど) に集まっています。

宣言型のプロセス

XAML や HTML とようなマークアップテクノロジは、UI の目的を "宣言" するため、宣言型と呼ばれます。このような宣言は、通常、コードの実装時とは異なる時点に行われ、場合によっては異なるチームが行ってもかまいません。たとえば、XAML テクノロジの場合、XAML を担当する UI 開発者は、「インテグレーター」、「インタラクション開発者」、「開発設計者」などとも呼ばれるようです。いずれにせよその役割として、アプリケーションに対する設計者のビジョンを十分理解する必要があります。多くのインテグレーターは開発者でもあるため、両者の視点を持ち合わせながら、アプリケーション開発において大きな創造性を発揮します。

データ バインディングによって、小さな開発チームでも UI 作成のプロセスを厳密に切り分けることができるようになります。ひとりで開発者している小さなアプリケーションの場合でも、開発の過程で頭を切り替えます。データ モデル、データ サービスなどのコードに取り組む場合は、開発者の頭で考えます。UI を作成する場合は、インテグレーターの頭に切り替えます。

このように役割を切り分けることは、最近のソフトウェア開発でよく言われる懸念事項の分離につながります。データ バインディングではこの切り分けが絶対に必要というわけではありません。ですが、正しい考え方を身に付けるのにかなり役立つアプローチではあります。

Xamarin とデータ バインディング

Windows Phone とまったく同様に、Xamarin.Android と Xamarin.iOS はどちらもソース コードで直接 UI をビルドできるだけでなく、宣言型マークアップを含む XML ファイルを使用する最新のアプローチを利用することもできます。Android の場合は、AXML ファイルを使用します。iOS の場合は簡易アプローチとしてストーリーボード ファイルを使用します。どちらも、Windows Phone で XAML を使用して行うことと同じです。このような宣言型マークアップ ファイルは Xamarin 固有のものではありません。バニラ Android や iOS の開発にも使用されます。

宣言型マークアップ ファイルは便利です。Xamarin Studio や Visual Studio で提供されるようなビジュアル デザイナー ツールと連携することができます。ただし、Android と iOS の宣言型マークアップ ファイルは、XAML とほど強力ではありません。具体的には、XAML でいうマークアップ拡張がありません。マークアップ拡張の式は、中かっこ ({ }) で囲んで定義します。多くのマークアップ拡張があり、たとえば、XAML ドキュメント内のリソースにアクセスしたり、以下のコード スニペットのようにデータ バインディングを作成するものがあります。

<Button Content="{Binding ButtonContent}"
        Command="{Binding ExecuteCommand}"
        CommandParameter="{Binding IsChecked, ElementName=LockCheckBox}" />

上記のマークアップはビューの一部で、Button コントロールの Content プロパティをソース プロパティ ButtonContent に、Command プロパティをソース プロパティ ExecuteCommand にバインドするように指示しています。このようなソースのプロパティは、Button の親 (多くの場合、ページ全体) の既定のデータまたはバインディング コンテキストとして機能するオブジェクト内にあります。通常はビューモデル オブジェクトです。ただし、バインディング コンテキストは、必要に応じて任意のレベルで設定できます。たとえば、マークアップの 3 行目では、Button の CommandParameter を、ページの別の要素 (LockCheckBox) の IsChecked プロパティにバインディングするよう明示的に指示しています。

残念ながら、データ バインディングの概念は、Android や iOS にはネイティブに存在しません。そのため、ここからは、この 2 つのプラットフォーム向けに Xamarin でデータ バインディングを実装する方法を説明します。その後、このプロセスを容易にするために使用できる、MVVM Light Toolkit が提供する実装を紹介します。また、Xamarin には Xamarin.Forms という拡張機能もあります。これは、UI 用に Windows に似た XAML マークアップ言語を使用します。Xamarin.Forms は、外部フレームワークを追加することなく、マークアップでの直接バインディング宣言をサポートします。

Android と iOS でのデータ バインディングの実装

ここでは、シンプルなデータ サービスに接続して、それぞれ名前と説明を添えて花の一覧を返す、小さなアプリケーションをビルドします。このアプリケーションは、XamDataBinding という名前で公開しています (galasoft.ch/s/xambinding、英語)。シンプルにするために、1 つのページだけを取り上げます (図 1 参照)。Android、iOS、Windows Phone に実装されるこのページには、以下の機能があります。

  • 花の一覧。基になるデータ ソースが変更されるたびに、花の一覧が自動更新されるのが理想です。
  • 一覧を更新するボタン。
  • 最終更新日時を示すテキスト。
  • [Lock] (ロック) チェックボックスまたはスイッチ。この要素は、実際のシナリオにはあまり意味はなく、デモ用であり、[Lock] (ロック) チェックボックスまたはスイッチをオンにした場合に、[Refresh] (更新) ボタンを "ロック" することが目的です。

Sample Application in Android, iOS and Windows Phone
図 1 Android、iOS、および Windows Phone でのサンプル アプリケーション

MVVM パターンは、以下のシンプルな原理を採用します。

  • 監視可能なオブジェクトが INotifyPropertyChanged インターフェイスを実装します。この .NET インターフェイスは、監視可能なプロパティが変更された場合に発生する PropertyChanged イベントを公開します。
  • アプリケーションの実行中に変化する可能性がある一覧は、ObservableCollection<T> インスタンスに格納します。
  • このような一覧では、コンテンツが変化 (追加、削除、並べ替え順序の変更) すると CollectionChanged イベントが発生します。
  • オブジェクトによっては、プロパティで ICommand インターフェイスを実装する機能を公開するものもあります。このインターフェイスは、Execute メソッドと CanExecute メソッドを定義します。CanExecute は、Execute メソッドを実行できるかどうかに応じて true または false を返します。また、ICommand では、CanExecuteChanged イベントを指定します。このイベントは、CanExecute メソッドによって返される値が変化した場合に発生します。これは、この変化がバインドされた UI コントロールの状態に影響を与える可能性があるためです。

MVVM Light Toolkit では、このようなインターフェイスは、ObservableObject、ViewModelBase (INotifyPropertyChanged)、RelayCommand (ICommand) などの具象クラスによって実装されます。XamDataBinding の例では、モデル層とビューモデル層が MVVM Light Toolkit を使用してビルドされ、XamDataBinding.Data というポータブル クラス ライブラリに格納されます。このライブラリを Xamarin と Windows の間で共有できます。

MVVM では、各ビューはビューモデルによって "駆動" されます。Windows や Windows Phone では、通常、ビューモデルを DataContext として使用します。DataContext は、ビュー (ページ、ウィンドウ、ユーザーコントロールなど) のプロパティの 1 つです。これはデータ バインディングを利用する場合の便利な方法の 1 つで、バインディングのソースを常に指定する必要をなくします。Android や iOS には DataContext という概念はありません。代わりに、ビューでビューモデルを宣言して直接使用します。XamDataBinding サンプルのような小さなアプリケーションでは、次のようにビュー (Android の Activity、iOS の MainViewController、および Windows Phone の MainPage) で直接ビューモデルを作成できます。

private MainViewModel _vm;
public MainViewModel Vm
{
  get
  {
    return _vm ?? (_vm = new MainViewModel());
  }
}

フレームワークを使用しないバインディングとコマンド処理の実装

同じことを何度も繰り返さないように、ここでは、XamDataBinding サンプルの Android アプリケーションのみに注目します。ただし、まったく同じ考え方が iOS アプリケーションにも当てはまります。これらのプラットフォームはどちらもデータ バインディングをサポートしないため、バインディングとコマンド処理を以下のように手動で実装します。

  • ビューで MainViewModel の PropertyChanged イベントを処理し、変化したプロパティの名前を確認して、それに応じてビューを更新します。
  • バインディングによっては 2 方向 (双方向バインディング) にします。たとえば、チェックボックスがクリックされたときは、MainViewModel の対応するブール値を更新する必要があります。バインディング フレームワークを使用しない場合は、チェックボックスのイベントをサブスクライブし、ビューから手動でビューモデルのプロパティを更新する必要があります。
  • [Refresh] ボタンでは、MainViewModel の RefreshCommand を実行する必要があります。コマンド処理フレームワークを使用しない場合、こちらも手動で操作する必要があります。ボタンの Click イベントを処理し、ビューから直接コマンドの Execute メソッドを呼び出します。
  • 最後に、コマンドではバインディング先のコントロールを無効にできることがわかっています。今回の場合は、コマンド処理フレームワークを使用しないで、コマンドの CanExecuteChanged イベントをサブスクライブする必要があります。このイベントが発生したときに、コマンドの CanExecute メソッドを呼び出して、返された値に応じてコントロールを有効または無効にします。

これらの手順はそれほど複雑ではありませんが、プロセス全体は繰り返しを伴う面倒な作業になる可能性があります。実際には、MVVM Light Toolkit によって提供されているようなバインディング フレームワークやコマンド処理フレームワークを使用する方が簡単です。

MVVM Light データ バインディング フレームワークの利用

前述の例は機能しますが、さまざまなイベントを操作して可能性のあるすべてのシナリオを考慮する必要があるため、コードの記述は煩雑になります。また、処理しなければならない UI 要素が多くある場合は特に、メンテナンスや状況の把握が困難になります。

そこで、MVVM Light Toolkit のシンプルなデータ バインディング フレームワークを使うと、UI 要素とコード間および UI 要素とコマンド間のリレーション シップを作成できます。このバインディング フレームワークによって、バインディングを作成して OnCreate メソッドのコマンドにアタッチできるようになります (図 2 参照)。

図 2 バインディングとコマンドの作成

_lastLoadedBinding = this.SetBinding(
  () => Vm.LastLoadedFormatted,
  () => LastLoadedText.Text);
_lockBinding = this.SetBinding(
  () => Vm.IsLocked,
  () => LockCheckBox.Checked,
  BindingMode.TwoWay);
_refreshCommandBinding = this.SetBinding(
  () => LockCheckBox.Checked);
RefreshButton.SetCommand("Click", Vm.RefreshCommand, _refreshCommandBinding);
FlowersList.Adapter = Vm.Flowers.GetAdapter(GetFlowerAdapter);

図 2 では、まず、MainViewModel の LastLoadedFormatted プロパティと TextView の Text プロパティの 1 方向バインディングを作成しています。1 行のコードで、2 つのプロパティ間の永続的なリレーションシップを確立します。ここでは MVVM Light によって提供される拡張メソッドの SetBinding を使用していることに注意してください。このメソッドは、作成したバインディング インスタンスを返します。バインディング オブジェクトは、メモリ リークを回避するために WeakReferences を使って記述しているため、プライベート フィールドとして保存し、自動的にデタッチされるのを防ぎます。

2 番目のステートメントは、MainViewModel の IsLocked プロパティと LockCheckBox の Checked プロパティの間に双方向バインディングを作成します。既定では、すべてのバインディングは 1 方向になるため、SetBinding メソッドで BindingMode に TwoWay を指定する必要があります。

3 番目のステートメントは、ターゲットを指定しないでソースだけを指定し、特別な種類のバインディングを作成します。ここでのソースは LockCheckBox の Checked プロパティです。このバインディングを SetCommand メソッドで使用して、LockCheckBox の Checked プロパティが変化するたびに LoadCommand を再評価するように MainViewModel に指示します。XAML では、Button の CommandParameter でバインディングを作成しました。ここでは、最初にバインディングを作成した後、このインスタンスを SetCommand メソッドに渡すことで、同様の構造を使用します。

4 番目のステートメントは、RefreshButton のコマンドを設定します。ここでは、Click イベントを処理しますが、実際には、任意の UI 要素の任意のイベントに対してコマンドを作動させることができます。SetCommand メソッドの 2 つ目のパラメーターは、作動させる必要があるコマンドです。最後に、3 つ目のパラメーターはオプションで、パラメーターを必要とするコマンドでのみに必要です。既に説明したように、ここでは、前述のバインディングを使用します。

一覧の設定

最後は、UI のリストビューです。MVVM Light では、任意の ObservableCollection または任意の IList で使用できる、GetAdapter というかなり便利な拡張メソッドが用意されています。このメソッドは、ListView の Adapter プロパティとして使用できる Android アダプターを返します。MainViewModel の Flowers プロパティは ObservableCollection なので、コレクションが変化すると必ず UI が自動更新されます。

FlowersList.Adapter = Vm.Flowers.GetAdapter(GetFlowerAdapter);

GetAdapter メソッドに渡す GetFlowerAdapter メソッドは、1 つの花を表す行ビューを取得するために使用します。今回の例では、花の画像と名前を表示する組み込みの行ビューを使用して、シンプルさを維持しています (図 3 参照)。ただし、詳細レイアウトや異なるレイアウトを使用してカスタマイズした行ビューを作成するのもかなり簡単です。

この時点でアプリケーションを実行すると、図 1 に示す UI が表示されます。以下の機能を試すこともできます。

  • [Lock] チェックボックスをクリックして、[Refresh] ボタンを無効にします。この無効化は、RefreshCommand と CanExecute メソッドを使って行います。MainViewModel のコードを調べて、このしくみを確認してください。付属の Windows Phone アプリケーションを実行すると、同じ効果を XAML で実現できることがわかります。
  • [Refresh] ボタンが有効な場合、これをクリックすると花の一覧が非同期に読み込まれます。Web サービスに接続するコードは MainViewModel に含まれていて、このポータブル クラス ライブラリによってサポートされるすべてのプラットフォームで完全に再利用可能です。
  • 一覧が読み込まれた後、TextView の状態は "最終読み込み" 情報を使用して更新されます。これも同様に、作成したバインディングを介して実行されます。
  • iOS 版または Windows Phone 版のアプリケーションを実行して、まったく同じ結果になることを確認します。iOS では、MainViewController の ViewDidLoad メソッドでバインディングを作成します。Windows Phone では、MainPage.xaml でバインディングを作成します。

図 3 カスタマイズしたリスト ビュー行の作成

private View GetFlowerAdapter(
  int position,
  FlowerViewModel flower,
  View convertView)
{
  if (convertView == null)
  {
    convertView = LayoutInflater.Inflate(
    Android.Resource.Layout.ActivityListItem, null);
  }
  var text = convertView.FindViewById<TextView>(Android.Resource.Id.Text1);
  text.Text = flower.Model.Name;
  var image = convertView.FindViewById<ImageView>(Android.Resource.Id.Icon);
  image.SetImageBitmap(GetImageBitmapFromUrl(flower.ImageUri.AbsoluteUri));
  return convertView;
}

Xamarin.Forms でのデータ バインディング

昨年、Xamarin は、サポート対象のすべてのプラットフォーム間で、コードだけでなく UI も共有できるようにする、Forms フレームワークをリリースしました。これは、たとえば、プロトタイプを作成する段階で UI を迅速にビルドしてコードをテストするのに役立ちます。Forms フレームワークは、複数のプラットフォームで実行する必要がある基幹業務アプリケーションなど、運用コードにも使用できます。

Xamarin.Android と Xamarin.iOS とは対照的に、Xamarin.Forms はデータ バインディングをネイティブにサポートします。Xamarin.Forms は XAML もサポートするため (ただし、WPF、Windows Phone、Windows ストアで利用可能な XAML とはバージョンが異なる)、直接 XAML マークアップでデータ バインディングを記述できます。したがって、次のようにコードも記述できます。

<Label Text="{Binding LastLoadedFormatted}"
       VerticalOptions="Center"
       HorizontalOptions="Center" />

付属のサンプルは、Xamarin.Forms を使用して各プラットフォームで UI を再利用することで、さらに簡潔にしています。もちろん、UX に細心の注意が求められるアプリケーションでは、Xamarin.Forms が最適なソリューションとは言えない場合もあります。しかし、Forms を使用することで、多くの作業を必要としないでアプリケーションのパフォーマンスや安定性が向上し、機能を追加できるため、UI の開発が大幅に簡略化されるアプリケーションの種類は多岐に渡ります。

まとめ

MVVM Light のバインディングは、マークアップではなくコードで作成するという点で、XAML バインディングとは異なります。ただし、多くの時間を費やして、API に問題がないことが確認されています。友人の Corrado Cavalli は、バインディングの実装と、XAML バインディング ワークフローに可能な限り近い構文の作成を助けてくれました。この機会を利用して心より感謝の意を表明します。

また、James Montemagno は、初期バージョンのバインディング フレームワークをレビューして、大変貴重なアドバイスをくれました。Xamarin の同僚 (特に James Montemagno) にも心より感謝いたします。

最後になりますが、Windows Phone、Xamarin.Android、Xamarin.iOS、および Xamarin.Forms に関する詳細情報および完全なコード サンプルについては、Xamarin Evolve のプレゼンテーション (galasoft.ch/s/evolve14、英語) を参照してください。


Laurent Bugnion* は、WPF、Silverlight、Xbox、Kinect、Windows ストア、Windows Phone、Xamarin、UX などのテクノロジに携わる Microsoft パートナーの IdentityMine Inc で、シニア ディレクターを務めています。彼は、スイスのチューリッヒを拠点に活動している Microsoft MVP、Microsoft Regional Director 兼 Xamarin MVP です。*

この記事のレビューに協力してくれた技術スタッフの Kraig Brockschmidt (マイクロソフト)、Norm Estabrook (マイクロソフト)、および James Montemagno (Xamarin) に心より感謝いたします。