こんにちは、働くC#プログラマーのさんさめです。
WPFでの実装をしていると、
ListBoxやDataGridなどの1つ1つの要素に、
ContextMenuを
実装したくなることがあります。
このとき、DataContextは
伝搬されて子要素となりますね。

「いや、まずそこも分からん」
という方はこちらもご覧ください。
【WPF】Binding入門1。DataContextの伝搬
このとき、ContextMenuにおいても
子のVMではなく親のVMに
Bindingしたくなることがあります。
通常ContextMenuでは、
FindAncestorを使った
間接的な親VMへのアクセスはできません。
こういう時、
ググるとよく出てくるのは
BindingProxyみたいな自作クラスを使って
Resource経由でBindingさせる方法です。
しかし、実はxaml上の実装だけで
これは実現可能です。
本記事では、
PlacementTargetプロパティを使うことで、
ContextMenuに対しても
FindAncestorした結果を
伝搬させる方法を解説します。
ビジュアルツリーが別なのでContextMenuでは親要素が見つからない
通常のコントロール、
すなわちListBoxやDataGridが存在する
ビジュアルツリーにぶら下がっているものについては、
RelativeSourceを使って間接的に
Bindingすることができます。

(詳細は
【WPF】Binding入門2。Binding対象を変更するには
をご覧ください)
しかし、ContextMenuはPopup派生であるため、
ビジュアルツリーが別です。
ビジュアルツリーの違いを
Snoopで見てみましょう
まず、通常のListBoxItemのビジュアルツリーです。
親を辿ることでListBox、
もっと上まで遡ることでWindow(MainWindow)クラスまで
取得することができますね。

一方こちらは、MenuItemの属する、
ContextMenuのビジュアルツリーです。
親を辿ってもPopupRootで終わってしまいます。

ビジュアルツリーが別なので、
FindAncestorで上に辿っても
目的の親コントロールが見つかりません。

それでは、
こういうことをしたい時は
どうすればよいのでしょうか
TagとPlacementTargetを使って伝搬させる
まずは結論のコードからお見せします。
<!-- 前略 -->
<ListBox.ItemTemplate>
<DataTemplate>
<!-- 中略 -->
<TextBlock Tag="{Binding DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBox}}}"
Text="{Binding Id}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="{Binding PlacementTarget.Tag.Name, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}" />
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
<!-- 中略 -->
</DataTemplate>
</ListBox.ItemTemplate>
<!-- 後略 -->
見るべき場所は、
TextBlockに記述しているTagプロパティと
MenuItemのBindnigの中身です。
TextBlockはListBoxと同じビジュアルツリーに
属していますので、
FindAncestorでListBoxを辿ることができます。
そこで、Tagプロパティに対して
FindAncestorを使うことで
親要素のDataContextを格納しています。
Tagプロパティは
利用者が任意に設定、取得することのできるプロパティです。
WPFフレームワーク内では使用されません。
次にもう一つのポイントとして、
ContextMenu.PlacementTargetプロパティ
の存在があります。
このプロパティには、
ContextMenuを開くきっかけとなったUI要素
が格納されています。
この場合TextBlockとなります。

…ん?このTextBlockは、
さっきTagを設定しておいた
TextBlock…?!

そうです。
なので親要素のDataContextを
持っているということです。
あとは、これをMenuItemから取得するだけです。
具体的にはFindAncestorでContextMenuクラスを探します。

そして、Bindingのパスで
「PlacementTarget.Tag.(Bindingしたいプロパティ名)」
と記述します。

これで、
MenuItemから親要素のVMに
間接的にBindingすることができました。

ContextMenuのPlacementTarget(=TextBlock)の
Tag(=DataContext)のNameプロパティですね。
1発で理解できたあなたはWPF慣れしています。

まとめ
まとめです。
- 子要素から親要素にアクセスするなら
普通はFindAncestorを使う - ContextMenuはビジュアルツリーが別なので
素直にFindAncestorできない - TagとPlacementTargetを使うことで
間接的に親要素にアクセス可能
最後までお読みいただき
ありがとうございました。
コメント