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.

01.Private Sub OnDataSourceChanged(ByVal sender As Object, _
02.    ByVal e As System.EventArgs) Handles DataGridView1.DataSourceChanged
03. 
04.    Dim OriginalColumn, NewColumn As DataGridViewColumn
05.    Dim ColumnIndex As Integer
06. 
07.    For Each OriginalColumn In DataGridView1.Columns
08.        'Reset the NewColumn variable
09.        NewColumn = Nothing
10. 
11.        'Decide if we need to replace the current column
12.        Select Case GetDBType(OriginalColumn.ValueType)
13.            Case SqlDbType.BigInt, SqlDbType.Int
14.                'Replace BigInt and Int columns with the ComboBoxColumn type
15.                'Pass the Sql used to display the content of the drop down
16.                NewColumn = _
17.                    New ComboBoxColumn(GetLookupSql(OriginalColumn.Name))
18. 
19.        End Select
20. 
21.        'If we have a new column to replace
22.        If NewColumn IsNot Nothing Then
23.            'Get the current index of the old column
24.            ColumnIndex = MyBase.Columns.IndexOf(OriginalColumn)
25.            'Copy the basic information from the old column
26.            NewColumn.DataPropertyName = OriginalColumn.DataPropertyName
27.            NewColumn.HeaderText = OriginalColumn.HeaderText
28.            NewColumn.Name = OriginalColumn.Name
29.            'Remove the old column
30.            MyBase.Columns.Remove(OriginalColumn)
31.            'Add the new one where the old one used to be
32.            MyBase.Columns.Insert(ColumnIndex, NewColumn)
33.        End If
34.   Next
35.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.

40.Public Class ComboBoxColumn
41.    Inherits DataGridViewColumn
42. 
43.    Friend DropDownDataSource As DataTable
44. 
45.    Public Sub New(ByVal DataSource As String)
46.        MyBase.New(New ComboBoxCell())
47. 
48.        'Open a DataTable from the specified Sql
49.        DropDownDataSource = OpenLookupDataTable(DataSource)
50.    End Sub
51.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.

060.Public Class ComboBoxCell
061.    Inherits DataGridViewTextBoxCell
062. 
063.    Private DisplayValue As String = Nothing
064. 
065.    Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, _
066.        ByVal initialFormattedValue As Object, _
067.        ByVal dataGridViewCellStyle As DataGridViewCellStyle)
068.         
069.        MyBase.InitializeEditingControl(rowIndex, _
070.            initialFormattedValue, _
071.            dataGridViewCellStyle)
072. 
073.        'Cast the EditingControl to a variable we can work with
074.        Dim ctl As ComboBoxEditingControl = _
075.            DirectCast(DataGridView.EditingControl, ComboBoxEditingControl)
076. 
077.        'Cast the OwningColumn to a variable we can work with
078.        Dim col As ComboBoxColumn = DirectCast(Me.OwningColumn, ComboBoxColumn)
079. 
080.        'Tell the ComboBox what to display in the drop down
081.        ctl.DataSource = col.DropDownDataSource
082.         
083.        'Important: Tell the ComboBoxEditingControl that this is now
084.        '           the owner cell for the control
085.        ctl.OwnerCell = Me
086.    End Sub
087. 
088.    Friend Sub SetDisplayValue(ByVal NewValue As String)
089.        DisplayValue = NewValue
090.    End Sub
091. 
092.    Public Overrides ReadOnly Property EditType() As Type
093.        Get
094.            ' Return the type of the editing contol that ComboBoxCell uses.
095.            Return GetType(ComboBoxEditingControl)
096.        End Get
097.    End Property
098. 
099.    Public Overrides ReadOnly Property ValueType() As Type
100.        Get
101.            ' Return the type of the value that ComboBoxCell contains.
102.            Return GetType(Long)
103.        End Get
104.    End Property
105. 
106.    Public Overrides ReadOnly Property DefaultNewRowValue() As Object
107.        Get
108.            ' Use DBNull as the default cell value.
109.            Return DBNull.Value
110.        End Get
111.    End Property
112. 
113.    Protected Overrides Sub Paint(ByVal graphics As System.Drawing.Graphics, _
114.        ByVal clipBounds As System.Drawing.Rectangle, _
115.        ByVal cellBounds As System.Drawing.Rectangle, ByVal rowIndex As Integer, _
116.        ByVal cellState As DataGridViewElementStates, _
117.        ByVal value As Object, ByVal formattedValue As Object, _
118.        ByVal errorText As String, ByVal cellStyle As DataGridViewCellStyle, _
119.        ByVal advancedBorderStyle As DataGridViewAdvancedBorderStyle, _
120.        ByVal paintParts As DataGridViewPaintParts)
121.         
122.        'The first time in, make sure that we get the initial DisplayValue
123.        If DisplayValue Is Nothing Then SetDisplayValue(LookupDisplayValue(value))
124.         
125.        'Override paint to pass DisplayValue instead of formattedValue
126.        MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, _
127.            DisplayValue, errorText, cellStyle, advancedBorderStyle, paintParts)
128.    End Sub
129. 
130.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.

