Remove from Observable collection in ViewModel does not update View, but Updates of Existing Items does update View
Asked Answered
S

1

4

I'm sure this will be a slam dunk for someone... fingers crossed

My ListView ItemsSource is bound to a Property on my ViewModel named TileItems.

Populating the list view updates perfectly.

In the ViewModel, where you see "existingTileItem.Transaction = e.Transaction" . . . The individual listview item updates perfectly.

In the ViewModel, where you see "Me.TileItems.Remove(existingTileItem)" ... The item is not removed from the View. It does successfully remove from the Me.TileItems collection, but the update is not depicted in the View.

Additional Info: AbstractViewModel implements INotificationPropertyChanged, I've tried overriding Equals in the TileItem and not overriding it, and the same results all around happen. I have seen this answer and this answer, but they do not answer the issue I am having.

XAML:

<UserControl.DataContext>
    <local:TransactionTileResultsViewControlViewModel />
</UserControl.DataContext>

<ListView Grid.Row="1"  Name="tileItems" ItemsSource="{Binding TileItems, Mode=TwoWay}" 
                  ItemTemplate="{StaticResource tileItemDataTemplate}" ScrollViewer.HorizontalScrollBarVisibility="Hidden"
                  HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />

ViewModel:

Public Class TransactionTileResultsViewControlViewModel
    Inherits AbstractViewModel
    Implements INavigationAware

    Private _tileItems As TileItems
    Public Property TileItems As TileItems
        Get
            Return Me._tileItems
        End Get
        Set(value As TileItems)
            Me._tileItems = value
            MyBase.RaisePropertyChanged("TileItems")
        End Set
    End Property

'....


    #Region "TransactionUpdateReceived Methods"

        Private Sub TransactionUpdateReceived_Handler(ByVal e As TransactionUpdatedEvent)

            If e.Transaction IsNot Nothing Then

                Dim existingTileItem As TileItem = Me.TileItems.Where(Function(t) t.Transaction.TransactionQueueID = e.Transaction.TransactionQueueID).FirstOrDefault()
                If existingTileItem IsNot Nothing Then

                    If e.Transaction.Canceled Then

                          Me.TileItems.Remove(existingTileItem)

                    Else

                        If e.Transaction.ContainsFailedActivites() OrElse e.Transaction.ContainsCallbackActivities() Then

                            existingTileItem.Transaction = e.Transaction

                        Else

                            Me.TileItems.Remove(existingTileItem)

                        End If

                    End If

                End If

            End If

        End Sub

#End Region

End Class

TileItems Model:

Public Class TileItems
    Inherits ObservableCollection(Of TileItem)

End Class

TileItem Model:

 Imports Microsoft.Practices.Prism.ViewModel

    Public Class TileItem
        Inherits NotificationObject

        Private _created As Date
        Public Property Created As Date
            Get
                Return _created
            End Get
            Set(value As Date)
                _created = value
                MyBase.RaisePropertyChanged("Created")
            End Set
        End Property

        Private _category As String
        Public Property Category As String
            Get
                Return _category
            End Get
            Set(value As String)
                _category = value
                MyBase.RaisePropertyChanged("Category")
            End Set
        End Property

        Private _tileField1 As String
        Public Property TileField1 As String
            Get
                Return _tileField1
            End Get
            Set(value As String)
                _tileField1 = value
                MyBase.RaisePropertyChanged("TileField1")
            End Set
        End Property

        Private _tileField2 As String
        Public Property TileField2 As String
            Get
                Return _tileField2
            End Get
            Set(value As String)
                _tileField2 = value
                MyBase.RaisePropertyChanged("TileField2")
            End Set
        End Property

        Private _tileField3 As String
        Public Property TileField3 As String
            Get
                Return _tileField3
            End Get
            Set(value As String)
                _tileField3 = value
                MyBase.RaisePropertyChanged("TileField3")
            End Set
        End Property

        Private _transaction As Transaction
        Public Property Transaction As Transaction
            Get
                Return _transaction
            End Get
            Set(value As Transaction)
                _transaction = value
                MyBase.RaisePropertyChanged("Transaction")
            End Set
        End Property

    Public Overrides Function Equals(obj As Object) As Boolean

        If TypeOf obj Is TileItem Then

            Dim tileItem As TileItem = DirectCast(obj, TileItem)

            If tileItem.Transaction IsNot Nothing AndAlso Me.Transaction IsNot Nothing Then

                Return tileItem.Transaction.TransactionQueueID = Me.Transaction.TransactionQueueID

            Else
                Return False

            End If

        Else
            Return False
        End If

    End Function


    End Class

