Click here to Skip to main content
15,879,535 members
Articles / Desktop Programming / Windows Forms
Article

A data-bound multi-column combobox

Rate me:
Please Sign up or sign in to vote.
4.90/5 (76 votes)
27 Jul 2007BSD4 min read 342K   12.1K   151   76
An ownerdrawn multi-column combobox class with support for data-binding

Image 1

Image 2

Image 3

Introduction

I had to write a multi-column combobox at work that supported generic data binding, and I thought it might prove useful for others too.

C#
MultiColumnComboBox
is a ComboBox derived class written entirely in C# and you can bind it to any data source that has multiple columns (though it doesn't matter if it only has a single column either). It also works in unbound mode though it doesn't make much sense to use it if you are not using data binding.

Class usage

Using the class is fairly straightforward. Once you have your data source you just need to set the DataSource property of the

C#
MultiColumnComboBox
class. The control does not support the
C#
ComboBoxStyle.Simple
style and in addition it will insist on
C#
DrawMode
being set to OwnerDrawVariable. Exceptions are thrown so that you won't inadvertently attempt to break either of those limitations which is pretty handy because the Visual Studio property grid will not then let you change those values.

Image 4

The class has been tested on Windows XP SP2 as well as on Windows Vista (Ultimate Edition) both from a Visual Studio design perspective as well as from a runtime perspective. Here are some examples of populating the control with various types of data sources. In this first example, we populate it using a DataTable.

C#
// Populate using a DataTable

DataTable dataTable = new DataTable("Employees");

dataTable.Columns.Add("Employee ID", typeof(String));
dataTable.Columns.Add("Name", typeof(String));
dataTable.Columns.Add("Designation", typeof(String));

dataTable.Rows.Add(new String[] { "D1", "Natalia", "Developer" });
dataTable.Rows.Add(new String[] { "D2", "Jonathan", "Developer" });
dataTable.Rows.Add(new String[] { "D3", "Jake", "Developer" });
dataTable.Rows.Add(new String[] { "D4", "Abraham", "Developer" });
dataTable.Rows.Add(new String[] { "T1", "Mary", "Team Lead" });
dataTable.Rows.Add(new String[] { "PM1", "Calvin", "Project Manager" });
dataTable.Rows.Add(new String[] { "T2", "Sarah", "Team Lead" });
dataTable.Rows.Add(new String[] { "D12", "Monica", "Developer" });
dataTable.Rows.Add(new String[] { "D13", "Donna", "Developer" });

multiColumnComboBox1.DataSource = dataTable;
multiColumnComboBox1.DisplayMember = "Employee ID";
multiColumnComboBox1.ValueMember = "Name";

The DisplayMember property will dictate the value that's visible in the edit box part of the combobox. And the ValueMember property will dictate which of the columns will show up in bold. If you look at the screenshots, you can clearly see this in action. In the next example, we use an array of a custom type.

C#
public class Student
{
    public Student(String name, int age)
    {
        this.name = name;
        this.age = age;
    }

    String name;

    public String Name
    {
        get { return name; }
    }

    int age;

    public int Age
    {
        get { return age; }
    }
}

// Populate using a collection

Student[] studentArray = new Student[] 
{ new Student("Andrew White", 10), new Student("Thomas Smith", 10), 
  new Student("Alice Brown", 11), new Student("Lana Jones", 10), 
  new Student("Jason Smith", 9), new Student("Amamda Williams", 11)
};

multiColumnComboBox2.DataSource = studentArray;
multiColumnComboBox2.DisplayMember = multiColumnComboBox2.ValueMember = "Name";

Notice how we've set both DisplayMember and ValueMember to the same column field - this is perfectly okay to do. By the way if you don't set the ValueMember it will use the first column by default. You must set the DisplayMember though, else you'll see some odd strings depending on how a specific type's ToString is implemented. I decided not to provide a default as it would most likely result in non-ideal columns getting displayed. I've used a drop-down list style combobox for my 3rd example and also used a List<> object - though by now it must be pretty obvious to anyone reading this that you can basically use any standard data source.

C#
// Drop-down list (non-editable)

List<Student> studentList = new List<Student>(studentArray);

The main difference in using a drop-down list will be that you'll see the multiple columns even when the combobox is not dropped down. Note that those who want to prevent this behavior can check if DrawItemEventArgs.State has the ComboBoxEdit flag (in the OnDrawItem method) and change the behavior accordingly. For our purposes this behavior was pretty good and I personally thought it to be the more intuitive way to do it. And finally, you can use it without data-binding, though I can't think of any reason why you'd want to do that.

C#
// Trying to use as a regular combobox

multiColumnComboBox4.Items.Add("Cat");
multiColumnComboBox4.Items.Add("Tiger");
multiColumnComboBox4.Items.Add("Lion");
multiColumnComboBox4.Items.Add("Cheetah");
multiColumnComboBox4.SelectedIndex = 0;

Implementation details

One of the first things I did was to hide both the DrawMode and the DropDownStyle properties to prevent users from inadvertently setting unsupported values.

