1
votes

I have a Windows form with two combo boxes. The SelectedValue property of each combo box is data bound to a property on a simple DTO. The options for each combo box are drawn from a list of model objects. I only require the controls on the form to update the DTO; I have no need to modify any of the DTO's properties programmatically and see the corresponding control being updated - i.e., I only need one-way (control -> source) data binding to work.

When the user changes the value of the first combo box, the options for the second combo box will change completely. However, I have run into two issues with this setup that I cannot figure out why they occur or how to solve them:

  1. Whenever the first combo box is changed, an NRE is generated and swallowed by the data binding framework (I can see it thrown in the Immediate Window of the Visual Studio IDE), which tips me off that something isn't set up correctly. Changing the second combo box or any other unrelated, data bound control (combo box or otherwise) does not generate an NRE.
  2. Also whenever the first combo box is changed, after generating the NRE mentioned above, the second combo box loads successfully, but the first combo box's selected index resets to -1. I suspect this is because the data binding's "push" event fires to update the controls, and for some reason, the value of my DTO's property backing the first combo box gets reset to NULL / Nothing.

Does anyone have any idea why these things occur? I mocked up my problem, which exhibits the two issues above. I also added a third combo box that has nothing to do with either of the first two, just as a sanity check to show that a combo box without any dependency on another combo box works fine.

This code replicates the issues - paste as the code for the default Form1 class of a Visual Basic Windows Forms project (3.5 Framework).

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Windows.Forms

Public Class Form1
    Inherits System.Windows.Forms.Form

    'Form overrides dispose to clean up the component list.
    <System.Diagnostics.DebuggerNonUserCode()> _
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    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.
    <System.Diagnostics.DebuggerStepThrough()> _
    Private Sub InitializeComponent()
        Me.cboA = New System.Windows.Forms.ComboBox()
        Me.cboB = New System.Windows.Forms.ComboBox()
        Me.cboC = New System.Windows.Forms.ComboBox()
        Me.SuspendLayout()
        '
        'cboA
        '
        Me.cboA.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList
        Me.cboA.FormattingEnabled = True
        Me.cboA.Location = New System.Drawing.Point(120, 25)
        Me.cboA.Name = "cboA"
        Me.cboA.Size = New System.Drawing.Size(121, 21)
        Me.cboA.TabIndex = 0
        '
        'cboB
        '
        Me.cboB.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList
        Me.cboB.FormattingEnabled = True
        Me.cboB.Location = New System.Drawing.Point(120, 77)
        Me.cboB.Name = "cboB"
        Me.cboB.Size = New System.Drawing.Size(121, 21)
        Me.cboB.TabIndex = 1
        '
        'cboC
        '
        Me.cboC.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList
        Me.cboC.FormattingEnabled = True
        Me.cboC.Location = New System.Drawing.Point(120, 132)
        Me.cboC.Name = "cboC"
        Me.cboC.Size = New System.Drawing.Size(121, 21)
        Me.cboC.TabIndex = 2
        '
        'Form1
        '
        Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
        Me.ClientSize = New System.Drawing.Size(284, 262)
        Me.Controls.Add(Me.cboC)
        Me.Controls.Add(Me.cboB)
        Me.Controls.Add(Me.cboA)
        Me.Name = "Form1"
        Me.Text = "Form1"
        Me.ResumeLayout(False)

    End Sub
    Friend WithEvents cboA As System.Windows.Forms.ComboBox
    Friend WithEvents cboB As System.Windows.Forms.ComboBox
    Friend WithEvents cboC As System.Windows.Forms.ComboBox

    Private _DataObject As MyDataObject
    Private _IsInitialized As Boolean = False

    Public Sub New()
        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        _DataObject = New MyDataObject()
        BindControls()
    End Sub

    Private Sub BindControls()
        LoadComboA(cboA)
        Dim cmbABinding As New Binding("SelectedValue", _DataObject, "ValueA", True, DataSourceUpdateMode.OnPropertyChanged)
        cboA.DataBindings.Add(cmbABinding)

        Dim cmbBBinding As New Binding("SelectedValue", _DataObject, "ValueB", True, DataSourceUpdateMode.OnPropertyChanged)
        cboB.DataBindings.Add(cmbBBinding)

        LoadComboC(cboC)
        Dim cmbCBinding As New Binding("SelectedValue", _DataObject, "ValueC", True, DataSourceUpdateMode.OnPropertyChanged)
        cboC.DataBindings.Add(cmbCBinding)
    End Sub

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        MyBase.OnLoad(e)
        _IsInitialized = True
        cboA.SelectedIndex = 0
        cboC.SelectedIndex = 0
    End Sub

    Private Sub ComboA_SelectedValueChanged(ByVal sender As Object, ByVal e As EventArgs) Handles cboA.SelectedValueChanged
        If _IsInitialized Then
            LoadComboB(cboB, cboA.SelectedValue.ToString())
            cboB.SelectedIndex = 0
        End If
    End Sub

    Private Sub LoadComboA(ByVal cmbBox As ComboBox)
        Dim someData As New Dictionary(Of String, String)()
        someData.Add("Value1", "Text 1")
        someData.Add("Value2", "Text 2")
        someData.Add("Value3", "Text 3")
        cmbBox.DataSource = someData.ToList()
        cmbBox.DisplayMember = "Value"
        cmbBox.ValueMember = "Key"
    End Sub

    Private Sub LoadComboB(ByVal cmbBox As ComboBox, ByVal selector As String)
        Dim someSubData As New Dictionary(Of String, String)()
        Select Case selector
            Case "Value1"
                someSubData.Add("SubValue1", "Value1 - Sub Text 1")
                someSubData.Add("SubValue2", "Value1 - Sub Text 2")
                someSubData.Add("SubValue3", "Value1 - Sub Text 3")
            Case "Value2"
                someSubData.Add("SubValue4", "Value2 - Sub Text 4")
                someSubData.Add("SubValue5", "Value2 - Sub Text 5")
                someSubData.Add("SubValue6", "Value2 - Sub Text 6")
            Case "Value3"
                someSubData.Add("SubValue7", "Value3 - Sub Text 7")
                someSubData.Add("SubValue8", "Value3 - Sub Text 8")
                someSubData.Add("SubValue9", "Value3 - Sub Text 9")
        End Select
        cmbBox.DataSource = someSubData.ToList()
        cmbBox.DisplayMember = "Value"
        cmbBox.ValueMember = "Key"
    End Sub

    Private Sub LoadComboC(ByVal cmbBox As ComboBox)
        Dim someData As New Dictionary(Of String, String)()
        someData.Add("Value100", "Text 100")
        someData.Add("Value101", "Text 101")
        cmbBox.DataSource = someData.ToList()
        cmbBox.DisplayMember = "Value"
        cmbBox.ValueMember = "Key"
    End Sub

