Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does my custom DataGridViewEditingControl never receive KeyDown events for the Enter Key?

I have a custom DataGridView editing control which uses the Enter key for some of its functionality. It implements the IDataGridViewEditingControl interface method '`EditingControlWantsInputKey' using the following code:

public bool EditingControlWantsInputKey(Keys keyData, bool dataGridViewWantsInputKey)
{
    switch (keyData & Keys.KeyCode)
    {
        case Keys.Left:
        case Keys.Right:
        case Keys.Up:
        case Keys.Down:
        case Keys.Home:
        case Keys.End:
        case Keys.Enter:
        case Keys.Delete:
            return true;

        default:
            return !dataGridViewWantsInputKey;
    }
}

However, it never receives the KeyDown event for the Enter key. I've put a conditional breakpoint in the EditingControlWantsInputKey method to see if the data grid view was ever calling it to find out if I want to respond to the Enter key only to find out it is never called.

In my editing control, I overrode the ProcessCmdKey method to see if the underlying Message was being sent to the control with the following code.

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if ((msg.Msg == User32.WM_KEYDOWN) &&
        ((Keys)msg.WParam == Keys.Enter))
    {
        Console.WriteLine("GOT HERE");
    }

    return base.ProcessCmdKey(ref msg, keyData);
}

It is in-fact getting the message, but it is never sent to the OnKeyDown method.

At one point I thought the DataGridView may have been registering a IMessageFilter to handle the key itself, but if that were the case, the control would get the call to ProcessCmdKey (I checked this myself by adding my own IMessageFilter).

Does anyone know what the DataGridView is doing to prevent my custom editing control from getting the call to OnKeyDown and if there is a way to change that behavior?

The only thing I can think of is to handle the routing of the message myself from the ProcessCmdKey method, but that just feel hackish.

[Edit]

To answer King King's comment:

The editing control is a custom subclass of a TextBox. The custom subclass works simply by adding more advanced auto-complete functionality (better than the built in support for a TextBox). The custom text box uses the KeyDown event to know when the user wants to select a suggested auto-complete item. It is used several other places in the application has been used in production code for months (so I'm pretty confident that it is not the culprit).

[Edit - with sample code]

I've constructed a minimal program which seems to show this happening. Create a new WinForms project and put a DataGridView in the Form. Sorry for the wall of code, but this is bare minimal needed to see the effect.

If ran, you'll notice that EditingControlWantsInputKey will never get called for the CustomEditingControl when the key code is Enter, OnKeyDown won't be called, but ProcessCmdKey will be called.

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

        dataGridView.EditingControlShowing += this.DataGridView_EditingControlShowing;
    }

    private void DataGridView_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
    {
        // Must remove first to avoid adding the same event handler twice.
        e.Control.KeyDown -= this.EditingControl_KeyDown;
        e.Control.KeyDown += this.EditingControl_KeyDown;
    }

    private void EditingControl_KeyDown(object sender, KeyEventArgs e)
    {
        Console.WriteLine(e.KeyData);
    }
}

public class CustomEditingControl : DataGridViewTextBoxEditingControl
{
    public override bool EditingControlWantsInputKey(Keys keyData, bool dataGridViewWantsInputKey)
    {
        if ((Keys.KeyCode & keyData) == Keys.Enter)
        {
            Console.WriteLine("EditingControlWantsInputKey: Enter");
        }
        else
        {
            Console.WriteLine("EditingControlWantsInputKey: Other");
        }

        return base.EditingControlWantsInputKey(keyData, dataGridViewWantsInputKey);
    }

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        const int WM_KEYDOWN = 0x0100;

        if ((msg.Msg == WM_KEYDOWN) &&
            ((Keys)msg.WParam == Keys.Enter))
        {
            Console.WriteLine("ProcessCmdKey: Enter");
        }

        return base.ProcessCmdKey(ref msg, keyData);
    }

    protected override void OnKeyDown(KeyEventArgs e)
    {
        if (e.KeyCode == Keys.Enter)
        {
            Console.WriteLine("OnKeyDown: Enter");
        }
        else
        {
            Console.WriteLine("OnKeyDown: Other");
        }

        base.OnKeyDown(e);
    }
}

public class CustomDataGridViewTextBoxCell : DataGridViewTextBoxCell
{
    public override Type EditType
    {
        get
        {
            return typeof(CustomEditingControl);
        }
    }
}

