かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF4.5入門 その55 「Binding その1」

WPFには、見た目とデータを分離して管理するための強力なデータバインディングの機能があります。WPFのデータバインディングは、依存関係プロパティとプロパティの間の同期をとる機能にないります。

単純なBinding

データバインディングは、ソースに設定されたオブジェクトのプロパティとターゲットに設定された依存関係プロパティ(添付プロパティも可)の間の同期をとります。例えば、以下のようなPersonクラスがあるとします。

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace DataBindingSample01
{
    public class Person : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
        {
            field = value;
            var h = this.PropertyChanged;
            if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); }
        }

        private int age;

        public int Age
        {
            get { return this.age; }
            set { this.SetProperty(ref this.age, value); }
        }

        private string name;

        public string Name
        {
            get { return this.name; }
            set { this.SetProperty(ref this.name, value); }
        }
    }
}

このオブジェクトをWindowのリソースに登録します。

<Window.Resources>
    <local:Person x:Key="Person" Name="tanaka" Age="34" />
</Window.Resources>

このPersonオブジェクトをソースに指定してTextBlockのTextプロパティにBindingするには以下のように記述します。

<TextBlock Text="{Binding Name, Source={StaticResource Person}}" />

Bindingの最初に指定するのがPathプロパティです。Pathにはプロパティ名を指定します。Bindingのソースは、指定されていない場合DataContextプロパティが自動的に使われるため以下のように書くことも出来ます。

<Window.DataContext>
    <local:Person Name="tanaka" Age="34" />
</Window.DataContext>
<Grid>
    <TextBlock Text="{Binding Name}" />
</Grid>

BindingのMode

Bindingには、値の同期方法を指定するためのModeプロパティがあります。Modeプロパティの値を以下に示します。

モード 説明
OneWay ソースからターゲットへの一方通行の同期になります。
TwoWay ソースとターゲットの双方向の同期になります。
OneWayToSource ターゲットからソースへの一方通行の同期になります。
OneTime ソースからターゲットへ初回の一度だけ同期されます。

ソースからターゲットへの同期をするには、ソースとなるオブジェクトがINotifyPropertyChangedを実装してプロパティの変更通知を実装している必要があります。ターゲットからソースへの同期は、特に実装すべきインターフェースなどはありません。

Modeは、依存関係プロパティごとにデフォルト値が指定されています。一般的にはOneWayが指定されていて、TextBoxのTextプロパティなどのように、双方向同期が必要なものについてはTwoWayが指定されています。以下のようにTextBlockとTextBoxをBindingすると、Personオブジェクトを通してTextBoxとTextBlockの値が同期されます。

<Window.DataContext>
    <local:Person Name="tanaka" Age="34" />
</Window.DataContext>
<StackPanel>
    <TextBlock Text="{Binding Name}" />
    <TextBox Text="{Binding Name}" />
    <Button Content="TextBoxからフォーカス外す用" />
</StackPanel>

上記のコードを動かすと、TextBoxからフォーカスを外したタイミングでTextBlockとTextBoxの値が同期されます。これはTextBoxがBindingされた値を同期するタイミングがフォーカスを外したタイミングだからです。この動きをカスタマイズするにはBindingのUpdateSourceTriggerプロパティを指定します。UpdateSourceTriggerプロパティには以下の値を設定できます。

説明
LostFocus フォーカスが外れたタイミングでソースの値を更新します。
PropertyChanged プロパティの値が変化したタイミングでソースの値を更新します。
Explicit UpdateSourceメソッドを呼び出して明示的にソースの更新を指示したときのみソースの値を更新します。

先ほどのTextBlockとTextBoxにPersonオブジェクトを同期した例で、TextBoxのBindingを以下のように書き換えると、TextBoxに入力した値が即座にPersonオブジェクトを経由してTextBlockに反映されます。

<Window.DataContext>
    <local:Person Name="tanaka" Age="34" />
</Window.DataContext>
<StackPanel>
    <TextBlock Text="{Binding Name}" />
    <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <Button Content="TextBoxからフォーカス外す用" />
</StackPanel>

ElementNameによるソースの指定

Bindingは、Sourceプロパティ指定やDataContextによる自動的なソースの指定以外に、いくつかのソースの指定方法があります。その中の1つがElementNameによる指定です。ElementNameは、コントロールをソースとして使う時に使用します。ソースに指定したいコントロールのNameプロパティかx:Nameで指定された名前と同じものをElementNameに指定します。以下にTextBoxをソースに指定して、TextプロパティとTextBlockのTextプロパティをバインドするコード例を示します。

<TextBox x:Name="textBox" />
<TextBlock Text="{Binding Text, ElementName=textBox}" />

RelativeSourceによるソースの指定

RelativeSourceは、Bindingターゲットから見た相対的な位置でソースを指定します。例えば自分自身をソースに指定するコード例を以下に示します。自分自身を指定するにはRelativeSourceにRelativeSourceマークアップ拡張のModeプロパティにSelfを指定したものを設定します。(Modeは省略可能です)

<TextBlock 
    Text="{Binding HorizontalAlignment, RelativeSource={RelativeSource Self}}" 
    HorizontalAlignment="Left"/>

上記の例はLeftと表示されます。

このほかに、自分の親へ親へ辿っていき、指定した型にたどり着くまで遡るAncestorTypeというものもあります。自分自身が置かれているWindowのTitleとBindingする例を以下に示します。

<TextBlock
    Text="{Binding Title, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />

WindowのTitleにMainWindowという文字列が設定されている場合、上記の設定でMainWindowとTextBlockに表示されます。

