【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をコピーしました