第 4 部 データ バインディングの基礎
データ バインディングを使うと、2 つのオブジェクトのプロパティをリンクできるため、一方を変更するともう一方も変更されます。 これはとても役立つツールです。データ バインディングはコード内で完全に定義でき、XAML はショートカットと利便性を提供します。 そのため、Xamarin.Forms の最も重要なマークアップ拡張の 1 つは Binding です。
データ バインディング
データ バインディングでは、ソースとターゲットと呼ばれる 2 つのオブジェクトのプロパティを接続します。 コードでは、2 つの手順が必要です。ターゲット オブジェクトの BindingContext
プロパティをソース オブジェクトに設定する必要があります。次に、SetBinding
メソッド (多くの場合、Binding
クラスと組み合わせて使われます) をターゲット オブジェクトで呼び出して、そのオブジェクトのプロパティをソース オブジェクトのプロパティにバインドする必要があります。
ターゲット プロパティはバインド可能なプロパティである必要があります。つまり、ターゲット オブジェクトは BindableObject
から派生する必要があります。 オンラインの Xamarin.Forms ドキュメントには、どのプロパティがバインド可能なプロパティであるかが示されています。 Text
などの Label
のプロパティは、バインド可能なプロパティ TextProperty
に関連付けられています。
マークアップでは、コードで必要なのと同じ 2 つの手順を実行する必要があります。ただし、SetBinding
呼び出しと Binding
クラスの代わりに Binding
マークアップ拡張が使われる点が異なります。
ただし、XAML でデータ バインディングを定義する場合、ターゲット オブジェクトの BindingContext
を設定する方法は複数あります。 分離コードファイルから設定される場合もあれば、StaticResource
や x: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>
Slider
は x:Name
属性を含み、x:Reference
マークアップ拡張を使用して、2 つの Label
ビューによって参照されます。
x:Reference
バインディング拡張は、参照される要素の名前、この場合は slider
に設定する Name
というプロパティを定義します。 ただし、x:Reference
マークアップ拡張を定義している ReferenceExtension
クラスは、Name
の ContentProperty
属性も定義しています。これは、明示的に要求されていないことを意味します。 変化を付けるために、最初の x:Reference
には "Name=" がありますが、2 番目にはありません。
BindingContext="{x:Reference Name=slider}"
…
BindingContext="{x:Reference slider}"
Binding
マークアップ拡張自体は、BindingBase
や Binding
クラスと同様に、いくつかのプロパティを持つことができます。 Binding
の ContentProperty
は Path
ですが、パスが 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'}"
実行中のプログラムを次に示します。
バインディング モード
1 つのビューで、いくつかのプロパティにデータ バインディングを設定できます。 ただし、各ビューに含められるのは 1 つの BindingContext
のみのため、そのビュー上の複数のデータ バインディングは、同じオブジェクトのすべてのプロパティを参照する必要があります。
この問題やその他の問題を解決するには、BindingMode
列挙型の要素に設定される Mode
プロパティを使用します。
Default
OneWay
- 値はソースからターゲットに転送されますOneWayToSource
- 値はターゲットからソースに転送されます。TwoWay
- 値はソースとターゲットの間で双方向に転送されますOneTime
- データは、ソースからターゲットに移動しますが、BindingContext
が変更された場合のみです
次のプログラムは、OneWayToSource
および TwoWay
バインディング モードの一般的な使用法の 1 つを示しています。 4 つの Slider
ビューは、Label
の Scale
、Rotate
、RotateX
、RotateY
プロパティを制御することを目的としています。 最初は、Label
のこれら 4 つのプロパティは、それぞれ Slider
によって設定されているので、データ バインディングのターゲットであるように思えます。 しかし、Label
の BindingContext
は 1 つのオブジェクトにしかなりえず、4 つの異なるスライダーがあります。
そのため、すべてのバインディングは一見逆のように設定されています。4 つの各スライダーの BindingContext
が Label
に設定され、バインディングはスライダーの Value
プロパティに設定されています。 OneWayToSource
モードと TwoWay
モードを使うと、これらの Value
プロパティは、Label
の Scale
、Rotate
、RotateX
、RotateY
プロパティであるソース プロパティを設定できます。
<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
ビューにより、Label
の Rotate
、RotateX
、RotateY
プロパティが変更されます。
ただし、Scale
プロパティのバインディングは TwoWay
です。 これは、Scale
プロパティの初期値が 1 であり、TwoWay
バインディングを使用すると Slider
の初期値が 0 ではなく 1 に設定されるためです。 このバインディングが OneWayToSource
である場合、Scale
プロパティの初期値は Slider
の初期値から 0 に設定されます。 Label
は表示されないため、ユーザーが混乱する可能性があります。
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
プロパティを ListView
の ItemsSource
に設定するのが簡単です。
<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
型であることが確認されます。
それほど多くの情報はありませんが、ListView
はスクロールと選択ができます。
項目のテンプレートを定義するには、ItemTemplate
プロパティをプロパティ要素として分割し、それを DataTemplate
に設定し、それから ViewCell
を参照します。 ViewCell
の View
プロパティに、各項目を表示する 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
要素は、ViewCell
の View
プロパティに設定されます (View
プロパティは ViewCell
のコンテンツ プロパティであるため、ViewCell.View
タグは必要ありません)。このマークアップは、各 NamedColor
オブジェクトの FriendlyName
プロパティを表示します。
ずっと良くなりました。 ここで必要なのは、より多くの情報と実際の色を使って項目テンプレートを整えることだけです。 このテンプレートをサポートするために、一部の値とオブジェクトがページのリソース ディクショナリで定義されています。
<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
構造の個々の R
、G
、B
プロパティが表示されます。 これらのプロパティは double
型で、範囲は 0 から 1 です。 16 進数の値を表示する場合は、単純に "X2" 書式指定で StringFormat
を使用することはできません。 この書式設定は整数に対してのみ機能するのに加え、double
値に 255 を乗算する必要があります。
この小さな問題は、"値コンバーター" ("バインディング コンバーター" とも呼ばれます) で解決されました。 これは IValueConverter
インターフェイスを実装するクラスなので、Convert
と ConvertBack
という 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}'}" />
結果は次のようになります。
ListView
は、基になるデータで動的に発生する可能性のある変更を処理する点で非常に高度ですが、これは特定の手順を実行した場合に限られます。 ListView
の ItemsSource
プロパティに割り当てられた項目のコレクションが実行時に変更される場合、つまり項目をコレクションに追加または削除できる場合、これらの項目に対して ObservableCollection
クラスを使用します。 ObservableCollection
は INotifyCollectionChanged
インターフェイスを実装し、ListView
は CollectionChanged
イベントのハンドラーをインストールします。
実行時に項目自体のプロパティが変更された場合、コレクション内の項目は INotifyPropertyChanged
インターフェイスを実装し、PropertyChanged
イベントを使用してプロパティ値の変更を通知する必要があります。 これについては、このシリーズの次のパート、「パート 5. データ バインディングから MVVM まで」で説明します。
まとめ
データ バインディングは、ページ内の 2 つのオブジェクト間、またはビジュアル オブジェクトと基になるデータの間でプロパティをリンクするための強力なメカニズムを提供します。 ただし、アプリケーションがデータ ソースと連携し始めると、一般的なアプリケーション アーキテクチャ パターンが有用なパラダイムとして浮上してきます。 これについては、「パート 5. データ バインディングから MVVM まで」で説明します。