3

I want a column of a DataGridView to use a ComboBoxStyle.DropDown style ComboBox, where the user can either select one of the entries in the drop-down, or type arbitrary text.

At the moment, I'm using the code from this answer and I can type freely into the text box part of the ComboBox, but if I type something that isn't in the drop-down list then it isn't committed back to the data source and the field reverts to the original selection. Furthermore, if I programmatically set the text to something not in the drop-down list I get a DataError event "DataGridViewComboBoxCell value is not valid."

I'm using data binding; the DataGridView itself is bound to a BindingList<T>.

Unlike this question I don't want the free text added to the drop-down list.

To be clear, the column data type is string and I don't want it validated against the drop-down list of the ComboBox (or anything else for that matter).

(Do I have to create my own custom DataGridViewColumn descendent as described in How to: Host Controls in Windows Forms DataGridView Cells?)

Community
  • 1
  • 1
Ian Goldby
  • 5,609
  • 1
  • 45
  • 81

4 Answers4

3

I found a simple, if verbose, answer. (But I'd still like to know if there is a way to do this with the standard DataGridViewComboBoxColumn type.)

I followed the method in How to: Host Controls in Windows Forms DataGridView Cells. My full solution is too long to post here, but I can summarise the changes to make it use a ComboBox instead of the example's DateTimePicker control.

  1. Rename the three classes DropDownComboBoxColumn, DropDownComboBoxCell, and DropDownComboBoxEditingControl respectively.

  2. Replace DateTime everywhere with string.

  3. Add property public ComboBoxStyle DropDownStyle { get; set; } to DropDownComboBoxColumn to allow the calling code to set the drop-down style.

  4. Remove code from DropDownComboBoxCell constructor.

  5. Remove code from DropDownComboBoxEditingControl constructor.

  6. Make DropDownComboBoxEditingControl derive from ComboBox instead of DateTimePicker.

  7. Replace OnValueChanged with OnTextChanged to account for the different naming in ComboBox versus DateTimePicker.

  8. Make the EditingControlFormattedValue property get and set the inherited Text property (instead of Value) and there is no parsing needed.

  9. Make ApplyCellStyleToEditingControl set ForeColor and BackColor instead of CalendarForeColor and CalendarMonthBackground.

  10. Make EditingControlWantsInputKey also claim F4 so it can be used to open and close the drop-down.

  11. Add the following code to PrepareEditingControlForEdit:

    DropDownComboBoxColumn col = _dataGridView.Columns[_dataGridView.CurrentCell.ColumnIndex] as DropDownComboBoxColumn;
    if (col == null)
    {
      throw new InvalidCastException("Must be in a DropDownComboBoxColumn");
    }
    DropDownStyle = col.DropDownStyle;
    // (If you don't explicitly set the Text then the current value is
    // always replaced with one from the drop-down list when edit begins.)
    Text = _dataGridView.CurrentCell.Value as string;
    SelectAll();
    

Handle the DataGridView's EditingControlShowing event as in OhBeWise's answer to a related question to set up the drop-down items and if desired the auto-completion mode:

private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
  ComboBox box = e.Control as ComboBox;
  if (box != null)
  {
    box.AutoCompleteSource = AutoCompleteSource.ListItems;
    box.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
    box.DataSource = _dropDownItems;
  }
}

If you want the same drop-down items for all rows then you could always make this a property of DropDownComboBoxColumn like DropDownStyle and set it up in PrepareEditingControlForEdit to avoid having to handle EditingControlShowing.

Community
  • 1
  • 1
Ian Goldby
  • 5,609
  • 1
  • 45
  • 81
1

Thank you Gero90 for the solution you provided!! However, I had some issues. I didn't realise that it wouldn't work if the ComboboxStyle was Simple. Also if you enter a custom value, then select the drop down and then navigate away it was resetting it back to the initial value. I've ironed out the issues that I discovered and here's the new code that can be copied if needed:

Public Class DataGridViewDropDownComboBoxColumn
    Inherits DataGridViewColumn

    Public Sub New()
        MyBase.New(New DataGridViewDropDownComboBoxCell)

    End Sub

    Public Property DropDownStyle As ComboBoxStyle
    Public Property DataSource As Object
    Public Property ValueMember As Object
    Public Property DisplayMember As Object

    Public Overrides Property CellTemplate As DataGridViewCell
        Get
            Return MyBase.CellTemplate
        End Get
        Set
            ' Ensure that the cell used for the template is a DataGridViewDropDownComboBoxCell.
            If ((Not (Value) Is Nothing) AndAlso Not Value.GetType.IsAssignableFrom(GetType(DataGridViewDropDownComboBoxCell))) Then
                Throw New InvalidCastException("Must be a DropDownCell")
            End If

            MyBase.CellTemplate = Value
        End Set
    End Property
