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は、エラー中は自動で赤色の矩形が表示されます。
検証エラーの結果は、コントロールに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プロパティを指定しています。バリデーションエラーを起こした状態でマウスカーソルを上に移動させると以下のようになります。
デフォルトの赤い矩形が表示されるエラーを変えたい場合は、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>
実行すると以下のように表示されます。
過去記事
- WPF4.5入門 その1 「はじめに」
- WPF4.5入門 その2 「WPFとは」
- WPF4.5入門 その3 「Hello world」
- WPF4.5入門 その4 「Mainメソッドはどこにいった?」
- WPF4.5入門 その5 「全てC#でHello world」
- WPF4.5入門 その6 「WPFを構成するものを考えてみる」
- WPF4.5入門 その7 「XAMLのオブジェクト要素と名前空間」
- WPF4.5入門 その8 「オブジェクト要素のプロパティ」
- WPF4.5入門 その9 「コレクション構文」
- WPF4.5入門 その10 「コンテンツ構文」
- WPF4.5入門 その11 「マークアップ拡張」
- WPF4.5入門 その12 「その他のXAMLの機能」
- WPF4.5入門 その13 「簡単なレイアウトを行うコントロール」
- WPF4.5入門 その14 「レイアウトコントロールのCanvasとStackPanel」
- WPF4.5入門 その15 「レイアウトコントロールのDockPanelとWrapPanel」
- WPF4.5入門 その16 「ViewBoxコントロール」
- WPF4.5入門 その17 「ScrollViewerコントロール」
- WPF4.5入門 その18 「Gridコントロール part 1」
- WPF4.5入門 その19 「Gridコントロール part 2」
- WPF4.5入門 その20 「レイアウトに影響を与えるプロパティ」
- WPF4.5入門 その21 「WPFのコンセプトと重要な機能つまみ食い」
- WPF4.5入門 その22 「Buttonコントロール」
- WPF4.5入門 その23 「DataGridコントロール その1」
- WPF4.5入門 その24 「DataGridコントロール その2」
- WPF4.5入門 その25 「TreeViewコントロール その1」
- WPF4.5入門 その26 「TreeViewコントロール その2」
- WPF4.5入門 その28 「Calendarコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その29 「ContextMenuコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その30「Menuコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その31 「ToolBarコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その32 「CheckBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その33 「ComboBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その34 「ListBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その35 「RadioButtonコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その36 「Sliderコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その37 「TabControl」 - かずきのBlog@hatena
- WPF4.5入門 その38 「ファイルダイアログ」 - かずきのBlog@hatena
- WPF4.5入門 その39 「情報を表示するコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その40 「Popup、ToolTip、TextBox、Image、MediaElementコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その41 「DispatcherObject」 - かずきのBlog@hatena
- WPF4.5入門 その42 「WPFのプロパティシステム」 - かずきのBlog@hatena
- WPF4.5入門 その43 「読み取り専用の依存関係プロパティ」 - かずきのBlog@hatena
- 拡張されたプロパティメタデータ - かずきのBlog@hatena
- WPF4.5入門 その45 「添付プロパティ」 - かずきのBlog@hatena
- WPF4.5入門 その46 「WPFのイベントシステム」 - かずきのBlog@hatena
- WPF4.5入門 その47 「コンテンツモデル」 - かずきのBlog@hatena
- WPF4.5入門 その48 「WPFのアニメーション その1」 - かずきのBlog@hatena
- WPF4.5入門 その49 「WPFのアニメーション その2」 - かずきのBlog@hatena
- WPF4.5入門 その50 「Style」 - かずきのBlog@hatena
- WPF4.5入門 その51 「リソース」 - かずきのBlog@hatena
- WPF4.5入門 その52 「コントロールテンプレート」 - かずきのBlog@hatena
- WPF4.5入門 その53 「ユーザーコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その54 「カスタムコントロール」 - かずきのBlog@hatena