第 4 部 データ バインディングの基礎

Download Sampleサンプルのダウンロード

データ バインディングを使うと、2 つのオブジェクトのプロパティをリンクできるため、一方を変更するともう一方も変更されます。 これはとても役立つツールです。データ バインディングはコード内で完全に定義でき、XAML はショートカットと利便性を提供します。 そのため、Xamarin.Forms の最も重要なマークアップ拡張の 1 つは Binding です。

データ バインディング

データ バインディングでは、ソースターゲットと呼ばれる 2 つのオブジェクトのプロパティを接続します。 コードでは、2 つの手順が必要です。ターゲット オブジェクトの BindingContext プロパティをソース オブジェクトに設定する必要があります。次に、SetBinding メソッド (多くの場合、Binding クラスと組み合わせて使われます) をターゲット オブジェクトで呼び出して、そのオブジェクトのプロパティをソース オブジェクトのプロパティにバインドする必要があります。

ターゲット プロパティはバインド可能なプロパティである必要があります。つまり、ターゲット オブジェクトは BindableObject から派生する必要があります。 オンラインの Xamarin.Forms ドキュメントには、どのプロパティがバインド可能なプロパティであるかが示されています。 Text などの Label のプロパティは、バインド可能なプロパティ TextProperty に関連付けられています。

マークアップでは、コードで必要なのと同じ 2 つの手順を実行する必要があります。ただし、SetBinding 呼び出しと Binding クラスの代わりに Binding マークアップ拡張が使われる点が異なります。

ただし、XAML でデータ バインディングを定義する場合、ターゲット オブジェクトの BindingContext を設定する方法は複数あります。 分離コードファイルから設定される場合もあれば、StaticResourcex:Static のマークアップ拡張子を使って設定される場合や、BindingContext プロパティ要素タグの内容として設定される場合もあります。

バインディングは、プログラムのビジュアルを基となるデータ モデルに接続するために最もよく使われます。通常は、MVVM (Model-View-ViewModel) アプリケーション アーキテクチャの実現において使われます (「パート 5. データ バインディングから MVVM まで」で説明) が、他のシナリオも可能です。

ビューからビューへのバインディング

データ バインディングを定義して、同じページ上の 2 つのビューのプロパティをリンクできます。 この場合、x:Reference マークアップ拡張を使ってターゲット オブジェクトの BindingContext を設定します。

以下は、1 つの Slider ビューと 2 つの Label ビューを含む XAML ファイルです。そのうちの 1 つは Slider 値によって回転され、もう 1 つは Slider 値を表示します。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SliderBindingsPage"
             Title="Slider Bindings Page">

    <StackLayout>
        <Label Text="ROTATION"
               BindingContext="{x:Reference Name=slider}"
               Rotation="{Binding Path=Value}"
               FontAttributes="Bold"
               FontSize="Large"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />

        <Slider x:Name="slider"
                Maximum="360"
                VerticalOptions="CenterAndExpand" />

        <Label BindingContext="{x:Reference slider}"
               Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
               FontAttributes="Bold"
               FontSize="Large"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>

Sliderx:Name 属性を含み、x:Reference マークアップ拡張を使用して、2 つの Label ビューによって参照されます。

x:Reference バインディング拡張は、参照される要素の名前、この場合は slider に設定する Name というプロパティを定義します。 ただし、x:Reference マークアップ拡張を定義している ReferenceExtension クラスは、NameContentProperty 属性も定義しています。これは、明示的に要求されていないことを意味します。 変化を付けるために、最初の x:Reference には "Name=" がありますが、2 番目にはありません。

BindingContext="{x:Reference Name=slider}"
…
BindingContext="{x:Reference slider}"

Binding マークアップ拡張自体は、BindingBaseBinding クラスと同様に、いくつかのプロパティを持つことができます。 BindingContentPropertyPath ですが、パスが Binding マークアップ拡張の最初の項目である場合、マークアップ拡張機能の「Path=」部分は省略できます。 最初の例には "Path=" がありますが、2 番目の例では省略されています。

Rotation="{Binding Path=Value}"
…
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"

プロパティはすべて 1 行に含めることも、複数行に分割することもできます。

Text="{Binding Value,
               StringFormat='The angle is {0:F0} degrees'}"

便利なことは何でも行いましょう。

2 番目の Binding マークアップ拡張の StringFormat プロパティに注目してください。 Xamarin.Forms では、バインディングは暗黙的な型変換を実行しません。文字列以外のオブジェクトを文字列として表示する必要がある場合は、型コンバーターを提供するか、StringFormat を使う必要があります。 バックグラウンドでは、静的 String.Format メソッドを使って StringFormat を実装します。 .NET の書式設定仕様には中かっこが含まれており、中かっこはマークアップ拡張子の区切りにも使われるため、潜在的に問題となります。 これにより、XAML パーサーが混乱するリスクが生じます。 これを避けるには、書式設定文字列全体を一重引用符で囲みます。

Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"