End Class

Public Class DataGridViewDropDownComboBoxCell
    Inherits DataGridViewTextBoxCell


    Public Sub New()
        MyBase.New
    End Sub

    Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, ByVal initialFormattedValue As Object, ByVal dataGridViewCellStyle As DataGridViewCellStyle)
        ' Set the value of the editing control to the current cell value.
        MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
        Dim ctl As DataGridViewDropDownComboBoxEditingControl = CType(DataGridView.EditingControl, DataGridViewDropDownComboBoxEditingControl)
        ' Use the default row value when Value property is null.
        If (Me.Value Is Nothing) Or IsDBNull(Me.Value) Then
            ctl.Text = CType(Me.DefaultNewRowValue, String)
        Else
            ctl.Text = CType(Me.Value, String)
        End If
        'ctl.BringToFront()
        'ctl.Focus()
        'ctl.DroppedDown = True

    End Sub

    Public Overrides ReadOnly Property EditType As Type
        Get
            ' Return the type of the editing control that DataGridViewDropDownComboBoxCell uses.
            Return GetType(DataGridViewDropDownComboBoxEditingControl)
        End Get
    End Property

    Public Overrides ReadOnly Property ValueType As Type
        Get
            ' Return the type of the value that DataGridViewDropDownComboBoxCell contains.
            Return GetType(String)
        End Get
    End Property

    Public Overrides ReadOnly Property DefaultNewRowValue As Object
        Get
            ' Use the current date and time as the default value.
            Return ""
        End Get
    End Property
