在同一数据源中每行数据绑定一个控件

Databinding one control per row in the same datasource

我一直在努力解决一个与 Winforms 中的数据绑定有关的令人沮丧的问题。

我有一个数据源,是DataSet中的一个DataTable。这个 DataTable 有三行。我的表单上有三个 CheckBox 控件和一个 Button。我想将每个 CheckBox 绑定到此数据源中的一行,并让数据源在 CheckBox 更新时反映 Checked 属性 中的值。我还希望通过调用 HasChanges() 和调用 GetChanges().

正确获取这些更改

单击 Button 时,将调用 EndCurrentEdit() 并传递绑定到的数据源,并使用 [=20= 检查 DataSet 的更改]方法。

但是,在尝试执行此操作时,我在调用 EndCurrentEdit() 后遇到了两种情况之一。

在第一种情况下,仅检测到第一个 CheckBox 的更改。在第二种情况下,所有其他 CheckBoxes 都更新为上次在调用 EndCurrentEdit().

时检查的 CheckBox 的值

在调用 EndCurrentEdit() 后查看 RowState 值,在场景 1 中,只有第一行的状态为 Modified。在场景 2 中,只有第三行的状态为 Modified。对于场景 2,用户是否更新了第三个 CheckBox 并不重要。

为了演示我的问题,我创建了一个简单的示例来演示它。

这是一个常用 Windows 表单,包含三个 CheckBox 控件和一个 Button 控件,所有控件都具有默认名称。

Option Strict On
Option Explicit On

Public Class Form1

    Public ds As DataSet

    Public Sub New()

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

        ds = New DataSet()
        Dim dt As New DataTable()

        dt.Columns.Add("ID", GetType(Integer))
        dt.Columns.Add("Selected", GetType(Boolean))

        dt.Rows.Add(1, False)
        dt.Rows.Add(2, False)
        dt.Rows.Add(3, False)

        dt.TableName = "Table1"

        ds.Tables.Add(dt)

        ds.AcceptChanges()

        For i As Integer = 1 To 3

            Dim bs As New BindingSource()

            'After the call to Me.BindingContext(ds.Tables("Table1")).EndCurrentEdit() there are two scenarios:
            'Scenario 1 - only changes to the first CheckBox are detected.
            'Scenario 2 - when any CheckBox is checked, they all become checked.

            'Uncomment the first and comment out the second to see Scenario 1.
            'Uncomment the second and comment out the first to see Scenario 2.
            'bs.DataSource = New DataView(ds.Tables("Table1")) 'Scenario 1
            bs.DataSource = ds.Tables("Table1") 'Scenario 2
            bs.Filter = "ID=" & i

            Dim db As New Binding("Checked", bs, "Selected")
            db.DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged

            If i = 1
                CheckBox1.DataBindings.Add(db)
            ElseIf i = 2
                CheckBox2.DataBindings.Add(db)
            ElseIf i = 3
                CheckBox3.DataBindings.Add(db)
            End If
        Next
    End Sub


    Private Sub Button1_Click( sender As Object,  e As EventArgs) Handles Button1.Click
        Me.BindingContext(ds.Tables("Table1")).EndCurrentEdit()

        If ds.HasChanges()
            MessageBox.Show("Number of rows changed: " & ds.GetChanges().Tables("Table1").Rows.Count)
            ds.AcceptChanges()
        End If
    End Sub
End Class

我已经做了很多搜索,但一直无法弄清楚发生了什么,因此完全不知所措。感觉我想做的事情很简单,但我怀疑我一定是在某处误解了某些东西,或者错过了一些关于绑定过程的重要内容。

编辑

它已经在第二段中了,但为了清楚起见,这里是我正在尝试做的事情的基本轮廓:

  1. 我有一个 DataSet 包含一个 DataTable 和值。对于此示例,有两列。 ID 可以是任何 Integer,'Selected' 可以是任何 Boolean 值。 None 其中永远是 NothingDBNull
  2. 我想将每一行绑定到一个 Checkbox 控件。每个 ID 值一个 CheckBoxChecked 属性 应绑定到 DataTable 中的 Selected 列。
  3. 当进行更改并且用户单击 Button 时,我希望能够使用DataSet(即,如果用户更改了两个 Checkbox 控件的 Checked 属性,则会更新 2 行)。

编辑 2

感谢@RezaAghaei,我想出了一个解决方案。我从我的问题示例中改进了代码,并使所有控件都基于数据生成(并相应地定位它们自己)以使该示例易于复制和粘贴。此外,这在 DataView 上使用了 RowFilter,而不是 BindingSource.

Position 属性
Option Strict On
Option Explicit On

