Programmatically Selecting an Item in a TreeView

WPF’s composition and visualization capabilities are incredible. As you can see from my QbDirectoryViz post, it takes very little to visualize data using WPF. I believe the capabilities gained far outweigh the drawbacks. One of the drawbacks happens to be the programmatic selection of an item in a databound TreeView.

Background
In WinForms, the TreeView must be manually built out of TreeNodes which forces the developer to put their code in the UI. In WPF you have two options: either use the similar kludgy UI development technique and build your TreeView out of TreeViewItems, or use the more powerful technique of templates and databinding. The latter has the benefit of keeping data and logic separate from the UI as well as providing the foundation for easy visualization of the data.

Consider the following class:

public class SomeDataObject
{
private int _id;
private ObservableCollection<SomeDataObject> _children;

public SomeDataObject(int id)
{
this._id = id;
this._children = new ObservableCollection<SomeDataObject>();
}

public int Id
{
get { return this._id; }
}

public ObservableCollection<SomeDataObject> Children
{
get { return this._children; }
}
}

Create a few instances of this class and add them to an item’s children collection, and so on, and you have yourself a data structure in the form of a tree. Creating the UI is now just a matter of using a HierarchicalDataTemplate to visualize these objects:

<HierarchicalDataTemplate DataType=”{x:Type WpfTreeViewTricks:SomeDataObject}” ItemsSource=”{Binding Path=Children}”>
<StackPanel Orientation=”Horizontal”>
<TextBlock Text=”{Binding Path=Id}”/>
</StackPanel>
</HierarchicalDataTemplate>

The result is a just a simple visual tree that shows the id of each object. WPF will build the visual tree automatically and create a TreeViewItem for every instance of SomeDataObject. I suggest the following articles for more information on how the TreeView works:

TreeView Overview
Beatriz Costa TreeView Tricks Part 1
Beatriz Costa TreeView Tricks Part 2
Beatriz Costa TreeView Tricks Part 3

The Problem
The WPF TreeView exposes a SelectedItem property (of Type object) that is read only. This property gets updated when a user clicks on one of the TreeViewItems. The data object associated with the selected TreeViewItem is the object returned from the TreeView’s SelectedItem property, and the TreeViewItem’s IsSelected property changes to true. This is great for identifying the data object that is selected, but how do we get the TreeViewItem that is selected for this data-bound tree? Or better yet, how do we set the selected item of the tree programmatically if this property is read-only? The answer to the second question is simple: it is just a matter of setting the IsSelected property of the corresponding TreeViewItem to true, and the SelectedItem property of the TreeView is updated automatically. This means for any data object in the data tree, we must find the TreeViewItem that is representing this item and set its IsSelected property. At the time of writing, this is the only way to ‘set’ the SelectedItem of the TreeView. As it turns out, getting the UI element for an item in a ListView is easy, but getting the UI element for a TreeView is very hard (for three reasons described later).

Digging Deeper
To get the UI element (TreeViewItem) associated with an item in an ItemsControl (TreeView), the ItemContainerGenerator property can be used. All ItemsControls (Listbox, ListView, TreeView, even TreeViewItem) have the ItemContainerGenerator property, and getting the TreeViewItem from a data item is as simple as:

TreeViewItem treeViewItem = myTreeView.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;

The Real Problems
The ItemContainerGenerator only works for a single list of items. Since a ListView’s items are in a flat list, the ListView can be used to get any of the UI elements representing any of its items. The TreeView’s children are hierarchical in nature, so only the first level of TreeViewItems can be retrieved using the TreeView’s ItemContainerGenerator. To get an item that is three levels deep the first level TreeViewItem must be retrieved and used (since it is an ItemsControl too) to get the second level TreeViewItem. Then this item must be used to finally get the third level TreeViewItem so we can set its IsSelected property to true. Having to iterate through the TreeViewItems is problem #1.

Problem #2 is the fact that TreeViewItems may not have been generated for children that are not expanded. This means we must expand every TreeViewItem just so we can navigate through them.

Problem #3 is the worst one of all: programmatically expanding a TreeViewItem is an asynchronous operation! Setting a the TreeViewItem’s IsExpanded property to true does not immediately generate the child UI elements. If the ItemContainerGenerator.ContainerFromItem(object item) method is called when these elements have not been generated, the return value is null (I’ve seen many people have this problem in the WPF forum).


The Solution

An extension method is used to simplify the usage of the solution.

public static void SelectItem(this TreeView treeView, object item)
{
ExpandAndSelectItem(treeView, item);
}

Using the extension method allows us to select an item in the WPF TreeView in the following way:

myTreeView.SelectItem(myItem);

Solving problem #1 requires iterating through all children at the top level first to see if any of them correspond to the item to select:

private static bool ExpandAndSelectItem(ItemsControl parentContainer, object itemToSelect)
{
foreach (Object item in parentContainer.Items)
{
TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;

if (item == itemToSelect && currentContainer != null)
{
currentContainer.IsSelected = true;
currentContainer.BringIntoView();
currentContainer.Focus();

//the item was found
return true;
}
}

If no TreeViewItem is a match, then we must iterate through the children again to expand each one and iterate through its children:

foreach (Object item in parentContainer.Items)
{
TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;

if (currentContainer != null && currentContainer.Items.Count > 0)
{
bool wasExpanded = currentContainer.IsExpanded;
currentContainer.IsExpanded = true;

Expanding the child solves problem #2, but to solve problem #3 (asynchronous generation of child UI elements) we must listen for an event from the ItemContainerGenerator that tells us the children have been generated. Since we want to execute the handler in line we can attach an anonymous delegate to the event so we can continue with the recursion. By storing the anonymous delegate in an EventHandler variable it allows us to remove the delegate when we are done (inside the delegate handler!).

if (currentContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
{
EventHandler eh = null;
eh = new EventHandler(delegate
{
if (currentContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
{
//The assumption is that code executing in this EventHandler is the result of the parent not
//being expanded since the containers were not generated.
//since the itemToSelect was not found in the children, collapse the parent since it was previously collapsed
currentContainer.IsExpanded = false;
}

//remove the StatusChanged event handler since we just handled it (we only needed it once)
currentContainer.ItemContainerGenerator.StatusChanged -= eh;
}
});
currentContainer.ItemContainerGenerator.StatusChanged += eh;
}
else //otherwise the containers have been generated, so look for item to select in the children
{
if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
{
//restore the current TreeViewItem’s expanded state
currentContainer.IsExpanded = wasExpanded;
}
else //otherwise the node was found and selected, so return true
{
return true;
}
}

I realize breaking the code up makes it difficult to see how it all works together, but hopefully it made for a better explanation of solving the three problems.

Attached is a project that demonstrates the solution by displaying a TreeView on the left and a ListView on the right. When an item is added it gets added to both the ListView’s collection and to a random location in the tree’s hierarchical collection. That way we can handle the selected event from the ListView and attempt to select that same item in the TreeView. Since the items are added randomly in the tree, more often than not selecting an item in the ListView will require the extension method to expand and dig through the TreeViewItems until the correct TreeViewitem is found. Try it out for yourself, it works!

You can thank Zhou Yong for coming up with a solution that I based my code off of here.

Some differences between this solution and others are that this one removes the attached event handler when done, and also collapses TreeViewItems to their original state. The downside of this solution (and all others that I know of) is that there is the potential that the entire tree is searched before the item is found. As long as your tree does not contain thousands of items it should not be an issue. Perhaps sometime in the future I will attempt to solve this problem.

The code is fully documented in the sample:
WpfTreeViewTricks.zip

Comments are closed.