読者です 読者をやめる 読者になる 読者になる

Xamarin Formsのデータバインディングを整理する(1)

昨今の開発言語やフレームワークにおいて「データバインディング」は非常に重要で有益な技術となっています。
Xamarin Formsにおいても データバインディング がサポートされており、これは WPF / Silverlight などから引き継がれた技術になります(といっても、詳細についてはWPF / Silverlight / Xamarinでは実装内容・実装レベルが異なりますが・・・WPFではDependencyPropertyであったものが Xamarin FormsではBindablePropertyであったり・・・)。

データバインディングは、Xamarin Formsアプリを開発するすべてのエンジニアが利用する技術だと思います。
そして、手軽に利用することもできるし、結構深い技術であるとも思っています。
自分自身の知識の整理も含めて、これから何回かに渡って「Xamarin Formsのデータバインディング」について書いていこうと思います。

※あくまでブログであり、書籍とかじゃないから体系的なまとめはあまり気にしないで、基本+自分の興味の赴くままの技術探求をしていこうかと思います。

データバインディングの基本(ソース と ターゲット)

概念として、データバインディングにおける「元データのオブジェクト側」=「ソース」、「データバインドされる側」 = 「ターゲット」と呼びます。
データバインディングを行う上では、「ソースオブジェクト」と「ターゲットオブジェクト」の2つを結びつける必要があります。その具体的な方法は以下になります。

  • ターゲットオブジェクトの「BindingContextプロパティ」に「ソースオブジェクト」を設定する

シンプルなデータバインディング

早速、以下にシンプルなデータバインディングの例を示します。
ソースは「Personオブジェクト」、ターゲットはフォームに配置した「Labelコントロール」です。
Personは単純なPOCOです。
PersonオブジェクトのNameプロパティの値を、LabelコントロールのTextプロパティにデータバインドします。

Personクラスの定義

ソースとなるPersonクラスの定義です。

// リスト1 person.cs
namespace Example1
{
  public class Person
  {
    public string Name { get; set; }
  }
}

フォームの定義

ターゲットとするLabelコントロールを配置したフォームの定義です。

// リスト2 Example1Page.xaml
<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
  xmlns="http://xamarin.com/schemas/2014/forms" 
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
  x:Class="Example1.Example1Page">
  <StackLayout Margin="0,20,0,0">
    <Label x:Name="Label1" Text="{Binding Path=Name}" />
  </StackLayout>
</ContentPage>

LabelコントロールのTextプロパティに対してデータバインドを行なっています。
「 {Binding Path=Name} 」がXAMLにおけるバインディング構文になります。
ターゲットオブジェクトに対するソースオブジェクトの設定は、この後に紹介するフォームのコードビハインドクラス上で行なっています。
ここでは、以下の2点を {Bindng} 構文によって定義しています。

  • ターゲットオブジェクトであるLabelの「Textプロパティ」に対してデータバインドを行う
  • Path=Nameによって、ソースオブジェクトのNameプロパティ値をバインドすることとする(つまりPersonオブジェクトのNameプロパティ値)

ソースとターゲットの結び付け定義

ソースとなるPersonオブジェクトを作成して、ターゲットとなるLabelコントロールのBindingContextプロパティに設定します。

// リスト3 Example1Page.xaml.cs
using Xamarin.Forms;

namespace Example1
{
  public partial class Example1Page : ContentPage
  {
    private Person person;

    public Example1Page()
    {
      InitializeComponent();
      
      // Personオブジェクト作成
      this.person = new Person();
      person.Name = "ryuichi daigo";

      // ターゲットのラベルコントロールにソースを設定
      this.Label1.BindingContext = person;
    }
  }
}

実行結果画面は以下の通りです。

f:id:daigo-knowlbo:20161225103551p:plain

BindingContextプロパティ

前述のように「ソースとターゲットの結び付け」は、「ターゲットオブジェクトのBindingContextプロパティにソースオブジェクトを設定する」事で行います。
BindingContextプロパティをXamarinヘルプで確認すると、「Xamain.Forms.BindableObjectクラスで定義されたObject型のプロパティ 」であることが分かります。
つまり、データバインディングにおけるターゲットとなりうるオブジェクトとは、BindableObjectを継承したクラスであると言う事が出来ます。
そして、Labelなどの多くのUIコントロールクラスは Viewクラス を継承しています。さらにViewクラスから継承関係を辿ると、以下の図のようなクラス継承関係となっています。

f:id:daigo-knowlbo:20161225174653p:plain

