こんにちは、働くC#プログラマーのさんさめです。
WPFにおいてBindingは
「データとビューの疎結合化」
「コード記述の省力化」
などなど非常に重要な役割を果たします。
(後者は慣れないと恩恵を感じにくいですが)
その一方で、
何が起きているのか分かりにくいため、
一度Bindingでハマってしまうと
「まずどこから調べればいいのか分からない」
ということになりがちです。
そこで、私自身の知識の整理も兼ねて
Bindingについてまとめることにしました。
と、いうわけでWPFのBinding入門編第2回です。
第1回は、
【WPF】Binding入門1。DataContextの伝搬
をご覧ください。
この記事は以下のような人に向けて書いています。
- ListBoxやDataGridの各要素から、
リストを持っているViewModelの
プロパティやコマンドにBindingしたい - RelativeSourceって何となく使ってるけど、
実際どんなことが起きているのか理解したい
本記事では、
「Binding対象にDataContext以外を指定する方法」
に絞って解説します。
xaml内のあらゆる要素に
Bindingできるようになることが目標です
RelativeSourceプロパティで親コントロールをBinding対象にする
Bindingの対象を変更したい時に、
最も便利に使えてさんさめ自身もよく行う方法が、
RelativeSourceプロパティによる指定です。
Bindingを設定するコントロールを基準に、
何をBindingの対象とするのかを指定することができます。
第1回では、
ListBoxなどのItemsSourceプロパティを持つ
コントロールにコレクションをBindingした時の、
DataContextの伝搬について説明しました。
ItemsSourceにコレクションをBindingすると
各要素のDataContextは、
Bindingしたコレクションのそれぞれの要素になります。
(以下画像を参照)
ところが、実際にアプリ開発を行うと、
このListBoxの各行に親の方のプロパティや
コマンドをBindingしたくなるケースが出てきます。
例えば以下のようなケースです。
- データをComboBoxで編集できるようにしたい。
その選択肢は親のViewModelが持っている - Buttonで行削除など、何らかの操作を行わせたい。
その処理内容は親のViewModelが持っている
こんな時に、
以下のようにItemTemplateを記述しても、
当然Bindingエラーになってしまいます。
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Id}"/>
<!-- Selectionは親のViewModelしか持っていない…! -->
<ComboBox SelectedItem="{Binding ItemName}"
ItemsSource="{Binding Selection}"
Margin="8 0 0 0"/>
<!-- RemoveCommandは親のViewModelしか(以下略) -->
<Button Command="{Binding RemoveCommand}"
Content="この行を削除"
CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
そんな時にRelativeSourceプロパティを使うと、
DataContextが伝搬する前のコントロールまで遡って
Binding対象とすることができます。
上記サンプルを以下のように書き換えます。
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Id}"/>
<!-- RelativeSourceでListBoxをBindingの対象に指定 -->
<ComboBox SelectedItem="{Binding ItemName}"
ItemsSource="{Binding DataContext.Selection, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}}"
Margin="8 0 0 0"/>
<!-- Binding対象がListBoxなので、パスに「DataContext.」を付ける -->
<Button Command="{Binding DataContext.RemoveCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}}"
Content="この行を削除"
CommandParameter="{Binding}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
これでまともに動くようになります。
呪文過ぎて何が起きているのか
全然分からない…!
ご安心ください。
1つずつ分解していけば、
難しいことは何1つありません。
必ず理解できます。
RelativeSourceの指定を1つずつ分解して読み解く
先ほどのコードサンプルから、
ComboBoxのItemsSourceプロパティだけを抜き出してみました。
読みやすくするために、カンマの部分で改行を入れています。
(※実際にxaml上でこんな改行をすると
パースエラーになるので注意が必要です)
ItemsSource="{Binding DataContext.Selection,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListBox}}}"
まず見るべきは、RelativeSourceプロパティの指定です。
Bindingクラスにはいくつかのプロパティがあり、
波括弧「{}」で括った中をさらにカンマで区切ると、
個別にプロパティの指定ができます。
そもそもxaml上のマークアップ拡張の読み方が分からない!
という方は、
【WPF】読める!xamlマークアップ拡張【入れ子でも怖くない】
をご覧ください。
RelativeSourceプロパティの=の先では、
さらに波括弧がでてきます。
これはつまり、
マークアップ拡張内で入れ子的に
マークアップ拡張を使っている形です。
ItemsSourceの=の先が
「Bindingマークアップ拡張」であり、
RelativeSourceの=の先が
「RelativeSourceマークアップ拡張」ということですね。
よく見たらx:Typeマークアップ拡張なんてのもある…
この構文はマークアップが計3つあるのか…
そういうことになりますね。
マークアップ拡張はxamlに慣れない初心者を
しばしば苦しめますが、
このような構造になっていることが理解できると、
一気に読解が楽になります。
そして、RelativeSourceマークアップ拡張の中では、
2つの指定を行っています。
それが、「FindAncestor」と「AncestorType」です。
厳密には、FindAncestorは設定値であり、
設定している本当のプロパティ名は「Mode」です。
RelatievSourceマークアップ拡張では、
プロパティ名を省略して記述した場合、
Modeプロパティが指定されたとみなされます。
(※ちなみに、Bindingマークアップ拡張では、
Pathプロパティが指定されたとみなされています。
興味が合れば個別のマークアップ拡張解説記事をご覧ください)
…
少し話が逸れました。
「FindAncestor」と「AncestorType」について、
個別に詳細を見ていきましょう。
FindAncestorは、祖先のコントロールを探索するモード
FindAncestorを指定すると、
VisualTree上の祖先コントロールを探索して
Binding対象を決定するモードになります。
このモードを指定した場合、
AncestorType(探索したい型)の指定が必須です。
指定しないと、xamlを読み込んだ瞬間例外になります。
ちなみに、他のモードもあるにはあるのですが、
ほとんど使わないので実質RelativeSourceを使うときは、
ほぼほぼFindAncestor一択です。
まれに「Self(そのコントロール自身)」は使いますが、
それはまた今度で。
AncestorTypeで探索したい型を指定する
AncestorTypeプロパティで、
実際にVisualTreeを辿って検索したい型を指定します。
サンプルコードでは「ListBoxを探索して」
と指定しています。
そして、ListBoxには当然「DataContext」
というプロパティがあるはずなので、
「.」で結んでSelectionプロパティを辿っています。
ListBoxのDataContextに設定されたインスタンスが、
Selectionプロパティを持っていれば
無事Binding解決です。
もし持っていなければ、
やはりBindingエラーとなってしまいます。
RelativeSourceの効用と気を付ける点をまとめると、
- ビュー上の祖先コントロールから、
好きな要素をBinding対象にできる - コントロールがBinding対象になるので、
ViewModelのプロパティにアクセスしたい場合は、
DataContext.Hogeのように、
DataContextプロパティを経由した指定をする必要がある
ということになります。
ElementNameプロパティで任意のコントロールを直接Binding対象にする
RelativeSource以外でもう一つ、
便利に使える方法が
ElementNameプロパティによる指定です。
こちらはRelativeSourceと違い、
VisualTree上の子コントロールや、
VisualTree上の直接の親子関係が無いコントロールに対しても
Binding対象として指定することができます。
使い時としては、
【WPF】ウィンドウを出した瞬間からキー入力可能にする方法
で解説している
FocusManager.FocusedElementなどがあります。
<Window (~中略~)
FocusManager.FocusedElement="{Binding ElementName=MyTextBox}">
<TextBox x:Name="MyTextBox"/>
</Window>
この例では、
Windowが表示されたときに、
フォーカスが当たって欲しいコントロールを、
ElementNameを使って指定しています。
このように、
ElementNameによる指定では、
xaml上に存在さえしていればツリーの親子関係に関係なく
Binding対象を指定することができます。
ただし、Bindingしたい対象のコントロールに
名前をつけなければいけないという制約があるため、
可能であればRelativeSourceの使用をおススメします。
名前を付けると、
そのコントロールが自動的に
internalフィールドとして公開されてしまうといった
副作用もあるため、
意図せず外側から操作される可能性を高めます。
(普通はそんなことしませんが…)
まとめ
まとめです。
- BindingはDataContext以外に対象を変更可能
- RelativeSourceを使うと型を指定して、
ビューの親要素をBinding対象にできる - ElementNameを使うと、
名前がついているコントロールを
Binding対象にできる
最後までお読みいただきありがとうございました。
シリーズ記事
Binding入門はシリーズ記事となっております。
全ての記事に以下からアクセスできます
コメント