UPDATE:

Per @ReedCopsey 's answer, here is the update I made to get this working.

I updated Me.TileItems.Remove(existingTileItem) to be this now

Me.View.Dispatcher.Invoke(Sub()
                              Me.TileItems.Remove(existingTileItem)
                          End Sub, DispatcherPriority.ApplicationIdle)
Sha answered 2/9, 2013 at 19:15 Comment(7)
Why are you creating a custom class for TileItems?Gordan
In a Billy Hollis course I took a while ago, he said to do that when using generic collections... so it's just habit, and probably not needed in this exact situation. I have tried not doing it, aka just using ObservableCollection(Of TileItem) as the TileItems property type, and it's the same results.Sha
In general, I'd actually argue that it's a bad practice - there's no need to create extra classes to maintain and test unless you're adding extra behavior.Gordan
@ReedCopsey ... ummm ok. not sure how this is helpful to my question exactly, but thanks for the observation. note taken :)Sha
Yes, won't help with this issue (I posted an answer which I suspect will, though), but is just something "odd" I noticed in your code ;) Also wanted to make sure that was the entire code, not just part of it.Gordan
Much appreciated sir :) On that note regarding wrapping a generic, you only have to do that if you're literally trying to bind to a ObservableCollection(Of MyObject) without it being a member of a class, right? So, as long as the collection is a part of my ViewModel, and I don't need to modify the collections behavior, there's no need to wrap it? Just curious... responding to your answer now @ReedCopseySha
In general, there's no need to wrap a collection into a custom class unless you're explicitly adding custom logic to it, ever.Gordan
G
7

The code you're pasting should work, from what I can see. The most likely culprit is that your TransactionUpdateReceived event is being raised on a thread that is not the user interface thread. In WPF, single items can be modified on a background thread, but collections cannot (prior to .NET 4.5, but in .NET 4.5, they require extra work).

There are two options. If you're using .NET 4.5, you can use BindingOperations.EnableCollectionSynchronization to allow the ObservableCollection to be modified from a background thread.

Alternatively, you can use Dispatcher.Invoke to push the Add/Remove calls onto the main thread.

Gordan answered 2/9, 2013 at 19:25 Comment(5)
You are correct in that event is raised via a subscribed service event, thus it's on another thread. We don't get to use 4.5 quite yet in our UI, but when we do, I will surely entertain that suggestion. I'm trying Dispatch.Invoke now... note that I have tried: Me.View.Dispatcher.BeginInvoke(Sub() Me.TileItems.Remove(existingTileItem) End Sub, DispatcherPriority.ApplicationIdle) ... and that did not work... but that is also asynchronous, so it would make sense that it doesn't work :)Sha
@Sha Yes, you'll need to use Dispatcher.Invoke, not begininvoke, or rework the logic in your calls so you can invoke all of the operations at the end.Gordan
Dispatcher.Invoke did the trick... many thanks sir. Answer Accepted!Sha
Can you elaborate how to use BindingOperations.EnableCollectionSynchronization in code? The documentation is confusing. What is a lock object ?Theomachy
@HendyIrawan You make a separate object that you lock whenever you change the collection, and WPF will lock as needed, so it can synchronize. Usually just object lockOnMe = new object();Gordan

© 2022 - 2024 — McMap. All rights reserved.