Skip navigation.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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]
  7. 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.


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.


Comment on This Article:

Your Name:
Your Email Address:
 

Your Email Address will not be made public.
Comment:
All HTML, except <i>, <b>, <u> will require your comment to be moderated before it is publicly displayed.
 
If you would like your own avatar displayed, read about comment avatars.