0

I know there are a lot of questions about this already, but I seem to have a fundamental misunderstanding about how BindingOperations.EnableCollectionSynchronization(observableC, padlock); works.

I have a WPF app using mvvm and in my viewmodel I want to update my observablecollection.

After some googling I landed on this solution that imo should work: Calling it the first time works fine, but after sleeping for 1 minute it gives me this:

System.NotSupportedException: 'This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.'

 public MainViewModel()
    {
        MainOc= new ObservableCollection<DataModel>();
        MainView= CollectionViewSource.GetDefaultView(MainOc);
        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
        {
            BindingOperations.EnableCollectionSynchronization(MainOc, padlock);
            BindingOperations.EnableCollectionSynchronization(MainView, padlock);
        }));
        
        Task.Run(() => GetData());
    }

private async void GetData()
    {
        while (true)
        {
            lock (padlock)
            {
                MainOc.Clear();
            }
            
            foreach (DataRow row in tempTable.Rows)
            {
                lock (padlock) {
                    MainOc.Add(new DataModel());
                }
            }
            lock (padlock)
            {
                MainView= CollectionViewSource.GetDefaultView(MainOc);
            }
            Thread.Sleep(TimeSpan.FromMinutes(1));
        }
    }
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
  • Why is there a CollectionView in your view model? There should only be the ObservableCollection. The CollectionView would be used automatically. – Clemens Aug 17 '22 at 12:02
  • It seems also odd that EnableCollectionSynchronization is wrapped in a Dispatcher call. Is the MainViewModel constructor not called in the UI thread? – Clemens Aug 17 '22 at 12:03
  • You should also not call Task.Run in a constructor, because the call must be awaited. Add a `public async Task Initialize()` method to your view model that calls and awaits Task.Run. – Clemens Aug 17 '22 at 12:05
  • Finally you are calling `lock (padlock)` far too often. Do it once in each loop cycle. – Clemens Aug 17 '22 at 12:06
  • Users need to be able to filter the observable collection. To display these filters I use the CollectionView. I didn't find a way to directly filter the "defaultview"of a observable collection without reassigning anyway. – Barbarian772 Aug 17 '22 at 12:07
  • You would create a CollectionViewSource in XAML and bind its Source to the ObservableCollection. See [Binding to collections](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/?view=netdesktop-6.0#binding-to-collections). – Clemens Aug 17 '22 at 12:13
  • Ok your points were pretty valid, but I still get the same errror, after implementing them. I wrapped it in a dispatcher call after it not working the first time. Even calling the dispatcher each time I am calling MainOc.Add is not working for me. – Barbarian772 Aug 17 '22 at 12:13
  • I read the article, but I don't want to add any code behind just for filtering. Especially since I need to update my filters dynamically from my viewmodel. – Barbarian772 Aug 17 '22 at 12:21
  • BindingOperations.EnableCollectionSynchronization must be called in the UI thread before the collection is used. Remove the Dispatcher call, make sure the view model is creaed in the UI thread. Do not call CollectionViewSource.GetDefaultView(MainOc) before EnableCollectionSynchronization. – Clemens Aug 17 '22 at 12:21
  • Ok, I now get System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.' Where would you put the EnableCollectionSynchronization to make sure it's in the UIThread? – Barbarian772 Aug 17 '22 at 12:32
  • As said, make sure the view model is created in the UI thread. – Clemens Aug 17 '22 at 12:48
  • @Barbarian772: On which line are you getting the exception? – mm8 Aug 18 '22 at 08:45
  • @mm8 on the first MainOc.Clear. I wrapped the while loop in Application.Current.Dispatcher.BeginInvoke(new Action(() => { })); Which seems to work for now, I have still no idea why EnableSynchronization is not working as intended – Barbarian772 Aug 18 '22 at 11:33

1 Answers1

0

I would suspect Application.Current.Dispatcher.BeginInvoke, since this will be run at some later time on the UI thread. Presumably the constructor is already running on the UI thread, so this code will be placed last in the queue of tasks the UI thread is scheduled to perform. The documentation for EnableCollectionSynchronization states

  • The call must occur on the UI thread.

  • The call must occur before using the collection on a different thread or before attaching the collection to the ItemsControl, whichever is later.

I'm not confident the later point is fulfilled. I would expect the order to be something like:

  1. MainViewModel() is run
  2. GetData() start updating the collection (on background thread)
  3. The viewmodel is attached to the view
  4. BindingOperations.EnableCollectionSynchronization is called

So I would at least try to call BindingOperations.EnableCollectionSynchronization directly, without the BeginInvoke. Or at the very least, place some breakpoints or logging to verify that the EnableCollectionSynchronization is called before the viewmodel is attached.

In any case I would probably not recommend using that kind of synchronization mechanism. My recommendation would be to:

  1. Use a dispatch timer to invoke a method on the UI thread every minute
  2. If method do any slow operation, like fetching data from the database, make the method async and either do a async database call, or a synchronous database call on a background thread that returns the result. In either case the call should be awaited.
  3. Once you have the data, update your collections to update the UI. This code will be run on the UI thread, even if you awaited work on a background thread, so there is no need for synchronization.
JonasH
  • 28,608
  • 2
  • 10
  • 23
  • I tried it with a few breakpoints and without BeginInvoke. EnableCollectionsSynchronization is definitely run before GetData(). Where would I put it normally to make sure it runs on the UIThread? – Barbarian772 Aug 17 '22 at 12:27
  • @Barbarian772 a common model is that everything should be run on the UI thread unless otherwise specified. I.e. you have a single threaded program, with small, selected parts that run in parallel or on a background thread. These parts should be scrutinized extra to ensure they are thread safe. So I would expect that the MainViewModel constructor should be run on the UI thread. It should be trivial to place a breakpoint to verify if that is the case. – JonasH Aug 17 '22 at 12:32