Overview

This sample will describe how to create a WinForms multi-column combo box in a DataGridView control.  After much searching the web for how to do this I did not find any clear articles that made this type of control in an easy to follow example.  Going through this example should provide you with the steps and the code to make this control and use it.  Needless to say there may still be some things needing a little attention before using it without testing it.  With that said this example should get you started on developing your own control if you need a quick jump start.  Solution Files.

Introduction

Windows Forms offers several cell and column types for its DataGridView control. For example, the product ships with a text-box–based cell and column (DataGridViewTextBoxCell/DataGridViewTextBoxColumn) and a check-box–based (DataGridViewCheckBoxCell/DataGridViewCheckBoxColumn) among others.  But even with these default set of cell and column types, you may need to create a custom cell and column type to extend the functionality of the grid. The DataGridView control architecture is extensible enough that such custom cells and columns can be built and used in the grid.

This document explains how to create and use a cell and column that easily lets users display a combo box drop down that allows the user to show multiple columns when the drop down view is displayed.  The process of creating a custom DataGridViewColumn is presented in details in the MSDN article How to: Host Controls in Windows Forms DataGridView Cells, and Build a Custom NumericUpDown Cell and Column for the DataGridView Control So I will only discuss the code parts that are specific to a Multi-Column Combo Box in a DataGridView implementation.  The second issue is how to implement the custom columns?  The answer to this can be found in many examples by searching for “multi-column combo box dropdown”.  The URL http://stackoverflow.com/questions/4899169/multi-column-combobox-controls-for-winforms led me to find many pay for and free solutions.  I spent some time looking at the free ones and worked with the following two to come up with the code to create my custom control to be put in a DataGrideView.

Prerequisites

  • VS 2012
  • Entity Framework 5.0
  • NuGet – For more information, see Installing NuGet.
  • Data source (this sample uses a SQL server database)

Create the Application

  • Open Visual Studio
  • File -> New -> Project….
  • Select Windows in the left pane and Windows Forms Application in the right pane
  • Enter DGMCCBD.WinFormsDemo as the name
  • Select OK
  • Rename the form created in the windows forms application from Form1 to DGMCCBDForm
  • Add a DataGridView control to the form and dock it to the parent control

At this point there is nothing more to do until the rest of the data has been set up and the control created other than adding EntityFramework to the project.

In order to add the EntityFramework assembly properly use NuGet package manager to install EntityFramework 5.0.  Instructions for this can be found at http://msdn.microsoft.com/en-us/data/ee712906.aspx.

Install the Entity Framework NuGet package

  • In Solution Explorer, right-click on the DGMCCBDForm project
  • Select Manage NuGet Packages…
  • In the Manage NuGet Packages dialog, Select the Online tab and choose the EntityFramework package
  • Click Install

Define a Model Project

In this walkthrough we use code first to make the model.  This allows us to quickly set up our model objects.  We will need to set up four classes (Product, Category, Supplier, ProductContext).  These classes will be created in a new project named DGMCCBD.Data.

  • Add a new class library project to your solution and name it DGMCCBD.Data.
  • Delete the class1.cs file that is created for you.
  • Add a new Product class to project
  • Replace the code generated by default with the following code:
using System;

namespace DGMCCBD.Data
{
    public partial class Product
    {
        public Product()
        {
        }

        public int ProductID { get; set; }
        public string ProductName { get; set; }
        public Nullable<int> SupplierID { get; set; }
        public Nullable<int> CategoryID { get; set; }
        public virtual Category Category { get; set; }
        public virtual Supplier Supplier { get; set; }
    }
}
  • Add a Category class to the project.
  • Replace the code generated by default with the following code:
using System.Collections.Generic;

namespace DGMCCBD.Data
{
    public class Category
    {
        public Category()
        {
            Products = new List<Product>();
        }

        public int CategoryID { get; set; }
        public string CategoryName { get; set; }
        public string Description { get; set; }
        public virtual ICollection<Product> Products { get; set; }
    }
}
  • Add a Supplier class to the project.
  • Replace the code generated by default with the following code:
using System.Collections.Generic;

namespace DGMCCBD.Data
{
    public partial class Supplier
    {
        public Supplier()
        {
            Products = new List<Product>();
        }

        public int SupplierID { get; set; }
        public string CompanyName { get; set; }
        public string ContactName { get; set; }
        public string ContactTitle { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string PostalCode { get; set; }
        public virtual ICollection<Product> Products { get; set; }
    }
}

 

In addition to defining entities, you need to define a class that derives from DbContext and exposes DbSet<TEntity> properties. The DbSet properties let the context know which types you want to include in the model. The DbContext and DbSet types are defined in the EntityFramework assembly.

An instance of the DbContext derived type manages the entity objects during run time, which includes populating objects with data from a database, change tracking, and persisting data to the database.