実行中のプログラムを次に示します。

View-to-View Bindings

バインディング モード

1 つのビューで、いくつかのプロパティにデータ バインディングを設定できます。 ただし、各ビューに含められるのは 1 つの BindingContext のみのため、そのビュー上の複数のデータ バインディングは、同じオブジェクトのすべてのプロパティを参照する必要があります。

この問題やその他の問題を解決するには、BindingMode 列挙型の要素に設定される Mode プロパティを使用します。

  • Default
  • OneWay - 値はソースからターゲットに転送されます
  • OneWayToSource - 値はターゲットからソースに転送されます。
  • TwoWay - 値はソースとターゲットの間で双方向に転送されます
  • OneTime - データは、ソースからターゲットに移動しますが、BindingContext が変更された場合のみです

次のプログラムは、OneWayToSource および TwoWay バインディング モードの一般的な使用法の 1 つを示しています。 4 つの Slider ビューは、LabelScaleRotateRotateXRotateY プロパティを制御することを目的としています。 最初は、Label のこれら 4 つのプロパティは、それぞれ Slider によって設定されているので、データ バインディングのターゲットであるように思えます。 しかし、LabelBindingContext は 1 つのオブジェクトにしかなりえず、4 つの異なるスライダーがあります。

そのため、すべてのバインディングは一見逆のように設定されています。4 つの各スライダーの BindingContextLabel に設定され、バインディングはスライダーの Value プロパティに設定されています。 OneWayToSource モードと TwoWay モードを使うと、これらの Value プロパティは、LabelScaleRotateRotateXRotateY プロパティであるソース プロパティを設定できます。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SliderTransformsPage"
             Padding="5"
             Title="Slider Transforms Page">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <!-- Scaled and rotated Label -->
        <Label x:Name="label"
               Text="TEXT"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />

        <!-- Slider and identifying Label for Scale -->
        <Slider x:Name="scaleSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="1" Grid.Column="0"
                Maximum="10"
                Value="{Binding Scale, Mode=TwoWay}" />

        <Label BindingContext="{x:Reference scaleSlider}"
               Text="{Binding Value, StringFormat='Scale = {0:F1}'}"
               Grid.Row="1" Grid.Column="1"
               VerticalTextAlignment="Center" />

        <!-- Slider and identifying Label for Rotation -->
        <Slider x:Name="rotationSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="2" Grid.Column="0"
                Maximum="360"
                Value="{Binding Rotation, Mode=OneWayToSource}" />

        <Label BindingContext="{x:Reference rotationSlider}"
               Text="{Binding Value, StringFormat='Rotation = {0:F0}'}"
               Grid.Row="2" Grid.Column="1"
               VerticalTextAlignment="Center" />

        <!-- Slider and identifying Label for RotationX -->
        <Slider x:Name="rotationXSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="3" Grid.Column="0"
                Maximum="360"
                Value="{Binding RotationX, Mode=OneWayToSource}" />

        <Label BindingContext="{x:Reference rotationXSlider}"
               Text="{Binding Value, StringFormat='RotationX = {0:F0}'}"
               Grid.Row="3" Grid.Column="1"
               VerticalTextAlignment="Center" />

        <!-- Slider and identifying Label for RotationY -->
        <Slider x:Name="rotationYSlider"
                BindingContext="{x:Reference label}"
                Grid.Row="4" Grid.Column="0"
                Maximum="360"
                Value="{Binding RotationY, Mode=OneWayToSource}" />

        <Label BindingContext="{x:Reference rotationYSlider}"
               Text="{Binding Value, StringFormat='RotationY = {0:F0}'}"
               Grid.Row="4" Grid.Column="1"
               VerticalTextAlignment="Center" />
    </Grid>
</ContentPage>

Slider ビューの 3 つのバインディングは OneWayToSource で、Slider の値が BindingContext のプロパティ (label という名前の Label) の変更を引き起こすことを意味します。 これら 3 つの Slider ビューにより、LabelRotateRotateXRotateY プロパティが変更されます。

ただし、Scale プロパティのバインディングは TwoWay です。 これは、Scale プロパティの初期値が 1 であり、TwoWay バインディングを使用すると Slider の初期値が 0 ではなく 1 に設定されるためです。 このバインディングが OneWayToSource である場合、Scale プロパティの初期値は Slider の初期値から 0 に設定されます。 Label は表示されないため、ユーザーが混乱する可能性があります。

