Question How do I host an arbitrary custom control in a DataGridView cell?

Joined
Sep 16, 2023
Messages
20
Programming Experience
10+
WHAT I HAVE:
Visual Basic 2019, .NET Framwork 4.0+, WinForms

MY ISSUE:
I've read the help topic "How to: Host Controls in Windows Forms DataGridView Cells", which shows how to host a DateTimePicker control in a cell via having a class that derives from it and implements IDataGridViewEditingControl, and having classes that derive from DataGridViewColumn and DataGridViewCell. I've since tried to rework the code to host a custom control I've created, ExtComboBox. (I know there's a column type for the standard combo box, but here I need my custom control!)

The code below has an InformationColumn class that derives from DataGridViewColumn and an InformationCell class that derives from DataGridViewTextCell (similar to the help example). It then has an InformationSelectionControl class that derives from ExtComboBox (my custom control type) and implements IDataGridViewEditingControl. I've tried to follow the provided help example as closely as possible while making the changes needed to accommodate a different kind of control being hosted.

The problem occurs when I try to add an InformationColumn at design time (after a build). Any such columns I add disappear (!!) from the grid as soon as I exit the Add Columns dialog. Also, if I, using Edit Colmuns, try to change a column to InformationColumn type, it tells me that the property change isn't valid (it says "object not set to a reference", and gives me no ability to trace the error to the offended statement [or lack of a statement]). Is it not possible to add a column hosting a custom control at design time, or am I doing something wrong?! In general, is there anything in the code below that would keep the column from being properly added at run-time?

Code as follows:

ExtComboBox control hosted in DataGridView cell:
Public Class InformationColumn

    Inherits DataGridViewColumn



    Public Sub New()

    MyBase.New(New InformationCell())

    End Sub



    Public Overrides Property CellTemplate() As DataGridViewCell

    Get

        Return MyBase.CellTemplate

    End Get

    Set(value As DataGridViewCell)

        If value IsNot Nothing _

                AndAlso Not value.GetType.IsAssignableFrom(GetType(InformationCell)) Then

            Throw New InvalidCastException("Cell must be of InformationCell type!")

        End If

        MyBase.CellTemplate = value

    End Set

    End Property

End Class



Public Class InformationCell

    Inherits DataGridViewTextBoxCell



    Dim InformationSelectionControl As InformationSelectionControl



    Public Sub New()

'    MyBase.New()

    With Me.Style

        .NullValue = "" : .Format = ""

        .Alignment = DataGridViewContentAlignment.MiddleCenter

        Dim Column As DataGridViewColumn = Me.OwningColumn, _

            ColumnFont As Font = Column.DefaultCellStyle.Font

        Select Case Column.Name

           Case "dgvc" & NameOf(Residence)

                '   2 lines per entry

                .Font = New Font(ColumnFont.FontFamily, ColumnFont.SizeInPoints * 2F)

            Case Else

                '   1 line per entry

                .Font = ColumnFont

        End Select

    End With

    End Sub



    Public Overrides ReadOnly Property EditType() As Type

    Get

        Return GetType(InformationSelectionControl)

    End Get

    End Property



    Public Overrides ReadOnly Property DefaultNewRowValue() As Object

    Get

        Return ""

    End Get

    End Property



    Public Overrides ReadOnly Property ValueType() As Type

    Get

        Return GetType(String)

    End Get

    End Property



    Public Overrides Sub InitializeEditingControl(ByVal RowIndex As Integer, _

        ByVal InitialFormattedValue As Object, ByVal DataGridViewCellStyle As DataGridViewCellStyle)

    MyBase.InitializeEditingControl(RowIndex, InitialFormattedValue, DataGridViewCellStyle)

    With Me.DataGridView

        MyBase.InitializeEditingControl(RowIndex, _

            InitialFormattedValue, DataGridViewCellStyle)

        Dim isc As InformationSelectionControl = _

            DirectCast(.EditingControl, InformationSelectionControl)

        If Not isc.DroppedDown Then

            '   force filling of list

            isc.DroppedDown = True : isc.DroppedDown = False

        End If

        If Me.Value Is Nothing Then

            isc.SelectedIndex = -1

         Else

            isc.SelectedIndex = isc.FindStringExact(Me.Value.ToString)

        End If

    End With

    End Sub



    Protected Overrides Function GetValue(rowIndex As Integer) As Object

    With Me.DataGridView

        Dim isc As InformationSelectionControl = _

            DirectCast(.EditingControl, InformationSelectionControl)

        Return isc.SelectedItem.ToString

    End With

    End Function



    Protected Overrides Function SetValue(rowIndex As Integer, value As Object) As Boolean

    With Me.DataGridView

        Dim isc As InformationSelectionControl = _

            DirectCast(.EditingControl, InformationSelectionControl)

        If value Is Nothing Then

            isc.SelectedIndex = -1

         Else

            isc.SelectedIndex = isc.FindStringExact(Me.Value.ToString)

        End If

        Return isc.SelectedIndex > -1

    End With

    End Function

