2016年4月18日月曜日

Xamarinのxamlのデータバインディングのpart4について訳してみた。


Part4. データバインディングの基礎

データバインディングは2つのオブジェクトのプロパティをリンクし、片方の変更を契機に もう片方の変更を発生させることができます。 これは大変価値のあるツールです。そしてデータバインディングはソースコード上で完璧に 定義することができ、XAMLはそのショートカットと利便性を提供します。 その結果として、バインティングがXamarin.Formsのマークアップ拡張は最も重要な機能 の一つとなります。

データバインディング

データバインティングは2つのオブジェクトのプロパティを接続し、ソースからターゲットへ 呼び出しを行います。コードの中では、2つのステップが要求されます。 一つ目は、ターゲットオブジェクトのBindingContextプロパティを必ずソースオブジェクト にセットする必要があります。 二つ目は、SetBindingメソッド(しばしばBindingクラスと組み合わせて使用される)で、 ソースオブジェクトのプロパティに、そのオブジェクトのプロパティをバインドするために、 ターゲットオブジェクトから呼び出されなければなりません。
ターゲットプロパティは、バインディング可能なプロパティである必要があります。 つまり、ターゲットオブジェクトは必ずBindingObjectから継承されている必要があります。 オンラインのXamain.Formsのドキュメントでは、プロパティはバインド可能なプロパティで あることを示しています。
マークアップ中では、これらは同じ2つのステップが要求され、バインディングマークアップ の拡張機能は、SetBindingの呼び出しと、Bindingクラスの代わりに使用されます。
しかしながら、これらはターゲットオブジェクトのBindingContextへセットする単一の テクニックではありません。 しばしば、StaticResourceもしくはx:Staticマークアップ拡張機能を使用して、コード ビハインドのファイルからこれらは設定されます。
バインディングが最も頻繁に使用されるのは、画面表示系のプログラムとベースのデータモデル を接続する時で、しばしばMVVM(Model-View-ViewModel)アプリケーションアーキテクチャ で実現されます。MVVMについてはPart5で議論しますが、他のシナリオでは可能です。

ViewからViewへのバインディング

データバインディングは同一ページの二つのビューのプロパティを結合の定義をすることができます。 このケースでは、ターゲットオブジェクトのx:Refrenceマークアップ拡張を使用して、Binding - Contextを設定します。
ここにSliderと二つのラベルが含まれたの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="XmalSamples.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"
               HorizontalOptions="Center"
               VerticalOptions="CenterAndExpand" />
    </StackLayout>
</ContentPage>
Sliderに含まれるx:Name属性の値を使用して、二つのラベルはx:Referenceマークアップ拡張を 使用してSliderを参照します。
x:Reference拡張は、参照される要素の名前に設定するNameというプロパティを定義します。 この場合は、「slider」です。 しかしながら、ReferenceExtensionクラスはx:Referenceマークアップ拡張はまた、 明示的に要求されていないことを意味し、NameのためにContentProperty属性を定義します。
幾つかのバラエティとして、最初のラベルのx:Referenceは”Name=”が含まれていますが、 二番目のラベルには"Name="が含まれていません。
BindingContext="{x:Reference Name=slider}"
....
BindingContext="{x:Reference slider}"
Bindingマークアップ拡張自体はちょうどBindingBaseとBindingクラスのように、幾つかの プロパティを持つことができます。 バインディングのためのContentPropertyはPathですが、バインディングマークアップ拡張の 最初の項目に限り、"Path="の部分を省略することができます。 最初の例では"Path="を設定してますが、次の例ではそれを省略しています。
Rotation="{Binding Path=Value}"
...
Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
プロパティはすべて、単一行でも複数行に分割された行でも設定可能です。
二番目のバインディングマークアップ拡張のStingFormatに注目してください。 Xamarin.Formsでは、バインディングは暗黙の型変換を行うことができません、そして、もし 非文字列オブジェクトを文字列として表示する場合、型のコンバータ(Type Converter)か StringFormatを使用する必要があります。 バックグラウンドの動きは、StringFormatはstaticなString.Formatメソッドを使用します。 .NETの書式化の仕様も、マークアップ拡張を区切るために、XAMLパーサーを混乱させる危険性を 持っている中括弧を伴うため、潜在的な問題を持っています。 それを無効化するために、書式化文字列はシングルクォート内に設定を行います。
Text="{Binding Value,
               StringFormat='The angle is {0:F0} degrees'}"