End Class
Class DataGridViewDropDownComboBoxEditingControl
    Inherits ComboBox
    Implements IDataGridViewEditingControl

    Private dataGridView As DataGridView

    Private valueChanged As Boolean = False

    Private rowIndex As Integer

    Public Sub New()
        MyBase.New
    End Sub

    Public Shadows Property DropDownStyle() As ComboBoxStyle
        Get
            Return MyBase.DropDownStyle
        End Get
        Set(ByVal value As ComboBoxStyle)
            If value = ComboBoxStyle.Simple Then
                'Throw New NotSupportedException("ComboBoxStyle.Simple not supported")
                value = ComboBoxStyle.DropDown
            End If
            MyBase.DropDownStyle = value
        End Set
    End Property

    ' Implements the IDataGridViewEditingControl.EditingControlFormattedValue 
    ' property.
    Public Property EditingControlFormattedValue As Object Implements IDataGridViewEditingControl.EditingControlFormattedValue
        Get
            Return Me.Text
        End Get
        Set
            If (TypeOf Value Is String) Then
                Me.Text = Value
            End If

        End Set
    End Property

    ' Implements the 
    ' IDataGridViewEditingControl.GetEditingControlFormattedValue method.
    Public Function GetEditingControlFormattedValue(ByVal context As DataGridViewDataErrorContexts) As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
        Return Me.EditingControlFormattedValue
    End Function

    ' Implements the 
    ' IDataGridViewEditingControl.ApplyCellStyleToEditingControl method.
    Public Sub ApplyCellStyleToEditingControl(ByVal dataGridViewCellStyle As DataGridViewCellStyle) Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
        Me.Font = dataGridViewCellStyle.Font
        Me.ForeColor = dataGridViewCellStyle.ForeColor
        Me.BackColor = dataGridViewCellStyle.BackColor
    End Sub

    ' Implements the IDataGridViewEditingControl.EditingControlRowIndex 
    ' property.
    Public Property EditingControlRowIndex As Integer Implements IDataGridViewEditingControl.EditingControlRowIndex
        Get
            Return Me.rowIndex
        End Get
        Set
            Me.rowIndex = Value
        End Set
    End Property

    ' Implements the IDataGridViewEditingControl.EditingControlWantsInputKey 
    ' method.
    Public Function EditingControlWantsInputKey(ByVal key As Keys, ByVal dataGridViewWantsInputKey As Boolean) As Boolean Implements IDataGridViewEditingControl.EditingControlWantsInputKey
        ' Let the DateTimePicker handle the keys listed.
        Select Case ((key And Keys.KeyCode))
            Case Keys.Left, Keys.Up, Keys.Down, Keys.Right, Keys.Home, Keys.End, Keys.PageDown, Keys.PageUp, Keys.F4
                Return True
            Case Else
                Return Not dataGridViewWantsInputKey
        End Select

    End Function

    ' Implements the IDataGridViewEditingControl.PrepareEditingControlForEdit 
    ' method.
    Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
        Dim col As DataGridViewDropDownComboBoxColumn = CType(dataGridView.Columns(dataGridView.CurrentCell.ColumnIndex), DataGridViewDropDownComboBoxColumn)
        If (col Is Nothing) Then
            Throw New InvalidCastException("Must be in a DropDownComboBoxColumn")
        End If


        DropDownStyle = col.DropDownStyle

        Items.Clear()
        If IsDBNull(dataGridView.CurrentCell.Value) Then
            Text = ""
        Else
            Text = CType(dataGridView.CurrentCell.Value, String)
        End If
        Items.Add(Text)

        Dim dt As DataTable = New DataTable
        Dim ct = 0, cx = 0
        Try
            dt = DirectCast(col.DataSource, DataTable)
            If Not col.DisplayMember Is Nothing Then
                For Each c As DataColumn In dt.Columns
                    If c.ColumnName = col.DisplayMember Then
                        cx = ct
                    End If
                    ct += 1
                Next
            End If
            For Each r As DataRow In dt.Rows
                If Not col.DisplayMember Is Nothing Then
                    If Not Items.Contains(r(cx)) Then Items.Add(r(cx))
                Else
                    If dt.Columns.Count = 1 Then
                        If Not Items.Contains(r(0)) Then Items.Add(r(0))
                    Else
                        If Not Items.Contains(r(dt.Columns.Count - 1)) Then Items.Add(r(dt.Columns.Count - 1))
                    End If
                End If
            Next
        Catch ex As Exception
        End Try

        'DropDownStyle = col.DropDownStyle
        'ValueMember = col.ValueMember
        'DisplayMember = col.DisplayMember
        'DataSource = col.DataSource

        ' (If you don't explicitly set the Text then the current value is
        ' always replaced with one from the drop-down list when edit begins.)
        'If IsDBNull(dataGridView.CurrentCell.Value) Then
        '    Text = ""
        'Else
        '    Text = CType(dataGridView.CurrentCell.Value, String)
        'End If

    End Sub

    ' Implements the IDataGridViewEditingControl
    ' .RepositionEditingControlOnValueChange property.
    Public ReadOnly Property RepositionEditingControlOnValueChange As Boolean Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
        Get
            Return False
        End Get
    End Property

    ' Implements the IDataGridViewEditingControl
    ' .EditingControlDataGridView property.
    Public Property EditingControlDataGridView As DataGridView Implements IDataGridViewEditingControl.EditingControlDataGridView
        Get
            Return Me.dataGridView
        End Get
        Set
            Me.dataGridView = Value
        End Set
    End Property

    ' Implements the IDataGridViewEditingControl
    ' .EditingControlValueChanged property.
    Public Property EditingControlValueChanged As Boolean Implements IDataGridViewEditingControl.EditingControlValueChanged
        Get
            Return Me.valueChanged
        End Get
        Set
            Me.valueChanged = Value
        End Set
    End Property

    ' Implements the IDataGridViewEditingControl
    ' .EditingPanelCursor property.
    Public ReadOnly Property EditingPanelCursor As Cursor Implements IDataGridViewEditingControl.EditingPanelCursor
        Get
            Return MyBase.Cursor
        End Get
    End Property

    Protected Overrides Sub OnTextChanged(ByVal eventargs As EventArgs)
        ' Notify the DataGridView that the contents of the cell
        ' have changed.
        Me.valueChanged = True
        Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)
        MyBase.OnTextChanged(eventargs)
    End Sub

    Protected Overrides Sub OnSelectedIndexChanged(ByVal e As EventArgs)
        ' Notify the DataGridView that the contents of the cell
        ' have changed.
        Me.valueChanged = True
        Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)
        MyBase.OnSelectedIndexChanged(e)
    End Sub
End Class
Sidupac
  • 651
  • 7
  • 11
0

I have a solution that builds on the Solution of Ian Goldby.

The only difference is that the DataSource, DisplayMember and ValueMember can be set like for a normal DataGridViewComboBoxColumn. This means the event "EditingControlShowing" does not have to be changed as in Ian Goldby's solution.