End Class



Public Class InformationSelectionControl

    Inherits ExtComboBox

    Implements IDataGridViewEditingControl



    Private dgvControl As DataGridView

    Private IsValueChanged As Boolean = False

    Private RowIndex As Integer



    Public Sub New()

        MyBase.New()

        Me.DropDownHorizontalScrollbar = True : Me.DropDownHorizontalExtent = -1

        Me.DropDownStyle = ComboBoxStyle.DropDownList

    End Sub



    Public Property EditingControlDataGridView As DataGridView _

        Implements IDataGridViewEditingControl.EditingControlDataGridView

    Get

        Return dgvControl

    End Get

    Set(value As DataGridView)

        dgvControl = value

    End Set

    End Property



    Public Property EditingControlFormattedValue() As Object _

        Implements IDataGridViewEditingControl.EditingControlFormattedValue

    Get

        Return Me.SelectedItem.ToString

    End Get

    Set(value As Object)

        Me.SelectedIndex = Me.FindStringExact(value.ToString)

    End Set

    End Property



    Public Property EditingControlRowIndex() As Integer _

        Implements IDataGridViewEditingControl.EditingControlRowIndex

    Get

        Return RowIndex

    End Get

    Set(value As Integer)

        RowIndex = value

    End Set

    End Property



    Public Property EditingControlValueChanged() As Boolean _

        Implements IDataGridViewEditingControl.EditingControlValueChanged

    Get

        Return IsValueChanged

    End Get

    Set(value As Boolean)

        IsValueChanged = value

    End Set

    End Property



    Public ReadOnly Property EditingPanelCursor() As Cursor _

        Implements IDataGridViewEditingControl.EditingPanelCursor

    Get

        Return MyBase.Cursor

    End Get

    End Property



    Public ReadOnly Property RepositionEditingControlOnValueChange() As Boolean _

        Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange

    Get

        Return False

    End Get

    End Property



    Public Sub ApplyCellStyleToEditingControl(dataGridViewCellStyle As DataGridViewCellStyle) _

        Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl

    With dataGridViewCellStyle

        Me.Font = .Font : Me.ForeColor = .ForeColor : Me.BackColor = .BackColor

    End With

    Exit Sub

    With dataGridViewCellStyle

        Me.Font = New Font(.Font.FontFamily.Name, 9.0F, .Font.Style)

        Me.ForeColor = .ForeColor : Me.BackColor = .BackColor

    End With

    With Me.EditingControlDataGridView

        Me.DrawMode = ExtListAndCombo.DrawMode.UseVariableHeight

        Dim Cell As DataGridViewCell = .CurrentCell

        If Cell IsNot Nothing AndAlso Cell.OwningColumn IsNot Nothing Then

            Select Case Cell.OwningColumn.Name

               Case "dgvc" & NameOf(Residence)

                    '   2 lines per entry

                    Me.Font = New Font(Me.Font.FontFamily, Me.Font.SizeInPoints * 2F)

            End Select

        End If

    End With

    End Sub



    Public Sub PrepareEditingControlForEdit(selectAll As Boolean) _

        Implements IDataGridViewEditingControl.PrepareEditingControlForEdit

    '   no preparation needed

    End Sub



    Public Function EditingControlWantsInputKey(keyData As Keys, _

            dataGridViewWantsInputKey As Boolean) As Boolean _

        Implements IDataGridViewEditingControl.EditingControlWantsInputKey

    '   anything but Tab key

    If (keyData And &HFFFF) <> Keys.Tab Then

        Return True

     Else

        Return Not dataGridViewWantsInputKey

    End If

    End Function



    Public Function GetEditingControlFormattedValue(context As DataGridViewDataErrorContexts) As Object _

        Implements IDataGridViewEditingControl.GetEditingControlFormattedValue

    Return Me.SelectedItem

    End Function



    Protected Overrides Sub OnSelectedIndexChanged(e As EventArgs)

    IsValueChanged = True : Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)

    MyBase.OnSelectedIndexChanged(e)

    End Sub

End Class

Please give any answers and corrections ASAP, in VB.NET, and as simply as possible. (BTW, Residence is a type defined elsewhere in my project.)
 
