如何通过工作线程更新 ObservableCollection?

我有一个 ObservableCollection<A> a_collection; 该集合包含“n”个项目。每个项目 A 如下所示:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

基本上,它都连接到 WPF 列表视图 + 详细信息视图控件,该控件在单独的列表视图中显示所选项目的 b_subcollection (2 路绑定、propertychanged 的​​更新等)。

当我开始实现线程时,问题就出现了。整个想法是让整个 a_collection 使用它的工作线程“工作”,然后更新它们各自的 b_subcollections 并让 gui 实时显示结果。

当我尝试它时,我得到一个异常,说只有 Dispatcher 线程可以修改 ObservableCollection,并且工作停止了。

谁能解释这个问题,以及如何解决它?

stack overflow How do I update an ObservableCollection via a worker thread?
原文答案
author avatar

接受的答案

Technically the problem is not that you are updating the ObservableCollection from a background thread. The problem is that when you do so, the collection raises its CollectionChanged event on the same thread that caused the change - which means controls are being updated from a background thread.

In order to populate a collection from a background thread while controls are bound to it, you'd probably have to create your own collection type from scratch in order to address this. There is a simpler option that may work out for you though.

Post the Add calls onto the UI thread.

public static void AddOnUI<T>(this ICollection<T> collection, T item) {
    Action<T> addMethod = collection.Add;
    Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}

...

b_subcollection.AddOnUI(new B());

This method will return immediately (before the item is actually added to the collection) then on the UI thread, the item will be added to the collection and everyone should be happy.

The reality, however, is that this solution will likely bog down under heavy load because of all the cross-thread activity. A more efficient solution would batch up a bunch of items and post them to the UI thread periodically so that you're not calling across threads for each item.

The BackgroundWorker class implements a pattern that allows you to report progress via its ReportProgress method during a background operation. The progress is reported on the UI thread via the ProgressChanged event. This may be another option for you.


答案:

作者头像

.NET 4.5 的新选项

从 .NET 4.5 开始,有一个内置机制可以自动同步对集合的访问并将 CollectionChanged 事件分派到 UI 线程。要启用此功能,您需要调用 BindingOperations.EnableCollectionSynchronization from within your UI thread

EnableCollectionSynchronization 做了两件事:

  1. 记住调用它的线程并导致数据绑定管道在该线程上编组 CollectionChanged 事件。
  2. 在处理完编组事件之前获取对集合的锁定,以便运行 UI 线程的事件处理程序在从后台线程修改集合时不会尝试读取集合。

非常重要的是,这并不能解决所有问题:为了确保对本质上不是线程安全的集合的线程安全访问您必须通过从后台线程获取相同的锁来与框架合作该集合即将被修改。

因此,正确操作所需的步骤是:

1. 决定你将使用什么样的锁定

这将确定必须使用 EnableCollectionSynchronization 的哪个重载。大多数情况下,一个简单的 lock 语句就足够了,因此 this overload 是标准选择,但如果您使用一些花哨的同步机制,还有 support for custom locks

2. 创建集合并启用同步

根据选择的锁定机制,在 UI 线程上调用适当的重载。如果使用标准的 lock 语句,您需要提供锁定对象作为参数。如果使用自定义同步,您需要提供 CollectionSynchronizationCallback 委托和上下文对象(可以是 null )。调用时,此委托必须获取您的自定义锁,调用传递给它的 Action 并在返回之前释放锁。

3. 修改前锁定集合进行合作

当您要自己修改集合时,您还必须使用相同的机制锁定集合;在简单场景中使用 lock() 在传递给 EnableCollectionSynchronization 的同一锁定对象上执行此操作,或者在自定义场景中使用相同的自定义同步机制。

作者头像

With .NET 4.0 you can use these one-liners:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
作者头像

Collection synchronization code for posterity. This uses simple lock mechanism to enable collection sync. Notice that you'll have to enable collection sync on the UI thread.

public class MainVm
{
    private ObservableCollection<MiniVm> _collectionOfObjects;
    private readonly object _collectionOfObjectsSync = new object();

    public MainVm()
    {

        _collectionOfObjects = new ObservableCollection<MiniVm>();
        // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
        Application.Current.Dispatcher.BeginInvoke(new Action(() => 
        { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
    }

    /// <summary>
    /// A different thread can access the collection through this method
    /// </summary>
    /// <param name="newMiniVm">The new mini vm to add to observable collection</param>
    private void AddMiniVm(MiniVm newMiniVm)
    {
        lock (_collectionOfObjectsSync)
        {
            _collectionOfObjects.Insert(0, newMiniVm);
        }
    }
}
作者头像

@Jon answer is good but it lacks a code sample:

// UI thread
var myCollection = new ObservableCollection<string>();
var lockObject = new object();
BindingOperations.EnableCollectionSynchronization(myCollection, lockObject );

[..]

// Non UI thread
lock (lockObject) 
{
   myCollection.Add("Foo")
}

Also note that the CollectionChanged event handler will still be called from the non UI thread.

作者头像

我使用了SynchronizationContext:

SynchronizationContext SyncContext { get; set; }

//在构造函数中:

SyncContext = SynchronizationContext.Current;

//在背景工人或活动处理程序中:

SyncContext.Post(o =>
{
    ObservableCollection.AddRange(myData);
}, null);
作者头像

Wrap all ObservableCollection calls with dispatcher invoke:

App.Current.Dispatcher.Invoke(() => _observableCollection.Add(...))

Or use a "ThreadSafe" ObservableCollection that calls dispatcher invoke by itself if the CollectionChanged event is triggered. No App.Current.Dispatcher required.

    public class ThreadSafeObservableCollection<T> : ObservableCollection<T>
    {
        public override event NotifyCollectionChangedEventHandler CollectionChanged;

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
        {
            var notifyCollectionChangedEventHandler = CollectionChanged;

            if (notifyCollectionChangedEventHandler == null)
            {
                return;
            }

            using (this.BlockReentrancy())
            {
                foreach (NotifyCollectionChangedEventHandler handler in notifyCollectionChangedEventHandler.GetInvocationList())
                {
                    var dispatcherObject = handler.Target as DispatcherObject;

                    if (dispatcherObject != null && !dispatcherObject.CheckAccess())
                    {
                        dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, args);
                    }
                    else
                    {
                        handler(this, args); // fallback if target is no dipatcher object
                    }
                }
            }
        }
    }

See also https://www.codeproject.com/Articles/64936/Threadsafe-ObservableImmutable-Collection

Note: It is not completely thread safe. It is only thread safe for DispatcherObject EventHandler when a CollectionChanged event is invoked.