【WPF】Viewの祖先要素をC#コードから取得する方法

C#

こんにちは、働く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>を使おう
で詳しく解説しています。

コメント

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