Toby
Toby

Reputation: 10154

Update data bound control when data changes

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

Answers (2)

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

Toby
Toby

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

Related Questions