Joe White's Blog Life, .NET, and cats

Aborting combobox dropdown, showing dialog box #.NET

So I'm trying to build a UI where the user fills in three edit boxes — server name, login, and password — and then drops down a combo box to select which "database" they want to use. When the user drops down the combo, the app connects to the server and queries the list of databases. If it can't connect to the server (bad password, server doesn't exist, etc.), the app should show an error message, and the combo box shouldn't open. Nice, compact, convenient — not totally elegant (a wizard might be better, since order matters), but you don't really want multiple steps on a login screen. And this should be easy to code. (Right...)

The WinForms ComboBox class has a DropDown event that makes a great place to query the server and populate the dropdown list. And that works just fine, no problems at all — as long as there are no errors. It's the "if there's an error, the combo box shouldn't open" bit that gave me headaches. Just how do you tell the combo box that you don't actually want it to drop down?

I was hoping the DropDown event would take a CancelEventArgs, but nope — and not surprising, since after a little digging, I found that CB_SHOWDROPDOWN doesn't give you any way to cancel the dropdown either. There's a Windows message you can send the combo box to close the dropdown, but it doesn't work from inside the DropDown event (no effect; the combo still opens as normal). Throwing in an Application.DoEvents didn't help. Just going ahead and showing the dialog didn't prevent the combo from dropping down — the dialog pops up, and then as soon as you close it, then the combo box opens!

Okay, try the brutal approach: force the combo box to re-create its window handle. That should darned well keep it from dropping down! RecreateHandle is protected, so I can't call it directly unless I descend a new class from ComboBox (messy if I'm using the form designer), so I opened Reflector and looked for methods that call RecreateHandle. I settled for the property setter for IntegralHeight.

Works great, as long as I don't show the dialog box. If I both recreate the handle and show a dialog (doesn't matter which order I do those in), then the combo box gets into a weird state where it thinks the left mouse button is still down, so after the dialog shows, you have to click the combo twice to open it again (once to cancel the mouse-down state, again to actually drop it down). It even shows the dropdown button as pressed when you mouse over it, until you click to cancel the phantom mouse-down state. Again, Application.DoEvents doesn't help. Nor do various combinations of trying to change the mouse capture, or recreate the handle before and/or after showing the dialog.

I finally got it to work, by doing a PostMessage (well, BeginInvoke, but that's implemented as a PostMessage), so that I was showing the dialog after the DropDown event returned. And once it actually does work (and once I work around a C# compiler bug with delegates and overloads), it actually works quite nicely. Here's the code:

private delegate void ShowMessageDelegate(IWin32Window owner, string message);
...
private void ComboBox1_DropDown(object sender, System.EventArgs e)
{
    try
    {
        // do something that might throw an exception
        ArrayList arrayList = GetList();
        cbxBudgetYear.Items.Clear();
        cbxBudgetYear.Items.AddRange(arrayList.ToArray());
    }
    catch (InvalidOperationException ex)
    {
        // Force the combo box to re-create its window handle. This seems
        // to be the only way to prevent it from dropping down. Toggle the
        // property twice, which leaves it unchanged but calls RecreateHandle.
        cbxBudgetYear.IntegralHeight = !cbxBudgetYear.IntegralHeight;
        cbxBudgetYear.IntegralHeight = !cbxBudgetYear.IntegralHeight;

        // Don't display the error dialog immediately -- wait until the
        // combo box is done sending CB_SHOWDROPDOWN and is in a stable state.
        // Should be able to point the delegate directly to MessageBox.Show,
        // but the compiler picks the wrong overload and complains.
        BeginInvoke(new ShowMessageDelegate(MessageBoxShow),
            new object[] { ParentForm, ex.Message });
    }
}
private void MessageBoxShow(IWin32Window owner, string message)
{
    MessageBox.Show(owner, message);
}