Backwards Bindings(後方バインディング)

単一のビューは様々なプロパティのデータバインディングを行うことができます。しかしながら 各々のビューは一つのバインディングコンテキストを使用することしかできず、複数のデータを 表示用にバインディングするには、全てのオブジェクトのプロパティを参照する必要があります。
この制約を避けるには、view−to-viewバインディングを用いて、OneWayToSourceもしくは TwoWayモードをする必要があります。以下に例を挙げます。
次のプログラムは4つのスライダーがあり、スケール・回転・X軸の回転・Y軸の回転の各プロパティ の表示を意図しています。 まず、ラベルの4つのプロパティはデータバインティングのターゲットにならなければならない ことがわかります。 しかしながら、ラベルのバインディングコンテキストはただ一つのオブジェクトしか設定でき ませんが、4つの異なるスライダーが存在しています。
  • 注)データバインディングターゲット(ラベル) = 1:データソース = Nの1:Nのバインディング の話をしていると思われ。
このような理由により、バインディングは周りと一見、後方に反転しています:4つのスライダーの 各バインディングコンテキストはラベルへ設定し、バインディングはスライダーのValueプロパティ へ設定を行います。 OneWayToSourceとTwoWayモードを使用することで、これらのValueプロパティはソース プロパティで、ラベルの各プロパティ(スケール、回転、X回転、Y回転)の設定に使用します。
  • 注)ラベルへのデータバインディングをするために、スライダーへのバインディング設定 となっているということを言っている。TwoWayモードでお互いのプロパティ値の変更を 互いに変更しあうので1:Nのバインディングができるということになる。
<?xml veresion="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com//schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamlSamples.SliderTransformsPage"
             Title="Slider Transforms Page">

    <Grid>
        <Grid.RowDifinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDifinitions>

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

        <StackLayout Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
            <!-- Scaled and rotated Lebel -->
            <Label x:Name="label"
                   Text="TEXT"
                   HorizontalOptions="Center"
                   VerticalOptions="CenterAndExpand" />
        </StackLayout>

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

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

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

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

    </Grid>
</ContentPage>
3つのスライダーのバインディングはOneWayToSourceである意味は、プロパティの値の 変更が、それ自体のバインディングコンテキストによって引き起こされるためです。 これら3つのスライダーはラベルのRotate、RotateX、RotateYプロパティの変更を発生 させます。
それに対し、スケースのバインディングはTwoWayです。 これは、Scaleプロパティのデフォルトの値が1となっており、TwoWayバインディングに よってスライダーの初期値を使用することは1ではなく、0が設定されてしまうためです。
もし、バインディングがOneWayToSourceだった場合、Scaleプロパティの初期値は スライダーのデフォルト値の0となってしまいます。そうなると、ラベルは非表示となり ユーザーは混乱すると想定できるからです。
スライダーの左にあるラベルは、スライダーのカレント値を指し示しています。 これらは各ラベルが通常のOneWayでバインディングされますが、ラベルのバインティング コンテキストはスライダーを参照する必要があります。
この意味は、各スライダーは、XAMLファイルの文法的に、ラベルが表示される前に対応する ラベルが表示されている必要があることを意味しています。
このプログラムでは、Gridを使用してスライダーがカラム番号1で最初に表示され、続いて ラベルをカラム番号0として表示するように制御しています。
  • 注)ラベルの方が先に定義されると、スライダーを参照しているので、まだ出現していない オブジェクトを参照することとなり不都合があるので、XAMLの記述では、Gridの小技を 使って、スライダーの次にラベルが出現するようにしている。

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