  • Add a new ProductContext class to the project.
  • Replace the code generated by default with the following code:
using System.Data.Entity;

namespace DGMCCBD.Data
{
    public partial class NorthwindContext : DbContext
    {
        static NorthwindContext()
        {
            Database.SetInitializer<NorthwindContext>(null);
        }

        public NorthwindContext()
            : base("Name=NorthwindContext")
        {
        }

        public DbSet<Category> Categories { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<Supplier> Suppliers { get; set; }
    }
}

At this point EF needs to be added to the new data project.  Right click the solution item in the Visual studio and select the context menu for managing NuGet packages for the solution.  Click the installed packages on the left side of the dialog box.  Click the manage button next to the Entity Framework item on the right side of the dialog box.  Check the box next to the data project to add EF to it.  Click the OK button and then the Close button.

Compile the project.

 

Create the Database

Visual Studio will be used to create the database.  Follow the instructions below to create the data base.

  • View -> Server Explorer
  • Right click on Data Connections -> Add Connection…
  • If you haven’t connected to a database from Server Explorer before you’ll need to select Microsoft SQL Server as the data source

Image01

  • Connect to either LocalDb ((localdb)\v11.0) and enter Products as the database name

Image02

  • Select OK and you will be asked if you want to create a new database, select Yes

Image03

