Reputation: 10154
I have a List(T)
that is bound to some controls, a read-only DataGridView, a ComboBox and a few Labels. This works fine, the controls are all populated correctly when the form loads, the Label.Text and DataGridView row focus all change as the ComboBox selection is changed.
But if I change the data in an object on the List the data shown in the controls does not update to reflect the changed data.
My class T
implements the INotifyChanged
interface and the label control data bindings update mode is set to OnPropertychanged
.
I can force the DataGridView to update by calling its Refresh()
method, but trying the same for the labels seems to have no effect.
So how can I make changes to the data in the objects in my list update the data shown in the Label controls? Have I done something wrong?
My MRE so far:
Class Form1
' Form1 has a DataGridView, a ComboBox, a Label, a Button and a TextBox
Dim FooList As New List(Of Foo)(3)
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
For Index As Integer = 0 To FooList.Capacity - 1
FooList.Add(New Foo() With {.Bar = Index, .Baz = 0})
Next
' Shows all Bar and Baz
DataGridView1.DataSource = FooList
' User selects Bar value
ComboBox1.DataSource = FooList
ComboBox1.DisplayMember = "Bar"
' Related Baz value shows
Label1.DataBindings.Add(New Binding("Text", FooList, "Baz", DataSourceUpdateMode.OnPropertyChanged))
End Sub
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
' This is not _actually_ how I'm selecting indexes and changing the data
' But for the MRE it changes the Baz property
'Change the Baz value on the List, should result in Label1 changing
FooList(ComboBox1.SelectedItem.Bar).Baz = TextBox1.Text.Convert.ToUInt16
' Should I even need this when my list objects have INotifyChanged?
DataGridView1.Refresh()
End Sub
End Class
Class Foo
Implements INotifyChanged
Private _bar As UInt16
Private _baz As UInt16
Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(ByVal PropertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
End Sub
Property Bar As UInt 16
Get
Return _bar
End Get
Set(value As Byte)
If Not (value = _bar) Then
_bar = Bar
NotifyPropertyChanged("Bar")
End If
End Set
End Property
Property Baz As UInt 16
Get
Return _baz
End Get
Set(value As Byte)
If Not (value = _baz) Then
_baz = Baz
NotifyPropertyChanged("Baz")
End If
End Set
End Property
End Class
Upvotes: 0
Views: 2819
Reputation: 38915
One way to have changes in the collection reflected in bound controls, is to "reset" the DataSource
:
FooList.Add(New Foo(...))
dgv1.DataSource = Nothing
dgv1.DataSource = FooList
If the control is something like a ListBox
, you have to also reset the DisplayMember
and ValueMember
properties because they get cleared. This is a greasy way to notify controls of changes to the list because many things get reset. In a ListBox
for instance, the SelectedItems
collection is cleared.
A much better way to let changes to your collection flow thru to controls is to use a BindingList(Of T)
. Changes to the collection/list (adds, removes) will automatically and instantly be shown in your control.
INotifyPropertyChanged
goes one step further. If a property value on an item in the list changes, the BindingList<T>
will catch the PropertyChanged
events your class raises and "forward" the changes to the control.
To be clear, the BindingList
handlea changes to the list, while INotifyPropertyChanged
handles changes to the items in the list.
FooList(13).Name = "Ziggy"
Using a List<T>
the name change won't show up unless you "reset" the DataSource
. Using a BindingList<T>
alone, it wont show up right away - when the list changes, it should show up. Implementing INotifyPropertyChanged
allows the change to show up right away.
Your code is mostly correct for it, except it is not INotifyChanged
. But there is also a shortcut as of Net 4.5:
Private Sub NotifyChange(<CallerMemberName> Optional propname As String = "")
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propname))
End Sub
CallerMemberName
is an attribute which allows you to forego actually passing the name; the named ends up being substituted at runtime:
Private _name As String
Public Property Name As String
Get
Return _name
End Get
Set(value As String)
If _name <> value Then
_name = value
NotifyChange()
End If
End Set
End Property
If nothing else it can cut down on copy/paste errors if there are lots of properties to raise events for (ie Bar property using NotifyChange("Foo")
because you copied the code from that setter).
Upvotes: 1
Reputation: 10154
I figured I can push the data around the other way. That is, instead of updating the data in the list and then trying to update the controls, I update the control and the List data is updated via the binding.
E.g. My click event from above now becomes:
Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
Label1.Text = TextBox1.Text
DataGridView1.Refresh()
End Sub
Though TBH Im not a fan of that and I'm still puzzled as to how I could better use the INotifyPropertyChanged
interface .
Upvotes: 1