データバインディングのパワーを示すのに、テンプレート化されたListViewに比較できる ものはありません。
ListViewはItemSourceプロパティによって列挙子の型を定義し、コレクションのアイテム を表示します。 これらのアイテムはあらゆる型のオブジェクトを扱うことができます。 既定では、ListViewはアイテムを表示するのにToStringメソッドを使用します。 大抵はこれは望む結果を生みますが、幾つかのケースではToStringメソッドはオブジェクトの 完全修飾クラス名を返します。
ListViewコレクションのアイテムは、Cellクラスから継承したクラスを含むテンプレートを 使用することによって、表示方法をコントロールすることができます。
テンプレートは、リストビュー内のすべてのアイテムのために複製され、テンプレートに設定 されているデータバインディングは、それぞれの複製されたテンプレートへ転送されます。
頻繁に、ViewCellクラスを使用してこれらのアイテムのカスタムセルを作ることを求めます。 この作業ではコードで書くと、煩雑であるが、XAMLを使うことでとても直感的に書くことが できます。
XamlSamplesプロジェクトにはNamedColorと呼ばれるクラスが含まれています。 各NamedColorオブジェクトはNameとFriendlyNameというString型のプロパティと Color型のColorプロパティを持ちます。 さらに、NamedColorは147種類のCSS3の使用に定義されたstaticなColor型のフィールド を持っています。 staticなコンストラクタで、これらstaticフィールドに対応するNamedColorオブジェクト を含むIEnumrable<NamedColor>コレクションを作成し、そのpublic-staticなすべての プロパティに割り当てます。
x;Staticマークアップ拡張を使用することでListViewのItemSourceへstaticな NamedColor.Allプロパティの設定が簡単にできます。
<?xml veresion="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XmalSamples;assembly=XmalSamples"
             x:Class="XmalSamples.ListViewDemoPage"
             Title="ListView Demo Page">

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

</ContentPage>

データバインドからMVVMへ

MVVM(Model-View-ViewModel)アーキテクチャに関する紹介

Model-View-ViewModel(MVVM)の構築パターンはXAMLを念頭に開発されました。 このパターンは ユーザーインンターフェース(View)からデータモデル(Model)を、 ビューとモデルをサービスクラスを媒介するクラス(ViewModel)を通して、両者の 分離を強制させます。
ViewとViewModelはしばしばXAMLファイルで定義されたデータバインドによって 接続されます。 Viewのバインディングコンテキストは大抵の場合、ViewModelのインスタンスと なります。

単純なViewModel

ViewModelの紹介をします。 最初にXMLの名前空間にXAMLファイルが他のアセンブリ参照の設定をどうやって行っている かを見つけることができます。 このプログラムは、アセンブリ:Systemの名前空間を修飾するXML名前空間の定義を行う プログラムがあります。
xmlns:sys="clr-namespace:System;assembly=mscorlib"
プログラムはx:Staticを使用して現在日付と時刻をstaticなDateTime.Nowプロパティ から取得して、DateTimeの値をバインディングコンテキストでStackLayoutに設定します。
<StackLayout BindingContext="{x:Static sys:DateTime.Now}" ....>
バインディングコンテキストはとても特別なプロパティで、すべてのその子供の要素に 継承されます。 この意味はStatckLayoutのすべての子供要素は同じバインディングコンテキストを持ち、 それらにオブジェクトのプロパティへの単純なバインドを含めることができます。
このプログラムでは、二つの子供要素がDateTimeのプロパティが含まれている値にバインド しており、二つの別の子供要素はバインディングパスが誤っているように見えるバインドが 行われています。 これは実際にDateTimeの値を、StringFormatのために使用されることを意味しています。
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.micosoft.com/winfx/2009/xaml"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             x:Class="XamlSamples.OneShotDateTimePage"
             Title="One-Shot DateTime Page">
    <StackLayout BindingContext="{x:Static sys:DateTime.Now}"
                 HorizontalOptions="Center"
                 VerticalOptions="Center">

        <Label Text="{Binding Year, StringFormat='The year is {0}'}" />
        <Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
        <Label Text="{Binding Day, StringFormat='The day is {0}'}" />
        <Label Text="{Binding StringFormat='The time is {0:T}'}" />

    </StatckLayout>
