So I've made a custom DataGridView-Column which is showing a DropDown for multiselecting different values and displaying them with a separator.
Picture of Cell with dropdown and selectedvalues
It's working fine so far, but the saved value is not displayed in the cell. Displaying the value after selecting them is working fine, but after reloading the cell is empty.
I think I've missed an Eventhandler which initializes "Me.Value"...
Code from Column-Class:
Imports System.Data
Imports System.ComponentModel
Public Class dgvCheckedComboBoxColumn
Inherits DataGridViewColumn
Private m_dt As DataTable
<Browsable(True), Category("Daten"), Description("Only DataTable allowed!")>
Public Shadows Property DataSource() As DataTable
Get
Return m_dt
End Get
Set(value As DataTable)
m_dt = value
End Set
End Property
Private m_DisplayMember As String
<Browsable(True), Category("Daten"), Description("The column that contains the displayed value in the dropdown")>
Public Shadows Property DisplayMember() As String
Get
Return m_DisplayMember
End Get
Set(value As String)
m_DisplayMember = value
End Set
End Property
Private m_ValueMember As String
<Browsable(True), Category("Daten"), Description("ValueMember")>
Public Shadows Property ValueMember As String
Get
Return m_ValueMember
End Get
Set(value As String)
m_ValueMember = value
'Primary-Key von DataSource entspricht Value-Member
If Me.DataSource IsNot Nothing Then
m_dt.PrimaryKey = New DataColumn() {m_dt.Columns(value)}
End If
End Set
End Property
Private m_CellDisplayMember As String
<Browsable(True), Category("Daten"), Description("Displayed Cell-Value")>
Public Property CellDisplayMember As String
Get
Return If(String.IsNullOrEmpty(m_CellDisplayMember), m_DisplayMember, m_CellDisplayMember)
End Get
Set(value As String)
m_CellDisplayMember = value
End Set
End Property
Private m_DropDownWidth As Integer
<DefaultValue(150), Browsable(True), Category("Visual"), Description("DropDown-Width")>
Public Property DropDownWidth As Integer
Get
Return m_DropDownWidth
End Get
Set(value As Integer)
m_DropDownWidth = value
End Set
End Property
Private m_UseValidation As Boolean
<DefaultValue(False), Browsable(True), Category("Data"), Description("Only allow values that exist in datasource")>
Public Property UseValidation As Boolean
Get
Return m_UseValidation
End Get
Set(value As Boolean)
m_UseValidation = value
End Set
End Property
Public Sub New()
MyBase.New(New dgvCheckedComboBoxCell())
Me.CellTemplate = New dgvCheckedComboBoxCell
Me.ReadOnly = True
End Sub
Public Overrides Function Clone() As Object
Dim copy As dgvCheckedComboBoxColumn = TryCast(MyBase.Clone(), dgvCheckedComboBoxColumn)
copy.DataSource = m_dt
copy.ValueMember = m_ValueMember
copy.DisplayMember = m_DisplayMember
copy.CellDisplayMember = m_CellDisplayMember
copy.DropDownWidth = m_DropDownWidth
copy.UseValidation = m_UseValidation
Return copy
End Function
End Class
Code from CustomCell:
Imports System.Data
Public Class dgvCheckedComboBoxCell
Inherits DataGridViewTextBoxCell
Private m_ucCheckList As UserControl
Public Sub New()
MyBase.New()
End Sub
#Region " Properties"
Private m_CheckedValues As DataTable
''' <summary>
''' Contains the selected values from current cell
''' </summary>
''' <returns></returns>
Public Property CheckedValues As DataTable
Get
Dim col As dgvCheckedComboBoxColumn = CType(Me.OwningColumn, dgvCheckedComboBoxColumn)
If m_CheckedValues Is Nothing AndAlso col.DataSource IsNot Nothing Then
Dim dt As DataTable = DirectCast(col.DataSource, DataTable).Clone
m_CheckedValues = dt
End If
Return m_CheckedValues
End Get
Set(value As DataTable)
m_CheckedValues = value
End Set
End Property
''' <summary>
''' Contains the selected values separated by String by ValueMember
''' </summary>
''' <returns></returns>
Private ReadOnly Property SelectedValue As String
Get
Dim col = DirectCast(Me.OwningColumn, dgvCheckedComboBoxColumn)
Try
Return String.Join(",", CheckedValues?.Rows.Cast(Of DataRow).Select(Function(r) r.Item(col.ValueMember).ToString))
Catch ex As Exception
Return ""
End Try
End Get
End Property
Private ReadOnly Property SelectedText As String
Get
Dim col = DirectCast(Me.OwningColumn, dgvCheckedComboBoxColumn)
Try
Return String.Join(",", CheckedValues?.Rows.Cast(Of DataRow).Select(Function(r) r.Item(col.CellDisplayMember).ToString))
Catch ex As Exception
Return ""
End Try
End Get
End Property
#End Region
#Region " Cell-Events"
Protected Overrides Sub OnMouseEnter(rowIndex As Integer)
MyBase.OnMouseEnter(rowIndex)
End Sub
Protected Overrides Sub OnEnter(rowIndex As Integer, throughMouseClick As Boolean)
MyBase.OnEnter(rowIndex, throughMouseClick)
End Sub
Protected Overrides Sub OnLeave(rowIndex As Integer, throughMouseClick As Boolean)
If DataGridView.Controls.Contains(m_ucCheckList) Then
m_ucCheckList.Visible = False
End If
Me.Value = Me.SelectedValue
MyBase.OnLeave(rowIndex, throughMouseClick)
End Sub
Protected Overrides Sub OnMouseClick(e As DataGridViewCellMouseEventArgs)
MyBase.OnMouseClick(e)
OpenDropDown()
End Sub
Private Sub OpenDropDown(Optional CursorLocation As Point = Nothing)
Try
If Me.DataGridView.Columns.Count = 0 Or RowIndex = -1 Then
Exit Sub
End If
Dim col As dgvCheckedComboBoxColumn = CType(Me.OwningColumn, dgvCheckedComboBoxColumn)
If col.DataSource Is Nothing OrElse CType(col.DataSource, DataTable).Rows.Count = 0 Then Exit Sub
If m_ucCheckList Is Nothing Then
m_ucCheckList = New UserControl
m_ucCheckList.Padding = New Padding(5, 0, 0, 0)
m_ucCheckList.BorderStyle = BorderStyle.FixedSingle
End If
If Not IsNothing(CursorLocation) Then
Dim ctrl = DataGridView.GetChildAtPoint(CursorLocation, GetChildAtPointSkip.Disabled)
End If
SetDefaultLocationOfListBox()
Dim chk As CheckBox
m_ucCheckList.Controls.Clear()
For Each row As DataRow In DirectCast(col.DataSource, DataTable).Rows
chk = New CheckBox
chk.Text = row(col.DisplayMember).ToString
chk.Tag = row
chk.Checked = (From dr In CheckedValues.AsEnumerable
Where CStr(dr(col.ValueMember)) = CStr(row(col.ValueMember))
Select True).FirstOrDefault
chk.Dock = DockStyle.Top
AddHandler chk.CheckedChanged, AddressOf ChkList_CheckedChanged
m_ucCheckList.Controls.Add(chk)
Next
With m_ucCheckList
If CType(col.DataSource, DataTable).Rows.Count > 20 Then
.Height = 250
Else
.Height = CType(col.DataSource, DataTable).Rows.Count * m_ucCheckList.Controls(0).Height
End If
.Width = col.DropDownWidth
If .Height < 24 Then
.Height = 24
End If
.Show()
.BringToFront()
.Refresh()
End With
Catch ex As Exception
End Try
End Sub
Private Sub SetDefaultLocationOfListBox()
Dim defaultPoint As Point
Try
defaultPoint = GetCurrentCellPosition()
m_ucCheckList.Location = defaultPoint
If Not Me.DataGridView.Controls.Contains(m_ucCheckList) Then
Me.DataGridView.Controls.Add(m_ucCheckList)
End If
m_ucCheckList.Visible = True
Catch ex As Exception
End Try
End Sub
Private Function GetCurrentCellPosition() As Point
Try
If Me.DataGridView IsNot Nothing AndAlso DataGridView.CurrentCell IsNot Nothing Then
Dim iRowHeight As Integer = DataGridView.Rows(DataGridView.CurrentCell.RowIndex).Height
Dim rCellRectangle As Rectangle = DataGridView.GetCellDisplayRectangle(DataGridView.CurrentCell.ColumnIndex, DataGridView.CurrentCell.RowIndex, False)
Return New Point(rCellRectangle.X, rCellRectangle.Y + iRowHeight)
End If
Catch ex As Exception
End Try
End Function
Protected Overrides Sub Paint(graphics As Graphics, clipBounds As Rectangle, cellBounds As Rectangle, rowIndex As Integer, cellstate As DataGridViewElementStates, value As Object,
formattedValue As Object, errorText As String, cellStyle As DataGridViewCellStyle, advancedBorderStyle As DataGridViewAdvancedBorderStyle, paintParts As DataGridViewPaintParts)
'formattedValue = SelectedText
'value = SelectedValue
MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellstate, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts)
End Sub
Private Sub ChkList_CheckedChanged(sender As Object, e As EventArgs)
Dim chk As CheckBox = CType(sender, CheckBox)
Dim row As DataRow = CType(chk.Tag, DataRow)
Dim col = CType(Me.OwningColumn, dgvCheckedComboBoxColumn)
If chk.Checked Then
Dim dr = CheckedValues.NewRow()
dr(col.ValueMember) = row(col.ValueMember)
dr(col.DisplayMember) = row(col.DisplayMember)
If col.CellDisplayMember <> col.DisplayMember Then
dr(col.CellDisplayMember) = row(col.CellDisplayMember)
End If
m_CheckedValues.Rows.Add(dr)
Else
Dim CheckedRow = (From dr In CheckedValues.AsEnumerable
Where CStr(dr(col.ValueMember)) = CStr(row(col.ValueMember))
Select dr).FirstOrDefault
If CheckedRow IsNot Nothing Then
CheckedValues.Rows.Remove(CheckedRow)
End If
End If
End Sub
Protected Overrides Sub OnKeyPress(e As KeyPressEventArgs, rowIndex As Integer)
MyBase.OnKeyPress(e, rowIndex)
End Sub
#End Region
End Class
UPDATE I've found the problem. The value was overwritten in the paint-Event. I am going to update the code once I've finished the Bugfix.
So I've fixed it. As already posted the Problem was in the Paint-Method which overwrited my Cell-Values. Instead of this...
Protected Overrides Sub Paint(graphics As Graphics, clipBounds As Rectangle, cellBounds As Rectangle,
rowIndex As Integer, cellstate As DataGridViewElementStates, value As Object,
formattedValue As Object, errorText As String, cellStyle As DataGridViewCellStyle,
advancedBorderStyle As DataGridViewAdvancedBorderStyle, paintParts As DataGridViewPaintParts)
formattedValue = SelectedText
value = SelectedValue
MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellstate, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts)
End Sub
I'm now using this...
Protected Overrides Sub Paint(graphics As Graphics, clipBounds As Rectangle, cellBounds As Rectangle,
rowIndex As Integer, cellstate As DataGridViewElementStates, value As Object,
formattedValue As Object, errorText As String, cellStyle As DataGridViewCellStyle,
advancedBorderStyle As DataGridViewAdvancedBorderStyle, paintParts As DataGridViewPaintParts)
If Not String.IsNullOrEmpty(CStr(formattedValue)) And (CStr(formattedValue) <> SelectedText) Then
'New Value
InitDBValue(formattedValue, value)
End If
'formattedValue = SelectedText
'value = SelectedValue
MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellstate, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts)
End Sub
Private Sub InitDBValue(ByRef formattedValue As Object, ByRef value As Object)
Dim col = CType(Me.OwningColumn, dgvCheckedComboBoxColumn)
Dim drNew, drSource As DataRow
If m_CheckedValues IsNot Nothing Then
m_CheckedValues.Rows.Clear()
End If
For Each str As String In CStr(formattedValue).Split(","c)
drNew = CheckedValues.NewRow
drNew(col.ValueMember) = str
drSource = (From r In col.DataSource.AsEnumerable
Where CStr(r(col.ValueMember)) = str
Select r).FirstOrDefault
drNew(col.DisplayMember) = CStr(drSource(col.DisplayMember))
If col.DisplayMember <> col.CellDisplayMember Then
drNew(col.CellDisplayMember) = CStr(drSource(col.CellDisplayMember))
End If
m_CheckedValues.Rows.Add(drNew)
Next
formattedValue = SelectedText
value = SelectedValue
End Sub
I know, not super clean but does its job...