共用方式為


Finding an Object TreeViewItem

 

The question often comes up, "how do I get to a certain TreeViewItem?"  The question arises because a TreeView is bound to a data source, TreeViewItems are implicitly created and wrap the data object.  However, the TreeView.SelectedItem property returns the data object, not the TreeViewItem.   This is good when you need to access the data, but what if you need to manipulate the TreeViewItem?  It has been suggested that if you design your data model correctly, you can avoid needing to manipulate TreeViewItems directly.  While that is good general advice, there might be cases where manipulating the TreeViewItem is unavoidable.

 

Finding the TreeViewItem in a TreeView is more involved than finding item containers in other ItemControls (such as a ListBoxItem in a ListBox) because TreeViewItems are, naturally, nested.   For example, to return the ListBoxItem of an object in a ListBox, you can call ListBox.ItemContainerGenerator.GetContainerFromItem. But if you call GetContainerFromItem on a TreeView, the ItemContainerGenerator searches only the direct child objects of the TreeView.  So you need to recursively traverse the TreeView and child TreeViewItem objects.  A further complication is that if the TreeView virtualizes its items (you enable virtualization by setting the VirtualizingStackPanel.IsVirtualizing property to true), the child items need to be created before you can check its data object.

 

Given the complexity of the problem, I (with help from the development team) created a method that traverses the TreeView and realizes any virtualized items.

 

/// <summary>

/// Recursively search for an item in this subtree.

/// </summary>

/// <param name="container">

/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.

/// </param>

/// <param name="item">

/// The item to search for.

/// </param>

/// <returns>

/// The TreeViewItem that contains the specified item.

/// </returns>

private TreeViewItem GetTreeViewItem(ItemsControl container, object item)

{

    if (container != null)

    {

        if (container.DataContext == item)

        {

            return container as TreeViewItem;

        }

 

        // Expand the current container

        if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)

        {

            container.SetValue(TreeViewItem.IsExpandedProperty, true);

        }

 

        // Try to generate the ItemsPresenter and the ItemsPanel.

        // by calling ApplyTemplate.  Note that in the

        // virtualizing case even if the item is marked

        // expanded we still need to do this step in order to

        // regenerate the visuals because they may have been virtualized away.

 

        container.ApplyTemplate();

        ItemsPresenter itemsPresenter =

            (ItemsPresenter)container.Template.FindName("ItemsHost", container);

        if (itemsPresenter != null)

        {

            itemsPresenter.ApplyTemplate();

        }

        else

        {

            // The Tree template has not named the ItemsPresenter,

            // so walk the descendents and find the child.

            itemsPresenter = FindVisualChild<ItemsPresenter>(container);

            if (itemsPresenter == null)

            {

                container.UpdateLayout();

 

                itemsPresenter = FindVisualChild<ItemsPresenter>(container);

            }

        }

 

        Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);

 

 

        // Ensure that the generator for this panel has been created.

        UIElementCollection children = itemsHostPanel.Children;

 

        MyVirtualizingStackPanel virtualizingPanel =

            itemsHostPanel as MyVirtualizingStackPanel;

 

        for (int i = 0, count = container.Items.Count; i < count; i++)

        {

            TreeViewItem subContainer;

            if (virtualizingPanel != null)

            {

                // Bring the item into view so

                // that the container will be generated.

                virtualizingPanel.BringIntoView(i);

 

                subContainer =

                    (TreeViewItem)container.ItemContainerGenerator.

                    ContainerFromIndex(i);

            }

            else

            {

                subContainer =

                    (TreeViewItem)container.ItemContainerGenerator.

                    ContainerFromIndex(i);

 

                // Bring the item into view to maintain the

                // same behavior as with a virtualizing panel.

                subContainer.BringIntoView();

            }

 

            if (subContainer != null)

            {

                // Search the next level for the object.

                TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);

                if (resultContainer != null)

                {

                    return resultContainer;

                }

                else

                {

                    // The object is not under this TreeViewItem

                    // so collapse it.

                    subContainer.IsExpanded = false;

                }

            }

        }

    }

 

    return null;

}

 

/// <summary>

/// Search for an element of a certain type in the visual tree.

/// </summary>

/// <typeparam name="T">The type of element to find.</typeparam>

/// <param name="visual">The parent element.</param>

/// <returns></returns>

private T FindVisualChild<T>(Visual visual) where T : Visual