Backwards Bindings

Note

VisualElement クラスには、ScaleX プロパティと ScaleY プロパティもあり、それぞれ x 軸とy 軸で VisualElement をスケーリングします。

バインディングとコレクション

テンプレート化された ListView ほど XAML とデータ バインディングの力を示すものはありません。

ListView は、IEnumerable 型の ItemsSource プロパティを定義し、そのコレクション内の項目を表示します。 これらの項目は、任意の型のオブジェクトにすることができます。 既定では、ListView は各項目の ToString メソッドを使用して、その項目を表示します。 これが望み通りの結果である場合もありますが、多くの場合、ToString は、オブジェクトの完全修飾クラス名のみを返します。

ただし、ListView コレクション内の項目は、テンプレート を使用して任意の方法で表示できます。これには、Cell から派生したクラスが含まれます。 テンプレートは、ListView 内のすべての項目に対して複製され、テンプレートに設定されているデータ バインディングが個々の複製に転送されます。

多くの場合、ViewCell クラスを使ってこれらの項目用のカスタム セルを作成できます。 この処理はコードではやや複雑ですが、XAML では非常に簡単になります。

XamlSamples プロジェクトには、NamedColor というクラスが含まれています。 各 NamedColor オブジェクトには、string 型の Name プロパティと FriendlyName プロパティ、Color 型の Color プロパティがあります。 さらに、NamedColor には、Xamarin.FormsColor クラスで定義された色に対応する Color 型の 141 個の静的読み取り専用フィールドがあります。 静的コンストラクターは、これらの静的フィールドに対応する NamedColor オブジェクトを含む IEnumerable<NamedColor> コレクションを作成し、それをパブリックの静的 All プロパティに割り当てます。

x:Static マークアップ拡張を使うと、静的な NamedColor.All プロパティを ListViewItemsSource に設定するのが簡単です。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ListViewDemoPage"
             Title="ListView Demo Page">

    <ListView ItemsSource="{x:Static local:NamedColor.All}" />

</ContentPage>

結果の表示では、項目が実際に XamlSamples.NamedColor 型であることが確認されます。

Binding to a Collection

それほど多くの情報はありませんが、ListView はスクロールと選択ができます。

項目のテンプレートを定義するには、ItemTemplate プロパティをプロパティ要素として分割し、それを DataTemplate に設定し、それから ViewCell を参照します。 ViewCellView プロパティに、各項目を表示する 1 つ以上のビューのレイアウトを定義できます。 簡単な例を次に示します。

<ListView ItemsSource="{x:Static local:NamedColor.All}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <ViewCell.View>
                    <Label Text="{Binding FriendlyName}" />
                </ViewCell.View>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Note

セルとセルの子のバインディング ソースは、ListView.ItemsSource コレクションです。

Label 要素は、ViewCellView プロパティに設定されます (View プロパティは ViewCell のコンテンツ プロパティであるため、ViewCell.View タグは必要ありません)。このマークアップは、各 NamedColor オブジェクトの FriendlyName プロパティを表示します。

Binding to a Collection with a DataTemplate

ずっと良くなりました。 ここで必要なのは、より多くの情報と実際の色を使って項目テンプレートを整えることだけです。 このテンプレートをサポートするために、一部の値とオブジェクトがページのリソース ディクショナリで定義されています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.ListViewDemoPage"
             Title="ListView Demo Page">

    <ContentPage.Resources>
        <ResourceDictionary>
            <OnPlatform x:Key="boxSize"
                        x:TypeArguments="x:Double">
                <On Platform="iOS, Android, UWP" Value="50" />
            </OnPlatform>

            <OnPlatform x:Key="rowHeight"
                        x:TypeArguments="x:Int32">
                <On Platform="iOS, Android, UWP" Value="60" />
            </OnPlatform>

            <local:DoubleToIntConverter x:Key="intConverter" />

        </ResourceDictionary>
    </ContentPage.Resources>

    <ListView ItemsSource="{x:Static local:NamedColor.All}"
              RowHeight="{StaticResource rowHeight}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <StackLayout Padding="5, 5, 0, 5"
                                 Orientation="Horizontal"
                                 Spacing="15">

                        <BoxView WidthRequest="{StaticResource boxSize}"
                                 HeightRequest="{StaticResource boxSize}"
                                 Color="{Binding Color}" />

                        <StackLayout Padding="5, 0, 0, 0"
                                     VerticalOptions="Center">

                            <Label Text="{Binding FriendlyName}"
                                   FontAttributes="Bold"
                                   FontSize="Medium" />

                            <StackLayout Orientation="Horizontal"
                                         Spacing="0">
                                <Label Text="{Binding Color.R,
                                       Converter={StaticResource intConverter},
                                       ConverterParameter=255,
                                       StringFormat='R={0:X2}'}" />

                                <Label Text="{Binding Color.G,
                                       Converter={StaticResource intConverter},
                                       ConverterParameter=255,
                                       StringFormat=', G={0:X2}'}" />

                                <Label Text="{Binding Color.B,
                                       Converter={StaticResource intConverter},
                                       ConverterParameter=255,
                                       StringFormat=', B={0:X2}'}" />
                            </StackLayout>
                        </StackLayout>
                    </StackLayout>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

