I'm trying to create a DataGridComboBoxColumn. I have used the code from
this article as my base:
http://msdn.microsoft.com/msdnmag/issues/03/08/DataGrids/default.aspx
I had to port it to VB.Net, and I modified it so it can support binding to
non-dataset objects (eg. an array of custom objects).
My problem is that when the combo loses focus the code gets stuck in an
infinite loop and I can't figure out why. The Edit method seems to get
called over and over for some unknown reason. (the combo is also displaying
in the wrong location, but that should be easily fixable).
If you copy-paste the following code into a .vb file and run it you will see
the problem.
Public Class Form1
Inherits System.Windows.Forms.Form
#Region " Windows Form Designer generated code "
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Add any initialization after the InitializeComponent() call
End Sub
'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents DataGrid1 As System.Windows.Forms.DataGrid
Friend WithEvents DataGridTableStyle1 As
System.Windows.Forms.DataGridTableStyle
Friend WithEvents DataGridTextBoxColumn1 As
System.Windows.Forms.DataGridTextBoxColumn
Friend WithEvents dataGridComboBoxColumnSite As DataGridComboBoxColumn
Friend WithEvents DataGridTextBoxColumn2 As
System.Windows.Forms.DataGridTextBoxColumn
<System.Diagnostics.DebuggerStepThrough()> Private Sub
InitializeComponent()
Me.DataGrid1 = New System.Windows.Forms.DataGrid
Me.DataGridTableStyle1 = New System.Windows.Forms.DataGridTableStyle
Me.DataGridTextBoxColumn2 = New
System.Windows.Forms.DataGridTextBoxColumn
Me.dataGridComboBoxColumnSite = New DataGridComboBoxColumn
Me.DataGridTextBoxColumn1 = New
System.Windows.Forms.DataGridTextBoxColumn
CType(Me.DataGrid1,
System.ComponentModel.ISupportInitialize).BeginInit()
Me.SuspendLayout()
'
'DataGrid1
'
Me.DataGrid1.AlternatingBackColor = System.Drawing.Color.Lavender
Me.DataGrid1.BackColor = System.Drawing.Color.WhiteSmoke
Me.DataGrid1.BackgroundColor = System.Drawing.Color.LightGray
Me.DataGrid1.BorderStyle = System.Windows.Forms.BorderStyle.None
Me.DataGrid1.CaptionBackColor = System.Drawing.Color.LightSteelBlue
Me.DataGrid1.CaptionForeColor = System.Drawing.Color.MidnightBlue
Me.DataGrid1.DataMember = ""
Me.DataGrid1.FlatMode = True
Me.DataGrid1.Font = New System.Drawing.Font("Tahoma", 8.0!)
Me.DataGrid1.ForeColor = System.Drawing.Color.MidnightBlue
Me.DataGrid1.GridLineColor = System.Drawing.Color.Gainsboro
Me.DataGrid1.GridLineStyle =
System.Windows.Forms.DataGridLineStyle.None
Me.DataGrid1.HeaderBackColor = System.Drawing.Color.MidnightBlue
Me.DataGrid1.HeaderFont = New System.Drawing.Font("Tahoma", 8.0!,
System.Drawing.FontStyle.Bold)
Me.DataGrid1.HeaderForeColor = System.Drawing.Color.WhiteSmoke
Me.DataGrid1.LinkColor = System.Drawing.Color.Teal
Me.DataGrid1.Location = New System.Drawing.Point(32, 16)
Me.DataGrid1.Name = "DataGrid1"
Me.DataGrid1.ParentRowsBackColor = System.Drawing.Color.Gainsboro
Me.DataGrid1.ParentRowsForeColor = System.Drawing.Color.MidnightBlue
Me.DataGrid1.SelectionBackColor = System.Drawing.Color.CadetBlue
Me.DataGrid1.SelectionForeColor = System.Drawing.Color.WhiteSmoke
Me.DataGrid1.Size = New System.Drawing.Size(496, 280)
Me.DataGrid1.TabIndex = 0
Me.DataGrid1.TableStyles.AddRange(New
System.Windows.Forms.DataGridTableStyle() {Me.DataGridTableStyle1})
'
'DataGridTableStyle1
'
Me.DataGridTableStyle1.DataGrid = Me.DataGrid1
Me.DataGridTableStyle1.GridColumnStyles.AddRange(New
System.Windows.Forms.DataGridColumnStyle() {Me.DataGridTextBoxColumn1,
Me.dataGridComboBoxColumnSite, Me.DataGridTextBoxColumn2})
Me.DataGridTableStyle1.HeaderForeColor =
System.Drawing.SystemColors.ControlText
Me.DataGridTableStyle1.MappingName = "CostInfo[]"
'
'DataGridTextBoxColumn2
'
Me.DataGridTextBoxColumn2.Format = ""
Me.DataGridTextBoxColumn2.FormatInfo = Nothing
Me.DataGridTextBoxColumn2.HeaderText = "Cost"
Me.DataGridTextBoxColumn2.MappingName = "PartCost"
Me.DataGridTextBoxColumn2.Width = 75
'
'dataGridComboBoxColumnSite
'
Me.dataGridComboBoxColumnSite.Format = ""
Me.dataGridComboBoxColumnSite.FormatInfo = Nothing
Me.dataGridComboBoxColumnSite.HeaderText = "Site"
Me.dataGridComboBoxColumnSite.MappingName = "SiteID"
Me.dataGridComboBoxColumnSite.Width = 150
'
'DataGridTextBoxColumn1
'
Me.DataGridTextBoxColumn1.Format = ""
Me.DataGridTextBoxColumn1.FormatInfo = Nothing
Me.DataGridTextBoxColumn1.HeaderText = "Part #"
Me.DataGridTextBoxColumn1.MappingName = "PartNo"
Me.DataGridTextBoxColumn1.ReadOnly = True
Me.DataGridTextBoxColumn1.Width = 75
'
'Form1
'
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(688, 630)
Me.Controls.Add(Me.DataGrid1)
Me.Name = "Form1"
Me.Text = "Form1"
CType(Me.DataGrid1,
System.ComponentModel.ISupportInitialize).EndInit()
Me.ResumeLayout(False)
End Sub
#End Region
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles MyBase.Load
Dim Sites(2) As Site
Dim Costs(2) As CostInfo
Sites(0) = New Site(0, "Central")
Sites(1) = New Site(61, "Winnipeg")
Sites(2) = New Site(63, "Regina")
Costs(0) = New CostInfo("P4211CEW", 61, 3429.32)
Costs(1) = New CostInfo("W23579", 0, 7839.29)
Costs(2) = New CostInfo("100044", 63, 23.54)
DataGrid1.DataSource = Costs
DataGridTableStyle1.DataGrid = DataGrid1
dataGridComboBoxColumnSite.ComboBox.DataSource = Sites
dataGridComboBoxColumnSite.ComboBox.DisplayMember = "SiteDesc"
dataGridComboBoxColumnSite.ComboBox.ValueMember = "SiteID"
End Sub
End Class
Public Class Site
Private _SiteID As Integer
Private _SiteDesc As String
Public Sub New(ByVal SiteID As Integer, ByVal SiteDesc As String)
_SiteID = SiteID
_SiteDesc = SiteDesc
End Sub
Public Property SiteID() As Integer
Get
Return _SiteID
End Get
Set(ByVal Value As Integer)
_SiteID = Value
End Set
End Property
Public Property SiteDesc() As String
Get
Return _SiteDesc
End Get
Set(ByVal Value As String)
_SiteDesc = Value
End Set
End Property
End Class
Public Class CostInfo
Private _PartNo As String
Private _SiteID As Integer
Private _PartCost As Double
Public Sub New(ByVal PartNo As String, ByVal SiteID As Integer, ByVal
PartCost As Double)
_PartNo = PartNo
_SiteID = SiteID
_PartCost = PartCost
End Sub
Public Property PartNo() As String
Get
Return _PartNo
End Get
Set(ByVal Value As String)
_PartNo = Value
End Set
End Property
Public Property SiteID() As Integer
Get
Return _SiteID
End Get
Set(ByVal Value As Integer)
_SiteID = Value
End Set
End Property
Public Property PartCost() As Double
Get
Return _PartCost
End Get
Set(ByVal Value As Double)
_PartCost = Value
End Set
End Property
End Class
Public Class DataGridComboBoxColumn
Inherits DataGridTextBoxColumn
'Hosted combobox control
Private WithEvents _ComboBox As ComboBox
Private _cm As CurrencyManager
Private _CurrentRow As Integer
'Constructor - create combobox,
'register selection change event handler,
'register lose focus event handler
Public Sub New()
_cm = Nothing
'Create combobox and force DropDownList style
_ComboBox = New ComboBox
_ComboBox.DropDownStyle = ComboBoxStyle.DropDownList
'Add event handler for notification when combobox loses focus
AddHandler _ComboBox.Leave, New EventHandler(AddressOf ComboBox_Leave)
End Sub
'Property to provide access to combobox
Public ReadOnly Property ComboBox() As ComboBox
Get
Return _ComboBox
End Get
End Property
'On edit, add scroll event handler, and display combobox
Protected Overloads Overrides Sub Edit(ByVal source As
System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, ByVal bounds
As System.Drawing.Rectangle, ByVal [readOnly] As Boolean, ByVal instantText
As String, ByVal cellIsVisible As Boolean)
MyBase.Edit(source, rowNum, bounds, [readOnly], instantText,
cellIsVisible)
If (Not [readOnly] And cellIsVisible) Then
'Save current row in the DataGrid and currency manager
'associated with the data source for the DataGrid
_CurrentRow = rowNum
_cm = source
'Add event handler for DataGrid scroll notification
AddHandler Me.DataGridTableStyle.DataGrid.Scroll, New
EventHandler(AddressOf DataGrid_Scroll)
'Site the combobox control within the current cell
_ComboBox.Parent = Me.DataGridTableStyle.DataGrid.Parent
Dim rect As Rectangle =
Me.DataGridTableStyle.DataGrid.GetCurrentCellBounds()
_ComboBox.Location = rect.Location
_ComboBox.Size = New Size(Me.TextBox.Size.Width,
_ComboBox.Size.Height)
'Set combobox selection to given text
_ComboBox.SelectedIndex =
_ComboBox.FindStringExact(Me.TextBox.Text)
'Make the combobox visible and place on top textbox control
_ComboBox.Show()
_ComboBox.BringToFront()
_ComboBox.Focus()
End If
End Sub
'given the value member, find and return the matching display member
Protected Overrides Function GetColumnValueAtRow(ByVal source As
System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer) As Object
'retrieve the value member from the grid
Dim obj As Object = MyBase.GetColumnValueAtRow(source, rowNum)
'get a reference to the CurrencyManager for the ComboBox data source
Dim cm As CurrencyManager =
CType(Me.DataGridTableStyle.DataGrid.BindingContext(_ComboBox.DataSource),
CurrencyManager)
Dim CurObj As Object
For Each CurObj In cm.List
'use reflection to retrieve the property from CurObj with the
name from _ComboBox.ValueMember
Dim TargetType As System.Type
Dim TargetProperty As Reflection.PropertyInfo
Dim ValueData As Object
TargetType = CurObj.GetType()
TargetProperty = TargetType.GetProperty(_ComboBox.ValueMember)
ValueData = TargetProperty.GetValue(CurObj, Nothing)
'compare this value with obj
If obj.Equals(ValueData) Then
'if they match we need to retrieve and return the
corresponding property
'from CurObj using _ComboBox.DisplayMember (via reflection)
TargetProperty =
TargetType.GetProperty(_ComboBox.DisplayMember)
Return TargetProperty.GetValue(CurObj, Nothing)
End If
Next
Return DBNull.Value
End Function
'given the new display value, iterate over the combo data source and
find the matching value
Protected Overrides Sub SetColumnValueAtRow(ByVal source As
System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, ByVal value As
Object)
'Iterate through the data source bound to the ColumnComboBox
Dim cm As CurrencyManager =
CType(Me.DataGridTableStyle.DataGrid.BindingContext(_ComboBox.DataSource),
CurrencyManager)
Dim CurObj As Object
For Each CurObj In cm.List
Dim TargetType As System.Type = CurObj.GetType()
Dim TargetProperty As Reflection.PropertyInfo =
TargetType.GetProperty(_ComboBox.DisplayMember)
If value.Equals(TargetProperty.GetValue(CurObj, Nothing)) Then
TargetProperty = TargetType.GetProperty(_ComboBox.ValueMember)
MyBase.SetColumnValueAtRow(source, rowNum,
TargetProperty.GetValue(CurObj, Nothing))
Exit Sub
End If
Next
MyBase.SetColumnValueAtRow(source, rowNum, DBNull.Value)
End Sub
'On DataGrid scroll, hide the combobox
Private Sub DataGrid_Scroll(ByVal sender As Object, ByVal e As EventArgs)
_ComboBox.Hide()
End Sub
'On combobox losing focus, set the column value, hide the combobox,
'and unregister scroll event handler
Private Sub ComboBox_Leave(ByVal sender As Object, ByVal e As EventArgs)
Dim TargetType As System.Type = _ComboBox.SelectedItem.GetType()
Dim TargetProperty As Reflection.PropertyInfo =
TargetType.GetProperty(_ComboBox.DisplayMember)
Dim s As String =
CType(TargetProperty.GetValue(_ComboBox.SelectedItem, Nothing), String)
SetColumnValueAtRow(_cm, _CurrentRow, s)
Invalidate()
_ComboBox.Hide()
RemoveHandler Me.DataGridTableStyle.DataGrid.Scroll, New
EventHandler(AddressOf DataGrid_Scroll)
End Sub
End Class
Optikal - 22 Aug 2005 21:01 GMT
You can view the code in a more friendly format here:
http://pastebin.com/343282