public class CustomDataGridViewColumn : DataGridViewTextBoxColumn
{
    public CustomDataGridViewColumn()
    {
        this.CellTemplate = new CustomDataGridViewTextBoxCell();
    }
}

[Edit - with more findings]

I added as much logging as I could to the message chain. For a normal text box, pressing the Enter key results in a message trail like the following:

PreFilterMessage: Return
PreProcessMessage: Return
ProcessCmdKey: Return
WndProc: Return
ProcessKeyPreview: Return
OnKeyDown: Return

For the editing control pressing the '1' key (for example) yields the following trail:

PreFilterMessage: D1
PreProcessMessage: D1
ProcessCmdKey: D1
WndProc: D1
EditingControlWantsInputKey: D1
ProcessKeyPreview: D1
OnKeyDown: D1
EditingControl_KeyDown: D1 (This is from the hooked up event handler)

For the editing control pressing the 'Enter' key yields the following trail

PreFilterMessage: Return
PreProcessMessage: Return
ProcessCmdKey: Return

So something (presumably the DataGridView) is capturing the message between the ProcessCmdKey and the WndProc methods.

like image 439
Anthony Avatar asked Jan 22 '26 20:01

Anthony


1 Answers

After much digging, I've found a solution that seems the least bit hackish.

Create a new IMessageFilter, I ended up with something of the following:

private sealed class RouteKeyDownMessageFilter : IMessageFilter
{
    private readonly Control mControl;
    private readonly Keys mKey;

    public RouteKeyDownMessageFilter(Control control, Keys key)
    {
        this.mControl = control;
        this.mKey = (Keys.KeyCode & key);
    }

    public bool PreFilterMessage(ref Message m)
    {
        if ((m.Msg == WM_KEYDOWN) &&
            (m.HWnd == this.mControl.Handle) &&
            (((Keys)m.WParam & Keys.KeyCode) == this.mKey))
        {
            SendMessage(m.HWnd, m.Msg, m.WParam, m.LParam);
        }

        return false;
    }

    public const int WM_KEYDOWN = 0x0100;

    [DllImport("user32.dll")]
    public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
}

Then, in the class implementing the IDataGridViewEditingControl interface, I added this additional logic to the EditingControlDataGridView property to register/unregister the message filter.

public DataGridView EditingControlDataGridView
{
    get { return this.mEditingControlDataGridView; }
    set
    {
        if (this.mEditingControlDataGridView != null)
        {
            this.mEditingControlDataGridView.Disposed -= this.EditingControlDataGridView_Disposed;

            Application.RemoveMessageFilter(this.mCurrentMessageFilter);
            this.mCurrentMessageFilter = null;
        }

        this.mEditingControlDataGridView = value;

        if (this.mEditingControlDataGridView != null)
        {
            this.mCurrentMessageFilter = new RouteKeyDownMessageFilter(this, Keys.Return);
            Application.AddMessageFilter(this.mCurrentMessageFilter);

            this.mEditingControlDataGridView.Disposed += this.EditingControlDataGridView_Disposed;
        }
    }
}

private void EditingControlDataGridView_Disposed(object sender, EventArgs e)
{
    if (this.mCurrentMessageFilter != null)
    {
        Application.RemoveMessageFilter(this.mCurrentMessageFilter);
    }
}

So all this does it ensure the message filter is in place whenever the editing control has a data grid view assigned to it. It also registers an event handler for the data grid view's Dispose event to make sure we unregister the message filter when the data grid view gets cleaned up.

The message filter itself just takes a control and a trigger key and re-routes the message when it gets the message for the given control. It is important to reference the Control and not the Handle of the Control because the Handle may change in the Control's lifetime.

I'm a little baffled that re-sending the message does the trick, but when this in place (and the appropriate logging) I get the following message chaining:

RoutingMessageFilter.PreFilterMessage: Return
RoutingMessageFilter, Routing Message.
WndProc: Return
EditingControlWantsInputKey: Return
PreProcessMessage: Return
ProcessCmdKey: Return
WndProc: Return
OnKeyDown: Return
EditingControl_KeyDown: Return

The only odd thing about this message flow is that WndProc is called twice. I'm not sure why this happened, but it doesn't seem to have any ill side-effects. More importantly, the EditingControlWantsInputKey now properly gets called along with the OnKeyDown method in the editing control.

like image 200
Anthony Avatar answered Jan 24 '26 16:01

Anthony



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!