9

We have a DataGridView with data in a form. To enable quick search, we added TextBox to DataGridView.Controls and highlight cells which contain text from TextBox.

However, there is an issue. DataGridView consumes the Left arrow , Right arrow , Home and End (with or without Shift) keys even if the cursor is in TextBox, and the user cannot change the caret position or select text from the keyboard.

TextBox generates a PreviewKeyDown event and nothing more happens.

Simplified code:

public partial class TestForm : Form
{
    public TestForm()
    {
        InitializeComponent();
        Width = 400;
        Height = 400;

        var txt = new TextBox { Dock = DockStyle.Bottom, BackColor = Color.Khaki };
        var dgv = new DataGridView
        {
            Dock = DockStyle.Fill,
            ColumnCount = 3,
            RowCount = 5
        };
        dgv.Controls.Add(txt);
        Controls.Add(dgv);

        dgv.PreviewKeyDown += DgvOnPreviewKeyDown;
        dgv.KeyDown += DgvOnKeyDown;

        txt.PreviewKeyDown += TxtOnPreviewKeyDown;
        txt.KeyDown += TxtOnKeyDown;
    }

    private void DgvOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
    {
        Debug.WriteLine(String.Format("Dgv Key Preview {0}", e.KeyCode));
        e.IsInputKey = true;
    }

    private void DgvOnKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine(String.Format("Dgv Key {0}", e.KeyCode));
    }

    private void TxtOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
    {
        Debug.WriteLine(String.Format("Txt Key Preview {0}", e.KeyCode));
    }

    private void TxtOnKeyDown(object sender, KeyEventArgs e)
    {
        Debug.WriteLine(String.Format("Txt Key {0}", e.KeyCode));
    }
}

Type 123 in TextBox and then try the Right arrow, Left arrow, End, or Home. DataGridView change the selected cell, but the TextBox caret doesn't move.

TextBox works just fine if not inside a DataGridView (no problem at all when using the same method adding it into TreeView for example). TextBox acts similar to the Quick search Panel in the browser and has to be on top of the DataGridView. Adding a TextBox to a Form (or to be more specific, to a DataGridView parent) creates its own set of issues (tracking Location, Size, Visibility, ...) and is not acceptable.

What can be done to make sure that TextBox receive those keys and change the caret position or select text?

2
  • If the textbox has focus and the user hits the Right key, is that supposed to move the caret position AND move the selected column in the DGV? Commented Mar 28, 2017 at 15:55
  • @David, primary concern is correct behavior of TextBox. DGV doesn't have to handle nav keys when TextBox has focus (in fact, DGV shouldn't do anything in that case) Commented Mar 28, 2017 at 17:29

5 Answers 5

5
+100

TextBox works just fine if not inside DataGridView (no problem at all when using the same method adding it into TreeView for example)

Apparently the problem is in DataGridView. It's because DataGridView overrides the Control.ProcessKeyPreview method:

This method is called by a child control when the child control receives a keyboard message. The child control calls this method before generating any keyboard events for the message. If this method returns true, the child control considers the message processed and does not generate any keyboard events.

The DataGridView implementation does just that - it maintains zero or one child controls internally (EditingControl), and when there is no such control active, it handles many keys (navigation, tab, enter, escape, etc.) by returning true, thus preventing the child TextBox keyboard events generation. The return value is controlled by the ProcessDataGridViewKey method.

Since the method is virtual, you can replace the DataGridView with a custom derived class which overrides the aforementioned method and prevents the undesired behavior when neither the view nor the view active editor (if any) has the keyboard focus.

Something like this:

public class CustomDataGridView : DataGridView
{
    bool SuppressDataGridViewKeyProcessing => ContainsFocus && !Focused &&
        (EditingControl == null || !EditingControl.ContainsFocus);

    protected override bool ProcessDataGridViewKey(KeyEventArgs e)
    {
        if (SuppressDataGridViewKeyProcessing) return false;
        return base.ProcessDataGridViewKey(e);
    }
}

The above is just the half of the story and solves the cursor navigation and selection keys issue. However DataGridView intercepts another key message preprocessing infrastructure method - Control.ProcessDialogKey and handles Tab, Esc, Return, etc. keys there. So in order to prevent that, the method has to be overridden as well and redirected to the parent of the data grid view. The later needs a little reflection trickery to call a protected method, but using one time compiled delegate at least avoids the performance hit.

With that addition, the final custom class would be like this:

public class CustomDataGridView : DataGridView
{
    bool SuppressDataGridViewKeyProcessing => ContainsFocus && !Focused &&
        (EditingControl == null || !EditingControl.ContainsFocus);

    protected override bool ProcessDataGridViewKey(KeyEventArgs e)
    {
        if (SuppressDataGridViewKeyProcessing) return false;
        return base.ProcessDataGridViewKey(e);
    }

    protected override bool ProcessDialogKey(Keys keyData)
    {
        if (SuppressDataGridViewKeyProcessing)
        {
            if (Parent != null) return DefaultProcessDialogKey(Parent, keyData);
            return false;
        }
        return base.ProcessDialogKey(keyData);
    }

    static readonly Func<Control, Keys, bool> DefaultProcessDialogKey =
        (Func<Control, Keys, bool>)Delegate.CreateDelegate(typeof(Func<Control, Keys, bool>),
        typeof(Control).GetMethod(nameof(ProcessDialogKey), BindingFlags.NonPublic | BindingFlags.Instance));
}
Sign up to request clarification or add additional context in comments.

