データバインディング
最終更新:
atachi
WPFのデータバインディングは、データモデル側がプロパティの変更があったことを通知しないとバインドされないので注意が必要。(Adobe Flexのバインディングとはその仕組みがぜんぜん違うので、AdobeFlexお経験したことがある方は注意)
バインドのタイミングについては、バインドのタイミング項を参照。
バインドする側(TextBoxなどのコントロール側)をバインドターゲットと呼び、バインドしたい値を持っているオブジェクトをバインドソースと呼ぶ。
データバインドの動作
次のXAMLは、リストコントロールに色のリストを表示し選択された項目によって文字の背景色を変更するサンプルです。
リストコントロールと文字コントロールの間でデータバインドが成立しています。
<StackPanel>
<TextBlock Width="248" Height="24" Text="Colors:"
TextWrapping="Wrap"/>
<ListBox x:Name="lbColor" Width="248" Height="56">
<ListBoxItem Content="Blue"/>
<ListBoxItem Content="Green"/>
<ListBoxItem Content="Yellow"/>
<ListBoxItem Content="Red"/>
<ListBoxItem Content="Purple"/>
<ListBoxItem Content="Orange"/>
</ListBox>
<TextBlock Width="248" Height="24" Text="You selected color:" />
<TextBlock Width="248" Height="24">
<TextBlock.Text><!-- TextBlockのText属性に関する設定を行うタグ -->
<Binding ElementName="lbColor" Path="SelectedItem.Content"/>
</TextBlock.Text>
<TextBlock.Background><!-- TextBlockのBackground属性に関する設定を行うタグ -->
<Binding ElementName="lbColor" Path="SelectedItem.Content" />
</TextBlock.Background>
</TextBlock>
</StackPanel>
Bindingタグがバインドを行うための記述で、
<Binding ElementName="lbColor" Path="SelectedItem.Content"/>
というのは、同一フォーム内のlbColorというコントロールをバインドするという意味となります。
バインドが実行された場合、SelectedItem.Contentの値をターゲット(TextBlock.TextまたはTextBlock.Background)に与えられます。
バインドが実行されるタイミングはListBoxの実装に含まれており、通常はいくつかの操作がバインド実行のトリガーとして機能する。
ListBoxではアイテムの選択状態が変更した時にバインドが実行される。
バインドソースの指定
バインドターゲットがバインドするバインドソースを指定する方法には次の方法があります。
- FrameworkElement.DataContext
- 親要素からの継承
-
Bindingクラスの次のプロパティからいずれか
- Binding.Source
- Binding.ElementName
- Binding.RelativeSource
親からの継承
親要素の継承とは、次の例ではGrid要素内のButton要素は、親要素に設定されたDataContext属性を継承しているので、Source属性にバインドソースを指定する必要がありません。
<Grid DataContext="{StaticResource myDataSource}">
<Button Text="{Binding Path=Name}" />
</Grid>
ListBox.ItemsSource属性のようにコレクションをバインドソースとする場合にmyDataSourcesそのものをバインドする為にBinding.Sourceを省略して記述することができます。
<Grid DataContext="{StaticResource myDataSources}">
<ListBox ItemsSource="{Binding}" >
...
</ListBox>
</Grid>
Binding.RelativeSource
RelativeSourceマークアップ拡張を使用してバインドソースを設定できる。(マークアップ拡張)
Binding.RelativeSourceの設定を行うために、RelativeSourceマークアップ拡張を使用しなければならないので記述が長くなってしまう。(*1)
RelativeSourceには指定可能なバインドソースが次の3つから選ぶことができる。
- Self
- 自分自身をバインドソースにする。
- TemplatedParent
- テンプレート(ControlTemplate)の親をバインドソースにする。
- FindAncestorモード
- 上記のものとは指定方法が異なる。
MSDNにはRelativeSourceプロパティについて次のような記述がされている。
バインディング ターゲットの位置に対して相対的な位置を指定することにより、バインディング ソースを取得または設定します。
Selfは自分自身をバインドソースとします。「{RelativeSource Self}」のように指定します。
<TextBox Text="{Binding RelativeSource={RelativeSource Self}, Path=Width}" Width="23" />
イメージ的には次のような形と同じ。
<TextBox Name="text1" Text="{Binding ElementName=text1,Path=Width}" Width="23" />
TextBoxにはWidthプロパティの値である「23」という文字が表示される。
TemplatedParentはテンプレート(ControlTemplate)を使用している場合に、テンプレートの親をバインドソースとして使用します。
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="120" Width="250" >
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Ellipse Fill="{TemplateBinding Background}"/>
<ContentPresenter Content="{Binding RelativeSource=
{RelativeSource TemplatedParent}
, Path=Name
}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<WrapPanel>
<Button Name="MyButton" Content="ぼたん" />
<Button Name="MyButton2" Content="ぼたん2" />
</WrapPanel>
</Window>
次のように表示が行われます。
XAMLではButtonのContentには「ぼたん」または「ぼたん2」としていますが、StyleによってButtonコントロールのテンプレートはオーバーライドされます。
ControlTemplateのContentPresenter.Contentはバインディングを設定していますが、ここで設定した「{RelativeSource TemplatedParent}」はテンプレート対象であるButtonコントロールを指します。Buttonコントロールをバインドソースとするため、バインド対象はButton.Nameプロパティとなります。
TemplatedParentはControlTemplateでしか作用しません。
テンプレートを使用できるケースとして、ListBoxのItemTemplateがありますが、ItemTemplate内のDataTemplateでBindingを使用する場合、TemplatedParentを使用することはできません。(エラーや警告は出ません)
FindAncestorモードでは指定した型が見つかるまでBindingから親へとオブジェクトツリーを遡りながらバインドソースを決定します。
FindAncestorモードでは、AncestorTypeプロパティに探したい型を指定します。(x:Typeマークアップ拡張を使用してAncestorTypeの値を記述する)
<Grid Name="MyGrid">
<TextBlock Text="{Binding RelativeSource=
{RelativeSource AncestorType=
{x:Type Grid}
}
,Path=Name
}" />
</Grid>
TextBlockには「MyGrid」という文字が表示される。
バインドモード
UIでバインドを使う場合、ほとんどのケースでバインドターゲットはUIになる。
TextBoxとデータ変数をバインドする場合は、ユーザーから入力した内容をデータ変数に値をコピーする(ターゲット→ソース方向)とシステムロジックによってデータ変数にセットされた値をTextBoxに表示するために値をコピーする(ソース→ターゲット方向)の両方向バインドを使うと値のやりとりが簡単です。そのため、TextBoxはデフォルトでバインドモードがTwoWayになっています。
OneWay | ソース | → | ターゲット | リードオンリーのUIに値を表示する場合に使用することが多い。 |
TwoWay | ソース |
→ ← |
ターゲット | TextBoxのように、ユーザーからの入力を受ける場合に使用することが多い。 |
OneWayToSource | ソース | ← | ターゲット | 通常のUIでは使用することはまれ。 |
OneWayモード
バインドソースからバインドターゲットへのデータバインディングを行うモード。
値の設定は一方通行なので、アプリケーションのデータをUIに表示することができる。
TwoWayモード
データを双方向バインドするモードです。
「ソース→ターゲット方向」へのバインドはPropertyChangeによって実行されます。
「ターゲット→ソース方向」へのバインドはターゲット側の設定内容によって実行タイミングが異なります。
下記のコードは、ListBoxとTextBoxがTwoWayモードでバインドされているので、TextBoxに色名(たとえば「Pink」)を入力するとListBoxへ入力値がバインドされます。
<StackPanel>
<TextBlock Width="248" Height="24" Text="Colors:"
TextWrapping="Wrap"/>
<ListBox x:Name="lbColor" Width="248" Height="56">
<ListBoxItem Content="Blue"/>
<ListBoxItem Content="Green"/>
<ListBoxItem Content="Yellow"/>
<ListBoxItem Content="Red"/>
<ListBoxItem Content="Purple"/>
<ListBoxItem Content="Orange"/>
</ListBox>
<TextBlock Width="248" Height="24" Text="You selected color:" />
<TextBlock Width="248" Height="24">
<TextBlock.Text>
<Binding ElementName="lbColor" Path="SelectedItem.Content"/>
</TextBlock.Text>
<TextBlock.Background>
<Binding ElementName="lbColor" Path="SelectedItem.Content" />
</TextBlock.Background>
</TextBlock>
<TextBox Height="24" x:Name="textBox1" Width="120" >
<TextBox.Text>
<!-- 双方向にバインドを形成する -->
<Binding ElementName="lbColor" Path="SelectedItem.Content" Mode="TwoWay" />
</TextBox.Text>
<TextBox.Background>
<Binding ElementName="lbColor" Path="SelectedItem.Content" />
</TextBox.Background>
</TextBox>
</StackPanel>
上記のコードは問題があります。それはTextBoxへ入力された値がListBoxにバインドするためのバインドの実行が行われないという問題です。TextBoxコントロールがバインドを実行するタイミングは、フォーカスを失った時なのでフォーカスが失われない間はバインドが実行されません。(バインドが実行されるタイミングについて)
<Binding ElementName="lbColor" Path="SelectedItem.Content" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" />
このように設定すると、プロパティが変更される度にバインドが実行されます。
OneWayToSourceモード
バインドターゲットからバインドソースへ値を代入する
バインドの実行タイミング
バインドが実行されるタイミングはBinding.UpdateSourceTriggerの値によって決定されます。
Explicit | BindingExpression.UpdateSource()のコールでのみバインドが実行 |
LostFocus | コントロールからフォーカスが失われたときにバインドが実行 |
PropertyChanged | プロパティの変更時にバインドが実行 |
OneWayモードのデータバインディングではデフォルトでPropertyChangedによるバインディングです。
TwoWayモードのデータバインディングでは、UIの種類によってタイミングは異なります。TextBoxではユーザーからの入力はLostFocusによってバインドソースへバインドされます。
PropertyChanged
このバインドタイミングでバインドを実行するには、バインドソース(バインドターゲット)が INotifyPropertyChanged を実装し、PropertyChangedEventHandlerイベントを適切に呼び出すように実装します。(データオブジェクトによるバインド)
LostFocus
フォーカスが失われたときにバインドを実行します。
TextBoxなどユーザーからの入力を伴うコントロールでは、このバインドタイミングがデフォルトに設定されています。
コレクションのバインド
データバインディングではバインドソースのコレクションをバインドできます。
しかし、バインドソースのコレクションをそのままバインドターゲットへバインドせずに、コレクションビューを作成しバインドソースとして使用します。
コレクションビューを使用する理由として、UIへの表示を行う際に並べ替え、フィルタ、およびグルーピングなどの処理を必要とするためです。コレクションビューを使用することで、直接バインドソースのコレクションを操作することなしに、並び替えなどのコレクションを使用できます。
ただし、バインドソースとするコレクションはINotifyCollectionChanged インターフェースを実装する必要があります。
コレクションビューの定義
<Window>
<Window.Resources>
<c:MyData x:Key="myDataSources"/>
<CollectionViewSource
Source="{Binding Source={StaticResource myDataSources} }"
x:Key="listDataView1" />
</Window.Resources>
...
</Window>
コレクションビューは同一のソースを複数定義することもできます。
並べ替え・フィルタ・グルーピング
並び替えを設定するにはSortDescriptions属性を設定します。
SortDescriptionはコレクションの要素が持つオブジェクトのプロパティをPropertyNameで指定します。
並び替えの対象フィールドは複数設定することが可能です。
<CollectionViewSource
Source="{Binding Source={StaticResource myDataSources} }"
x:Key="listDataView1" >
<!-- 並び替え設定「SortDescriptions」 -->
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Category" Direction="Ascending" />
<scm:SortDescription PropertyName="Age" Direction="Descending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
scm名前空間は次のようにルート要素に設定して使用してください。
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
グルーピングを設定するにはGroupDescriptions属性を指定します。
<CollectionViewSource
Source="{Binding Source={StaticResource myDataSources} }"
x:Key="listDataView1" >
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Category" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
データオブジェクトによるバインド
バインド機能を使用するには、INotifyPropertyChangedインターフェースを実装したクラスが必要です。
class Person : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
public String name {
get {
return _name;
}
set {
_name = value;
firePropertyChanged("name");
}
}
protected void firePropertyChanged(string name) {
if( PropertyChanged != null ) {
// デリゲートの実行
PropertyChanged(this, new PropertyChangedEventArgs(name) );
}
}
private String _name;
}
firePropertyChanged()はプロパティが変更したことを知らせるイベント(デリゲート)を実行するメソッドです。引数はバインドのパス名で、上記の場合「firePropertyChanged("name")」とはバインドするプロパティ名である。
テキストボックスのTextプロパティにバインドする。
<TextBox Name="tb" Text="{Binding Path=name}" />
プロパティがバインドすることを簡単に記述するにはBindingマークアップ拡張を使用する。
バインドされるソースを上記のTextBoxに設定する必要があります。
通常はDataContextに設定を行います。
public Form() {
tb.DataContext = person;
}
private Person person = new Person();
またはXAMLではプロパティは継承される為、TextBoxの上位の要素のDataContextに設定します。
<Window>
<Grid>
<TextBox Text="{Binding Path=name}" />
</Grid>
</Window>
// XAMLの分離コード
public MainWindow : Window {
public MainWindow() {
InitializeComponent();
Perons obj = new Person();
this.DataContext = obj;
obj.name = "星井美希";
}
}
バインドターゲットとなるカスタムコントロールの作成
カスタムコントロールの作成はUserControlやPanelといったクラスを継承して作成すればいい。
ただし、カスタムコントロールでバインディング使う場合やWPFデザイナから値を設定するカスタムコントロールを作る場合は、依存関係プロパティという方法でプロパティを実装する必要がある。
実質すべての公開プロパティは依存関係プロパティで実装しなければならない。
依存関係プロパティの実装についてはこちら。
class MyControl : UserControl, INotifyPropertyChanged {
public string Idol {
get {
return (string)GetValue(IdolProperty);
}
set {
SetValue(IdolProperty, value);
}
}
/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty IdolProperty =
DependencyProperty.Register(
"Idol",
typeof(string),
typeof(MyControl),
new FrameworkPropertyMetadata(
string.Empty, // 初期値
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(OnIdolChanged)
)
);
private static void OnIdolChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
Console.WriteLine("Call OnIdolChanged");
Console.WriteLine("\t 古い値={0}", e.OldValue);
Console.WriteLine("\t 新しい値={0}",e.NewValue);
}
}