Enabling tab on WinForms property grids
An in-depth look at how to extend the WinForms PropertyGrid control to enable tab-navigation between properties.

The WinForms PropertyGrid control is an incredibly useful control. However, one unfortunate limitation is that pressing TAB with the property grid selected will take you to the next control on the form, rather than taking you to the next item in the property grid. This post will detail the process of extending the PropertyGrid to work around this limitation.

This functionality isn't trivial to implement. Firstly, there is no simple way to use the base PropertyGrid API to find out which item is selected, or to find out what the next item is. Secondly, while the PropertyGrid control does expose KeyDown/KeyUp/KeyPressed events, they never actually get raised.

To get started, we'll open up a .NET decompiler (my current favorite is DotPeek) or the Microsoft Reference Source website and have a look at how the PropertyGrid class is implemented internally.

From browsing the code, we can see that the internal structure is represented as a tree of GridItem nodes. Great, now we know that we can find the root of the tree, and then perform a depth-first traversal to enumerate all of our items! From there, moving to the next or the previous item is as simple as finding the index of the selected GridItem, incrementing or decrementing the index, and then selecting the GridItem at that new index.

While there are no KeyPressed/KeyDown/KeyUp events raised, we can still capture key press details by overriding the ProcessKeyPreview hander and looking for WM_KEYDOWN messages.

Putting it all together, we get something like this:

/// <summary>
/// Extends System.Windows.Forms.PropertyGrid, allowing the TAB key to toggle through
/// properties, rather than moving focus to the next control.
/// </summary>
class TabEnabledPropertyGrid : PropertyGrid
{
    /// <summary>
    /// Initializes a new instance of the TabEnabledPropertyGrid class.
    /// </summary>
    public TabEnabledPropertyGrid() { }

    /// <summary>
    /// Selects the next grid item on this control.
    /// </summary>
    public void SelectNextGridItem()
    {
        GridItem selected = SelectedGridItem;
        GridItem root = GetRootItem(selected);
        List<GridItem> descendents = GetDescendents(root).ToList();
        int selectedIndex = descendents.IndexOf(selected);
        int nextIndex = selectedIndex == descendents.Count - 1 ? 0 : selectedIndex + 1;
        GridItem next = descendents[nextIndex];
        next.Select();
    }

    /// <summary>
    /// Returns the root GridItem.
    /// </summary>
    /// <param name="item">The GridItem to start the search at.</param>
    /// <returns>The root GridItem.</returns>
    private GridItem GetRootItem(GridItem item)
    {
        return item.GridItemType == GridItemType.Root ? item : GetRootItem(item.Parent);
    }

    /// <summary>
    /// Returns all descendents of the specified GridItem.
    /// </summary>
    /// <param name="item">The GridItem to return the children of.</param>
    /// <returns>All descendents of the specified GridItem.</returns>
    private IEnumerable<GridItem> GetDescendents(GridItem item)
    {
        if (item.GridItemType == GridItemType.ArrayValue)
        {
            yield break;
        }

        if (item.GridItemType == GridItemType.Property)
        {
            yield return item;
        }

        foreach (GridItem subItem in item.GridItems)
        {
            foreach (GridItem childItem in GetDescendents(subItem))
            {
                yield return childItem;
            }
        }
    }

    /// <summary>
    /// Previews a keyboard message.
    /// </summary>
    /// <param name="m">
    /// A Message, passed by reference, that represents the window message to process.</param>
    /// <returns>true if the message was processed by the control; otherwise, false.</returns>
    protected override bool ProcessKeyPreview(ref Message m)
    {
        if (m.Msg == 0x0100) // WM_KEYDOWN
        {
            if ((int)m.WParam == (int)Keys.Tab)
            {
                SelectNextGridItem();
                return true;
            }
        }

        return base.ProcessKeyPreview(ref m);
    }
}

Hopefully you find this useful!


Posted by Matthew King on 24 May 2016
Permission is granted to use all code snippets under CC BY-SA 3.0 (just like StackOverflow), or the MIT license - your choice!
If you enjoyed this post, and you want to show your appreciation, you can buy me a beverage on Ko-fi or Stripe