{

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)

    {

        Visual child = (Visual)VisualTreeHelper.GetChild(visual, i);

        if (child != null)

        {

            T correctlyTyped = child as T;

            if (correctlyTyped != null)

            {

                return correctlyTyped;

            }

 

            T descendent = FindVisualChild<T>(child);

            if (descendent != null)

            {

                return descendent;

            }

        }

    }

 

    return null;

}

 

Note that this code looks for a Panel called, MyVirtualizingStackPanel.  This new type exposes a method that allows you to bring an item into view by passing in the item's index.

 

public class MyVirtualizingStackPanel : VirtualizingStackPanel

{

    /// <summary>

    /// Publically expose BringIndexIntoView.

    /// </summary>

    public void BringIntoView(int index)

    {

 

        this.BringIndexIntoView(index);

    }

}

 

The final step is to use the custom VirtualizingStackPanel as the TreeView's ItemsPanel:

 

<TreeView VirtualizingStackPanel.IsVirtualizing="True">

  <!--Use the custom class MyVirtualizingStackPanel
      as the ItemsPanel for the TreeView and
      TreeViewItem object.-->
  <TreeView.ItemsPanel>
    <ItemsPanelTemplate>
      <src:MyVirtualizingStackPanel/>
    </ItemsPanelTemplate>
  </TreeView.ItemsPanel>
  <TreeView.ItemContainerStyle>
    <Style TargetType="TreeViewItem">
      <Setter Property="ItemsPanel">
        <Setter.Value>
          <ItemsPanelTemplate>
            <src:MyVirtualizingStackPanel/>
          </ItemsPanelTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </TreeView.ItemContainerStyle>
</TreeView>

Now that you have the code, you can search for any object in any TreeView, regardless of its depth in the tree.

 

I attached a sample that uses this technique to find any item in the TreeView. The sample asks for a number, finds the corresponding data object in the data model, and then selects the TreeViewItem that contains the object.  Note that the way I find the data object in the model is specific to the organization of my data.  I could have kept track of the data object's location within the data hierarchy, and then find the corresponding TreeViewItem in the TreeView. 

 

For example, the item that corresponds to 41 is, starting from the root of the TreeView, under the second item, then under the first item, then under the second item, and finally the first item.  I could have kept track of its location with a list of indices, 2,1,2,1, and then used those to navigate the TreeView.  However, this approach is dependent on the organization of the underlying TreeView.  The implementation I show above works on any TreeView and it doesn't require knowledge of the data model.

 

The attached sample has C# and Visual Basic versions.

FindTreeViewItem.zip

Comments

  • Anonymous
    June 22, 2010
    Hi Carole : Thanks very much for writing this. It was helpful, especially not having to reference private BindingFlags in accessing the internal Scroll control.   I found that, after utilizing your FindTreeViewItem, with a very large, flattened list (which still requires a TreeView rather than a ListView), such as 15k items, the ApplyTemplate() followed by the BringIndexIntoView() takes a very significant amount of time. I was able to get around much of it, for my use case in the following way: I added a delegate to GetTreeViewItem(...)'s representing whether the object to test/ApplyTemplate/BringIntoView is worth expanding (i.e. has child items) (containerItem) => ((MyInterface) conainterItem).HasItems();  If the item doesn't match the queried item, and if the item has no children, it is skipped and no ApplyTemplate or BringIntoView is applied, thereby dramatically speeding up my use case anyway. This, of course, implies a few things.  (1) The MVVM model supports holding all item's children in each element (or other way via the delegate) and (2) the huge list of items are not parents themselves. (So, 15k items all with one child would still be slow as each would be expanded).   The way I would mitigate that would be to capture the hierarchy and test for one parent at a time, on down, knowing what level they are in, before expanding. Thanks again - I just wanted to share that in case others were or would run into similar challenges. -Jim Gale

  • Anonymous
    July 05, 2010
    The comment has been removed

  • Anonymous
    July 31, 2010
    Thanks a lot Carole, this helped a lot for my task.

  • Anonymous
    September 10, 2010
    The fact that this much code is necessary for the simple task of finding an item in a TreeView is WPF's achilles heel.  This is ridiculous.  And how in the world does Microsoft expect beginners to come up with this solution?  Clearly WPF is meant for gurus who have weeks to pour over documentation and analyzer Reflector results to figure out convoluted ways to do SIMPLE things!!