もちろん、大きな問題として、日付と時間はページの初期表示に一回だけ設定されるので、 表示は更新されません。
表示結果 -> (今は2014/6/11 17:4:54)

 The year is 2014
 The month is June
 The day is 11
 The time is 5:04:54 PM
XAMLファイルは現在時刻を常に表示する時計を表示することができますが、そのためには 幾つかのヘルプコードが外部に必要となります。 MVVMの観点で考えると、モデルとモデルビューはコードで書かれたクラスになります。 ビューは大抵XAMLファイルで記述され、データバインドを通じて定義されたビューモデルの プロパティを参照します。
本来、モデルはビューモデルに依存せず、ビューモデルはビューに依存していません。 しかしながら、大抵のプログラマーは部分的なユーザーインターフェースに関連付けられた データ型をビューモデルを通じて外部に公開するような構造として作ります。
例えば、もし、モデルがデータベースの8bitキャラクターのASCII文字列にアクセスする場合、 ビューモデルはユーザーインターフェースが使用するユニコード文字列にそれらを変換する 必要があるでしょう。
この単純なMVVMの例では、モデルがなく、ビューとビューモデルが直接データバインドで接続 しています。
ビューモデルはDateTimeと名付けられた一つのプロパティを持ち、毎秒毎にDateTimeの プロパティが更新されます。
namespace XamlSamples {

class ClockViewModel : INotifyPropertyChanged
{
    DateTime dateTime;
    public event PropertyChangedEventHandler PropertyChanged = delegate {};

    public ClockViewModel()
    {
        this.DateTime = DateTime.Now;
        Device.StartTimer(TimeSpan.FromSeconds(1), () =>
        {
            this.DateTime = DateTime.Now;
            return true;
        });
    }

    public DateTime DateTime
    {
        set
        {
            if (dateTime != value)
            {
                dateTime = value;
                PropertyChanged(this,
                    new PropertyChangedEventArgs("DateTime"));
            }
        }
        get
        {
            return dateTime;
        }
    }
}

} // end of XamlSamples namespace;
ビューモデルは通常INotifyPropertyChangedインターフェースを実装します。これは プロパティの変更に伴い、PropertyChangedイベントを発火させます。 このXamarin.Formsにおけるデータバインドのメカニズムは、ProperyChangedイベントに ハンドラーを接続することで、プロパティの変更を通知することができ、ターゲットを更新して 新しい値に保つことができるようになります。
時計におけるビューモデルは以下のように単純な形で実装することができます。
<?xml version="1.0" encoding="utf-8"?>
<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.ClockPage"
             Title="Clock Page">

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="Large"
           HorizontalOptions="Center"
           VerticalOptions="Center">
        <Label.BindingContext>
            <local:ClockViewModel />
        </Label.BindingContext>
    </Label>
</ContentPage>
ClockViewModelがプロパティ要素のタグを使用したバインディングコンテキストへ設定を 行う方法に注目してください。 もしくは、ResourcesのコレクションでClockViewModelをインスタンス化して、StaticResource マークアップ拡張を通じたBindingContextによる設定をすることもできます。 もしくはコードビハインドファイルでViewModelをインスタンス化することもできます。
ラベルのTextプロパティにバインディングしたマークアップ拡張はDateTimeを書式化して 表示します。 以下のように表示されるでしょう。

5:05:16 PM