Public Class Form1

    Public ds As DataSet

    Private Sub Form1_Load( sender As Object,  e As EventArgs) Handles MyBase.Load
        ds = New DataSet()
        Dim dt As New DataTable()

        dt.Columns.Add("ID", GetType(Integer))
        dt.Columns.Add("Selected", GetType(Boolean))

        dt.Rows.Add(1, False)
        dt.Rows.Add(2, False)
        dt.Rows.Add(3, False)

        dt.TableName = "Table1"
        ds.Tables.Add(dt)
        ds.AcceptChanges()

        AddHandler dt.ColumnChanged,    Sub(sender2 As Object, e2 As DataColumnChangeEventArgs)
                                            e2.Row.EndEdit()
                                        End Sub

        For i As Integer = 0 To dt.Rows.Count-1
            Dim cb As New CheckBox() With {.Text = "CheckBox" & i+1, .Location = New Point(10, 25 * (i))}

            Dim dv As New DataView(dt)
            dv.RowFilter =  "ID=" & DirectCast(dt.Rows(i)(0), Integer)

            Dim bs As New BindingSource(dv, Nothing)
            Dim db As New Binding("Checked", bs, "Selected")

            db.DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged
            cb.DataBindings.Add(db)

            Me.Controls.Add(cb)
        Next

        Dim btn As New Button()
        btn.Location = New Point(10, 30 * dt.Rows.Count)
        btn.Text = "Submit"
        AddHandler btn.Click, AddressOf Button1_Click
        Me.Controls.Add(btn)
    End Sub


    Private Sub Button1_Click( sender As Object,  e As EventArgs) 
        'Me.BindingContext(ds.Tables("Table1")).EndCurrentEdit() 'Doesn't cut the mustard!

        If ds.HasChanges()
            MessageBox.Show("Number of rows changed: " & ds.GetChanges().Tables("Table1").Rows.Count)
            ds.AcceptChanges()
        Else
            MessageBox.Show("Number of rows changed: 0")
        End If
    End Sub

End Class

关键是 DataTableColumnChanged 事件中对 EndEdit() 的调用(对 EndCurrentEdit() 的一般调用似乎并没有削减它),尽管我遇到的另一个问题是,如果这段代码在表单的 New() 方法中,则它不会工作,即使它在调用 InitializeComponent() 之后也是如此。我猜这是因为 Winforms 在调用 New() 之后进行了一些初始化,这是数据绑定正常工作所必需的。

我希望这个例子可以节省其他人我花在研究这个问题上的时间。

考虑这些更正,问题将得到解决:

  • 要将控件绑定到列表的特定索引,请将控件绑定到包含该列表的绑定源,然后将绑定源的位置设置为特定索引。
  • CheckBox 控件添加数据绑定时,将更新模式设置为 OnPropertyChanged
  • 处​​理 CheckBox 控件的 CheckedChanged 事件并调用网格的 Invalidate 方法以显示网格的变化。
  • CheckedChanged 中也调用 BindingSourceEndEditCheckBox 绑定到 BindingSource
  • 注意:调用EndEdit usnig BeginInvoke让勾选复选框的所有过程(包括更新数据源)完成。

C# 示例

DataTable dt = new DataTable();
private void Form3_Load(object sender, EventArgs e)
{
    dt.Columns.Add("Id");
    dt.Columns.Add("Selected", typeof(bool));
    dt.Rows.Add("1", true);
    dt.Rows.Add("2", false);
    dt.Rows.Add("3", true);
    this.dataGridView1.DataSource = dt;
    var chekBoxes = new CheckBox[] { checkBox1, checkBox2, checkBox3 };
    for (int i = 0; i < dt.Rows.Count; i++)
    {
        var bs = new BindingSource(dt, null);
        chekBoxes[i].DataBindings.Add("Checked", bs, "Selected",
            true, DataSourceUpdateMode.OnPropertyChanged);
        chekBoxes[i].CheckedChanged += (obj, arg) =>
        {
            this.dataGridView1.Invalidate();
            var c = (CheckBox)obj;
            var b = (BindingSource)(c.DataBindings[0].DataSource);
            this.BeginInvoke(()=>{b.EndEdit();});
        };
        bs.Position = i;
    }
}

VB 示例

Dim dt As DataTable = New DataTable()
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles MyBase.Load
    dt.Columns.Add("Id")
    dt.Columns.Add("Selected", GetType(Boolean))
    dt.Rows.Add("1", True)
    dt.Rows.Add("2", False)
    dt.Rows.Add("3", True)
    dt.AcceptChanges()
    Me.DataGridView1.DataSource = dt
    Dim chekBoxes = New CheckBox() {CheckBox1, CheckBox2, CheckBox3}
    For i = 0 To dt.Rows.Count - 1
        Dim bs = New BindingSource(dt, Nothing)
        chekBoxes(i).DataBindings.Add("Checked", bs, "Selected", _
            True, DataSourceUpdateMode.OnPropertyChanged)
        AddHandler chekBoxes(i).CheckedChanged, _
             Sub(obj, arg)
                 Me.DataGridView1.Invalidate()
                 Dim c = DirectCast(obj, CheckBox)
                 Dim b = DirectCast(c.DataBindings(0).DataSource, BindingSource)
                 Me.BeginInvoke(Sub() b.EndEdit())
             End Sub
        bs.Position = i
    Next i
End Sub