C#
public new DrawMode DrawMode 
{ 
    get
    {
        return base.DrawMode;
    } 
    set
    {
        if (value != DrawMode.OwnerDrawVariable)
        {
            throw new NotSupportedException("Needs to be DrawMode.OwnerDrawVariable");
        }
        base.DrawMode = value;
    }
}

public new ComboBoxStyle DropDownStyle
{ 
    get
    {
        return base.DropDownStyle;
    } 
    set
    {
        if (value == ComboBoxStyle.Simple)
        {
            throw new NotSupportedException("ComboBoxStyle.Simple not supported");
        }
        base.DropDownStyle = value;
    } 
}

I overrode OnDataSourceChanged so that the column names could be initialized.

C#
protected override void OnDataSourceChanged(EventArgs e)
{
    base.OnDataSourceChanged(e);

    InitializeColumns();
}

private void InitializeColumns()
{
    PropertyDescriptorCollection propertyDescriptorCollection = 
        DataManager.GetItemProperties();

    columnWidths = new float[propertyDescriptorCollection.Count];
    columnNames = new String[propertyDescriptorCollection.Count];

    for (int colIndex = 0; colIndex < propertyDescriptorCollection.Count; colIndex++)
    {
        String name = propertyDescriptorCollection[colIndex].Name;
        columnNames[colIndex] = name;
    }
}

I use the DataManager property which returns the

C#
CurrencyManager
objects that managed the bound objects for the control. Initially I've also set a widths array to 0 (later the required widths will be calculated). I also override the
C#
OnValueMemberChanged
method so that I could correctly set the value member column internally which I use in the drawing code to make the value column drawn in bold text.

C#
protected override void OnValueMemberChanged(EventArgs e)
{
    base.OnValueMemberChanged(e);

    InitializeValueMemberColumn();
}

private void InitializeValueMemberColumn()
{
    int colIndex = 0;
    foreach (String columnName in columnNames)
    {
        if (String.Compare(columnName, ValueMember, true, 
            CultureInfo.CurrentUICulture) == 0)
        {
            valueMemberColumnIndex = colIndex;
            break;
        }
        colIndex++;
    }
}

OnMeasureItem will be called once for every row in the combobox and that's where I do my width calculations.

C#
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
    base.OnMeasureItem(e);

    if (DesignMode)
        return;

    for (int colIndex = 0; colIndex < columnNames.Length; colIndex++)
    {
        string item = Convert.ToString(
            FilterItemOnProperty(Items[e.Index], columnNames[colIndex]));
        SizeF sizeF = e.Graphics.MeasureString(item, Font);
        columnWidths[colIndex] = Math.Max(columnWidths[colIndex], sizeF.Width);
    }

    float totWidth = CalculateTotalWidth();

    e.ItemWidth = (int)totWidth;
}

The interesting trick here is to use FilterItemOnProperty to get the text associated with a specific column. The width calculation is elementary and I calculate the total width using a CalculateTotalWidth method which merely adds all the individual column widths. I also add width for the vertical scrollbar (in case one shows up). We must also remember to override OnDropDown to set the drop down width appropriately (remember this is different from the width of the combobox itself).

C#
protected override void OnDropDown(EventArgs e)
{
    base.OnDropDown(e);
    this.DropDownWidth = (int)CalculateTotalWidth();
}

Now we come to the meat of the class -the OnDrawItem override.

C#
protected override void OnDrawItem(DrawItemEventArgs e)
{
    base.OnDrawItem(e);

    if (DesignMode)
        return;

    e.DrawBackground();

    Rectangle boundsRect = e.Bounds;
    int lastRight = 0;

    using (Pen linePen = new Pen(SystemColors.GrayText))
    {
        using (SolidBrush brush = new SolidBrush(ForeColor))
        {
            if (columnNames.Length == 0)
            {
                e.Graphics.DrawString(Convert.ToString(Items[e.Index]), 
                    Font, brush, boundsRect);
            }
            else
            {
                for (int colIndex = 0; colIndex < columnNames.Length; colIndex++)
                {
                    string item = Convert.ToString(FilterItemOnProperty(
                        Items[e.Index], columnNames[colIndex]));

                    boundsRect.X = lastRight;
                    boundsRect.Width = (int)columnWidths[colIndex] + columnPadding;
                    lastRight = boundsRect.Right;

                    if (colIndex == valueMemberColumnIndex)
                    {
                        using (Font boldFont = new Font(Font, FontStyle.Bold))
                        {
                            e.Graphics.DrawString(item, boldFont, brush, boundsRect);
                        }
                    }
                    else
                    {
                        e.Graphics.DrawString(item, Font, brush, boundsRect);
                    }

                    if (colIndex < columnNames.Length - 1)
                    {
                        e.Graphics.DrawLine(linePen, boundsRect.Right, boundsRect.Top, 
                            boundsRect.Right, boundsRect.Bottom);
                    }
                }
            }
        }
    }

    e.DrawFocusRectangle();
}

Though it's the longest function in the class (and probably exceeds the Marc Clifton approved limit for maximum number of lines in a method), it's quite straightforward. For each row, it iterates through all the columns, gets the column text and draws the text along with vertical lines that will act as column separators.

