Inner label in ComboBox

You’ve seen this kind of label on a dropdown menu before right? (the example is from Ebay.com)

A dropdown menu with an inner label from the homepage of Ebay.com

Yes, of course you did. And so did your customer. So you are not at all surprised when he says he wants that in his Windows Forms application. Easy enough, you should be done within a few minutes, yet you refrain from telling him that (but that’s matter for another post).

What he is asking for is actually a well known usability guideline.

However, the default Windows Forms ComboBox component does not support a DefaultText property or anything of the sort. If you are using data binding, the ComboBox will even show the first item in the underlying data source as the default. This is quite dangerous because the user may just skip the field and save this random first entry.

The common solutions

I’ve seen many solutions to this problem, having searched for one myself, but most of them are quite bad! Some advocate adding a row to the ComboBox.Items collection. Now, the ComboBox.Items solution would not be so bad as it affects only the presentation layer, but the Items property has no effect when the ComboBox is data bound, so that’s out.

However, if we are using data binding, the equivalent is to add a dummy “Please select a value” row to the DataTable data source as many forum posts suggest. It does work. But this is an extremely bad solution to the problem! To do that, we must create a row that meets all the constraints of the data source, then we add it to the DataTable and (if this data source is used for updating the data elsewhere in the application) we must call AcceptChanges() so the TableAdapter believes it’s already in the database and doesn’t insert it the next time you update the data source to the data store. Voila, we just hacked ADO.NET to contain this row, yet not synchronize it back to database.

But we’re not done yet, there’s more to change! Do we iterate over the years in the data source to calculate some statistics or whatnot in a completely different part of the application? Let’s not forget to skip the “Please select a year” year! We just messed up with our presentation/data layers distinction and we must now pay the price. So we will document this condition so no one uses the “Please select a year” row hoping everyone actually read the document. This is a recipee for disaster and a documentation nightmare!

An interesting suggestion I read is to swap a Label in front of the ComboBox’s text part when the ComboBox is empty (first answer to this forum post). It is a little more work, but totally doable and doesn’t require any subclassing. It may get messy in the designer having two overlapping Components, but overall, it is a perfectly valid solution.

The clean solution

The ComboBox doesn’t give me what I want, well I’m going to make it do it! Should be fairly easy right? Let’s just override OnPaint to write some text when nothing is selected. Well, this doesn’t work either! The OnPaint method of the ComboBox isn’t executed unless you add the UserPaint style. And doing that makes it look much less pretty…

But we’re not out of options yet! Let’s try overriding WndProc and the WM_PAINT message! PInvoke.net is pretty useful with these things so we can just head over there to get WM_PAINT’s value.

Here’s the code.

public partial class ComboBoxEx : ComboBox {

    public ComboBoxEx() {
        InitializeComponent();
    }

    [Description("The default value shown when there is no selection.")]
    [Category("Data")]
    [Localizable(true)]
    public String DefaultText {
        get;
        set;
    }

    protected override void WndProc(ref Message m) {
        base.WndProc(ref m);

        const int WM_PAINT = 0xF;

        if (m.Msg == WM_PAINT &&
                this.DropDownStyle == ComboBoxStyle.DropDownList &&
                this.SelectedIndex < 0) {

            DrawDefaultText();
        }
    }

    private void DrawDefaultText() {
        using (Graphics graph = this.CreateGraphics()) {
            StringFormat format = new StringFormat()
            {
                    Alignment = StringAlignment.Center,
                    LineAlignment = StringAlignment.Center
            };

            graph.DrawString(this.DefaultText, this.Font,
                    new SolidBrush(this.ForeColor),
                    new RectangleF(0, 0, this.Width, this.Height), format);
        }
    }
}

And the results.

Final ComboBox

Obviously, we won’t get this unless nothing is selected and data binding makes the ComboBox automatically select the first entry so we will need to clear the selection using comboBoxEx1.SelectedItem = null; from our client code. Additionally, this doesn’t add an entry in the items list of the ComboBox so the user sees the hint, but he cannot select it as a value, avoiding any misunderstanding.

Conclusion

I’ve recently read a book called Designing Interfaces: Patterns for Effective Interaction Design by Jennifer Tidwell. It’s an excellent book which describes a list of UI design guidelines or “patterns” (this word is so overused, it’s starting to sound like a marketing scheme). One of the guidelines she mentions is to provide good defaults. This kind of guideline is so very obvious yet so often overlooked in the software development industry, it’s no wonder the customer ends up asking for so many changes!

So just remember: if you are about to use this to add a hint to your dropdown menu when no value is selected, maybe you can just have a default value…

6 Responses to Inner label in ComboBox

  1. Hi

    I created the class: ComboboxEX.cs
    and when building the project, i get the error:

    InitializeComponent()does not exist in the current context.

    thanks

  2. Components work using both a ComboBoxEx.cs and ComboBoxEx.designer.cs files.

    Just create an empty custom control using the builtin VS template. It will generate a ComboBoxEx.designer.cs file with the InitializeComponent() method and a dummy ComboBoxEx.cs file.

    Replace the class in the ComboBoxEx.cs file with the one in this post and it should find the InitializeComponent() method without any problem (make sure you name the component exactly the same as this class, including case).

  3. Excellent approach. I had been fudging with this issue and I must admit that it has simplified my code tremendously. Thank you.

  4. Hi! i’m just a begginer (i’ve done programming course but don’t have any experience). i’m writting an application and this is exactly what i need, but i’m not sure how am i suppose to use this code. i’ve got a combobox of my own with data binded to it, could you please explain step by step what to do? hope i’m not asking for too much! thanx

  5. Hi – I don’t see how to use the control? Do I need to change the Combobox defintion in the Designer? Yes, I’m real ‘green’, please help. Thank you

  6. Thanks for this. Do you not see flicking though when you move the mouse over the control? It seems to be calling the UpdateDefaultText lots of times on mouse over. I’ve tried drawing to an offscreen image, but this doesn’t help. I’ve also tried adding BeginPaint/EndPaint calls around it, but that’s not helped. I also tried calling GetUpdateRect to determine whether to update or not, but that already returns false when when the text should be drawn. I’d be interested if I’m the only person experiencing this, as that then means I’ve done something stupid! Thanks again.

Leave a Reply