我有一个ObservableCollection<A> a_collection;
该集合包含'n'个项目。每个项目A看起来像这样:
public class A : INotifyPropertyChanged
{
public ObservableCollection<B> b_subcollection;
Thread m_worker;
}
基本上,它都连接到WPF列表视图+详细信息视图控件,它在单独的列表视图中显示所选项目的b_subcollection(双向绑定,propertychanged上的更新等)。当我开始实现线程时,问题出现了。整个想法是让整个a_collection使用它的工作线程来“工作”,然后更新它们各自的b_subcollections并让gui实时显示结果。
当我尝试它时,我得到一个例外,说只有Dispatcher线程可以修改ObservableCollection,并且工作停止了。
任何人都可以解释这个问题,以及如何解决它?
干杯
答案 0 :(得分:104)
从.NET 4.5开始,有一个内置机制可以自动同步对集合的访问并将CollectionChanged
事件调度到UI线程。要启用此功能,您需要从UI线程中调用BindingOperations.EnableCollectionSynchronization
。
EnableCollectionSynchronization
做了两件事:
CollectionChanged
个事件。非常重要的是,这并不能解决所有问题:确保线程安全访问本质上不是线程安全的集合你必须与框架合作在即将修改集合时从后台线程获取相同的锁。
因此,正确操作所需的步骤是:
这将确定必须使用EnableCollectionSynchronization
的哪个重载。大多数情况下,一个简单的lock
语句就足够了,因此this overload是标准选择,但如果你使用一些奇特的同步机制,那么support for custom locks也是如此。
根据所选的锁定机制,在UI线程上调用适当的重载 。如果使用标准lock
语句,则需要提供锁定对象作为参数。如果使用自定义同步,则需要提供CollectionSynchronizationCallback
委托和上下文对象(可以是null
)。调用时,此委托必须获取自定义锁,调用传递给它的Action
并在返回之前释放锁。
当您要自行修改集合时,您还必须使用相同的机制锁定集合;在简单方案中传递给lock()
的同一个锁对象上使用EnableCollectionSynchronization
执行此操作,或在自定义方案中使用相同的自定义同步机制。
答案 1 :(得分:62)
从技术上讲,问题不在于您是从后台线程更新ObservableCollection。问题是,当你这样做时,集合会在导致更改的同一个线程上引发其CollectionChanged事件 - 这意味着控件正在从后台线程更新。
为了在控件绑定到后台线程时从后台线程填充集合,您可能必须从头开始创建自己的集合类型以解决此问题。有一个更简单的选项可能会对你有用。
将添加调用发布到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());
此方法将立即返回(在项目实际添加到集合之前),然后在UI线程上,该项目将添加到集合中,每个人都应该感到高兴。
然而,实际情况是,由于所有的跨线程活动,此解决方案可能会在重负载下陷入困境。一个更有效的解决方案是批量处理一堆项目并定期将它们发布到UI线程,这样你就不会跨每个项目的线程调用。BackgroundWorker类实现了一种模式,允许您在后台操作期间通过其ReportProgress方法报告进度。通过ProgressChanged事件在UI线程上报告进度。这可能是你的另一种选择。
答案 2 :(得分:15)
使用.NET 4.0,您可以使用这些单行代码:
<强> .Add
强>
Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));
<强> .Remove
强>
Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
答案 3 :(得分:2)
后代的集合同步代码。这使用简单的锁定机制来启用集合同步。请注意,您必须在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);
}
}
}