このことから、Xamarin Formsにおける殆どのUIコントロールが、データバインディングにおける「ターゲット」となる事ができると言う事、及びその意味が分かります。

BindableProperty

ターゲットオブジェクトに対してソースオブジェクトをデータバインドする事が分かりました。
ただ、上述の例でも示したように、ターゲット自体ではなくターゲットオブジェクトのプロパティ(LabelコントロールクラスのTextプロパティ)に対して、ソースオブジェクトのプロパティ(PersonクラスのNameプロパティ)をデータバインドします。
これはごく当たり前の自然なお話ですね。
ただし、ここにも技術的仕組み上の あるルール が存在します。
ターゲットオブジェクトの中で「データバインドの対象とする事ができるプロパティ」とは、「BindableProperty」と呼ばれる特殊なプロパティである必要があります。
Labelクラスのヘルプページを参照すると、Textプロパティの項目には以下のような記述があるはずです(https://developer.xamarin.com/api/type/Xamarin.Forms.Label/)。

String. Gets or sets the text for the Label. This is a bindable property.

そしてBindablePropertyは少し特殊な定義の仕方をする必要があり、例えばstring型のTextプロパティをBindablePropertyとして定義する場合、以下のような実装を行うことになります。

// リスト4 BindablePropertyの実装方法(カスタムコントロール実装時などに使う)
public static readonly BindableProperty TextProperty =
  BindableProperty.Create("Text", typeof(string), typeof(Example1Page), default(string), 
                          propertyChanged: (bindable, oldValue, newValue) =>
                          ((Example1Page)bindable).Text = (string)newValue);    
public string Text
{
  get { return (string)GetValue(TextProperty); }
  set { SetValue(TextProperty, value); }
}

そう、以下のような単純なプロパティ定義では、データバインド可能なプロパティにはなりません。

// リスト5 こんな定義ではデータバインド可能なプロパティとはならない・・・
public string Text { get; set; }

この辺りの詳細は、カスタムコントロールを実装する際に必要になります。
ひとまず、提供されているコントロールはこのような実装を行なっている、そしてBindablePropertyがデータバインド可能なプロパティである、と理解しておけばまずは良いのではないかと思います。

バインディングの方向

データバインディングとは「ターゲットオブジェクトとソースオブジェクト」の結び付けです。
そして、バインディングには方向の概念があります。
以下の3つの方向の方式があります。これは、Xamarin.Forms.BindingMode列挙体で定義されています(WPFと異なりOneTimeという設定がなくなっています)。

  • OneWay
    「ソース → ターゲット」の単方向に対してデータバインディングが行われます。
    プログラムロジック上でソースとなるモデルオブジェクトの値を設定・変更し、そのソース値をターゲット(UIコントロール等)にバインドする事で画面表示を切り替えるような用途が想定されます。

  • OneWayToSource
    「ターゲット → ソース」の単方向に対してデータバインディングが行われます。
    UI上の入力項目(例えばEntryコントロール)をターゲットとし、画面上で入力された値をソースオブジェクトにバインディングさせるような用途が考えられます。

  • TwoWay
    「ターゲット ←→ ソース」の両方向に対してデータバインディングが行われます。
    プログラムロジック上でのソースオブジェクトの変更をターゲットであるUIに反映させる、また、逆にターゲットであるUI上での値変更をソースオブジェクトに反映させる、という相互でのデータバインディングが行われます。

3つのバインディングモードを使用したサンプル

3つのバインディングモードの動作の違いを確認できるサンプルプログラムを作成します。

ソースオブジェクト

データバインディングのソースオブジェクトとして ValueModelクラス を用意します。
単純に Value1 / Value2 / Value3 というstring型のプロパティを3つ持ちます。
いくつか補足が必要な実装が含まれていますが、ソース下部で説明を行います。

// リスト6 ValueModel.cs
using System;
using System.ComponentModel;

namespace Example2
{
  /// <summary>
  /// ソースオブジェクトとして利用するオブジェクト
  /// </summary>
  public class ValueModel : INotifyPropertyChanged
  {
    /// <summary>
    /// INotifyPropertyChangedインターフェイスの実装
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    // データを保持するフィールド
    private string value1;
    private string value2;
    private string value3;

    // Value1プロパティ
    public string Value1
    {
      get
      {
        return this.value1;
      }
      set
      {
        if (this.value1 != value)
        {
          this.value1 = value;
          OnPropertyChanged("Value1");
        }
      }
    }

    // Value2プロパティ
    public string Value2
    {
      get
      {
        return this.value2;
      }
      set
      {
        if (this.value2 != value)
        {
          this.value2 = value;
          OnPropertyChanged("Value2");
        }
      }
    }

    // Value3プロパティ
    public string Value3
    {
      get
      {
        return this.value3;
      }
      set
      {
        if (this.value3 != value)
        {
          this.value3 = value;
          OnPropertyChanged("Value3");
        }
      }
    }

    // プロパティ値の変更を通知します
    protected virtual void OnPropertyChanged(string propertyName)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this,
          new PropertyChangedEventArgs(propertyName));
      }
    }
  }
}

