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.
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. 
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. 
- At what point should I replace standard columns with custom ones?
Use the DataGridView.DataSourceChanged event. 
- 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.
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.
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.
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. 
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.
And finally, the class that only has a single instance during the lifetime of the grid... 
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!
- 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. 
- No! If the columns are replaced in Grid.ColumnAdded then they will later be re-added outside of the developer's control 
- 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  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. 
- 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.Theo Gray on June 2, 2009 | Permalink | Comment