WinForms: Subclassing the TextBox inside a ComboBox

I learned something new today and decided to share it here.

 

Problem:

I have a form with a ComboBox that has a style of DropDown. This style allows the user to type anything into the ComboBox. To aid the user AutoComplete is turned on. But when the user used Ctrl+Z or Undo from the context menu. The value was not returned to its original value. The last autocompleted value was put back into the ComboBox. Generally, this meant that only the last character typed was removed. I wanted to control how Undo worked on the ComboBox.

 

Solution:

I tried subclassing the ComboBox and overriding WndProc in the ComboBox, but the EM_UNDO and EM_CANUNDO messages didn’t go there. So, I guessed that I had to subclass the TextBox inside the ComboBox. In windows, the ComboBox is a composite control made up of a TextBox, button, and a listbox.

 

Obstacle #1: WinForms doesn’t seem to expose the TextBox window in any public manner. For this reason, I resorted to calling a native method GetWindow to get the first child window of the combobox, which is the textbox.

 

Obstacle #2: GetWindow just returns a handle, what do I do with that? WinForms has a class called NativeWindow. It allows you to override the WndProc of any control given just the handle. So, I build a class called ComboTextBox that derived from NativeWindow and overrode WndProc for EM_UNDO and EM_CANUNDO.

 

Obstacle #3: Ctrl+Z worked great, just like I had planned. But the context menu for the ComboBox was not behaving. It was ignoring my ComboTextBox class or so I thought. I loaded Spy++ and took a look at the messages that the context menu was sending. I was surprised to find out that the context menu doesn’t send EM messages at all. It was sending WM_UNDO instead. So, I changed my WndProc to catch that message as well.

 

Here is the code that I ended up with (I think I got the clean up code correct, but I haven’t done much testing, yet):

 

class ComboBoxInternal : ComboBox

{

    private ComboTextBox m_textBox;

    private string m_undoValue;

    public ComboBoxInternal()

    {

    }

    public void Undo()

    {

        // code to undo to previous value

        this.Text = m_undoValue;

    }

    public bool CanUndo()

    {

        // code to check if Undo is allowed

   // for now return true

        return true;

    }

    protected override void OnGotFocus(EventArgs e)

    {

        base.OnGotFocus(e);

        m_undoValue = this.Text;

    }

    protected override void OnHandleCreated(EventArgs e)

    {

        base.OnHandleCreated(e);

        m_textBox = new ComboTextBox(this);

    }

    protected override void OnHandleDestroyed(EventArgs e)

    {

        m_textBox.Dispose();

        m_textBox = null;

        base.OnHandleDestroyed(e);

    }

    #region ComboTextBox class

    /// <summary>

    /// Internal class to perform subclassing on the textbox

    /// inside the Combo Box.

    /// </summary>

    private class ComboTextBox : NativeWindow, IDisposable

    {

        private IntPtr m_handle;

        private ComboBoxInternal m_owner;

        #region Unmanaged Code

        [DllImport("user32")]

        private static extern IntPtr GetWindow(IntPtr hWnd, int wCmd);

        private const int GW_CHILD = 5;

        private const int EM_CANUNDO = 0x00C6;

        private const int EM_UNDO = 0x00C7;

        private const int WM_UNDO = 0x0304;

        #endregion

        public ComboTextBox(ComboBoxInternal comboBox)

        {

            m_owner = comboBox;

            m_handle = GetWindow(comboBox.Handle, GW_CHILD);

            base.AssignHandle(m_handle);

        }

        protected override void WndProc(ref Message m)

        {

            switch (m.Msg)

            {

                case EM_CANUNDO:

                    m.Result = (IntPtr)(m_owner.CanUndo() ? 1 : 0);

                    break;

                case WM_UNDO:

                case EM_UNDO:

                    m_owner.Undo();

                    m.Result = (IntPtr)1;

                    break;

                default:

                    base.WndProc(ref m);

                    break;

            }

        }

        #region IDisposable Members

        public void Dispose()

        {

            base.ReleaseHandle();

            m_handle = IntPtr.Zero;

            m_owner = null;

        }

        #endregion

    }

    #endregion

}

There is one remaining question that I need to figure out. What message or mechanism is being used to determine if Undo should be enabled on the context menu. Spy doesn’t seem to show any message that I think could be it. And there is not a WM_CANUNDO. For now this remains a mystery. If I wanted to handle more complicated Undo scenarios like multiple levels of undo, this would be more important to me. If you know the answer, please share it ;)