Friday, May 15, 2009

Adding Tab-key Support to the PropertyGrid Control

The PropertyGrid control is convenient for adding object editing support with minimal code.  However, for some reason it does not support property navigation with the tab key.  Instead, clicking tab key will progressively set focus to other controls in your form.  This post provides a simple subclassed PropertyGrid with support for tab-key navigation of properties.

image

Below is the source code to a subclassed PropertyGrid control called TabbedPropertyGrid.  There were two major issues that needed to be overcome.  The first is that the PropertyGrid does not raise any keyboard events, to workaround this the subclassed PropertyGrid must hijack keyboard events from the parent form.  The second issue is that there is no intuitive way to navigate GridItems.  However, this MSDN forum thread provided a few clues to solve this using the Griditems and Parent property.

public class TabbedPropertyGrid : PropertyGrid {
    public TabbedPropertyGrid() : base() { }
    public void SetParent(Form form) {
        // Catch null arguments
        if (form == null) {
            throw new ArgumentNullException("form");
        }

        // Set this property to intercept all events
        form.KeyPreview = true;

        // Listen for keydown event
        form.KeyDown += new KeyEventHandler(this.Form_KeyDown);
    }
    private void Form_KeyDown(object sender, KeyEventArgs e) {
        // Exit if cursor not in control
        if (!this.RectangleToScreen(this.ClientRectangle).Contains(Cursor.Position)) {
            return;
        }

        // Handle tab key
        if (e.KeyCode != Keys.Tab) { return; }
        e.Handled = true;
        e.SuppressKeyPress = true;

        // Get selected griditem
        GridItem gridItem = this.SelectedGridItem;
        if (gridItem == null) { return; }

        // Create a collection all visible child griditems in propertygrid
        GridItem root = gridItem;
        while (root.GridItemType != GridItemType.Root) {
            root = root.Parent;
        }
        List<GridItem> gridItems = new List<GridItem>();
        this.FindItems(root, gridItems);

        // Get position of selected griditem in collection
        int index = gridItems.IndexOf(gridItem);

        // Select next griditem in collection
        this.SelectedGridItem = gridItems[++index];
    }
    private void FindItems(GridItem item, List<GridItem> gridItems) {
        switch (item.GridItemType) {
            case GridItemType.Root:
            case GridItemType.Category:
                foreach (GridItem i in item.GridItems) {
                    this.FindItems(i, gridItems);
                }
                break;
            case GridItemType.Property:
                gridItems.Add(item);
                if (item.Expanded) {
                    foreach (GridItem i in item.GridItems) {
                        this.FindItems(i, gridItems);
                    }
                }
                break;
            case GridItemType.ArrayValue:
                break;
        }
    }
}

After adding the TabbedPropertyGrid to your form, the form must be parsed into the control using the SetParent method.

public partial class Form1 : Form {
    public Form1() {
        InitializeComponent();

        // Assign the form to the propertygrid
        this.tabbedPropertyGrid1.SetParent(this);
        this.tabbedPropertyGrid1.SelectedObject = this;
    }
}

Known Issues:  Shift-Tab does not select properties in reverse order.

Technorati Tags: ,,

8 comments:

  1. instead of checking whether cursor is inside (why??) think better to check ContainsFocus property

    ReplyDelete
  2. The line of code:
    this.SelectedGridItem = gridItems[++index];
    will overstep the bounds of the gridItems array if you tab past the last grid item.

    I changed it to:
    int nextIndex = index + 1;
    if (nextIndex >= gridItems.Count){return;}
    // Select next griditem in collection
    this.SelectedGridItem = gridItems[nextIndex];

    ReplyDelete
  3. Has anyone been able to implement the Shift-Tab key to navigate backwards?

    ReplyDelete
  4. This Property Grid control is able to display the properties of any object in a user friendly way and allows the end users of your applications edit the properties of the object.

    ReplyDelete
  5. I couldn't get Shift+Tab to work for reasons mentioned below. But I came up with a compromise. If someone else solves it I'll be really grateful.

    The other part of my story is that I also wanted the cursor to be in the value cell, not the property name cell. The original code puts focus on the property name, not the value.

    If you forget about wanting the cursor in the value cell, you can get Tab and Shift+Tab to step forwards and backwards between property names. with the following code:

    if ((Control.ModifierKeys & Keys.Shift) == 0)
    {
    int nextIndex = index + 1;
    if (nextIndex >= gridItems.Count) { return; }
    this.SelectedGridItem = gridItems[nextIndex];
    }
    else
    {
    int prevIndex = index - 1;
    if (prevIndex < 0) { return; }
    this.SelectedGridItem = gridItems[prevIndex];
    }

    The 'gotcha' is that you have to press Tab twice to move to another property (once for the property and once for the value). Shift+Tab is another whole world of hurt as after pressing Shift+Tab you have to press Tab again to enter the value cell before pressing another Shift+Tab. Otherwise the focus steps out of the PropertyGrid. You cannot press successive Shift+Tabs.

    Of course this wasn't what I was after.

    My solution to getting into the value cell, with one Tab, was to send another Tab using 'SendKey.Send("{Tab}");' straight after the next grid item had been set as in the following code for the 'Tab' key:

    if ((Control.ModifierKeys & Keys.Shift) != 0)
    {
    int nextIndex = index + 1;
    if (nextIndex >= gridItems.Count) { return; }
    this.SelectedGridItem = gridItems[nextIndex];
    SendKeys.Send("{Tab}");
    }

    This is highly counter-intuitive as you'd expect it to move to the next property name field as it would get captured by this code again. Anyway it worked.

    However this doesn't work for Shift+Tab handling.

    The 'Shift' key is still being held down when the additional Tab key is sent.

    If you attempted a single Shift+Tab without the extra SendKey Tab, and then pressed Tab you'd end up in the value cell for the property name you'd Shift+Tabbed(?) to. Have I lost anyone yet? Good.

    The best I could do was force the Tab to cycle through the property grid starting from the top again when the last property was reached. I've kept the 'Control.Modifiers & Keys.Shift' if ... because... I could.

    if ((Control.ModifierKeys & Keys.Shift) == 0)
    {
    int nextIndex = index + 1;
    if (nextIndex >= gridItems.Count)
    this.SelectedGridItem = gridItems[0]; // Valid Alternative : gridItems[0].Select();
    else
    this.SelectedGridItem = gridItems[nextIndex]; // Valid Alternative : gridItems[nextIndex].Select();

    SendKeys.Send("{Tab}");
    }

    ReplyDelete
  6. Thanks for the sharing this code. Is the code above (if ((Control.ModifierKeys & Keys.Shift) == 0)) supposed to be in the keydown event? If you could share where you placed this snippet for the extra tab press using SendKeys, that would be great. I realize this was published years ago, so I will be lucky to get an answer :)

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. Thanks for sharing the code, but can you provide step wise clarification of the code please.

    ReplyDelete