これもまた、期間が分離されたViewModelのDateTimeプロパティの個別のプロパティに アクセスすることができます。
<Label Text="{Binding DateTime.Second
                      StringFormat='{0}'}" .... >

インタラクティブなMVVM

MVVMはデータモデルをベースとしたインタラクティブな表示のためにtwo-wayなデータバインド しばしば使用します。 これは、HslViewModelと名づけられた色相、彩度、明度の値とColorを相互変換するクラスが あります。

public class HslViewModel : INotifyPropertyChanged
{
    double hue, saturation, luminosity;
    Color color;

    public event PropertyChangedEventHandler ProperyChanged;

    public double Hue
    {
        set
        {
            if (hue != value)
            {
                hue = value;
                OnPropertyChanged("Hue");
                SetNewColor();
            }
        }
        get { return hue; }
    }

    public double Saturation
    {
        set
        {
            if (saturation != value)
            {
                hue = value;
                OnPropertyChanged("Saturation");
                SetNewColor();
            }
        }
        get { return Saturation; }
    }

    public double Luminosity
    {
        set
        {
            if (luminosity != value)
            {
                hue = value;
                OnPropertyChanged("Luminosity");
                SetNewColor();
            }
        }
        get { return Luminosity; }
    }

    public Color Color
    {
        set
        {
            if (color != value)
            {
                color = value;
                OnPropertyChanged("Color");

                this.Hue = value.Hue;
                this.Saturation = value.Saturation;
                this.Luminosity = value.Luminosity;
            }
        }
        get { return color; }
    }

    void SetNewColor()
    {
        this.Color = Color.FromHsla(this.Hue, this.Saturation, this.Luminosity);
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (ProperyChanged != null)
        {
            ProperyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
色相、彩度、明度の各プロパティに変更が発生した場合、Colorプロパティが変更されColorの 変更は他の3つのプロパティへの変更を発生させます。 これはプロパティが実際に変更されていない限り、PropertyChangedイベントが発火されない ことを除いて、無限ループのように見えるかもしれません。 さもなければ制御不能なフィードバックのループに終止符を打ちます。
XAMLファイルに含まれたBoxViewは、ColorプロパティをViewModelのColorプロパティと 接着させて、3つのスライダーと3つのラベルに色相・彩度・明度プロパティを接着させます。
<?xml version="1.0" encoding="utf-8"?>
<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.HslColorScrollPage"
             Title="HSL Color Scroll Page">

    <ContentPage.BindingContext>
        <local:HslViewModel Color="Aqua"/>
    </ContentPage.BindingContext>

    <StackLayout Padding="10, 0">
        <BoxView Color="{Binding Color}"
                 VerticalOptions="FillAndExpand" />
        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
               HorizontalOptions="Center" >
        <Slider Value="{Binding Hue, Mode=TwoWay}" />

        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
               HorizontalOptions="Center" >
        <Slider Value="{Binding Saturation, Mode=TwoWay}" />

        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
               HorizontalOptions="Center" >
        <Slider Value="{Binding Luminosity, Mode=TwoWay}" />
    </StackLayout>


</ContentPage>
各ラベルへのデフォルトのバインド方法はOneWayです。これは値を表示するのみが必要な時に 限られます。しかし、各スライダーはTwoWayでバインドされます。 この方法はViewModelによってスライダーが初期化される良い方法です。 ColorプロパティはViewModelがインスタンス化されるタイミングで青に設定される一方で、 スライダーの変化も、ViewModelで新しい色の値を算出し、プロパティに設定する必要が あることに注目してください。

2 件のコメント:

Nmk446 さんのコメント...

非常に参考になりました。ありがとうございます。
ところで、ClockViewModelクラスのDateTimeプロパティの宣言の部分が、おそらく「DateTime」となるところが「dateTime」になっているように思います。

protain protain さんのコメント...

ご指摘ありがとうございました。
修正しました。