SelfとAncestorType以外にTemplatedParentというTemplateBindingと同様の機能を提供する方法もあります。TemplateBindingが一方通行なBindingなのに対してTemplatedParentを指定した場合はTwoWayなどのBindingを指定することが出来る点が異なります。

入力値の検証

データバインディングには、ターゲットに入力された値を検証する方法も備わっています。歴史的な経緯から、ValidationRuleを指定する方法、ソースのプロパティで例外をスローする方法、IDataErrorInfoをソースに実装する方法、INotifyDataErrorInfoをソースに実装する方法のように様々な方法が提供されています。ここでは、デフォルトで有効になっていて、もっとも柔軟な指定が可能なINotifyDataErrorInfoインターフェースを実装する方法について解説します。

INotifyDataErrorInfoインターフェースは同期、非同期の値の検証を実装するためのインターフェースで以下のように定義されています。

public interface INotifyDataErrorInfo
{
    bool HasErrors { get; }
    event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    IEnumerable GetErrors(string propertyName);
}

HasErrorsプロパティでオブジェクトにエラーが有るか無いかを返します。ErrorsChangedイベントでプロパティのエラーの状態に変化があったことを外部に通知します。GetErrorsメソッドでプロパティのエラーを返します。

Nameプロパティが必須入力で、Ageプロパティに0以上を設定しないといけないPersonクラスの実装例を以下に示します。まず、必須のインターフェースのINotifyPropertyChangedと、INotifyDataErrorInfoのイベントやメソッドなどを実装します。

public class Person : INotifyPropertyChanged, INotifyDataErrorInfo
{
    // INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    private void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        field = value;
        var h = this.PropertyChanged;
        if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); }
    }

    // INotifyErrorsInfo
    private Dictionary<string, IEnumerable> errors = new Dictionary<string, IEnumerable>();
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    private void OnErrorsChanged([CallerMemberName] string propertyName = null)
    {
        var h = this.ErrorsChanged;
        if (h != null) { h(this, new DataErrorsChangedEventArgs(propertyName)); }
    }

    public IEnumerable GetErrors(string propertyName)
    {
        IEnumerable error = null;
        this.errors.TryGetValue(propertyName, out error);
        return error;
    }

    public bool HasErrors
    {
        get { return this.errors.Values.Any(e => e != null); }
    }
}

これらのメソッドを使ってNameプロパティとAgeプロパティを実装します。エラーがあればerrorsにエラーの内容を追加します。エラーが無い場合はエラーの情報をnullにします。そして最後にエラーに変化があったことを通知するErrorsChangedイベントを発行します。

public class Person : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private string name;
    public string Name
    {
        get { return this.name; }
        set 
        { 
            this.SetProperty(ref this.name, value); 
            if (string.IsNullOrEmpty(value))
            {
                this.errors["Name"] = new[] {"名前を入力してください" };
            }
            else
            {
                this.errors["Name"] = null;
            }
            this.OnErrorsChanged();
        }
    }

    private int age;
    public int Age
    {
        get { return this.age; }
        set 
        { 
            this.SetProperty(ref this.age, value); 
            if (value < 0)
            {
                this.errors["Age"] = new[] { "年齢は0以上を入力してください" };
            }
            else
            {
                this.errors["Age"] = null;
            }
            this.OnErrorsChanged();
        }
    }
}

入力値の検証を追加したオブジェクトをソースにして、Bindingを行います。

<Window x:Class="DataBindingSample04.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataBindingSample04"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:Person />
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding Name}" Margin="2.5" />
        <TextBox Text="{Binding Age}" Margin="2.5" />
    </StackPanel>
</Window>

TextBoxは、エラー中は自動で赤色の矩形が表示されます。

f:id:okazuki:20140915200838p:plain

検証エラーの結果は、コントロールにValidation.Errors添付プロパティに格納されます。Validation.Errors添付プロパティは、コレクション型で、その中のErrorContentプロパティに実際のエラーの内容が入っています。BindingのPathを工夫して書くことで、Validation.Errors添付プロパティに値があるときだけToolTipに表示させることが出来ます。以下に記述例を示します。

<TextBox Text="{Binding Name}" 
    Margin="2.5" 
    ToolTip="{Binding (Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Self}}" />
<TextBox Text="{Binding Age}" 
    Margin="2.5" 
    ToolTip="{Binding (Validation.Errors)/ErrorContent, RelativeSource={RelativeSource Self}}" />

ソースを自分自身にしてValidation.Errors添付プロパティを取り出しています。Validation.Errors添付プロパティの現在選択中の項目を表す/を指定して、さらに、その中のErrorContentプロパティを指定しています。バリデーションエラーを起こした状態でマウスカーソルを上に移動させると以下のようになります。

f:id:okazuki:20140915200924p:plain

デフォルトの赤い矩形が表示されるエラーを変えたい場合は、Validation.ErrorTemplate添付プロパティにControlTemplateを指定します。ControlTemplate内では、AdornedElementPlaceholderを使ってTextBoxの表示箇所を指定できます。また、DataContextにはValidation.Errors添付プロパティの値が入ってくるためエラーの内容を簡単に表示することが出来ます。例えば、エラーが起きた時に赤色の*をTextBoxの右側に表示して、そこのToolTipにエラーの内容を表示する例を以下に示します。

<TextBox Text="{Binding Name}" 
         Margin="2.5, 2.5, 10, 2.5">
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <DockPanel>
                <AdornedElementPlaceholder />
                <TextBlock
                    DockPanel.Dock="Right"
                    Text="*"
                    Foreground="Red"
                    ToolTip="{Binding /ErrorContent}" />
            </DockPanel>
        </ControlTemplate>
    </Validation.ErrorTemplate>
</TextBox>

実行すると以下のように表示されます。

f:id:okazuki:20140915201021p:plain

過去記事