The result is also a little different in that values - that are not "part of the DataSource" - added into the DataGridViewDropDownComboBoxColumn will not be added to the DropDown.

See below for the VB code:

Public Class DataGridViewDropDownComboBoxColumn
    Inherits DataGridViewColumn

    Public Sub New()
        MyBase.New(New DataGridViewDropDownComboBoxCell)

    End Sub

    Public Property DropDownStyle As ComboBoxStyle
    Public Property DataSource As Object
    Public Property ValueMember As Object
    Public Property DisplayMember As Object

    Public Overrides Property CellTemplate As DataGridViewCell
        Get
            Return MyBase.CellTemplate
        End Get
        Set
            ' Ensure that the cell used for the template is a DataGridViewDropDownComboBoxCell.
            If ((Not (Value) Is Nothing) AndAlso Not Value.GetType.IsAssignableFrom(GetType(DataGridViewDropDownComboBoxCell))) Then
                Throw New InvalidCastException("Must be a DropDownCell")
            End If

            MyBase.CellTemplate = Value
        End Set
    End Property
End Class
Public Class DataGridViewDropDownComboBoxCell
    Inherits DataGridViewTextBoxCell

    Public Sub New()
        MyBase.New
    End Sub

    Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer, ByVal initialFormattedValue As Object, ByVal dataGridViewCellStyle As DataGridViewCellStyle)
        ' Set the value of the editing control to the current cell value.
        MyBase.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle)
        Dim ctl As DataGridViewDropDownComboBoxEditingControl = CType(DataGridView.EditingControl, DataGridViewDropDownComboBoxEditingControl)
        ' Use the default row value when Value property is null.
        If (Me.Value Is Nothing) Then
            ctl.Text = CType(Me.DefaultNewRowValue, String)
        Else
            ctl.Text = CType(Me.Value, String)
        End If

    End Sub

    Public Overrides ReadOnly Property EditType As Type
        Get
            ' Return the type of the editing control that DataGridViewDropDownComboBoxCell uses.
            Return GetType(DataGridViewDropDownComboBoxEditingControl)
        End Get
    End Property

    Public Overrides ReadOnly Property ValueType As Type
        Get
            ' Return the type of the value that DataGridViewDropDownComboBoxCell contains.
            Return GetType(String)
        End Get
    End Property

    Public Overrides ReadOnly Property DefaultNewRowValue As Object
        Get
            ' Use the current date and time as the default value.
            Return ""
        End Get
    End Property
