Interminable Interminable - 4 months ago 19
Vb.net Question

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()

For i As Integer = 0 To dt.Rows.Count-1
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

Dim cb As New CheckBox() With {.Text = "CheckBox" & i+1, .Location = New Point(10, 25 * (i))}
cb.DataBindings.Add(db)

Me.Controls.Add(cb) 'If the control is added after the setting of the EventHandler, the first change the user makes to the CheckBox will not be detected by HasChanges().

Dim checkedChangedEvent As EventHandler = Sub(sender2 As Object, e2 As EventArgs)
TryCast(TryCast(sender2, CheckBox).DataBindings(0).DataSource, BindingSource).EndEdit()
End Sub
AddHandler cb.CheckedChanged, checkedChangedEvent

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
CheckedChanged
event for each
Checkbox
(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.

Answer

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.

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);
            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)
                 b.EndEdit()
             End Sub
        bs.Position = i
    Next i
End Sub
Comments