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)
Community
  • 1
  • 1
wakurth
  • 1,644
  • 1
  • 23
  • 39
  • Why are you creating a custom class for `TileItems`? – Reed Copsey Sep 02 '13 at 19:18
  • 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. – wakurth Sep 02 '13 at 19:21
  • 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. – Reed Copsey Sep 02 '13 at 19:22
  • @ReedCopsey ... ummm ok. not sure how this is helpful to my question exactly, but thanks for the observation. note taken :) – wakurth Sep 02 '13 at 19:25
  • 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. – Reed Copsey Sep 02 '13 at 19:25
  • 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 @ReedCopsey – wakurth Sep 02 '13 at 19:28
  • 1
    In general, there's no need to wrap a collection into a custom class unless you're explicitly adding custom logic to it, ever. – Reed Copsey Sep 02 '13 at 19:37

1 Answers1

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.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 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 :) – wakurth Sep 02 '13 at 19:35
  • 1
    @wakurth 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. – Reed Copsey Sep 02 '13 at 19:36
  • Dispatcher.Invoke did the trick... many thanks sir. Answer Accepted! – wakurth Sep 02 '13 at 19:38
  • Can you elaborate how to use BindingOperations.EnableCollectionSynchronization in code? The documentation is confusing. What is a lock object ? – Hendy Irawan Aug 26 '17 at 10:30
  • 1
    @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();` – Reed Copsey Aug 28 '17 at 18:17