Last edited:
Solution
I eliminated the column-and-font getting logic from the InformationCell constructor and made the following changes to its class:

Protected Overrides Function GetValue(rowIndex As Integer) As Object
If Me.DataGridView Is Nothing Then
Return ""
End If

With Me.DataGridView
Dim isc As InformationSelectionControl = _
DirectCast(.EditingControl, InformationSelectionControl)
Return isc.SelectedItem.ToString
End With
End Function

Protected Overrides Function SetValue(rowIndex As Integer, value As Object) As Boolean
If Me.DataGridView Is Nothing Then
Return False
End If

With Me.DataGridView
Dim isc As InformationSelectionControl = _...
The first thing I would do is add the normal combobox to the grid as a test to see if it works okay, if so, then move on to add your custom control. This way you're limiting the number of possible hurdles.

EDIT: also, which framework are you using? 4.8, 5, 6, 7?
 
The first thing I would do is add the normal combobox to the grid as a test to see if it works okay, if so, then move on to add your custom control. This way you're limiting the number of possible hurdles.

EDIT: also, which framework are you using? 4.8, 5, 6, 7?

There already is a column type for the “normal” combo box, so I would expect that to work;—in fact, adding that column-type (good for most purposes but inadequate for mine) does work so I think I’ve already cleared that hurdle. My framework is 4.0 and up; for this app, I’m using 4.6.1. When it comes to adding a column at design time, the type for the standard combo box column inserts fine; it’s the column type I’ve created for my own control doesn’t get permanently inserted. The help example for hosting a date-time-picker shows its custom column being added at run time.
I need to know if a custom column type can be added at design time, or if it has to be added at run time—particularly if the code for implementing the column type is in the same project as the data grid view control. Also, I need to know if there are any clear coding errors in my implementation code for my ExtComboBox-derived custom column type that would keep the column from at least being added at run time. My code for the form hosting and filling the DataGridView control is kind of complex and unfinished—which makes it a little hard to properly test at home yet—so I’d like an early heads up if any changes are clearly needed to the code I’ve provided here. As far as I know, there are no general “code templates” for hosting an arbitrary control in a a DGV column, so it’d be nice if someone could give the code I’ve provided a quick looksy to see if there’s a reason the column won’t add. Is there anything there that’s wrong or omitted?! As I’ve indicated, there seems to be no way to debug the problem when using the grid’s property pages to add the column; it simply won’t tell me exactly why it won’t add!
 
The article you mentioned is here: Host Controls in DataGridView Cells - Windows Forms .NET Framework
I copied the code for CalendarColumn/CalendarCell/CalendarEditingControl and could add the custom column in designer.

Then I copying your code, add this just to get it to compile:
VB.NET:
Public Class Residence
End Class
Public Class ExtComboBox
    Inherits ComboBox
End Class
It does not add in designer, the most likely cause is that the code is throwing exceptions.

I started another instance of Visual Studio with no solution loaded and used Attach To Process to debug the other devenv.exe process. When I try to add your column there sure enough it throws NullReferenceException on this line in InformationCell constructor:
VB.NET:
ColumnFont As Font = Column.DefaultCellStyle.Font
Column is Nothing here. The following code that also uses Column would also throw NullReferenceExceptions.
I just comment that styling code out to move on.

Recompile, Attach debugger again, try to add column now it throws NullReferenceException again in InformationCell.SetValue where you refer to Me.DataGridView which is Nothing.
So there seems to be some things to look into and fix here.
 
I eliminated the column-and-font getting logic from the InformationCell constructor and made the following changes to its class:

Protected Overrides Function GetValue(rowIndex As Integer) As Object
If Me.DataGridView Is Nothing Then
Return ""
End If

With Me.DataGridView
Dim isc As InformationSelectionControl = _
DirectCast(.EditingControl, InformationSelectionControl)
Return isc.SelectedItem.ToString
End With
End Function

Protected Overrides Function SetValue(rowIndex As Integer, value As Object) As Boolean
If Me.DataGridView Is Nothing Then
Return False
End If

With Me.DataGridView
Dim isc As InformationSelectionControl = _
DirectCast(.EditingControl, InformationSelectionControl)
If value Is Nothing Then
isc.SelectedIndex = -1
Else
isc.SelectedIndex = isc.FindStringExact(Me.Value.ToString)
End If
Return isc.SelectedIndex > -1
End With
End Function

This seems to allow me to permanently add the custom columns at design time! I plan to check my code for other cases where the grid or a column might not yet be available to provide conditional logic.
 
Solution
Back
Top