【WPF】Binding入門2。Binding対象を変更するには

C#

こんにちは、働く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対象にできる

最後までお読みいただきありがとうございました。

コメント

タイトルとURLをコピーしました