Interminable
Interminable

Reputation: 1410

Databinding one control per row in the same datasource

I've been trying to deal with a frustrating issue with regards to databinding in Winforms.

I have a data source, which is a DataTable in a DataSet. This DataTable has three rows. I have three CheckBox controls on my form, and a Button. I want to bind each CheckBox to one row in this data source, and have the data source reflect the value in the Checked property whenever the CheckBox is updated. I also want these changes to be correctly picked up by calls to HasChanges() and calls to GetChanges().

When the Button is clicked, EndCurrentEdit() is called and passed the data-source that was bound to, and the DataSet is checked for changes using the HasChanges() method.

However, in my attempts to do this, I encounter one of two scenarios after the call to EndCurrentEdit().

In the first scenario, only the first CheckBox has its changes detected. In the second scenario, all other CheckBoxes are updated to the value of the CheckBox that was last checked on the call to EndCurrentEdit().

In looking at the RowState values after the call to EndCurrentEdit(), in Scenario 1, only the first row ever has a state of Modified. In Scenario 2, only the third row ever has a state of Modified. For Scenario 2, it doesn't matter whether the user updated the third CheckBox or not.

To demonstrate my problem, I have created a simple example which demonstrates it.

It's a stock Windows form containing three CheckBox controls and a Button control, all with their default names.

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

I've done a lot of searching but haven't been able to work out what's happening, and am thus at a complete loss. It feels like what I'm trying to do is pretty simple, but I suspect that I must have misunderstood something somewhere or missed something important with regards to the binding process.

EDIT

It's already in the second paragraph, but just to make it clear, here is the basic outline of what I'm trying to do:

  1. I have a DataSet containing a DataTable with values. For this example, there's two columns. ID could be any Integer, 'Selected' could be any Boolean value. None of these will ever be Nothing or DBNull.
  2. I want to bind each row to ONE Checkbox control. One CheckBox per ID value. The Checked property should be bound to the Selected column in the DataTable.
  3. When changes are made, and the user clicks the Button, I want to be able to tell what changes the user made, using the HasChanges() and GetChanges() methods of the DataSet (ie 2 rows updated if the user has changed the Checked property of two of the Checkbox controls).

EDIT 2

Thanks to @RezaAghaei I have come up with a solution. I have refined the code from my example of the problem, and made all controls generate based on the data (and position themselves accordingly) to make this example simple to copy-and-paste. Also, this uses a RowFilter on the DataView, rather than the Position property of the BindingSource.

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

The key is the call to EndEdit() in the ColumnChanged event for DataTable (a general call to EndCurrentEdit() just doesn't appear to cut it), although one additional problem I encountered was that this code won't work if it's in the form's New() method, even if it's after the call to InitializeComponent(). I'm guessing this is because there's some initialisation that Winforms does after the call to New() which is necessary for data binding to work properly.

I hope this example saves others the time I spent looking into this.

Upvotes: 2

Views: 2046

Answers (1)

Reza Aghaei
Reza Aghaei

Reputation: 125197

Consider these corrections and the problem will be solved:

  • To bind a control to an specific index of a list, bind control to a binding source containing the list, and then set the Position of binding source to the specifixindex.
  • When adding data-binding to CheckBox controls, set update mode to OnPropertyChanged.
  • Handle CheckedChanged event of CheckBox controls and call Invalidate method of grid to show changes in grid.
  • In CheckedChanged also call EndEdit of the BindingSource which the CheckBox is bound to.
  • Note: Call EndEdit usnig BeginInvoke to let all processes of checking the checkbox (including updating data source) be completed.

enter image description here

C# Example

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 Example

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

Upvotes: 1

Related Questions