我如何通过一个工作线程更新观察集?

我得到了一个 ObservableCollection<A> a_collection;。这个集合包含 n 个项目。每个项目 A 看起来像这样:

public class A : INotifyPropertyChanged
{


public ObservableCollection<B> b_subcollection;
Thread m_worker;
}

基本上,它都连接到一个 WPF 列表视图 + 一个详细视图控件,该控件在一个单独的列表视图中显示所选项的 b_subcollection(双向绑定,属性更新等)。

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

当我尝试使用它时,出现了一个异常,说明只有 Dispatcher 线程可以修改 Observer ableCollection,因此工作停止了。

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

78380 次浏览

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.

New option for .NET 4.5

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

EnableCollectionSynchronization does two things:

  1. Remembers the thread from which it is called and causes the data binding pipeline to marshal CollectionChanged events 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.

Very importantly, this does not take care of everything: to ensure thread-safe access to an inherently not thread-safe collection you have to cooperate with 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

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

2. Create the collection and enable synchronization

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

3. Cooperate by locking the collection before modifying it

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 EnableCollectionSynchronization in the simple scenario, or with the same custom sync mechanism in the custom scenario.

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.

I used a SynchronizationContext:

SynchronizationContext SyncContext { get; set; }

// in the Constructor:

SyncContext = SynchronizationContext.Current;

// in the Background Worker or Event Handler:

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

MicrosoftDocs

Platform code for UI (layout, input, raising events, etc.) and your app’s code for UI all are executed on the same UI thread

ObservableCollection is raising CollectionChanged event when one of these actions occurs: Add, Remove, Replace, Move, Reset.. And this event must be raised on UI thread, otherwise, an exception will occur in the caller thread

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

And the UI won't updated.

If you want to update the UI from a background thread, Run the code in Application's dispatcher

Application.Current.Dispatcher.Invoke(() => {
// update UI
});