上記 ValueModelクラス の実装に関して「INotifyPropertyChangedインターフェイスの実装」について補足説明を行う必要があります。
INotifyPropertyChangedインターフェイスとは、その名称の通り「プロパティの変更を通知する」インターフェイスです。
INotifyPropertyChangedインターフェイスを実装する事で、「ソースオブジェクトのプロパティ値に変更があった際に、ターゲットオブジェクトへの通知」を行う事ができます。
つまり、OneWay / TwoWayモードのデータバインディングにおいてソースオブジェクトの変更がリアルタイムにターゲットに伝達されます。

フォームコントロール(ターゲット)の定義

フォーム上に3つの Entryコントロール を配置します。それぞれ Value1へのOneWayモードバインディング / Value2へのOneWayToSourceモードバインディング / Value3へのTwoWayモードバインディング とします。
また、3つのボタンを用意し、それぞれ、クリックされたらプログラム側で Value1 / Value2 / VAlue3 の値を変更します。各ソース値の変更が、ターゲット(Entryコントロール)に反映される様子を確認します。
もう1つ、「show data」ボタンを配置します。これはクリックすると、Value1 / Value2 / Value3 の値をアラートダイアログで表示します。これにより、ターゲット(Entryコントロール)からソースの値への変更の反映を確認します。

// リスト7 Example2Page.xaml
<?xml version="1.0" encoding="utf-8"?>
<ContentPage 
  xmlns="http://xamarin.com/schemas/2014/forms" 
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
  x:Class="Example2.Example2Page">

  <StackLayout x:Name="stack1" Margin="0,20,0,0">
    <Label Text="OneWay(ソース→ターゲット)" />
    <Entry x:Name="entry1" Text="{Binding Path=Value1,Mode=OneWay}"/>

    <Label Text="OneWayToSource(ターゲット→ソース)" />
    <Entry x:Name="entry2" Text="{Binding Path=Value2,Mode=OneWayToSource}"/>

    <Label Text="TwoWay(ソース←→ターゲット)" />
    <Entry x:Name="entry3" Text="{Binding Path=Value3,Mode=TwoWay}"/>

    <Button Clicked="onClickedModifyValue1" Text="ソースのValue1値を変更"/>
    <Button Clicked="onClickedModifyValue2" Text="ソースのValue2値を変更"/>
    <Button Clicked="onClickedModifyValue3" Text="ソースのValue3値を変更"/>

    <Button Clicked="onClickedShowData" Text="show data"/>
  </StackLayout>
</ContentPage>

以下がフォームのコードビハインドクラスになります。
ソースオブジェクトは適当な値で生成しています。
stack1(StackLayoutコントロール)の BindingContextプロパティ にソースオブジェクトを設定しています。
実際にデータバインディングを行うのは「StackLayoutの子コントロールである 3つのEntryコントロール」です。しかし、親であるStackLayoutコントロールにソースオブジェクトを設定しています。これは「BindingContextによるデータソースのバインド情報は、子要素に継承される」というデータバインディングの特徴を生かしています。

// リスト8 Example2Page.xaml.cs
using Xamarin.Forms;

namespace Example2
{
  public partial class Example2Page : ContentPage
  {
    // ソースオブジェクト
    private ValueModel valueModel;

    public Example2Page()
    {
      InitializeComponent();

      // ソースオブジェクト(Valuemodel)を生成します
      this.valueModel = new ValueModel(){
        Value1 = "default value 1",
        Value2 = "default value 2", 
        Value3 = "default value 3" };

      // データバインディングのターゲットオブジェクトとソースオブジェクトを紐付けます
      this.stack1.BindingContext = this.valueModel;
    }

    public void onClickedModifyValue1(object sender, System.EventArgs e)
    {
      // Value1データ値をプログラムロジックで変更します
      this.valueModel.Value1 = "Modify1:" + System.DateTime.Now.ToString("yyyy/MM/dd");
    }

