Since this question comes up most prominently when searching, I wanted to post an answer to it.
The above post by lars doesn't work for me when I'm using a databound TreeView with a HierarchicalDataTemplate, because the Items collection returns the actual databound items, not the TreeViewItem.
I ended up solving this by using the ItemContainerGenerator for individual data items, and the VisualTreeHelper to search "up" to find the parent node (if any). I implemented this as a static helper class so that I can easily reuse it (which for me is basically every TreeView).
Here's my helper class:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace TreeViewHelpers
{
public static class TreeViewItemTextSearcher
{
private static bool checkIfMatchesText(TreeViewItem node, string searchterm, StringComparison comparison)
{
return node.Header.ToString().StartsWith(searchterm, comparison);
}
//https://stackoverflow.com/questions/26624982/get-parent-treeviewitem-of-a-selected-node-in-wpf
public static TreeViewItem getParentItem(TreeViewItem item)
{
try
{
var parent = VisualTreeHelper.GetParent(item as DependencyObject);
while ((parent as TreeViewItem) == null)
{
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
catch (Exception e)
{
//could not find a parent of type TreeViewItem
return null;
}
}
private static bool tryFindChild(
int startindex,
TreeViewItem node,
string searchterm,
StringComparison comparison,
out TreeViewItem foundnode
)
{
foundnode = null;
if (!node.IsExpanded) { return false; }
for (int i = startindex; i < node.Items.Count; i++)
{
object item = node.Items[i];
object tviobj = node.ItemContainerGenerator.ContainerFromItem(item);
if (tviobj is null)
{
return false;
}
TreeViewItem tvi = (TreeViewItem)tviobj;
if (checkIfMatchesText(tvi, searchterm, comparison))
{
foundnode = tvi;
return true;
}
//recurse:
if (tryFindChild(tvi, searchterm, comparison, out foundnode))
{
return true;
}
}
return false;
}
private static bool tryFindChild(TreeViewItem node, string searchterm, StringComparison comparison, out TreeViewItem foundnode)
{
return tryFindChild(0, node, searchterm, comparison, out foundnode);
}
public static bool SearchTreeView(TreeViewItem node, string searchterm, StringComparison comparison, out TreeViewItem found)
{
//search children:
if (tryFindChild(node, searchterm, comparison, out found))
{
return true;
}
//search nodes same level as this:
TreeViewItem parent = getParentItem(node);
object boundobj = node.DataContext;
if (!(parent is null || boundobj is null))
{
int startindex = parent.Items.IndexOf(boundobj);
if (tryFindChild(startindex + 1, parent, searchterm, comparison, out found))
{
return true;
}
}
found = null;
return false;
}
}
}
I also save the last selected node, as described in this post:
<TreeView ... TreeViewItem.Selected="TreeViewItemSelected" ... />
private TreeViewItem lastSelectedTreeViewItem;
private void TreeViewItemSelected(object sender, RoutedEventArgs e)
{
TreeViewItem tvi = e.OriginalSource as TreeViewItem;
this.lastSelectedTreeViewItem = tvi;
}
And here's the above TextInput, modified to use this class:
private void treeView_TextInput(object sender, TextCompositionEventArgs e)
{
if ((DateTime.Now - LastSearch).Seconds > 1) { searchterm = ""; }
LastSearch = DateTime.Now;
searchterm += e.Text;
if (lastSelectedTreeViewItem is null)
{
return;
}
TreeViewItem found;
if (TreeViewHelpers.TreeViewItemTextSearcher.SearchTreeView(
node: lastSelectedTreeViewItem,
searchterm: searchterm,
comparison: StringComparison.CurrentCultureIgnoreCase,
out found
))
{
found.IsSelected = true;
found.BringIntoView();
}
}
Note that this solution is a little bit different from the above, in that I only search the children of the selected node, and the nodes at the same level as the selected node.