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.
- http://www.codeproject.com/Articles/20014/Searchable-MultiColumn-ComboBox-with-Linked-TextBo
- http://www.codeproject.com/Articles/25471/Customizable-ComboBox-Drop-Down
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
- Connect to either LocalDb ((localdb)\v11.0) and enter Products as the database name
- Select OK and you will be asked if you want to create a new database, select Yes
- 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.
- Select the project DGMCCBD.Data on this screen and click Finish.
- 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.
- Set the DataSource property on each of them to the corresponding data source.
- Select the DataGridView on the form and set the properties using the flyout window. Set the Data Source to the ProductBindingSource.
- 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.
- Now all you need to do is set up the data properties.
- Set up the CategoryID Column similarly and then click the OK button.
- 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.
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.
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.
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.
Set the column widths property now to the following values (30, 225, 150).
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.
Set the Odd rows also to a color that will have a high contrast to the even row setting.
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.
Glad you did this Mitch! I’m using it in my current project.
David,
A great how to demo. It helped me to get my own MultiColumnComboBox working in a
DatagridView, without having to recode anything in the control itself.
I did notice that the autocomplete in your demo does not work as expected. With the
settings:
DropDownStyle = ComboBoxStyle.DropDown;
editingControl.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
editingControl.AutoCompleteSource = AutoCompleteSource.ListItems;
I would expect to be able to edit the text and not be limited to the items in the list
and get a list of suggestions.
The remedy is to handle the EditingControlShowing event of the DataGridView to make sure that the ComboBox control properties are set as well. Adding these lines in the event
handler will give you the expected behavior. Its VB code sorry, but you will get the idea.
If TypeOf e.Control Is ComboBox Then
Dim cb As ComboBox =e.Control
‘ Set the dropdown style of the combobox to the properties of the editingcontrol
‘ One would expect them to be pushed automaticaly but they are not
cb.DropDownStyle = cb.DropDownStyle
cb.AutoCompleteMode = cb.AutoCompleteMode
cb.AutoCompleteSource =cb.AutoCompleteSource
End If
Thanks again!
Thanks for the comment on how to fix the issue. I have not tried to implement your fix yet but I will give it a try and replace my code when I have a moment.
Your welcome,
Looks like the trick I used in my VB project does not work with your code, but if you do it like this:
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
if (e.Control is ComboBox)
{
ComboBox cb = ((ComboBox)e.Control);
// Set the dropdown style of the combobox
cb.DropDownStyle = ComboBoxStyle.DropDown;
// set the property of the combobox to autocomplete mode.
cb.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
cb.AutoCompleteSource = AutoCompleteSource.ListItems;
}
}
It sort of works. The behaviour is still a lttle different but that maybe because your control is different from mine.
Turns out that the reason the trick did not work is because the DropDownStyle is not DropDown after converting to ComboBox.
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
if (e.Control is ComboBox)
{
ComboBox cb = ((ComboBox)e.Control);
// Set the dropdown style of the combobox
if (cb.DropDownStyle != ComboBoxStyle.DropDown) {cb.DropDownStyle = ComboBoxStyle.DropDown;}
cb.DropDownStyle = cb.DropDownStyle;
// set the property of the combobox to autocomplete mode and source of the editing control.
cb.AutoCompleteMode = cb.AutoCompleteMode;
cb.AutoCompleteSource = cb.AutoCompleteSource;
}
}
I just downloaded the whole solution and ran it with out issues at that line. Can you verify that the sample solution I provided works for you. I did have to update the app.config to point to what ever data source I am using. The demo describes using the DB Products the app.config points to a DB named NorthwindTest. Make sure you are pointing to the correct DB you are using. The variable ‘Value’ should contain the ID of the row of the drop down item you currently have set or clicked. Let me know if any of this is helpful.
Hi David,
It has been over 3 years that I looked at this. I will get back to you when I find the time to set it all up again.
hi David.
MultiColumnComboBox is very good. I thank you.
I want to get current item include multi item has select.
I think MultiColumnComboBox not support.
please help me.
Thanks again!
Great piece of work.
I’m running your Demo, but have problems with using it in my application:
Added whole project file DGMCCBD.Controls to my project and I see the control running.
When I start my application – the form shows the datagrid correctly, but the problem occurs when I click on the ComboBox to choose the data: program stops giving info:
An exception of type ‘System.ArgumentOutOfRangeException’ occurred in System.Windows.Forms.dll but was not handled in user code
// 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) //<— error here
What am I doing wrong?