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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2091988/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 23:25:42  来源:igfitidea点击:

How do I update an ObservableCollection via a worker thread?

c#wpfmultithreadingobservablecollection

提问by Maciek

I've got an ObservableCollection<A> a_collection;The collection contains 'n' items. Each item A looks like this:

我有一个ObservableCollection<A> a_collection;包含“n”个项目的集合。每个项目 A 看起来像这样:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

Basically, it's all wired up to a WPF listview + a details view control which shows the b_subcollectionof the selected item in a separate listview (2-way bindings, updates on propertychanged etc.).

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

The problem showed up for me when I started to implement threading. The entire idea was to have the whole a_collectionuse it's worker thread to "do work" and then update their respective b_subcollectionsand have the gui show the results in real time.

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

When I tried it , I got an exception saying that only the Dispatcher thread can modify an ObservableCollection, and work came to a halt.

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

Can anyone explain the problem, and how to get around it?

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

采纳答案by Josh

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.

从技术上讲,问题不在于您正在从后台线程更新 ObservableCollection。问题是,当您这样做时,集合会在导致更改的同一线程上引发其 CollectionChanged 事件 - 这意味着正在从后台线程更新控件。

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.

将 Add 调用发布到 UI 线程上。

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.

此方法将立即返回(在项目实际添加到集合之前)然后在 UI 线程上,项目将被添加到集合中,每个人都应该很高兴。

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.

然而,现实情况是,由于所有跨线程活动,此解决方案可能会在重负载下陷入困境。更有效的解决方案是将一堆项目批处理并定期将它们发布到 UI 线程,这样您就不会为每个项目跨线程调用。

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

BackgroundWorker的类实现的模式,使您可以通过汇报其进展ReportProgress后台操作过程中的方法。通过 ProgressChanged 事件在 UI 线程上报告进度。这可能是您的另一种选择。

回答by Jon

New option for .NET 4.5

.NET 4.5 的新选项

Starting from .NET 4.5 there is a built-in mechanism to automatically synchronize access to the collection and dispatch CollectionChangedevents to the UI thread. To enable this feature you need to call BindingOperations.EnableCollectionSynchronizationfrom within your UI thread.

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

EnableCollectionSynchronizationdoes two things:

EnableCollectionSynchronization做两件事:

  1. Remembers the thread from which it is called and causes the data binding pipeline to marshal CollectionChangedevents on that thread.
  2. Acquires a lock on the collection until the marshalled event has been handled, so that the event handlers running UI thread will not attempt to read the collection while it's being modified from a background thread.
  1. 记住调用它的线程并导致数据绑定管道CollectionChanged在该线程上封送事件。
  2. 在处理完编组事件之前获取集合上的锁定,以便运行 UI 线程的事件处理程序不会在从后台线程修改集合时尝试读取该集合。

Very importantly, this does not take care of everything: to ensure thread-safe access to an inherently not thread-safe collection you have to cooperatewith the framework by acquiring the same lock from your background threads when the collection is about to be modified.

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

Therefore the steps required for correct operation are:

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

1. Decide what kind of locking you will be using

1. 决定您将使用哪种锁定方式

This will determine which overload of EnableCollectionSynchronizationmust be used. Most of the time a simple lockstatement will suffice so this overloadis the standard choice, but if you are using some fancy synchronization mechanism there is also support for custom locks.

这将决定EnableCollectionSynchronization必须使用哪个重载。大多数情况下,一个简单的lock语句就足够了,因此这种重载是标准选择,但如果您使用一些奇特的同步机制,也可以支持自定义锁

2. Create the collection and enable synchronization

2. 创建集合并启用同步

Depending on the chosen lock mechanism, call the appropriate overload on the UI thread. If using a standard lockstatement you need to provide the lock object as an argument. If using custom synchronization you need to provide a CollectionSynchronizationCallbackdelegate and a context object (which can be null). When invoked, this delegate must acquire your custom lock, invoke the Actionpassed to it and release the lock before returning.

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

3. Cooperate by locking the collection before modifying it

3.通过在修改之前锁定集合进行协作

You must also lock the collection using the same mechanism when you are about to modify it yourself; do this with lock()on the same lock object passed to EnableCollectionSynchronizationin the simple scenario, or with the same custom sync mechanism in the custom scenario.

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

回答by WhileTrueSleep

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

在 .NET 4.0 中,您可以使用这些单行代码:

.Add

.Add

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

.Remove

.Remove

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

回答by LadderLogic

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.

子孙后代的集合同步代码。这使用简单的锁定机制来启用集合同步。请注意,您必须在 UI 线程上启用集合同步。

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);
        }
    }
}