End Class
Class DataGridViewDropDownComboBoxEditingControl
    Inherits ComboBox
    Implements IDataGridViewEditingControl

    Private dataGridView As DataGridView

    Private valueChanged As Boolean = False

    Private rowIndex As Integer

    Public Sub New()
        MyBase.New
    End Sub

    ' Implements the IDataGridViewEditingControl.EditingControlFormattedValue 
    ' property.
    Public Property EditingControlFormattedValue As Object Implements IDataGridViewEditingControl.EditingControlFormattedValue
        Get
            Return Me.Text
        End Get
        Set
            If (TypeOf Value Is String) Then
                Me.Text = Value
            End If

        End Set
    End Property

    ' Implements the 
    ' IDataGridViewEditingControl.GetEditingControlFormattedValue method.
    Public Function GetEditingControlFormattedValue(ByVal context As DataGridViewDataErrorContexts) As Object Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
        Return Me.EditingControlFormattedValue
    End Function

    ' Implements the 
    ' IDataGridViewEditingControl.ApplyCellStyleToEditingControl method.
    Public Sub ApplyCellStyleToEditingControl(ByVal dataGridViewCellStyle As DataGridViewCellStyle) Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
        Me.Font = dataGridViewCellStyle.Font
        Me.ForeColor = dataGridViewCellStyle.ForeColor
        Me.BackColor = dataGridViewCellStyle.BackColor
    End Sub

    ' Implements the IDataGridViewEditingControl.EditingControlRowIndex 
    ' property.
    Public Property EditingControlRowIndex As Integer Implements IDataGridViewEditingControl.EditingControlRowIndex
        Get
            Return Me.rowIndex
        End Get
        Set
            Me.rowIndex = Value
        End Set
    End Property

    ' Implements the IDataGridViewEditingControl.EditingControlWantsInputKey 
    ' method.
    Public Function EditingControlWantsInputKey(ByVal key As Keys, ByVal dataGridViewWantsInputKey As Boolean) As Boolean Implements IDataGridViewEditingControl.EditingControlWantsInputKey
        ' Let the DateTimePicker handle the keys listed.
        Select Case ((key And Keys.KeyCode))
            Case Keys.Left, Keys.Up, Keys.Down, Keys.Right, Keys.Home, Keys.End, Keys.PageDown, Keys.PageUp, Keys.F4
                Return True
            Case Else
                Return Not dataGridViewWantsInputKey
        End Select

    End Function

    ' Implements the IDataGridViewEditingControl.PrepareEditingControlForEdit 
    ' method.
    Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
        Dim col As DataGridViewDropDownComboBoxColumn = CType(dataGridView.Columns(dataGridView.CurrentCell.ColumnIndex), DataGridViewDropDownComboBoxColumn)
        If (col Is Nothing) Then
            Throw New InvalidCastException("Must be in a DropDownComboBoxColumn")
        End If

        DropDownStyle = col.DropDownStyle
        DataSource = col.DataSource
        ValueMember = col.ValueMember
        DisplayMember = col.DisplayMember

        ' (If you don't explicitly set the Text then the current value is
        ' always replaced with one from the drop-down list when edit begins.)
        Text = CType(dataGridView.CurrentCell.Value, String)
    End Sub

    ' Implements the IDataGridViewEditingControl
    ' .RepositionEditingControlOnValueChange property.
    Public ReadOnly Property RepositionEditingControlOnValueChange As Boolean Implements IDataGridViewEditingControl.RepositionEditingControlOnValueChange
        Get
            Return False
        End Get
    End Property

    ' Implements the IDataGridViewEditingControl
    ' .EditingControlDataGridView property.
    Public Property EditingControlDataGridView As DataGridView Implements IDataGridViewEditingControl.EditingControlDataGridView
        Get
            Return Me.dataGridView
        End Get
        Set
            Me.dataGridView = Value
        End Set
    End Property

    ' Implements the IDataGridViewEditingControl
    ' .EditingControlValueChanged property.
    Public Property EditingControlValueChanged As Boolean Implements IDataGridViewEditingControl.EditingControlValueChanged
        Get
            Return Me.valueChanged
        End Get
        Set
            Me.valueChanged = Value
        End Set
    End Property

    ' Implements the IDataGridViewEditingControl
    ' .EditingPanelCursor property.
    Public ReadOnly Property EditingPanelCursor As Cursor Implements IDataGridViewEditingControl.EditingPanelCursor
        Get
            Return MyBase.Cursor
        End Get
    End Property

    Protected Overrides Sub OnTextChanged(ByVal eventargs As EventArgs)
        ' Notify the DataGridView that the contents of the cell
        ' have changed.
        Me.valueChanged = True
        Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)
        MyBase.OnTextChanged(eventargs)
    End Sub
End Class
Gero90
  • 1
  • 1
-1

For that you can add column in data grid view via back end code mentioned below:

using System.Data.SqlServerCe;

string sqlConnection = "Data Source";
SqlCeConnection conn = new SqlCeConnection(sqlConnection);
//Get bind from database.
string qryGetCategory = "Query to get data for combo box";
SqlCeCommand cmdCat = new SqlCeCommand(qryGetCategory, conn);
SqlCeDataAdapter daCat = new SqlCeDataAdapter(qryGetCategory, conn);
DataTable dtCat = new DataTable();
daCat.Fill(dtCat);

//Combobox column.
DataGridViewComboBoxColumn ComboBoxCol = new DataGridViewComboBoxColumn();
ComboBoxCol.DataSource = dtCat;
ComboBoxCol.Name = "Column name";
ComboBoxCol.ValueMember = "Value of member";
ComboBoxCol.DisplayMember = "Member to be show";
ComboBoxCol.DropDownStyle = ComboBoxStyle.DropDown;
datagridview.Columns.Add(ComboBoxCol);

Please try it may be it resolve your problem.

Nayan Katkani
  • 806
  • 8
  • 18
  • @Ian Goldby, Please mark it as answer if it is helpful so other's can also get benefit from it and in case you have any query or problem please feel free to ask me. – Nayan Katkani Dec 22 '16 at 11:57
  • Could you elaborate on what is different about this approach to those in the links I provided in the question? It's not especially helpful just to paste a block of code. – Ian Goldby Dec 22 '16 at 13:06
  • 2
    Also, `DropDownStyle` is not a property of `DataGridViewComboBoxColumn`. Is this a typo? – Ian Goldby Dec 22 '16 at 13:07