End Class

Public Class MyDataObject  ' DTO class

    Private _ValueA As String
    Public Property ValueA() As String
        Get
            Return _ValueA
        End Get
        Set(ByVal value As String)
            _ValueA = value
        End Set
    End Property

    Private _ValueB As String
    Public Property ValueB() As String
        Get
            Return _ValueB
        End Get
        Set(ByVal value As String)
            _ValueB = value
        End Set
    End Property

    Private _ValueC As String
    Public Property ValueC() As String
        Get
            Return _ValueC
        End Get
        Set(ByVal value As String)
            _ValueC = value
        End Set
    End Property

End Class
1
This question attempts to describe how a set of dependent combo boxes, data bound to an object, causes the basic functionality of at least one of the combo boxes to fail. Cascading / dependent combo boxes are quite common. Data binding is also a common practice. I think that this situation is not extraordinarily narrow. Please reopen this question.Matt Hamsmith

1 Answers

3
votes

DataBinding can be difficult to debug when it misbehaves. There are two things going wrong here, enough to make it hard to diagnose. The first thing you didn't count on is that the SelectedValueChanged event fires before the currency manager updates the bound object. That normally isn't a problem but your event handler has a side-effect. The next thing you didn't count on is that changing one property of the bound object causes the binding of all other properties to be updated as well.

And there's the rub, _DataObject.ValueA is still Nothing when you update combo B. Which updates _DataObject.ValueB. So the currency manager updates combo A again, trying to make it match the value of Nothing in property ValueA. Which is what also produced the NullReferenceException.

One possible fix is to delay the side-effect in your SelectedValueChanged event handler and postpone it until the currency manager updated the bound object. Than can be cleanly done by using Control.BeginInvoke(), the target runs when the UI thread goes idle again. This fixed your problem:

Private Sub ComboA_SelectedValueChanged(ByVal sender As Object, ByVal e As EventArgs) Handles cboA.SelectedValueChanged
    If _IsInitialized Then Me.BeginInvoke(New MethodInvoker(AddressOf LoadB))
End Sub

Private Sub LoadB()
    LoadComboB(cboB, cboA.SelectedValue.ToString())
    cboB.SelectedIndex = 0
End Sub

There's probably a cleaner fix, updating _DataObject instead of trying to update the combobox. But you made that a bit difficult by using a Dictionary, I didn't pursue it.