    public void onClickedModifyValue2(object sender, System.EventArgs e)
    {
      // Value2データ値をプログラムロジックで変更します
      this.valueModel.Value2 = "Modify2:" + System.DateTime.Now.ToString("yyyy/MM/dd");
    }

    public void onClickedModifyValue3(object sender, System.EventArgs e)
    {
      // Value3データ値をプログラムロジックで変更します
      this.valueModel.Value3 = "Modify3:" + System.DateTime.Now.ToString("yyyy/MM/dd");
    }

    public async void onClickedShowData(object sender, System.EventArgs e)
    {
      // ソースオブジェクトの値をポップアップ表示します
      await DisplayAlert(
        "info", 
        string.Format("Value1: {0} /" +
                      "Value2: {1} /" +
                      "Value3: {2}", 
                      this.valueModel.Value1, 
                      this.valueModel.Value2, 
                      this.valueModel.Value3),
        "close");
    }
  }
}

実行してみる

ここではiOSシミュレータで実行します。
①実行
実行直後の画面が以下です。1つ目のEntry(OneWay) および 3つ目のEntry(TwoWay)にはソースの値が反映されています。2つ目のEntryはOneWayToSourceモードである為、ソース→ターゲットへのデータバインディングは行われません。

f:id:daigo-knowlbo:20161225115342p:plain

②次に、3つのEntryにUI上から「変更」の文字を追記します。

f:id:daigo-knowlbo:20161225115403p:plain

③ここで、「show data」ボタンをクリックし、ソースオブジェクトの値を確認します。
Value2 / Value3にはEntryコントロールUI上での「変更」文字の追記が反映されました。この2つはそれぞれ、OneWayToSource / TwoWayモードでデータバインディングされている為、「ターゲット→ソース」へのデータ反映が行われました。

f:id:daigo-knowlbo:20161225115128p:plain

④次に「ソースのValue1値を変更」「ソースのValue2値を変更」「ソースのValue3値を変更」ボタンをクリックします。
これにより、プログラム上で Value1 / Value2 / Value3 の値が変更されます。
以下がその結果画面です。1つ目、3つ目のEntryコントロールの値が変更されました。OneWay / TwoWayモードデータバインディングである為、「ソース→ターゲット」へのデータ変更反映が行われました。
1つ重要な点は、ソース→ターゲットの変更反映を行う為に、ソースオブジェクト(ここではValueModelクラス)に対してINotifyPropertyChangedインターフェイス の実装が必要であるということです。

f:id:daigo-knowlbo:20161225115158p:plain

⑤最後にもう1度「show data」ボタンをクリックします。
④での操作により、ターゲットであるEntryコントロールへのデータ変更の反映の有無に関わらず、ソースオブジェクトの値自体は変更されていることを確認することができます。

f:id:daigo-knowlbo:20161225115221p:plain

コードでデータバインディングを定義

実際には多くの場合、XAMLのデータバインド構文を利用することになりますが、同じことをコードから行う事が出来ます。
先程のリスト1 / リスト2を、コードによるデータバインディング定義で書き換えると次のようになります。
Xamlへの {Binding Path=Name} の書き換えになります)

// リスト8 「リスト2 Example1Page.xaml」をコードで書き換えると
this.Label1.BindingContext = person;
this.Label1.SetBinding(
    Label.TextProperty, 
    "Name", 
    BindingMode.OneWay, 
    null, 
    null);

SetBinding()メソッドは「BindableObjectの拡張メソッド」として定義されています。
BindableObjectとは、つまりデータバインディングのターゲットとなりうる(多くの)UIコントロールが該当します。
Label1の SetBinding() メソッドを呼び出します。
第1引数は、バインディング対象の BindableProperty を指定します。前述でバインド可能なプロパティは「通常のプロパティではなくBindablePropertyという特殊なプロパティである」と説明しました。Xaml定義上では「暗黙的にTextプロパティへの {Binding} 定義」を行いますが、コード上では「明示的にLabel.TextPropertyというBindableProperty型」を指定します。
第2引数は、Pathの指定になります。ソースはオブジェクト Person の Nameプロパティ がバインド対象なので「"Name"」となります。

まとめ & つづく・・・

ということで、Xamarin Formsにおけるデータバインディングの基本をまとめてみました。
基本といっても、まだまだ情報不足な部分もありますが、それでも結構長くなってしまったので、本投稿ではここまでとします。
今後のポストではコレクションコントロールへのデータバインディングとかにも触れていこうと思っています。