OnPlatform を使って BoxView のサイズと ListView 行の高さを定義していることに注意してください。 すべてのプラットフォームの値は同じですが、簡単にマークアップを他の値に適合させて表示を微調整できます。

値コンバーターのバインディング

前の ListView Demo XAML ファイルでは、Xamarin.FormsColor 構造の個々の RGB プロパティが表示されます。 これらのプロパティは double 型で、範囲は 0 から 1 です。 16 進数の値を表示する場合は、単純に "X2" 書式指定で StringFormat を使用することはできません。 この書式設定は整数に対してのみ機能するのに加え、double 値に 255 を乗算する必要があります。

この小さな問題は、"値コンバーター" ("バインディング コンバーター" とも呼ばれます) で解決されました。 これは IValueConverter インターフェイスを実装するクラスなので、ConvertConvertBack という 2 つのメソッドがあります。 Convert メソッドは、値がソースからターゲットに転送されるときに呼び出されます。ConvertBack メソッドは、OneWayToSource または TwoWay バインディングのターゲットからソースへの転送に対して呼び出されます。

using System;
using System.Globalization;
using Xamarin.Forms;

namespace XamlSamples
{
    class DoubleToIntConverter : IValueConverter
    {
        public object Convert(object value, Type targetType,
                              object parameter, CultureInfo culture)
        {
            double multiplier;

            if (!Double.TryParse(parameter as string, out multiplier))
                multiplier = 1;

            return (int)Math.Round(multiplier * (double)value);
        }

        public object ConvertBack(object value, Type targetType,
                                  object parameter, CultureInfo culture)
        {
            double divider;

            if (!Double.TryParse(parameter as string, out divider))
                divider = 1;

            return ((double)(int)value) / divider;
        }
    }
}

バインディングはソースからターゲットへの一方向のみであるため、ConvertBack メソッドはこのプログラムでは役割を果たしません。

バインディングは、Converter プロパティを使用してバインディング コンバーターを参照します。 バインディング コンバーターは、ConverterParameter プロパティで指定されたパラメーターを受け入れることもできます。 ある程度の汎用性のために、これは乗数の指定方法です。 バインディング コンバーターは、有効な double 値のコンバーター パラメーターをチェックします。

コンバーターはリソース ディクショナリ内でインスタンスが作成されるため、複数のバインディング間で共有できます。

<local:DoubleToIntConverter x:Key="intConverter" />

3 つのデータ バインディングがこの単一インスタンスを参照します。 Binding マークアップ拡張には埋め込みの StaticResource マークアップ拡張が含まれていることに注意してください。

<Label Text="{Binding Color.R,
                      Converter={StaticResource intConverter},
                      ConverterParameter=255,
                      StringFormat='R={0:X2}'}" />

結果は次のようになります。

Binding to a Collection with a DataTemplate and Converters

ListView は、基になるデータで動的に発生する可能性のある変更を処理する点で非常に高度ですが、これは特定の手順を実行した場合に限られます。 ListViewItemsSource プロパティに割り当てられた項目のコレクションが実行時に変更される場合、つまり項目をコレクションに追加または削除できる場合、これらの項目に対して ObservableCollection クラスを使用します。 ObservableCollectionINotifyCollectionChanged インターフェイスを実装し、ListViewCollectionChanged イベントのハンドラーをインストールします。

実行時に項目自体のプロパティが変更された場合、コレクション内の項目は INotifyPropertyChanged インターフェイスを実装し、PropertyChanged イベントを使用してプロパティ値の変更を通知する必要があります。 これについては、このシリーズの次のパート、「パート 5. データ バインディングから MVVM まで」で説明します。

まとめ

データ バインディングは、ページ内の 2 つのオブジェクト間、またはビジュアル オブジェクトと基になるデータの間でプロパティをリンクするための強力なメカニズムを提供します。 ただし、アプリケーションがデータ ソースと連携し始めると、一般的なアプリケーション アーキテクチャ パターンが有用なパラダイムとして浮上してきます。 これについては、「パート 5. データ バインディングから MVVM まで」で説明します。