  • The new database will now appear in Server Explorer, right-click on it and select New Query
  • Copy the following SQL into the new query, then right-click on the query and select Execute
CREATE TABLE [dbo].[Categories](
	[CategoryID] [int] IDENTITY(1,1) NOT NULL,
	[CategoryName] [nvarchar](15) NOT NULL,
	[Description] [ntext] NULL,
 CONSTRAINT [PK_Categories] PRIMARY KEY CLUSTERED 
(
	[CategoryID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

CREATE TABLE [dbo].[Products](
	[ProductID] [int] IDENTITY(1,1) NOT NULL,
	[ProductName] [nvarchar](40) NOT NULL,
	[SupplierID] [int] NULL,
	[CategoryID] [int] NULL,
 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED 
(
	[ProductID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE TABLE [dbo].[Suppliers](
	[SupplierID] [int] IDENTITY(1,1) NOT NULL,
	[CompanyName] [nvarchar](40) NOT NULL,
	[ContactName] [nvarchar](30) NULL,
	[ContactTitle] [nvarchar](30) NULL,
	[Address] [nvarchar](60) NULL,
	[City] [nvarchar](15) NULL,
	[PostalCode] [nvarchar](10) NULL,
 CONSTRAINT [PK_Suppliers] PRIMARY KEY CLUSTERED 
(
	[SupplierID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Products]  WITH NOCHECK ADD  CONSTRAINT [FK_Products_Categories] FOREIGN KEY([CategoryID])
REFERENCES [dbo].[Categories] ([CategoryID])

ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_Categories]

ALTER TABLE [dbo].[Products]  WITH NOCHECK ADD  CONSTRAINT [FK_Products_Suppliers] FOREIGN KEY([SupplierID])
REFERENCES [dbo].[Suppliers] ([SupplierID])

ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_Suppliers]
-- ADD ACTUAL DATA
INSERT INTO [Categories] ([CategoryName],[Description]) VALUES
			('Beverages','Soft drinks, coffees, teas, beers, and ales'),
			('Condiments','Sweet and savory sauces, relishes, spreads, and seasonings'),
			('Confections','Desserts, candies, and sweet breads'),
			('Dairy Products','Cheeses'),
			('Grains/Cereals','Breads, crackers, pasta, and cereal'),
			('Meat/Poultry','Prepared meats'),
			('Produce','Dried fruit and bean curd'),
			('Seafood','Seaweed and fish')

INSERT INTO [Suppliers]
           ([CompanyName],[ContactName],[ContactTitle],[Address],[City],[PostalCode])
        VALUES
			('Exotic Liquids','Charlotte Cooper','Purchasing Manager','49 Gilbert St.','London','EC1 4SD'),
			('New Orleans Cajun Delights','Shelley Burke','Order Administrator','P.O. Box 78934','New Orleans','70117'),
			('Grandma Kelly''s Homestead','Regina Murphy','Sales Representative','707 Oxford Rd.','Ann Arbor','48104'),
			('Tokyo Traders','Yoshi Nagase','Marketing Manager','9-8 Sekimai Musashino-shi','Tokyo','100'),
			('Cooperativa de Quesos ''Las Cabras''','Antonio del Valle Saavedra','Export Administrator','Calle del Rosal 4','Oviedo','33007'),
			('Mayumi''s','Mayumi Ohno','Marketing Representative','92 Setsuko Chuo-ku','Osaka','545'),
			('"Pavlova, Ltd."','Ian Devling','Marketing Manager','74 Rose St. Moonie Ponds','Melbourne','3058'),
			('"Specialty Biscuits, Ltd."','Peter Wilson','Sales Representative','29 King''s Way','Manchester','M14 GSD'),
			('PB Knäckebröd AB','Lars Peterson','Sales Agent','Kaloadagatan 13','Göteborg','S-345 67'),
			('Refrescos Americanas LTDA','Carlos Diaz','Marketing Manager','Av. das Americanas 12.890','Sao Paulo','5442'),
			('Heli Süßwaren GmbH & Co. KG','Petra Winkler','Sales Manager','Tiergartenstraße 5','Berlin','10785'),
			('Plutzer Lebensmittelgroßmärkte AG','Martin Bein','International Marketing Mgr.','Bogenallee 51','Frankfurt','60439'),
			('Nord-Ost-Fisch Handelsgesellschaft mbH','Sven Petersen','Coordinator Foreign Markets','Frahmredder 112a','Cuxhaven','27478'),
			('Formaggi Fortini s.r.l.','Elio Rossi','Sales Representative','"Viale Dante, 75"','Ravenna','48100'),
			('Norske Meierier','Beate Vileid','Marketing Manager','Hatlevegen 5','Sandvika','1320'),
			('Bigfoot Breweries','Cheryl Saylor','Regional Account Rep.','3400 - 8th Avenue Suite 210','Bend','97101'),
			('Svensk Sjöföda AB','Michael Björn','Sales Representative','Brovallavägen 231','Stockholm','S-123 45'),
			('Aux joyeux ecclésiastiques','Guylène Nodier','Sales Manager','"203, Rue des Francs-Bourgeois"','Paris','75004'),
			('New England Seafood Cannery','Robb Merchant','Wholesale Account Agent','Order Processing Dept. 2100 Paul Revere Blvd.','Boston','2134'),
			('Leka Trading','Chandra Leka','Owner','"471 Serangoon Loop, Suite #402"','Singapore','512'),
			('Lyngbysild','Niels Petersen','Sales Manager','Lyngbysild Fiskebakken 10','Lyngby','2800'),
			('Zaanse Snoepfabriek','Dirk Luchte','Accounting Manager','Verkoop Rijnweg 22','Zaandam','9999 ZZ'),
			('Karkki Oy','Anne Heikkonen','Product Manager','Valtakatu 12','Lappeenranta','53120'),
			('"G''day, Mate"','Wendy Mackenzie','Sales Representative','170 Prince Edward Parade Hunter''s Hill','Sydney','2042'),
			('Ma Maison','Jean-Guy Lauzon','Marketing Manager','2960 Rue St. Laurent','Montréal','H1J 1C3'),
			('Pasta Buttini s.r.l.','Giovanni Giudici','Order Administrator','"Via dei Gelsomini, 153"','Salerno','84100'),
			('Escargots Nouveaux','Marie Delamare','Sales Manager','"22, rue H. Voiron"','Montceau','71300'),
			('Gai pâturage','Eliane Noz','Sales Representative','"Bat. B 3, rue des Alpes"','Annecy','74000'),
			('Forêts d''érables','Chantal Goulet','Accounting Manager','148 rue Chasseur','Ste-Hyacinthe','J2S 7S8')

INSERT INTO [Products]
           ([ProductName],[SupplierID],[CategoryID])
        VALUES ('Chai',1,1), ('Chang',1,1), ('Aniseed Syrup',1,2),
('Chef Anton''s Cajun Seasoning',2,2), ('Chef Anton''s Gumbo Mix',2,2),
('Grandma''s Boysenberry Spread',3,2), ('Uncle Bob''s Organic Dried Pears',3,7), ('Northwoods Cranberry Sauce',3,2),
        ('Mishi Kobe Niku',4,6), ('Ikura',4,8), ('Queso Cabrales',5,4), ('Queso Manchego La Pastora',5,4), ('Konbu',6,8),
        ('Tofu',6,7), ('Genen Shouyu',6,2), ('Pavlova',7,3), ('Alice Mutton',7,6), ('Carnarvon Tigers',7,8),
        ('Teatime Chocolate Biscuits',8,3),('Sir Rodney''s Marmalade',8,3),('Sir Rodney''s Scones',8,3),
        ('Gustaf''s Knäckebröd',9,5),('Tunnbröd',9,5),('Guaraná Fantástica',10,1),('NuNuCa Nuß-Nougat-Creme',11,3),
        ('Gumbär Gummibärchen',11,3),('Schoggi Schokolade',11,3),('Rössle Sauerkraut',12,7),('Thüringer Rostbratwurst',12,6),
		('Nord-Ost Matjeshering',13,8),('Gorgonzola Telino',14,4),('Mascarpone Fabioli',14,4),('Geitost',15,4),
		('Sasquatch Ale',16,1),('Steeleye Stout',16,1),('Inlagd Sill',17,8),('Gravad lax',17,8),('Côte de Blaye',18,1),
		('Chartreuse verte',18,1),('Boston Crab Meat',19,8),('Jack''s New England Clam Chowder',19,8),
		('Singaporean Hokkien Fried Mee',20,5),('Ipoh Coffee',20,1),('Gula Malacca',20,2),('Rogede sild',21,8),
		('Spegesild',21,8),('Zaanse koeken',22,3),('Chocolade',22,3),('Maxilaku',23,3),('Valkoinen suklaa',23,3),
		('Manjimup Dried Apples',24,7),('Filo Mix',24,5),('Perth Pasties',24,6),('Tourtière',25,6),
		('Pâté chinois',25,6),('Gnocchi di nonna Alice',26,5),('Ravioli Angelo',26,5),('Escargots de Bourgogne',27,8),
		('Raclette Courdavault',28,4),('Camembert Pierrot',28,4),('Sirop d''érable',29,2),('Tarte au sucre',29,3),
		('Vegie-spread',7,2),('Wimmers gute Semmelknödel',12,5),('Louisiana Fiery Hot Pepper Sauce',2,2),
		('Louisiana Hot Spiced Okra',2,2),('Laughing Lumberjack Lager',16,1),('Scottish Longbreads',8,3),
		('Gudbrandsdalsost',15,4),('Outback Lager',7,1),('Flotemysost',15,4),('Mozzarella di Giovanni',14,4),
		('Röd Kaviar',17,8),('Longlife Tofu',4,7),('Rhönbräu Klosterbier',12,1),('Lakkalikööri',23,1),
		('Original Frankfurter grüne Soße',12,2)

 

Create a Controls Project

This project will contain the classes needed to create the custom DataGridView column.

  • Add a new class library project to your solution and name it DGMCCBD.Controls.
  • Delete the class1.cs file that is created for you.

Characteristics of the Cell

Because the DataGridViewMultiColumnComboBoxCell cell type is close in functionality to the standard DataGridViewComboBoxCell cell type, it makes sense to derive from that class. By doing this, the custom cell can take advantage of numerous characteristics of its base class and the work involved in creating the cell is significantly reduced.

Editing experience

The DataGridViewMultiColumnComboBoxCell cell provides a complex user interaction and editing experience, therefore a rich user interaction for changing their value is needed.  This requires a Windows Forms control to be shown to enable that complex user input.

Cell and Column Classes

The minimum requirement for being able to use a custom cell type is to develop one class for that cell type and if it has a rich editing experience and can’t use one of the standard editing controls, DataGridViewTextBoxEditingControl and DataGridViewComboBoxEditingControl, then a second class needs to be created for the custom editing control. Finally the creation of a custom column.  The custom column typically replicates the specific properties of the custom cell and implements any custom properties needed.  For the DataGridViewMultiColumnComboBoxCell type, three classes are created in three files:

  • DataGridViewMultiColumnComboBoxCell, in DataGridViewMultiColumnComboBoxCell.cs, defines the custom cell type.
  • DataGridViewMultiColumnComboBoxEditingControl, in DataGridViewMultiColumnComboBoxEditingControl.cs, defines the custom editing control that is shown for editing.
  • DataGridViewMultiColumnComboBoxColumn, in DataGridViewMultiColumnComboBoxColumn.cs, defines the custom column type.

All the classes were put in the DGMCCBD.Controls namespace.

Cell Implementation Details

Because the DataGridViewMultiColumnComboBoxCell cell works similar to a DataGridComboBoxCell we can derive from this class and inherit much of its existing functionality.  Make sure to add a reference to System.Windows.Forms and a using statement.

using System.Windows.Forms;

Class definition

namespace DGMCCBD.Controls
{
    public class DataGridViewMultiColumnComboBoxCell : DataGridViewComboBoxCell
    {
	…
    }
}

 

Defining custom cell properties

The DataGridViewMultiColumnComboBoxCell has the following custom properties it uses to display when showing the editing control.

  • ColumnNames
  • ColumnWidths
  • EvenRowsBackColor
  • OddRowsBackColor

The custom properties are implemented in the same manner as follows:

public class DataGridViewMultiColumnComboBoxCell : DataGridViewComboBoxCell
{
        #region "Member Variables"
        private List<string> _columnNames = new List<string>();
        #endregion

        #region "Properties"

        public List<String> ColumnNames
        {
            get
            {
                return _columnNames;
            }
            set
            {
                _columnNames = value ?? new List<string>();
            }
        }

        #endregion
}

 

Key properties to override

When creating a custom cell type for the DataGridView control, the following base properties from the DataGridViewCell class often need to be overridden.

The EditType property

The EditType property points to the type of the editing control associated with the cell. The default implementation in DataGridViewCell returns the System.Windows.Forms.DataGridViewTextBoxEditingControl type. Cell types that have no editing experience or have a simple editing experience (i.e., don’t use an editing control) must override this property and return null.  Cell types that have a complex editing experience must override this property and return the type of their editing control.

Implementation for the DataGridViewMultiColumnComboBoxCell class:

public class DataGridViewMultiColumnComboBoxCell : DataGridViewComboBoxCell
{
       #region "Member Variables"

// Type of this cell's editing control
private static Type _defaultEditType = typeof(DataGridViewMultiColumnComboBoxEditingControl);

       #endregion

       #region "Properties"

       public override Type EditType
       {
           get { return _defaultEditType; }
       }

       #endregion
}

 

The FormattedValueType property

The FormattedValueType property represents the type of the data displayed on the screen, i.e., the type of the cell’s FormattedValue property.  The DataGridViewMultiColumnComboBoxCell displays text on the screen, so the FormattedValueType is System.String, which is the same as the base class DataGridViewComboBoxCell. So this particular property does not need to be overridden here.

The ValueType property

The ValueType property represents the type of the underlying data, i.e., the type of the cell’s Value property.  The DataGridViewMultiColumnComboBoxCell inherits its implementation of ValueType so it does not need to be overridden.

Key methods to override

When developing a custom cell type some virtual methods need to be overridden.  This control derives from a DataGridViewComboBoxCell so most of them do not have to be overridden.

The Clone() method

The DataGridViewCell base class implements the ICloneable interface. Each custom cell type typically needs to override the Clone() method to copy its custom properties. Cells are cloneable because a particular cell instance can be used for multiple rows in the grid. This is the case when the cell belongs to a shared row. When a row gets unshared, its cells need to be cloned. Following is the implementation for the DataGridViewMultiColumnComboBoxCell:

public override object Clone()
{
    var clone = (DataGridViewMultiColumnComboBoxCell)base.Clone();

    // Make sure to copy added properties.
    clone.ColumnNames = ColumnNames;
    clone.ColumnWidths = ColumnWidths;
    clone.EvenRowsBackColor = EvenRowsBackColor;
    clone.OddRowsBackColor = OddRowsBackColor;

    return clone;
}

 

The InitializeEditingControl(int, object, DataGridViewCellStyle) method

This method is called by the grid control when the editing control is about to be shown. This occurs only for cells that have a complex editing experience of course. This gives the cell a chance to initialize its editing control based on its own properties and the formatted value provided. The DataGridViewMultiColumnComboBoxCell’s implementation is as follows:

public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
        {
            base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle);

            var editingControl = DataGridView.EditingControl as DataGridViewMultiColumnComboBoxEditingControl;
            // Just return if editing control is null.
            if (editingControl == null) return;

            // Set custom properties of Multi Column Combo Box.
            editingControl.ColumnNames = ColumnNames;
            editingControl.ColumnWidths = ColumnWidths;
            editingControl.BackColorEven = EvenRowsBackColor;
            editingControl.BackColorOdd = OddRowsBackColor;
            editingControl.OwnerCell = this;

            if (Value != null)
                editingControl.SelectedValue = Value;

            editingControl.AutoComplete = AutoComplete;
            if (!AutoComplete) return;

            editingControl.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
            editingControl.AutoCompleteSource = AutoCompleteSource.ListItems;
        }

 

The ToString() method

This method returns a compact string representation of the cell. The DataGridViewMultiColumnComboBoxCell’s implementation follows the standard cells’ standard.

public override string ToString()
{
    return string.Format("DataGridViewMultiColumnComboBoxCell {{ ColumnIndex={0}, RowIndex={1} }}", ColumnIndex.ToString(CultureInfo.CurrentCulture), RowIndex.ToString(CultureInfo.CurrentCulture));
}

 

Editing Control Implementation Details

The custom editing control DataGridViewMultiColumnComboBoxEditingControl for the custom cell needs to be made.

Class definition and constructor

Once again we will derive from a base class to give us most of the default behavior we want.  Notice the custom properties we will be using to show our multi-column drop down being set here.

namespace DGMCCBD.Controls
{
    /// <summary>
    /// Represents the hosted Multi-Column Combo Box control in a <see cref="T:DGMCCBD.Controls.DataGridViewMultiColumnComboBoxCell"/>.
    /// </summary>
    public class DataGridViewMultiColumnComboBoxEditingControl : DataGridViewComboBoxEditingControl
    {
        #region "Member Variables"

        private readonly List<Int32> _columnWidths = new List<int>();
        private List<string> _columnWidthStringList = new List<string>();
        private List<string> _columnNames = new List<string>();

        #endregion

        #region "Constructor"
        public DataGridViewMultiColumnComboBoxEditingControl()
        {
            // Initialize all properties.
            AutoDropdown = false;
            BackColorEven = Color.White;
            BackColorOdd = Color.White;
            ColumnWidths = new List<string>();
            ColumnWidthDefault = 75;
            TotalWidth = 0;
            ColumnNames = new List<string>();
            DrawMode = DrawMode.OwnerDrawVariable;
            DropDownStyle = ComboBoxStyle.DropDown;
            OwnerCell = null;
            ContextMenu = new ContextMenu();
            EditingControlValueChanged = false;
        }

        #endregion

    }
}

 

Custom Properties

At this point we have some custom properties that need to be created to allow the editing control to display properly.  Only some of them are shown here and with partial code.  See the attached project for complete code.

#region "Properties"
[DefaultValue(typeof(Color), "White")]
public Color BackColorEven { get; set; }

[DefaultValue(typeof(Color), "White")]
public Color BackColorOdd { get; set; }

public List<String> ColumnWidths
{
get
{
return _columnWidthStringList;
}
       set
       {
           if (value == null) value = new List<string>();

             	…
       }
}

[DefaultValue(75)]
public int ColumnWidthDefault { get; set; }

[DefaultValue(0)]
public int TotalWidth { get; private set; }

public List<String> ColumnNames
{
get
       {
           return _columnNames;
       }
       set
       {
           if (value == null) value = new List<string>();

           …
       }
}

#endregion

 

Key event handlers to override

When developing a custom edit control type some virtual methods need to be overridden.  This control derives from a DataGridViewComboBoxEditingControl so most of them do not have to be overridden.

The OnDataSourceChanged(EventArgs e) method

This custom class draws multiple columns for the data attached to the control therefore when the data source changes it needs to initialize the columns it will draw. Following is the implementation for the DataGridViewMultiColumnComboBoxEditingControl:

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

 

The OnDropDown(EventArgs e) method

This method allows sets the correct drop down width based on if scroll bars are used.  The DataGridViewMultiColumnComboBoxEditingControl implementation is as follows:

protected override void OnDropDown(EventArgs e)
{
    if (TotalWidth <= 0) return;

    if (Items.Count > MaxDropDownItems)
    {
        DropDownWidth = TotalWidth + SystemInformation.VerticalScrollBarWidth;
    }
    else
    {
        DropDownWidth = TotalWidth;
    }
}

 

The OnDrawItem(DrawItemEventArgs e) method

This method is crucial to how the control will draw the drop down and needs to be overridden.  Not all code is shown below, see project for the rest.

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

    if (e.Index < 0) return;

    if (DesignMode)
        return;

    e.DrawBackground();

    var boundsRect = e.Bounds;
    var lastRight = 0;

    Color brushForeColor;
    if ((e.State & DrawItemState.Selected) == 0)
    {
        // Item is not selected. Use BackColorOdd & BackColorEven
        var backColor = Convert.ToBoolean(e.Index % 2) ? BackColorOdd : BackColorEven;
        using (var brushBackColor = new SolidBrush(backColor))
        {
            e.Graphics.FillRectangle(brushBackColor, e.Bounds);
        }
        brushForeColor = Color.Black;
    }
    else
    {
        // Item is selected. Use ForeColor = White
        brushForeColor = Color.White;
    }

    using (var linePen = new Pen(SystemColors.GrayText))
    {
        …  SEE THE PROJECT FOR THIS CODE
    }

    e.DrawFocusRectangle();
}

 

The InitializeColumns() method

This method is used to set the decide the columns that need to be shown and the widths the columns should be set to.

private void InitializeColumns()
{
    if (ColumnNames.Count == 0)
    {
        var propertyDescriptorCollection = DataManager.GetItemProperties();

        TotalWidth = 0;
        ColumnNames.Clear();

        for (var colIndex = 0; colIndex < propertyDescriptorCollection.Count; colIndex++)
        {
            ColumnNames.Add(propertyDescriptorCollection[colIndex].Name);

            // If the index is greater than the collection of explicitly
            // set column widths, set any additional columns to the default
            if (colIndex >= ColumnWidths.Count)
            {
                _columnWidths.Add(ColumnWidthDefault);
            }
            TotalWidth += _columnWidths[colIndex];
        }
    }
    else
    {
        TotalWidth = 0;

        for (var colIndex = 0; colIndex < ColumnNames.Count; colIndex++)
        {
            // If the index is greater than the collection of explicitly
            // set column widths, set any additional columns to the default
            if (colIndex >= ColumnWidths.Count)
            {
                _columnWidths.Add(ColumnWidthDefault);
            }
            TotalWidth += _columnWidths[colIndex];
        }

    }
}

 

Column Implementation Details

As mentioned earlier, the creation of a custom column type is optional. The DataGridViewMultiColumnComboBoxCell can be used by any column type, but because we want to expose the special properties of the cell type they’re associated with we create a custom column type to hold them. The DataGridViewMultiColumnComboBoxColumn exposes the ColumnNames, ColumnWidths, EvenRowsBackColor, OddRowsBackColor properties.

Class definition and constructor

The DataGridViewMultiColumnComboBoxColumn class simply derives from the DataGridViewComboBoxColumn class and its constructor uses a default DataGridViewMultiColumnComboBoxCell for the cell template.

namespace DGMCCBD.Controls
{
    public class DataGridViewMultiColumnComboBoxColumn : DataGridViewComboBoxColumn
    {
        #region "Constructor"

        public DataGridViewMultiColumnComboBoxColumn()
        {
            CellTemplate = new DataGridViewMultiColumnComboBoxCell();
        }

        #endregion 
    }
}

 

Defining column properties

Let’s take a closer look at how a column type typically implements a property. The ColumnNames property of the DataGridViewMultiColumnComboBoxCell class is implemented as follows for example:

[Category("Data"), DefaultValue("")]
[Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design", "System.Drawing.Design.UITypeEditor, System.Drawing")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Description("Which columns to show. Leave blank to show all. put entries in [] to rename Column Headers.")]
public List<String> ColumnNames
        {
            get
            {
                if (MultiColumnComboBoxCellTemplate == null)
                {
                    throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate.");
                }
                return MultiColumnComboBoxCellTemplate.ColumnNames;
            }
            set
            {
                if (MultiColumnComboBoxCellTemplate == null)
                {
                    throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate.");
                }
                // Update the template cell so that subsequent cloned cells use the new value.
                MultiColumnComboBoxCellTemplate.ColumnNames = value;
                if (DataGridView == null) return;

                // Update all the existing DataGridViewMultiColumnComboBoxCell cells in the column accordingly.
                var dataGridViewRows = DataGridView.Rows;
                var rowCount = dataGridViewRows.Count;
                for (var rowIndex = 0; rowIndex < rowCount; rowIndex++)
                {
                    // Be careful not to unshare rows unnecessarily. 
                    // This could have severe performance repercussions.
                    var dataGridViewRow = dataGridViewRows.SharedRow(rowIndex);
                    var dataGridViewCell = dataGridViewRow.Cells[Index] as DataGridViewMultiColumnComboBoxCell;
                    if (dataGridViewCell != null)
                    {
                        // Call the internal SetColumnNames method instead of the property to avoid invalidation 
                        // of each cell. The whole column is invalidated later in a single operation for better performance.
                        dataGridViewCell.SetColumnNames(rowIndex, value);
                    }
                }
                DataGridView.InvalidateColumn(Index);

                // TODO: Call the grid's autosizing methods to autosize the column, rows, column headers / row headers as needed.
            }
        }

Regarding the last comment about auto-sizing features see the article Build a Custom NumericUpDown Cell and Column for the DataGridView Control

Besides the ColumnNames, ColumnWidths, EvenRowsBackColor, OddRowsBackColor properties, the column is also defining the critical CellTemplate property as follows:

[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override sealed DataGridViewCell CellTemplate
{
    get
    {
        return base.CellTemplate;
    }
    set
    {
        // Ensure that the cell used for the template is a DataGridViewMultiColumnComboBoxCell. 
        if (value != null &&
            !value.GetType().IsAssignableFrom(typeof(DataGridViewMultiColumnComboBoxCell)))
        {
            throw new InvalidCastException("Must be a DataGridViewMultiColumnComboBoxCell");
        }
        base.CellTemplate = value;
    }
}

The CellTemplate property is used for example when the DataGridViewRowCollection.Add() gets called. Because no explicit cells are provided, a clone of the DataGridView.RowTemplate is added. By default, the RowTemplate is populated with clones of each column’s CellTemplate.

Key methods to override

A custom column typically overrides the ToString() method.

// Returns a standard compact string representation of the column.
public override string ToString()
{
    var sb = new StringBuilder(100);
    sb.Append("DataGridViewMultiColumnComboBoxColumn { Name=");
    sb.Append(Name);
    sb.Append(", Index=");
    sb.Append(Index.ToString(CultureInfo.CurrentCulture));
    sb.Append(" }");
    return sb.ToString();
}

The column class needs to override the Clone method to copy over the custom properties.

public override object Clone()
{
    var clone = (DataGridViewMultiColumnComboBoxColumn)base.Clone();
    if (clone == null) return null;

    clone.ColumnNames = ColumnNames;
    clone.ColumnWidths = ColumnWidths;
    clone.EvenRowsBackColor = EvenRowsBackColor;
    clone.OddRowsBackColor = OddRowsBackColor;
    return clone;
}

 

Sample application

Now that the database has been created and populated, the control has been defined we can finish the creation of the application that will use it.  Go back to the win forms project and add references to the DGMCCBD.Controls project and the DGMCCBD.Data project.  Compile the project.

Setup Data Sources

Click on the project file in solution explorer and then use the menu option in Visual Studio to add project data sources.

  • Project -> Add New Data Source… will start the wizard.  Choose the Object option for the data source and click the Next button.

Image04

  • Select the project DGMCCBD.Data on this screen and click Finish.

Image05

  • Open the windows form created earlier that has the DataGridView control on it.  You will need to create three data binding sources for this form to use.  Go ahead and drag three different data binding source items from the toolbox window to the form.  Rename each of them to correspond to one of the following:  ProductBindingSource, CategoryBindingSource, SupplierBindingSource.

Image06

  • Set the DataSource property on each of them to the corresponding data source.

Image789

  • Select the DataGridView on the form and set the properties using the flyout window.  Set the Data Source to the ProductBindingSource.

Image10

  • Choose the edit columns menu item on the fly out.  This will allow you to set up the columns to be data bound.  Delete the columns for Category and Supplier as they will not be used.  Then select the SupplierID column on the left then change the ColumnType property to the new  DataGridViewMultiColumnComboBoxColumn control.  Optionally update the (Name) property.

Image11

  • Now all you need to do is set up the data properties.

Image12

  • Set up the CategoryID Column similarly and then click the OK button.

Image13

  • Now double click on the form which should create a load event handler for the form where you can insert code to load data into the binding sources.  Add the following local variable to the form and code to the form load event handler.
public partial class DGMCCBDForm : Form
{
    private NorthwindContext _northwindContext = new NorthwindContext();

    public DGMCCBDForm()
    {
        InitializeComponent();
    }

    private void DGMCCBDForm_Load(object sender, EventArgs e)
    {
        CategoryBindingSource.DataSource = _northwindContext.Categories.ToList();
        SupplierBindingSource.DataSource = _northwindContext.Suppliers.ToList();
        ProductBindingSource.DataSource = _northwindContext.Products.ToList();
    }
}

Finally you need to add a connection string value to the app.config for the project to run.  Copy the following connection string section into the app.config file right after the configSections.

<connectionStrings>
    <add name="NorthwindContext" connectionString="Data Source=(localdb)\V11.0;Initial Catalog=NorthwindTest;Integrated Security=True;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
</connectionStrings>

 

Screenshot of a DataGridViewNumericUpDownColumn

This is a screenshot of the sample application that makes use of the custom cell and column.

Image14 

Custom Properties

Now that we have the control up and running let’s take a look at how the special properties we added to the control have an effect on the edit control.  Notice that the columns do not show the full width of the values being shown in the drop down.  This is because we did not set the column widths and it is using the default value of the control.  Also notice some of the columns have values that we do not want to show to the user so we can also limit them by setting one of the properties.  In the next section we will set both the column names and width properties.  Also we will set the background color properties to highlight alternating rows to increase visibility.

ColumnNames

This custom property allows us to set the names of the columns that we want to display in the drop down list.  If this property is not set as we saw earlier the control will display all columns.  Go back to the DataGridView and set this property now.

Image15 Click on the button to the right of the property field and then add the three column names as shown below and then click the OK button.

Image16

Run the application again and you will see that the supplier drop down now only shows the columns entered into the property.

ColumnWidths

When the drop down shows in edit view the columns are being cut off.  The ColumnWidths property allows you to set a value for each column for its width.  If none are specified the default value is used as was shown earlier.

Image17

Set the column widths property now to the following values (30, 225, 150).

Image18 Run the application again and you will see that the supplier drop down sets the column width of each column to the value set.

EvenRowsBackColor/OddRowsBackColor

Finally these two properties allow the rows to be set to different colors to make the drop down alternating rows show up clearer.  Set the value of these two properties allowing a better visual cue between rows.

Image19

Set the Odd rows also to a color that will have a high contrast to the even row setting.

Image20

Now run the program and select the supplier drop down and see the changes.

Conclusion

In this article, you learned how to build a custom cell and column by extending the functionality of the built in DataGridView associated controls.  In turn, the custom cell and column easily lets users enter show more detail in the drop downs.