Blog
Custom DataGridViewColumn & IDataGridViewEditingControl classes
Having just spent the last few hours getting my head round how custom DataGridView column code fitted together in VB.Net I thought it only fair to share in the hope that I can speed up the process for others.
The setup
In my case I was looking to use a custom usercontrol in an editable grid; a custom usercontrol that extends the standard ComboBox in all sorts of ways (and is already written & tested).
Unlike examples such as the DateTimePicker column and others such as the Colour Pickers on CodeProject etc, I was looking to both display a plain text value in the cell when not in edit mode and also to store/load a completely different numeric lookup value within the bound data. [1]
Useful points
A few points that I would have found useful to know before starting are:
- What is FormattedValue?
It appears that FormattedValue is the information stored within the bound data which is also referred to as just Value in other places. [2] - At what point should I replace standard columns with custom ones?
Use the DataGridView.DataSourceChanged event. [3] - How many instances of each control will be created?
The creation of the Cell and EditingControl control instances is handled by the DataGridView control and is out of the developer's hands. This gives you:- One ComboBoxColumn instance per column.
- One ComboBoxCell instance per cell.
- One ComboBoxEditingControl instance PER GRID.
This last one is very important to recognise as it affects decisions later.
- How does the DataGridView know which Cell and EditingControl classes to instantiate?
- [line 46 below]: The CellTemplate property of the custom Column class defines the custom Cell class to use. This can be specified in the DataGridViewColumn constructor.
- [line 95 below]: The EditType property in the custom Cell class tells DataGridView which class to use as the editing control for the cell.
On to the code
A note about the Extended ComboBox control: The control supports properties such as an ImageList used for displaying images in the drop down, and a SelectedIDValue which is forced to be either DBNull.Value or a Long value for easy binding to a field in a database.
To walk through the code, we'll start at the outer DataGridView level and work inwards.
DataSourceChanged event
First the DataSourceChanged event handler which we use to replace the standard DataGridViewTextColumns with custom columns. The code is structured with a Select Case so that other custom column types (like CalendarColumn for DateTime types) are simple to add later.
Private Sub OnDataSourceChanged(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles DataGridView1.DataSourceChanged Dim OriginalColumn, NewColumn As DataGridViewColumn Dim ColumnIndex As Integer For Each OriginalColumn In DataGridView1.Columns 'Reset the NewColumn variable NewColumn = Nothing 'Decide if we need to replace the current column Select Case GetDBType(OriginalColumn.ValueType) Case SqlDbType.BigInt, SqlDbType.Int 'Replace BigInt and Int columns with the ComboBoxColumn type 'Pass the Sql used to display the content of the drop down NewColumn = _ New ComboBoxColumn(GetLookupSql(OriginalColumn.Name)) End Select 'If we have a new column to replace If NewColumn IsNot Nothing Then 'Get the current index of the old column ColumnIndex = MyBase.Columns.IndexOf(OriginalColumn) 'Copy the basic information from the old column NewColumn.DataPropertyName = OriginalColumn.DataPropertyName NewColumn.HeaderText = OriginalColumn.HeaderText NewColumn.Name = OriginalColumn.Name 'Remove the old column MyBase.Columns.Remove(OriginalColumn) 'Add the new one where the old one used to be MyBase.Columns.Insert(ColumnIndex, NewColumn) End If Next End Sub
See CodeProject for the implementation of GetDbType.
ComboBoxColumn class
Next we need to create the basic ComboBoxColumn class. Line 46 is important to hooking the Column to its related CellTemplate class, and the rest of the code allows us to simply pass in the Sql to use for populating the drop down list.
Public Class ComboBoxColumn Inherits DataGridViewColumn Friend DropDownDataSource As DataTable Public Sub New(ByVal DataSource As String) MyBase.New(New ComboBoxCell()) 'Open a DataTable from the specified Sql DropDownDataSource = OpenLookupDataTable(DataSource) End Sub End Class
We should override the CellTemplate property and check that the value being passed to it is of the correct type, but I leave that for the reader to add if they want (along with the simple code for things like OpenLookupDataTable [ln49] and GetLookupSql [ln17]). This article is keeping to the basics of what we need to get things working and show how the classes interact.
ComboBoxCell class
Next we need the ComboBoxCell class. This is where most of the extra work is done.
Public Class ComboBoxCell Inherits DataGridViewTextBoxCell Private DisplayValue As String = Nothing Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, _ ByVal initialFormattedValue As Object, _ ByVal dataGridViewCellStyle As DataGridViewCellStyle) MyBase.InitializeEditingControl(rowIndex, _ initialFormattedValue, _ dataGridViewCellStyle) 'Cast the EditingControl to a variable we can work with Dim ctl As ComboBoxEditingControl = _ DirectCast(DataGridView.EditingControl, ComboBoxEditingControl) 'Cast the OwningColumn to a variable we can work with Dim col As ComboBoxColumn = DirectCast(Me.OwningColumn, ComboBoxColumn) 'Tell the ComboBox what to display in the drop down ctl.DataSource = col.DropDownDataSource 'Important: Tell the ComboBoxEditingControl that this is now ' the owner cell for the control ctl.OwnerCell = Me End Sub Friend Sub SetDisplayValue(ByVal NewValue As String) DisplayValue = NewValue End Sub Public Overrides ReadOnly Property EditType() As Type Get ' Return the type of the editing contol that ComboBoxCell uses. Return GetType(ComboBoxEditingControl) End Get End Property Public Overrides ReadOnly Property ValueType() As Type Get ' Return the type of the value that ComboBoxCell contains. Return GetType(Long) End Get End Property Public Overrides ReadOnly Property DefaultNewRowValue() As Object Get ' Use DBNull as the default cell value. Return DBNull.Value End Get End Property Protected Overrides Sub Paint(ByVal graphics As System.Drawing.Graphics, _ ByVal clipBounds As System.Drawing.Rectangle, _ ByVal cellBounds As System.Drawing.Rectangle, ByVal rowIndex As Integer, _ ByVal cellState As DataGridViewElementStates, _ ByVal value As Object, ByVal formattedValue As Object, _ ByVal errorText As String, ByVal cellStyle As DataGridViewCellStyle, _ ByVal advancedBorderStyle As DataGridViewAdvancedBorderStyle, _ ByVal paintParts As DataGridViewPaintParts) 'The first time in, make sure that we get the initial DisplayValue If DisplayValue Is Nothing Then SetDisplayValue(LookupDisplayValue(value)) 'Override paint to pass DisplayValue instead of formattedValue MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, _ DisplayValue, errorText, cellStyle, advancedBorderStyle, paintParts) End Sub End Class
The reason we have to reset ComboBox.DataSource [ln81] every time is due to the fact that the DataGridView only creates a single instance of ComboBoxEditingControl regardless of which cell the user attempts to edit.
We use the OwnerCell property [ln85] to ensure that the ComboBoxEditingControl knows which ComboBoxCell to communicate with when the user selected a new item in the drop down list.
Using the Paint method in this way allows us to keep the styling of the cells controlled by the underlying Microsoft provided code. [6]
The LookupDisplayValue method finds the text to display using OwningColumn.DropDownDataSource so that the first time the cell is painted we know which value to display.
ComboBoxEditingControl class
And finally, the class that only has a single instance during the lifetime of the grid... [7]
In a similar way to the CalendarColumn from MSDN we inherit from the underlying extended ComboBox control and then use the SelectedValueChanged event to update the currently connected ComboBoxCell.
Public Class ComboBoxEditingControl Inherits ExtendedComboBox Implements IDataGridViewEditingControl Private dataGridViewControl As DataGridView Private valueIsChanged As Boolean = False Private rowIndexNum As Integer Private currentCell As ComboBoxCell = Nothing Public Property OwnerCell() As ComboBoxCell Get Return currentCell End Get Set(ByVal value As ComboBoxCell) 'Clear currentCell so DoSelectedValueChanged doesn't cause an endless loop currentCell = Nothing 'Set SelectedIDValue MyBase.SelectedIDValue = value.Value 'Show that the value hasn't changed yet valueIsChanged = False 'Finally remember the new Owner Cell currentCell = value End Set End Property Public Sub ApplyCellStyleToEditingControl(_ ByVal dataGridViewCellStyle As DataGridViewCellStyle) _ Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl Me.Font = dataGridViewCellStyle.Font Me.ForeColor = dataGridViewCellStyle.ForeColor Me.BackColor = dataGridViewCellStyle.BackColor End Sub Public Property EditingControlDataGridView() As DataGridView _ Implements IDataGridViewEditingControl.EditingControlDataGridView Get Return dataGridViewControl End Get Set(ByVal value As DataGridView) dataGridViewControl = value End Set End Property Public Property EditingControlFormattedValue() As Object _ Implements IDataGridViewEditingControl.EditingControlFormattedValue Get Return MyBase.SelectedIDValue End Get Set(ByVal value As Object) MyBase.SelectedIDValue = value End Set End Property Public Property EditingControlRowIndex() As Integer _ Implements IDataGridViewEditingControl.EditingControlRowIndex Get Return rowIndexNum End Get Set(ByVal value As Integer) rowIndexNum = value End Set End Property Public Property EditingControlValueChanged() As Boolean _ Implements IDataGridViewEditingControl.EditingControlValueChanged Get Return valueIsChanged End Get Set(ByVal value As Boolean) valueIsChanged = value End Set End Property Public Function EditingControlWantsInputKey(ByVal keyData As Keys, _ ByVal dataGridViewWantsInputKey As Boolean) As Boolean _ Implements IDataGridViewEditingControl.EditingControlWantsInputKey Return True End Function Public ReadOnly Property EditingPanelCursor() As Cursor _ Implements IDataGridViewEditingControl.EditingPanelCursor Get Return MyBase.Cursor End Get End Property Public Function GetEditingControlFormattedValue( _ ByVal context As DataGridViewDataErrorContexts) As Object _ Implements IDataGridViewEditingControl.GetEditingControlFormattedValue Return MyBase.SelectedIDValue End Function Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) _ Implements IDataGridViewEditingControl.PrepareEditingControlForEdit End Sub Public ReadOnly Property RepositionEditingControlOnValueChange() As Boolean _ Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange Get Return False End Get End Property Private Sub DoSelectedValueChanged(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.SelectedValueChanged If currentCell IsNot Nothing Then 'Remember that the value has changed valueIsChanged = True 'Pass back the new ID currentCell.Value = MyBase.SelectedIDValue 'Pass back the new display value currentCell.SetDisplayValue(MyBase.Text) End If End Sub End Class
I hope these four blocks of code give a simple overview of how the custom DataGridViewColumn classes connect and interact and will make the setup a little easier for at least one other person!
Footnotes
- One option would have been to extend the DataGridViewComboBoxColumn and DataGridViewComboBoxCell classes, but this would have meant duplicating the majority of the code from the already tested control.
- There are many methods and properties in relation to custom columns that mention FormattedValue and to begin with I presumed the difference between this and Value was that one was the display value and the other was the value stored in the bound data. After spending a long time trying to make this work, all of the error messages suggested that FormattedValue and Value should in fact be left containing same data.
- My first thought was that the best place to switch from the default Grid.DataGridTextColumn to the custom column would be in the Grid.ColumnAdded event as I presumed this would be fired before any rows were added and so save the grid doing things twice. [4]
- No! If the columns are replaced in Grid.ColumnAdded then they will later be re-added outside of the developer's control [5]
- The next idea was to do it during Grid.DataBindingComplete at which point I found that this event is called twice after setting Grid.DataSource [5] and that the only safe way to switch columns was to do it on the second call to Grid.DataBindingComplete (this event is fired for many different reasons so using it seemed rather hacky).
- Finally I found the answer lay in the Grid.DataSourceChanged event which fires only once after setting Grid.DataSource and appears to work as expected.
- This was the VB6 developer in me that had previously spent years using SGrid2 and its predecessor and so presumed that the data would be fully parsed while being assigned to the grid costing cycles.
- This took a while to figure out! It appears that if the column is replaced in either the ColumnAdded event or the first firing of the DataBindingComplete event, then on the second round of binding (no one seems to understand why this has to happen twice) the original version of the custom column that had custom properties assigned to it is dumped and replaced with a fresh one that only has standard DataGridViewColumn properties assigned such as DataPropertyName.
- It was the structure of the Paint method in taking value and formattedValue parameters, and only displaying the contents of formattedValue that led me to believe that FormattedValue in the EditingControl class could be different from Cell.Value. [2]
- Although only one instance of the EditingControl is created by default, I would imagine there are methods within the custom Cell class that can be overridden to provide the developer with more flexibility.
Disclaimer: All sample code is provided for illustrative purposes only. These examples have not been thoroughly tested under all conditions. There is no guarantee or implied reliability, serviceability, or function of these programs.
By Theo Gray on June 2, 2009 | Permalink | Comment
Reader Comments
Skip to form
July 23, 2009
,Juan Lara says:Hi. Your article is great. I just have one problem though. If I have several columns of the same type and try to bind them to different sources (dataviews) I get an Argument Exception that says: "Cannot bind to the new display member. Parameter name: newDisplayMember"
Any help would be very much appreaciated. Thanks!
July 27, 2009
,Theo Gray says:Hello Juan,
Presumably you will need to change the DisplayMember property of your ComboBox before you change the DataSource at your equivalent of line 81 above to make this work in your situation.
April 20, 2010
,Jorge Mota says:Hello,
Great Job!
I have a little problem to implement my C# version of your code because I can't find the "LookupDisplayValue" Function.
This function is used in the override of Paint (line 123), and because of that I can't initialy show the data that I retrieve from DB.
Tia,
Jorge Mota
April 20, 2010
,Theo Gray says:Hello Jorge,
The LookupDisplayValue method finds the text to display using OwningColumn.DropDownDataSource so that the first time the cell is painted we know which value to display.
I have left this up to the reader to create this method as it should be very simple, but may differ depending on your data.
July 12, 2011
,Prem Singh Rawat says:Thanks ,that solved lots of my problem.
psr
February 28, 2012
,Hi says:Hi,
Can you post a sample code Custom DataGridViewColumn? thanks,
September 6, 2012
,DSB says:i want to make a custom textbox datagridvirew columns.
September 25, 2012
,Mark says:Theo, are we supposed to create the Inherits ExtendedComboBox?
September 25, 2012
,Theo Gray says:Hello Mark,
Yes, the ExtendedComboBox is fairly lightweight in terms of code and I thought would have different properties depending on what you wanted it to do. The only properties relevant to the code above are mentioned in the "On to the code" section.
February 26, 2013
,Michael Newman says:Hi Theo,
I want to cascade 2 comboboxes in a data bound datagridview. The first combobox has a display member of "Category". There may be dozens of categories stored in a database table with varing values of tax for all of them. For instance, office stationery will have tax but software may not. Office stationery will have a tax code of "S" for standard and software would have a tax code of "E" for exempt. To get around updating the tax applied to all of the categories when the government changes the rates, I have another database table matching the categories with the rates of tax. The program is to work like this:
I choose a category and that shows in the 1st combobox and the tax code, not the percentage, must appear in the second combobox. The second combobox must be selectable so that I can change the tax code manually if desired, i.e. change from "S" to "E". The "S" or "E" to pick up the tax percentage from the tax table in the database. "S" would have a tax percentage of 20 and "E" would have a tax percentage of 0 (zero).
Does this make sense and can it be done in visual basic 2008? I have seen it done in commercial programs which I think were written in VB.
Mike
February 28, 2013
,Theo Gray says:Hello Michael,
A user control with two combo boxes on and the appropriate code inside to do what you want should be easy to build and test before trying to integrate it into the grid.
Once you have that control built, it is a case of deciding how you want to return the data back from the user control as a value; possibly a comma-separated string that your code can then interpret when displaying or saving (e.g. "IDCategory,TaxCode"), or to keep things closer to the example code above, you could return a positive or negative value for IDCategory where +IDCateogy => S and -IDCategory => E.
I hope that gives you some ideas of how to progress :-)
March 4, 2013
,Michael Newman says:Hi Theo and thanks for the response.
I have managed to get it working with stand alone with 2 comboboxes but cannot integrate it into a datagridview. I will dig out the code that I have working and put that on your web site. Perhaps what I have done can be turned into a user control the same way a date column is used in a datagridview.
Your comments on my code, when submitted, will be appreciated.
Mike
March 4, 2013
,Michael Newman says:Hi Theo,
I have just looked at the project I created to test the cascading comboboxes in stand alone mode and it uses an SQL 2005 Express database. Would it be possible to put a zipped version of the project on your web site to show what I want to do in a datagridview?
Mike
December 18, 2013
,Arthur says:Can you share your complete source code (project)?
December 18, 2013
,Theo Gray says:The original code is based on Microsoft's Date/Time Picker DataGrid example.
March 28, 2015
,Vergo77 says:Hi Theo,
Graet article,
but I still have the same problem: the Combo appear only when i click on a cell to edit.
The same that happen in the "Microsoft's Date/Time Picker DataGrid example".
My question is: Is there a method to fix for each cells the view of combo, not only in "edit" mode?
Thanks.
February 22, 2017
,Zhyke says:Hi Theo,
Can you share the code for LookupDisplayValue function? I think this is also missing in my project. My datagridviewcolumn set to nothing once the Mouse leave on the datagridviewcolumn. Thanks.