Navigating TreeViewItems in C#

I have written a number of tools for navigating our data in Dauntless. One of these tools gathers data about how our assets are referenced and the Unreal Engine pak files they go into for streaming installs for some platforms.

What I wanted to be able to do is from a particular node in the tree of assets (tree representation of a directed graph with cycles) was to search for a subnode (breadth first search) that matched search criteria by either name and/or chunk it belonged to (for cross-chunk references). Given the results of my search through my tree nodes and the collection of objects I then needed to navigate to the corresponding TreeViewItem and scroll to it.

If a TreeViewItem is collapsed, its visual children TreeViewItem instances may not be generated yet. This could might be simplified further based on the ItemContainerGenerator Status but I think it helps illustrate the stages.

My nodes are all based on a single type, the template parameter T. For more heterogeneous situations a base class, interface, or even just object would suffice.

Code follows.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace <Your Namespace Here>
{
  /// <summary>
  /// Helper class for navigating a chain of items in a tree view to the final node.
  /// </summary>
  public static class TreeViewNavigateTo
  {
    /// <summary>
    /// Navigates from the <paramref name="current"/> <seealso cref="TreeViewItem"/> through the
    /// tree identifying nodes in the <seealso cref="TreeView"/>  underneath corresponding to the items in the
    /// <paramref name="itemList"/>.
    /// </summary>
    public static void NavigateTo<T>(this TreeViewItem current, IEnumerable<T> itemList)
    {
      // If the current node is already expanded, the contained items in the UI may be generated
      // so we can find the corresponding TreeViewItem for the next item in the itemList.
      if (current.IsExpanded)
      {
        // If the item container generator status is failed, we can't reliably progress further.
        if (current.ItemContainerGenerator.Status == GeneratorStatus.Error)
        {
          current.IsSelected = true;
          current.BringIntoView();
          return;
        }
        // The item containers are not generated yet. So we need to wait for that to occur
        // before we can search the sub items for the corresponding next element.
        else if (current.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
        {
          // Wait for the item contaner generator status to change, then retry.
          EventHandler onStatusChanged = null;
          onStatusChanged = (object sender, EventArgs e) =>
          {
            current.ItemContainerGenerator.StatusChanged -= onStatusChanged;
            NavigateTo(current, itemList);
          };
          current.ItemContainerGenerator.StatusChanged += onStatusChanged;
          return;
        }

        // The items are generated. Get the next item to find.
        var nextItem = itemList.FirstOrDefault();
        if (nextItem == null)
        {
          // No more entries, so the current is the last one. Select it and bring it into view.
          current.IsSelected = true;
          current.BringIntoView();
          return;
        }

        // Find the container for the next item. If we don't find it, something went awry with the
        // search that generated the itemList.
        var next = current.ItemContainerGenerator.ContainerFromItem(nextItem) as TreeViewItem;
        // Couldn't find the corresponding TreeViewItem for the next entry. At least navigate to there.
        if(next == null)
        {
          current.IsSelected = true;
          current.BringIntoView();
          return;
        }
        NavigateTo(next, itemList.Skip(1));
      }
      else
      {
        // We need to trigger the expansion so subitems can be generated to continue the search.
        RoutedEventHandler onExpand = null;
        onExpand = (sender, e) =>
        {
          current.Expanded -= onExpand;
          NavigateTo(current, itemList);
        };
        current.Expanded += onExpand;
        // Expand the subitems...
        current.IsExpanded = true;
      }
    }
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.