150.Public Class ComboBoxEditingControl
151.    Inherits ExtendedComboBox
152.    Implements IDataGridViewEditingControl
153. 
154.    Private dataGridViewControl As DataGridView
155.    Private valueIsChanged As Boolean = False
156.    Private rowIndexNum As Integer
157.    Private currentCell As ComboBoxCell = Nothing
158. 
159.    Public Property OwnerCell() As ComboBoxCell
160.        Get
161.            Return currentCell
162.        End Get
163.        Set(ByVal value As ComboBoxCell)
164.            'Clear currentCell so DoSelectedValueChanged doesn't cause an endless loop
165.            currentCell = Nothing
166.            'Set SelectedIDValue
167.            MyBase.SelectedIDValue = value.Value
168.            'Show that the value hasn't changed yet
169.            valueIsChanged = False
170.            'Finally remember the new Owner Cell
171.            currentCell = value
172.        End Set
173.    End Property
174. 
175.    Public Sub ApplyCellStyleToEditingControl(_
176.        ByVal dataGridViewCellStyle As DataGridViewCellStyle) _
177.        Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
178.         
179.        Me.Font = dataGridViewCellStyle.Font
180.        Me.ForeColor = dataGridViewCellStyle.ForeColor
181.        Me.BackColor = dataGridViewCellStyle.BackColor
182.    End Sub
183. 
184.    Public Property EditingControlDataGridView() As DataGridView _
185.        Implements IDataGridViewEditingControl.EditingControlDataGridView
186.        Get
187.            Return dataGridViewControl
188.        End Get
189.        Set(ByVal value As DataGridView)
190.            dataGridViewControl = value
191.        End Set
192.    End Property
193. 
194.    Public Property EditingControlFormattedValue() As Object _
195.        Implements IDataGridViewEditingControl.EditingControlFormattedValue
196.        Get
197.            Return MyBase.SelectedIDValue
198.        End Get
199.        Set(ByVal value As Object)
200.            MyBase.SelectedIDValue = value
201.        End Set
202.    End Property
203. 
204.    Public Property EditingControlRowIndex() As Integer _
205.        Implements IDataGridViewEditingControl.EditingControlRowIndex
206.        Get
207.            Return rowIndexNum
208.        End Get
209.        Set(ByVal value As Integer)
210.            rowIndexNum = value
211.        End Set
212.    End Property
213. 
214.    Public Property EditingControlValueChanged() As Boolean _
215.        Implements IDataGridViewEditingControl.EditingControlValueChanged
216.        Get
217.            Return valueIsChanged
218.        End Get
219.        Set(ByVal value As Boolean)
220.            valueIsChanged = value
221.        End Set
222.    End Property
223. 
224.    Public Function EditingControlWantsInputKey(ByVal keyData As Keys, _
225.        ByVal dataGridViewWantsInputKey As Boolean) As Boolean _
226.        Implements IDataGridViewEditingControl.EditingControlWantsInputKey
227.        
228.        Return True
229.    End Function
230. 
231.    Public ReadOnly Property EditingPanelCursor() As Cursor _
232.        Implements IDataGridViewEditingControl.EditingPanelCursor
233.        Get
234.            Return MyBase.Cursor
235.        End Get
236.    End Property
237. 
238.    Public Function GetEditingControlFormattedValue( _
239.        ByVal context As DataGridViewDataErrorContexts) As Object _
240.        Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
241.         
242.        Return MyBase.SelectedIDValue
243.    End Function
244. 
245.    Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) _
246.        Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
247.    End Sub
248. 
249.    Public ReadOnly Property RepositionEditingControlOnValueChange() As Boolean _
250.        Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
251.        Get
252.            Return False
253.        End Get
254.    End Property
255. 
256.    Private Sub DoSelectedValueChanged(ByVal sender As Object, _
257.        ByVal e As System.EventArgs) Handles Me.SelectedValueChanged
258. 
259.        If currentCell IsNot Nothing Then
260.            'Remember that the value has changed
261.            valueIsChanged = True
262.            'Pass back the new ID
263.            currentCell.Value = MyBase.SelectedIDValue
264.            'Pass back the new display value
265.            currentCell.SetDisplayValue(MyBase.Text)
266.        End If
267.    End Sub
268.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.

By 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.


Comment on This Article:

Your Name:
Your Email Address:
 

Your Email Address will not be made public,
but the MD5 hash of it will be used to display your Gravatar.
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.