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