1

我有一个带有两个组合框的 Windows 窗体。每个组合框的SelectedValue属性都是绑定到简单 DTO 上的属性的数据。每个组合框的选项都来自模型对象列表。我只需要表单上的控件来更新 DTO;我不需要以编程方式修改任何 DTO 的属性并查看正在更新的相应控件 - 即,我只需要单向(控件 -> 源)数据绑定即可工作。

当用户更改第一个组合框的值时,第二个组合框的选项将完全改变。但是,我在此设置中遇到了两个问题,我无法弄清楚它们发生的原因或如何解决它们:

  1. 每当更改第一个组合框时,数据绑定框架就会生成并吞下 NRE(我可以在 Visual Studio IDE 的即时窗口中看到它抛出),这提示我某些设置不正确。更改第二个组合框或任何其他不相关的数据绑定控件(组合框或其他)不会生成 NRE。
  2. 同样,每当第一个组合框发生变化时,在生成上述 NRE 后,第二个组合框加载成功,但第一个组合框的选定索引重置为 -1。我怀疑这是因为数据绑定的“推送”事件触发以更新控件,并且由于某种原因,支持第一个组合框的 DTO 属性的值被重置为 NULL/Nothing。

有谁知道为什么会发生这些事情?我模拟了我的问题,它展示了上述两个问题。我还添加了第三个组合框,它与前两个中的任何一个都没有关系,就像一个完整性检查以表明一个不依赖于另一个组合框的组合框可以正常工作。

此代码复制了问题 - 粘贴为 Visual Basic Windows 窗体项目(3.5 框架)的默认 Form1 类的代码。

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
4

1 回答 1

3

当 DataBinding 行为不端时,它可能难以调试。这里有两件事出了问题,足以使其难以诊断。您没有想到的第一件事是 SelectedValueChanged 事件货币管理器更新绑定对象之前触发。这通常不是问题,但您的事件处理程序有副作用。下一件事是,更改绑定对象的一个​​属性会导致所有其他属性的绑定也被更新。

问题是,当您更新组合 B 时,_DataObject.ValueA 仍然是 Nothing。这会更新 _DataObject.ValueB。因此货币经理再次更新组合 A ,试图使其与属性 ValueA 中 Nothing 的值匹配。这也是产生 NullReferenceException 的原因。

一种可能的解决方法是延迟 SelectedValueChanged 事件处理程序中的副作用并将其推迟到货币管理器更新绑定对象为止。通过使用 Control.BeginInvoke() 可以干净地完成,目标在 UI 线程再次空闲时运行。这解决了您的问题:

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

可能有一个更干净的修复,更新 _DataObject 而不是尝试更新组合框。但是您使用字典使这有点困难,我没有追求它。

于 2013-03-22T02:19:25.783 回答