こんにちは、働くC#プログラマーのさんさめです。
WPFに慣れてくると、
C#コード上から
VisualTreeの親をたどって特定の要素を取得したい
ケースがでてきます。
このようなことをしたい場合は
VisualTreeHelperというstaticクラスに
GetParentというメソッドがあるので
これを活用することになります。
…が…、
なんだかとっても使いにくい…
このメソッドは非常にプリミティブなAPIとなっており、
実用するにはちょっと面倒です。
というわけで、本記事では、
さんさめが実際に働く上で用いている
GetParentのラッパー実装を紹介します。
直近の祖先要素を取得するラッパーメソッド
祖先要素を取得したい場合、
「特定の型」で「最初に見つかったもの」
が欲しい場合がほとんどです。
というわけでコードサンプルです。
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;
}
}
使用例は以下の通りです。
サンプルを出すのが難しいので、
なんの意味もないコードにですがご了承ください。
きちんと親要素であるDockPanelが取得できて、
その名前も「MyDockPanel」となっており、
xamlに記述したDockPanelであることが分かりますね。
直近以外の要素も取得できるコード例
祖先要素の中からもっと柔軟に要素を取得したい場合は、
列挙を返すメソッドを作ると便利です。
以下はコード例です。
public static IEnumerable<DependencyObject> FindAncestors(this DependencyObject depObj)
{
if (depObj == null) { yield break; }
depObj = VisualTreeHelper.GetParent(depObj);
while (depObj != null)
{
yield return depObj;
depObj = VisualTreeHelper.GetParent(depObj);
}
}
型を指定する部分が無くなって、
シンプルに親方向に辿るだけのコードになっています。
(※型指定が無くなった分、
結果に自分を含めてしまわないように配慮しています)
上記メソッドを使えば、
以下のように柔軟に取得できます。
型を指定する部分が無くなっているのは、
OfType<T>を使えば十分だからですね。
var dockPanel = MyButton.FindAncestors()
.OfType<DockPanel>() // 祖先要素のDockPanelをすべて列挙
.Skip(1) // 最初に見つかったものは飛ばす
.FirstOrDefault(); // 2番目に見つかったものを取得
var namedElement = MyButton.FindAncestors()
.OfType<FrameworkElement>()
.FirstOrDefault(x => !string.IsNullOrEmpty(x.Name));
// 「先祖の中から名前をつけている最初の要素」を取得
とはいえ、繰り返しになりますが、
親要素をたどりたい時は大抵、
型を指定して直近のものを取得できれば
充分なケースがほとんどです。
さんさめ自身もめったに使用しませんが、
「特定の添付プロパティがついている祖先の最初の要素」
を探すときなどは列挙の方を活用しています。
おまけ:xamlで祖先を辿ってBindingするならRelativeSource
ところで、C#でなくxaml上で書くなら
どのようにすればよいのでしょうか。
たとえば、先のButtonのContentを
親要素であるDockPanelの名前にしたいなら
次のように記述します。
<Window x:Class="VisualTreeSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="240" Width="400">
<DockPanel x:Name="HogeDockPanel">
<DockPanel Margin="4"
x:Name="MyDockPanel">
<Button Content="{Binding Name, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DockPanel}}}"
DockPanel.Dock="Bottom"
x:Name="MyButton"/>
</DockPanel>
</DockPanel>
</Window>
長いですね。
必要なところだけ抜粋すると以下です。
Content="{Binding Name, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DockPanel}}}"
それでもまだ長いですね。
マークアップ構文は途中で改行できないのがつらいです。
しっかり説明すると大変なので要点を説明すると
以下のようになります。
- BindingするプロパティはNameを指定
- RelativeSourceを指定すると
DataContext以外をBinding対象にできる - FindAncestorでRelativeSourceの辿り方を祖先に指定できる
- AncestorTypeをDockPanelに指定すると
祖先からDockPanelを探してきてくれる
xamlのマークアップ構文の読み方は、
今後、しっかり解説する記事が必要そうですね…。
まとめ
まとめです。
- C#コードでVisualTreeを辿るAPIは
プリミティブで扱いづらい - 親要素から指定した型を取得する方法を紹介
- 親要素を列挙として返すと
祖先要素の中から柔軟に必要なものを取得できる - xamlで書くならRelativeSource FindAncestorを使う
最後までお読みいただきありがとうございました。
関連記事
本記事では祖先要素を取得する
ラッパーメソッドについて紹介しましたが、
逆に子孫要素を取得したいこともあると思います。
【WPF】Viewの子孫要素をC#コードから取得する方法
にて、そちらについてもラッパーメソッドを紹介しています。
また、今回のサンプルコードで使ったOfType<T>には、
Cast<T>というよく似たメソッドがあります。
ただし、指定した型が存在するかどうか明らかでない場合は、
Cast<T>は絶対に使わないようにしましょう。
例外になってしまいます。
【LINQ】Cast<T>は使わない方が良い?OfType<T>を使おう
で詳しく解説しています。
コメント