3 Comments

thank you for investigation and solution. fixing ProcessDataGridViewKey is good enough. Handling Tab triggers Tab navigation to next control which is not desirable. Very unfortunate, that this solution requires replacing every DGV in every view. I wish I could split a bounty between your answer and vito's
You are welcome :) I guess the Tab handling is arbitrary, but w/o the ProcessDialogKey hook the grid view would perform go to next / prev cell and eventually tab out from the grid view when it is on the first/last cell. So IMO it still has to be intercepted, but eventually handled differently. Replacing each DGV is definitely unfortunate, but from the other side it allows you to play and adjust the desired behaviors in a single place, which should worth the efforts.
I think, this is a simple and best solution. But it is great, that one can choose from several solutions, which will be more appropriate.
3

You can try this.

I created my own textbox and overrode method ProcessKeyMessage.

public class MyTextBox : TextBox
{
    private const int WM_KEYDOWN = 0x0100;
    private const int WM_SYSKEYDOWN = 0x0104;

    protected override bool ProcessKeyMessage(ref Message m)
    {
        if (m.Msg != WM_SYSKEYDOWN && m.Msg != WM_KEYDOWN)
        {
            return base.ProcessKeyMessage(ref m);
        }

        Keys keyData = (Keys)((int)m.WParam);
        switch (keyData)
        {
            case Keys.Left:
            case Keys.Right:
            case Keys.Home:
            case Keys.End:
            case Keys.ShiftKey:
                return base.ProcessKeyEventArgs(ref m);
            default:
                return base.ProcessKeyMessage(ref m);
        }
    }
}

And then you can call:

var txt = new MyTextBox { Dock = DockStyle.Bottom, BackColor = Color.Khaki };

4 Comments

ok, it seems to work. and replacing TextBox with its derived class is a local change in our QuickSearch feature, hidden by incapsulation, no need in external changes. but it is unclear to me - why it works? docs for ProcessKeyMessage says "a control should return true to indicate that it has processed the key". but here MyTextBox returns false (not processed) and then text selection works somehow?! does it trigger TextBox processing?
I looked closer to how DataGridView and TextBox handle the keys. When the datagridview is empty (without columns), then the textbox has the expected behavior. But when DGW has columns and some rows, then the textbox does not respond to some keys like home, end, left... I thing, it is because the DGW row has focus.
After pressing the key, the DGW is first to process the key and the textbox is the second. When I write some text into textbox, the DGW does not process keys. The return value of 'ProcessKeyPreview' of DGW is false. But when I press key 'home' or 'left arrow', the DGW process the key through 'ProcessDataGridViewKey' and return true.
But you are right. I have an error in my code. I should not simple return value false. After reading the article Notes to Inheritors, I have changed code to return value from call 'ProcessKeyEventArgs'.
0

Try to just add the TextBox to the main form instead of the DataGridView:

Controls.Add(txt);
Controls.Add(dgv);

txt.PreviewKeyDown += DgvOnPreviewKeyDown;
txt.KeyDown += DgvOnKeyDown;

txt.PreviewKeyDown += TxtOnPreviewKeyDown;
txt.KeyDown += TxtOnKeyDown;

1 Comment

thank you for your time. TextBox works just fine if not inside DataGridView (no problem at all when adding it into TreeView for example). TextBox acts similar to Quick search Panel in browser and has to be on top of DGV. Adding TextBox to Form (or to be more specific, to DGV parent) creates its own set of issues (tracking Location, Size, Visibility, ...)
0

It sounds a bit like an exercise in futility.

It may be easier to encapsulate the behavior of both the TextBox and DataGridView controls by placing them into a UserControl together with a little code to handle events.

1 Comment

This way you will lose all the design-time features of DataGridView.
0

Here is a partial solution to the issue. TextBox still doesn't receive navigation keys input natively, but I reproduced a normal caret and selection behavior.

PreviewKeyDownEventArgs contains information about the pressed key and modifiers (Shift). For each key combination I set a new SelectionStart and SelectionLength for the TextBox.

private void TxtOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
    TextBox txt = (TextBox)sender;
    if (e.KeyCode == Keys.Home)
    {
        int idx = txt.SelectionStart;

        txt.SelectionStart = 0;
        txt.SelectionLength = e.Shift ? idx : 0;
    }
    else if (e.KeyCode == Keys.End)
    {
        int idx = txt.SelectionStart;

        if (e.Shift)
            txt.SelectionLength = txt.TextLength - idx;
        else
        {
            txt.SelectionStart = txt.TextLength;
            txt.SelectionLength = 0;
        }
    }
    else if (e.KeyCode == Keys.Left)
    {
        if (e.Shift)
        {
            if (txt.SelectionStart > 0)
            {
                txt.SelectionStart--;
                txt.SelectionLength++;
            }
        }
        else
        {
            txt.SelectionStart = Math.Max(0, txt.SelectionStart - 1);
            txt.SelectionLength = 0;
        }
    }
    else if (e.KeyCode == Keys.Right)
    {
        if (e.Shift)
            txt.SelectionLength++;
        else
        {
            txt.SelectionStart = Math.Min(txt.TextLength, txt.SelectionStart + 1);
            txt.SelectionLength = 0;
        }
    }
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.