Helge Klein
Helge Klein

Reputation: 9095

WPF treeview: how to implement keyboard navigation like in Explorer?

I am using the WPF treeview for the first time and am astonished of all the basic things it does not do. One of those is keyboard navigation, implemented in any self-respecting treeview, e.g. in Windows Explorer or Regedit.

This is how it should work:

If the treeview has the focus and I type (letters/numbers) the selection should move to the first visible (aka expanded) item below the currently selected item that matches the string I typed and bring that into view. If not match is found below the current item the search should continue from the top. If no match is found, the selected item should not change.

As long as I continue typing, the search string grows and the search is refined. If I stop typing for a certain time (2-5 seconds), the search string is emptied.

I am prepared to program this "by hand" from scratch, but since this is so very basic I thought surely someone has already done exactly this.

Upvotes: 9

Views: 6396

Answers (5)

DomF
DomF

Reputation: 21

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.

Upvotes: 1

lars pehrsson
lars pehrsson

Reputation: 163

I know that is an old topic, but I guess it is still relevant for some people. I made this solution. It is attached to the KeyUp and the TextInput event on a WPF TreeView. I'm using TextInput in addition to KeyUp as I had difficulty translating "national" chars to real chars with KeyEventArgs. That went much more smooth with TextInput.

// <TreeView Name="treeView1" KeyUp="treeView1_KeyUp" TextInput="treeView1_TextInput"/>

    private bool searchdeep = true;             // Searches in subitems
    private bool searchstartfound = false;      // true when current selected item is found. Ensures that you don't seach backwards and that you only search on the current level (if not searchdeep is true)
    private string searchterm = "";             // what to search for
    private DateTime LastSearch = DateTime.Now; // resets searchterm if last input is older than 1 second.

    private void treeView1_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
    {  
        // reset searchterm if any "special" key is pressed
        if (e.Key < Key.A)
            searchterm = "";

    }

    private void treeView1_TextInput(object sender, TextCompositionEventArgs e)
    {
        if ((DateTime.Now - LastSearch).Seconds > 1)
            searchterm = "";

        LastSearch = DateTime.Now;
        searchterm += e.Text;
        searchstartfound = treeView1.SelectedItem == null;

        foreach (var t in treeView1.Items)
            if (SearchTreeView((TreeViewItem) t, searchterm.ToLower()))
                break;
    }

   private bool SearchTreeView(TreeViewItem node, string searchterm)
    {
        if (node.IsSelected)
            searchstartfound = true;

        // Search current level first
        foreach (TreeViewItem subnode in node.Items)
        {
            // Search subnodes to the current node first
            if (subnode.IsSelected)
            {
                searchstartfound = true;
                if (subnode.IsExpanded)
                    foreach (TreeViewItem subsubnode in subnode.Items)
                        if (searchstartfound && subsubnode.Header.ToString().ToLower().StartsWith(searchterm))
                        {
                            subsubnode.IsSelected = true;
                            subsubnode.IsExpanded = true;
                            subsubnode.BringIntoView();
                            return true;
                        }
            }
            // Then search nodes on the same level
            if (searchstartfound && subnode.Header.ToString().ToLower().StartsWith(searchterm))
            {
                subnode.IsSelected = true;
                subnode.BringIntoView();
                return true;
            }
        }

        // If not found, search subnodes
        foreach (TreeViewItem subnode in node.Items)
        {
            if (!searchstartfound || searchdeep)
                if (SearchTreeView(subnode, searchterm))
                {
                    node.IsExpanded = true;
                    return true;
                }
        }

        return false;
    }

Upvotes: 4

digitguy
digitguy

Reputation: 1058

It is not very straightforward as we expect it to be. But the best solution I have found is here: http://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode

Let me know if you need more details.

Upvotes: 0

Miri Pruzan
Miri Pruzan

Reputation: 11

I was also looking for keyboard navigation, amazing how not obvious the solution was for templated items.

Setting SelectedValuePath in ListView or TreeView gives this behavior. If the items are templated then setting the attached property: TextSearch.TextPath to the path of the property to search on will also do the trick.

Hope this helps, it definitely worked for me.

Upvotes: 1

Helge Klein
Helge Klein

Reputation: 9095

Funny, this does not seem to be a popular topic. Anyway, in the meantime I have developed a solution to the problem that satisfies me:

I attach a behavior to the TreeViewItems. In that behavior, I handle KeyUp events. In the KeyUp event handler, I search the visual tree top to bottom as it is displayed. If I find a first matching node (whose name starts with the letter on the key pressed) I select that node.

Upvotes: 5

Related Questions