Visual Tree Enumeration

Posted by codingbloke on Geeks with Blogs See other posts from Geeks with Blogs or by codingbloke
Published on Sun, 19 Dec 2010 13:32:34 GMT Indexed on 2010/12/20 17:49 UTC
Read the original article Hit count: 269

Filed under:

I feel compelled to post this blog because I find I’m repeatedly posting this same code in silverlight and windows-phone-7 answers in Stackoverflow.

One common task that we feel we need to do is burrow into the visual tree in a Silverlight or Windows Phone 7 application (actually more recently I found myself doing this in WPF as well).  This allows access to details that aren’t exposed directly by some controls.  A good example of this sort of requirement is found in the “Restoring exact scroll position of a listbox in Windows Phone 7”  question on stackoverflow.  This required that the scroll position of the scroll viewer internal to a listbox be accessed.

A caveat

One caveat here is that we should seriously challenge the need for this burrowing since it may indicate that there is a design problem.  Burrowing into the visual tree or indeed burrowing out to containing ancestors could represent significant coupling between module boundaries and that generally isn’t a good idea.

Why isn’t this idea just not cast aside as a no-no?  Well the whole concept of a “Templated Control”, which are in extensive use in these applications, opens the coupling between the content of the visual tree and the internal code of a control.   For example, I can completely change the appearance and positioning of elements that make up a ComboBox.  The ComboBox control relies on specific template parts having set names of a specified type being present in my template.  Rightly or wrongly this does kind of give license to writing code that has similar coupling.

Hasn’t this been done already?

Yes it has.  There are number of blogs already out there with similar solutions.  In fact if you are using Silverlight toolkit the VisualTreeExtensions class already provides this feature.  However I prefer my specific code because of the simplicity principle I hold to.  Only write the minimum code necessary to give all the features needed.  In this case I add just two extension methods Ancestors and Descendents, note I don’t bother with “Get” or “Visual” prefixes.  Also I haven’t added Parent or Children methods nor additional “AndSelf” methods because all but Children is achievable with the addition of some other Linq methods.  I decided to give Descendents an additional overload for depth hence a depth of 1 is equivalent to Children but this overload is a little more flexible than simply Children.

So here is the code:-

VisualTreeEnumeration
public static class VisualTreeEnumeration
{
    public static IEnumerable<DependencyObject> Descendents(this DependencyObject root, int depth)
    {
        int count = VisualTreeHelper.GetChildrenCount(root);
        for (int i = 0; i < count; i++)
        {
            var child = VisualTreeHelper.GetChild(root, i);
            yield return child;
            if (depth > 0)
            {
                foreach (var descendent in Descendents(child, --depth))
                    yield return descendent;
            }
        }
    }

    public static IEnumerable<DependencyObject> Descendents(this DependencyObject root)
    {
        return Descendents(root, Int32.MaxValue);
    }

    public static IEnumerable<DependencyObject> Ancestors(this DependencyObject root)
    {
        DependencyObject current = VisualTreeHelper.GetParent(root);
        while (current != null)
        {
            yield return current;
            current = VisualTreeHelper.GetParent(current);
        }
    }
}

 

Usage examples

The following are some examples of how to combine the above extension methods with Linq to generate the other axis scenarios that tree traversal code might require.

Missing Axis Scenarios
var parent = control.Ancestors().Take(1).FirstOrDefault();

var children = control.Descendents(1);

var previousSiblings = control.Ancestors().Take(1)
    .SelectMany(p => p.Descendents(1).TakeWhile(c => c != control));

var followingSiblings = control.Ancestors().Take(1)
    .SelectMany(p => p.Descendents(1).SkipWhile(c => c != control).Skip(1));

var ancestorsAndSelf = Enumerable.Repeat((DependencyObject)control, 1)
    .Concat(control.Ancestors());

var descendentsAndSelf = Enumerable.Repeat((DependencyObject)control, 1)
    .Concat(control.Descendents());

You might ask why I don’t just include these in the VisualTreeEnumerator.  I don’t on the principle of only including code that is actually needed.  If you find that one or more of the above  is needed in your code then go ahead and create additional methods.  One of the downsides to Extension methods is that they can make finding the method you actually want in intellisense harder.

Here are some real world usage scenarios for these methods:-

Real World Scenarios
//Gets the internal scrollviewer of a ListBox
ScrollViewer sv = someListBox.Descendents().OfType<ScrollViewer>().FirstOrDefault();

// Get all text boxes in current UserControl:-
var textBoxes = this.Descendents().OfType<TextBox>();

// All UIElement direct children of the layout root grid:-
var topLevelElements = LayoutRoot.Descendents(0).OfType<UIElement>();

// Find the containing `ListBoxItem` for a UIElement:-
var container = elem.Ancestors().OfType<ListBoxItem>().FirstOrDefault();

// Seek a button with the name "PinkElephants" even if outside of the current Namescope:-
var pinkElephantsButton = this.Descendents()
    .OfType<Button>()
    .FirstOrDefault(b => b.Name == "PinkElephants");

//Clear all checkboxes with the name "Selector" in a Treeview
foreach (CheckBox checkBox in elem.Descendents()
    .OfType<CheckBox>().Where(c => c.Name == "Selector"))
{
    checkBox.IsChecked = false;
}

 

The last couple of examples above demonstrate a common requirement of finding controls that have a specific name.  FindName will often not find these controls because they exist in a different namescope.

Hope you find this useful, if not I’m just glad to be able to link to this blog in future stackoverflow answers.

© Geeks with Blogs or respective owner