こんにちは、働くC#プログラマーのさんさめです。
今回はWPFの中でもちょっとニッチな話です。
ContextMenuのMenuItemに、
ElementNameを使ったBindingを行おうとしても、
見つからないと言われてしまいます。
この解決法が分からず、さんさめは
Viewだけで完結するはずのBindingに、
ViewModelのプロパティを介したりしていました。
しかし、
実はこの問題はViewだけでも解決できました。
というわけで、本記事では、
この問題を解決できるようにするための
方法について解説します。
なんで見つからないのか?
さて、そもそもどうしてElementNameで
同じxaml内の要素を指定しているにもかかわらず
Bindingに失敗してしまうのでしょうか。
それは、ContextMenuが
独立したVisualTreeを持っていることに起因しています。
ContextMenuは厳密にはPopupであり、
元のWindowとVisualTree上のつながりがありません。
それゆえに、名前スコープが別物となり、
ElementNameで指定しても、
そんなものは無いと言われてしまうのです。
ただし、この時DataContextは伝搬しています。
つまり、ViewModelとのBindingは正常に行えるのです。
このため、私は長らく
ContextMenuで他のUI要素のプロパティと
Bindingしたい時は仕方なくViewModelにプロパティを生やして、
プロキシ的な役割を持たせていました。
NameScopeを設定してやれば見つかるように
しかし、この名前スコープという代物、
実はC#コードから設定することができます。
ContextMenuのNameScopeは、
何もしなければnullになっています。
そこで、NameScope.SetNameScopeメソッドを使って、
名前スコープを設定してみます。
public MainWindow()
{
InitializeComponent();
NameScope.SetNameScope(menu, NameScope.GetNameScope(this));
NameScope.SetNameScope(tooltip, NameScope.GetNameScope(this));
}
さて、実行してみます。
Bindingエラーが起きず、
きちんとテキストがでるようになっています。
デフォルトでNameScopeを引き継ぐようにする
しかし、いちいちコードビハインドで書くのは大変ですね。
このためだけにUI要素に名前を付けるのも面倒です。
ここはビヘイビアにしてみましょう。
ビヘイビアを使うための準備については、
DataGridのSelectedItemsをどんな時でも取得する方法
に載せているので良く分からない方はこちらをどうぞ。
といっても、Nugetから
Microsoft.xaml.Behaviors.Wpfを
インストールするだけです。
そして、以下のようなクラスを作ります。
public class NameScopeInheritBehavior : Behavior<DependencyObject>
{
protected override void OnAttached()
{
base.OnAttached();
if (!(AssociatedObject is FrameworkElement elem)) { return; }
elem.Dispatcher?.BeginInvoke((Action)(async () =>
{
// VisualTreeにつながるのを待つ
await Task.Delay(100);
var sourceNameScope = NameScope.GetNameScope(elem.FindAncestor<Window>());
var cm = elem.ContextMenu;
if (cm != null)
{
NameScope.SetNameScope(cm, sourceNameScope);
}
var tt = elem.ToolTip;
if (tt is DependencyObject dObj)
{
NameScope.SetNameScope(dObj, sourceNameScope);
}
}));
}
}
上記のコードは
【WPF】Viewの祖先要素をC#コードから取得する方法
で紹介しているコードを利用しています。
その中身は以下のものとなります。
public static class VisualTreeHelperWrapper
{
/// <summary>
/// VisualTreeを親側にたどって、
/// 指定した型の要素を探す
/// </summary>
public static T FindAncestor<T>(this DependencyObject depObj)
where T : DependencyObject
{
while (depObj != null)
{
if (depObj is T target)
{
return target;
}
depObj = VisualTreeHelper.GetParent(depObj);
}
return null;
}
}
やっていることは単純で、
ビヘイビアが設定されたUI要素の
ルートオブジェクト(今回はWindow限定)を探索し、
その名前スコープを、
ContextMenuおよびToolTipに伝搬させています。
xaml上での使い方は以下の通りです。
ElementNameによるBindingを使いたい
コンテキストメニューを持つUI要素に設定します。
これで、UI要素に名前を付けたり、
コードビハインドにコードを書く必要が無くなりました。
まとめ
まとめです。
- ContextMenuやToolTipでは、
ElementNameによるBindingが失敗する - 失敗する理由は名前スコープが異なるせい
- 名前スコープはC#コードから上書きが可能
- 名前スコープを自動で伝搬させるビヘイビアを作成
最後までお読みいただきありがとうございました。
コメント
メチャクチャ参考になりました!
「Viewだけで完結するはずのBindingに、ViewModelのプロパティを介したりしていました。」
まさにこれ!
うまくいかない理由はわかっていたのですが、こんな解決方法があったとは。。。もっと早く知りたかった。。
ありがとうございました!
記載されているコードですが、コンパイル通りますか?
NameScope.SetNameScope(menu, NameScope.GetNameScope(this));
FindAncestor
これらはビルドエラーで失敗するのですが。。。
ご指摘ありがとうございます。
たしかに、他のページで紹介しているクラスを利用してしまっていました。
ページ内にビルドに必要なコードを追記いたしました。
ご参考になれば幸いです。