user4638379
user4638379

Reputation: 45

How does BindingOperations.EnableCollectionSynchronization work, and how can you wrap it?

So I'm trying to understand how BindingOperations.EnableCollectionSynchronization works, and this basic toy example works as you'd expect, it just magically sends the collection update to the UI thread:

using DynamicData.Binding;
using DynamicData;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Reactive.Linq;

namespace testapp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public ObservableCollection<string> List1 { get; set; } = new ObservableCollection<string>();

        private static object lockObject = new object();

        public MainWindow()
        {
            InitializeComponent();

            BindingOperations.EnableCollectionSynchronization(List1, lockObject);

            DataContext = this;
        }

        private void btn_Click(object sender, RoutedEventArgs e)
        {
            //without EnableCollectionSynchronization this would just fail

            Task.Run(() =>
            {
                lock (lockObject) //it seems to work even without locking, but probably prudent to use it
                {
                    for (int i = 0; i < 1000; i++)
                    {
                        List1.Add("test");
                    }
                }
            });
        }
    }
}

Then I thought that it would be much more convenient to wrap up all the locking and stuff into a wrapper class, so I wrote this:

public class ThreadedObservableCollection<T> : IEnumerable, IEnumerable<T>, INotifyCollectionChanged
{
    public int Count => Collection.Count;
    public ObservableCollection<T> Collection;
    private readonly object _lockObj = new object();

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    public ThreadedObservableCollection()
    {
        Collection = new ObservableCollection<T>();
        Collection.CollectionChanged += Collection_CollectionChanged;

        BindingOperations.EnableCollectionSynchronization(Collection, _lockObj);
    }

    public void Clear()
    {
        lock (_lockObj)
        {
            Collection.Clear();
        }
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return Collection.GetEnumerator();
    }

    public IEnumerator GetEnumerator()
    {
        return Collection.GetEnumerator();
    }

    public T this[int index]
    {
        get
        {
            lock (_lockObj)
            {
                return Collection[index];
            }
        }

        set
        {
            lock (_lockObj)
            {
                Collection[index] = value;
            }
        }
    }

    public void Add(T value)
    {
        lock (_lockObj)
        {
            Collection.Add(value);
        }
    }

    public bool Remove(T value)
    {
        lock (_lockObj)
        {
            return Collection.Remove(value);
        }
    }

    private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        CollectionChanged?.Invoke(sender, e);
    }
}

But testing it out I just get the standard "This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread." exception. What's going on here? Why doesn't it work? How does EnableCollectionSynchronization actually operate in general?

Edit: Changing

BindingOperations.EnableCollectionSynchronization(Collection, _lockObj);

to

BindingOperations.EnableCollectionSynchronization(this, _lockObj);

actually makes it work as expected! But how and why?

Upvotes: 1

Views: 544

Answers (1)

mm8
mm8

Reputation: 169350

But how and why?

Because EnableCollectionSynchronization enables the CollectionView that sits in between the source collection and the ItemsControl to participate in synchronized access. So you need to call it on the actual INotifyCollectionChanged that you bind to in the view.

CollectionViewSource.GetDefaultView(Collection) is not equal to CollectionViewSource.GetDefaultView(this). In fact, Collection should be a private field of ThreadedObservableCollection<T> because it's an implementation detail. The view and WPF binds to an instance of ThreadedObservableCollection<T> and knows nothing about the internal ObservableCollection<T>.

Upvotes: 1

Related Questions