Reputation: 1410
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:
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
.Checkbox
control. One CheckBox
per ID
value. The Checked
property should be bound to the Selected
column in the DataTable
.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
Reputation: 125197
Consider these corrections and the problem will be solved:
CheckBox
controls, set update mode to OnPropertyChanged
.CheckedChanged
event of CheckBox
controls and call Invalidate
method of grid to show changes in grid.CheckedChanged
also call EndEdit
of the BindingSource
which the CheckBox
is bound to.EndEdit
usnig BeginInvoke
to let all processes of checking the checkbox (including updating data source) be completed.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