【问题标题】:Why does binding to a set of Winforms cascading combo boxes cause the root combo box to not set its value properly?为什么绑定到一组 Winforms 级联组合框会导致根组合框无法正确设置其值?
【发布时间】:2013-03-01 19:51:34
【问题描述】:

我有一个带有两个组合框的 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

【问题讨论】:

  • 这个问题试图描述一组相关的组合框,即绑定到对象的数据,如何导致至少一个组合框的基本功能失效。级联/依赖组合框很常见。数据绑定也是一种常见的做法。我认为这种情况并不是特别狭隘。请重新打开这个问题。

标签: vb.net winforms data-binding binding combobox


【解决方案1】:

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

问题是,当您更新组合 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 而不是尝试更新组合框。但是您使用字典使这有点困难,我没有追求它。

【讨论】:

  • +1 感谢您的回答和推理。我的问题只是触发事件的顺序问题,您的快速解决方案效果很好。
猜你喜欢
  • 2014-06-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-13
  • 2012-05-16
  • 1970-01-01
  • 1970-01-01
  • 2016-08-23
相关资源
最近更新 更多