Acknowledgements

  • Rama Krishna Vavilala - For some awesome suggestions on the implementation.

History

  • July 27th 2007 - Article first published.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
United States United States
Nish Nishant is a technology enthusiast from Columbus, Ohio. He has over 20 years of software industry experience in various roles including Chief Technology Officer, Senior Solution Architect, Lead Software Architect, Principal Software Engineer, and Engineering/Architecture Team Leader. Nish is a 14-time recipient of the Microsoft Visual C++ MVP Award.

Nish authored C++/CLI in Action for Manning Publications in 2005, and co-authored Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his WordPress blog. Nish is experienced in technology leadership, solution architecture, software architecture, cloud development (AWS and Azure), REST services, software engineering best practices, CI/CD, mentoring, and directing all stages of software development.

Nish's Technology Blog : voidnish.wordpress.com

Comments and Discussions

 
QuestionUsing this control in a UITypeEditor? Pin
Tim8w16-Oct-20 5:07
Tim8w16-Oct-20 5:07 
Questionmulticolumn combobox Pin
Member 1368536420-Feb-18 7:47
Member 1368536420-Feb-18 7:47 
QuestionSuggestions as you type... Pin
Wrangly3-Jan-17 4:38
Wrangly3-Jan-17 4:38 
AnswerRe: Suggestions as you type... Pin
Tim8w16-Oct-20 4:46
Tim8w16-Oct-20 4:46 
QuestionColumn width are 0 when changing datasourcce Pin
ivanmdeb25-Dec-16 5:47
ivanmdeb25-Dec-16 5:47 
QuestionColumns Header Pin
saeedrajayi29-Apr-16 19:28
saeedrajayi29-Apr-16 19:28 
QuestionVB.Net version of multicolumn Pin
SumitSaha11-Nov-15 9:10
SumitSaha11-Nov-15 9:10 
QuestionRight-to-Left Pin
javadtoosi3-Aug-15 22:18
javadtoosi3-Aug-15 22:18 
QuestionValues from "bound to list" Pin
CHCodix24-Jan-15 22:58
CHCodix24-Jan-15 22:58 
AnswerRe: Values from "bound to list" Pin
CHCodix25-Jan-15 7:43
CHCodix25-Jan-15 7:43 
QuestionNice work Pin
Mike Hankey20-Dec-14 11:22
mveMike Hankey20-Dec-14 11:22 
AnswerRe: Nice work Pin
Nish Nishant20-Dec-14 11:50
sitebuilderNish Nishant20-Dec-14 11:50 
GeneralRe: Nice work Pin
Mike Hankey20-Dec-14 11:53
mveMike Hankey20-Dec-14 11:53 
QuestionHeader display and columns removal ? Pin
Vincent DUVERNET (Nolmë Informatique)2-Dec-14 11:41
Vincent DUVERNET (Nolmë Informatique)2-Dec-14 11:41 
BugRefreshing content of control Pin
Member 25245277-Nov-14 17:15
Member 25245277-Nov-14 17:15 
GeneralRe: Refreshing content of control Pin
Waqas Ahmed (Ax)3-Feb-16 7:43
Waqas Ahmed (Ax)3-Feb-16 7:43 
Questionhow can i show the Column HeadText? Pin
Shakeeb Sadikeen5-Nov-14 23:37
Shakeeb Sadikeen5-Nov-14 23:37 
QuestionColumn HeadText Pin
Member 1119455830-Oct-14 19:56
Member 1119455830-Oct-14 19:56 
QuestionThis is not quite functional Pin
Socarsky25-Sep-14 21:05
Socarsky25-Sep-14 21:05 
BugSelection resets to first matching text. (My Vote of 5) Pin
Senthilkumar Veeramani23-Sep-14 0:32
Senthilkumar Veeramani23-Sep-14 0:32 
QuestionProblem if I Filter one ComboBox based on the selection in another ComboBox Pin
Lee NeeHau10-Sep-14 0:38
Lee NeeHau10-Sep-14 0:38 
QuestionChanging DataSource in real time get error Pin
esaaco13-May-14 0:53
esaaco13-May-14 0:53 
GeneralMy vote of 5 Pin
yeng thao19-Jan-13 4:17
yeng thao19-Jan-13 4:17 
AnswerMy vote of 5... plus one little bug Pin
OpethManiac19-Dec-12 2:19
OpethManiac19-Dec-12 2:19 
Great article, the 5 points are earned.

However, I have found a small bug that occurs when at run time once more a DataSource is bound to the ComboBox. Then all the column widths in the ComboBox are resetted to 0. The reason is that base.OnDataSourceChanged(e) is called first and then the InitializeColumns()-Call resets the column widths again. Solution: Call base.OnDataSourceChanged(e) at the end:

C#
protected override void OnDataSourceChanged(EventArgs e)
        {
            InitializeColumns();
            base.OnDataSourceChanged(e);
        }

QuestionWorks as indicated Pin
karenpayne9-Dec-12 3:15
karenpayne9-Dec-12 3:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.