1

I'm trying to get an ObservableCollection to fire the CollectionChanged event when a property of an item is changed. I have used the code from here to do it but maybe I have made an error in translating from C# to Vb.net. In any case item_PropertyChanged is not firing. What am I missing here?

Code:

Imports System.ComponentModel
Imports System.Collections.Specialized
Imports System.Collections.ObjectModel

Class MainWindow

    Public Class TrulyObservableCollection(Of T As INotifyPropertyChanged)
        Inherits ObservableCollection(Of T)
        Public Sub New()
            MyBase.New()
            AddHandler CollectionChanged, AddressOf TrulyObservableCollection_CollectionChanged
        End Sub

        Private Sub TrulyObservableCollection_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
            If e.NewItems IsNot Nothing Then
                For Each item As [Object] In e.NewItems
                    AddHandler TryCast(item, INotifyPropertyChanged).PropertyChanged, AddressOf item_PropertyChanged
                Next
            End If
            If e.OldItems IsNot Nothing Then
                For Each item As [Object] In e.OldItems
                    RemoveHandler TryCast(item, INotifyPropertyChanged).PropertyChanged, AddressOf item_PropertyChanged
                Next
            End If
        End Sub

        Private Sub item_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
            Dim a As New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)
            OnCollectionChanged(a)
        End Sub
    End Class

    Public Class edm
        Implements INotifyPropertyChanged
        Property ip As String
        Property status As String
        Public Sub New(ip As String, status As String)
            Me.ip = ip
            Me.status = status
        End Sub

        Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
    End Class

    Public Property edms As New TrulyObservableCollection(Of edm)

    Public Sub New()

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        DataContext = Me
        edms.Add(New edm("192.168.1.111", "On"))
        edms.Add(New edm("192.168.1.112", "Off"))
        edms.Add(New edm("192.168.1.113", "On"))

    End Sub

    Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
        edms.Where(Function(edm) edm.ip = "192.168.1.111").First().status = "Off"
    End Sub
End Class

Edit:

Both Bjørn and Liero provided great answers and in this situation I find it difficult to mark one as correct and not the other so my reasoning for my choice is that while Bjørn's post answered my question as I put it, I marked Liero's answer, since his comments guided me to the best solution for my scenario.

Community
  • 1
  • 1
doovers
  • 8,545
  • 10
  • 42
  • 70

2 Answers2

2

You need to raise the property changed event in the setter of each property if the value differs from the backing field.

Public Property Foo() As String
    Get
        Return Me.m_foo
    End Get
    Set(value As String)
        If (value <> Me.m_foo) Then
            Me.m_foo = value
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("Foo"))
        End If
    End Set
End Property

So your edm class should look more like this:

Public Class Edm
    Implements INotifyPropertyChanged

    Public Sub New(ip As String, status As String)
        Me.m_ip = ip
        Me.m_status = status
    End Sub

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public Property Ip() As String
        Get
            Return Me.m_ip
        End Get
        Set(value As String)
            If (value <> Me.m_ip) Then
                Me.m_ip = value
                Me.NotifyPropertyChanged("Ip")
            End If
        End Set
    End Property

    Public Property Status() As String
        Get
            Return Me.m_status
        End Get
        Set(value As String)
            If (value <> Me.m_status) Then
                Me.m_status = value
                Me.NotifyPropertyChanged("Status")
            End If
        End Set
    End Property

    Private Sub NotifyPropertyChanged(propertyName As String)
        Me.OnPropertyChanged(New PropertyChangedEventArgs(propertyName))
    End Sub

    Protected Overridable Sub OnPropertyChanged(e As PropertyChangedEventArgs)
        RaiseEvent PropertyChanged(Me, e)
    End Sub

    Private m_ip As String
    Private m_status As String

End Class

And, as correctly pointed out by liero in his/her answer, you better override InsertItem, SetItem, RemoveItem and ClearItems rather than handling the CollectionChanged event.

Public Class TrulyObservableCollection(Of T As INotifyPropertyChanged)
    Inherits ObservableCollection(Of T)

    Protected Overrides Sub InsertItem(index As Integer, item As T)
        MyBase.InsertItem(index, item)
        Me.HookItem(item)
    End Sub

    Protected Overrides Sub SetItem(index As Integer, newItem As T)
        Dim oldItem As T = Me.Items(index)
        MyBase.SetItem(index, newItem)
        Me.UnhookItem(oldItem)
        Me.HookItem(newItem)
    End Sub

    Protected Overrides Sub RemoveItem(index As Integer)
        Dim item As T = Me.Items(index)
        MyBase.RemoveItem(index)
        Me.UnhookItem(item)
    End Sub

    Protected Overrides Sub ClearItems()
        For Each item As T In Me.Items
            Me.UnhookItem(item)
        Next
        MyBase.ClearItems()
    End Sub

    Private Sub HookItem(item As T)
        If (Not item Is Nothing) Then AddHandler item.PropertyChanged, AddressOf Me.HandleItemPropertyChanged
    End Sub

    Private Sub UnhookItem(item As T)
        If (Not item Is Nothing) Then RemoveHandler item.PropertyChanged, AddressOf Me.HandleItemPropertyChanged
    End Sub

    Private Sub HandleItemPropertyChanged(sender As Object, e As PropertyChangedEventArgs)
        Me.OnCollectionChanged(New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
    End Sub

End Class
Community
  • 1
  • 1
Bjørn-Roger Kringsjå
  • 9,849
  • 6
  • 36
  • 64
1

You are right, that you need to attach to PropertyChanged event of each item. But you cannot relly on CollectionChanged event when subscribing. For example, when you clear the collection, old items are not in the event arguments. Also items can be passed to ObservableCollection ctor.

Better way is to override methods ClearItems, RemoveItem, InsertItem, SetItem

There are already some implementations, e.g.:

Of course I have written my own some time ago :) But since WPF introduced live shaping I don't really need it:

Edit: Dont forget to fire propertychanged event when property changes, just like @Bjørn-Roger Kringsjå proposed. Based on comments, your problem is not in observablecollection, but INotifyPropertyChanged implementation of your class

Liero
  • 25,216
  • 29
  • 151
  • 297
  • Thanks for the info mate. I wasn't aware of live shaping so have been reading up on it. I'm wondering though, in my scenario where I have a static list of machines and want to update the status at a set interval and have those changes reflected in the `DataGrid`, how should I implement live shaping? I'm not really grouping, filtering or sorting so is it the right approach here? – doovers Mar 06 '15 at 22:43
  • It should just work. You don't need ObservableCollection at all. CollectionChanged event makes sense only when you need add/remove items from collection and reflect it in datagrid. ObervableCollection.PropertyChanged only notifies that Count property has changed. If you want just to update cell in datagrid when status chages, you need to fire propertychanged event in your 'machine' entity. Binding will update cell automatically. If it doesn't problem is somewhere else, but certainly not in CollectionChanged event – Liero Mar 07 '15 at 10:46
  • Thank you. It seemed I was going about it the wrong way. Got it working by firing `PropertyChanged` in my class property setter as you suggested. – doovers Mar 07 '15 at 22:33