OnCollectionChanged和后台线程的问题

时间:2014-11-07 19:01:09

标签: c# wpf multithreading observablecollection inotifycollectionchanged

在我的程序中,我使用后台工作线程来打开文件。我的程序的主要结构是数据绑定TreeView。在文件读入过程中,动态TreeView节点在从文件中读入时添加到TreeView。我提到的这些TreeView节点绑定到名为UICollections的容器(继承自ObservableCollection<T>的自定义类)。我已经创建了UICollection<T>类,以确保此类型的CollectionViews永远不会从后台工作线程更改SourceCollections。我是通过将名为UICollection的{​​{1}}中的媒体资源更改为IsNotifying来实现此目的的。

我的false课程:

UICollection<T>

话虽如此,我在使用我的控制结构实现此public class UICollection<T> : ObservableCollection<T> { public UICollection() { IsNotifying = true; } public UICollection(IEnumerable<T> source) { this.Load(source); } public bool IsNotifying { get; set; } protected override void OnPropertyChanged(PropertyChangedEventArgs e) { if (IsNotifying) base.OnPropertyChanged(e); } //Does not raise unless IsNotifying = true protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (IsNotifying) base.OnCollectionChanged(e); } //Used whenever I re-order a collection public virtual void Load(IEnumerable<T> items) { if (items == null) throw new ArgumentNullException("items"); this.IsNotifying = false; foreach (var item in items) this.Add(item); //ERROR created on this line because IsNotifying is always set to true this.IsNotifying = true; this.Refresh(); } public Action<T> OnSelectedItemChanged { get; set; } public Func<T, bool> GetDefaultItem { get; set; } public void Refresh() { OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } 时遇到问题,这涉及从后台工作线程中添加UICollection<T>

为清楚起见,我的程序如下:

  1. UICollections

  2. 在后台工作线程中:Is a file being opened?: YES -> go into Background worker thread(根据需要迭代)

  3. 关闭帖子。

  4. 需要理解的主要概念是,如果后台工作线程处于打开状态,则Do we need to create new UICollections?: YES -> go to method in UIThread that does so必须设置为UICollection.IsNotifying。对于已经知道的集合,我没有问题,但对于动态的集合,我遇到了问题。

    我的后台工作线程的示例:

    false

    UIThread中将集合添加到private void openFile() { //Turn off CollectionChanged for known Collections KnownCollections1.IsNotifying = false; KnownCollections2.IsNotifying = false; //... and so on //Do we need to create new collections? YES -> Call to AddCollection in UIThread //Refresh known collections App.Current.Dispatcher.BeginInvoke((Action)(() => KnownCollections1.Refresh())); App.Current.Dispatcher.BeginInvoke((Action)(() => KnownCollections2.Refresh())); //... and so on //If new collections exist find them and refresh them... } 的方法:

    TreeView

    所有这一切,将会发生以下两件事之一...如果我注释掉public void AddCollection(string DisplayName, int locationValue) { node.Children.Add(CreateLocationNode(displayName, locationValue)); //Add to parent node for (int i = 0; i < node.Children.Count(); i++) { //make sure IsNotifying = false for newly added collection if (node.Children[i].locationValue == locationValue) node.Children[i].Children.IsNotifying = false; } //Order the collection based on numerical value var ordered = node.Children.OrderBy(n => n.TreeView_LocValue).ToList(); node.Children.Clear(); node.Children.Load(ordered); //Pass to UICollection class -- *RUNS INTO ERROR* } 行,则会在this.IsNotifying = true;中引发异常,因为它会在背景线程打开时被提升。如果我按原样保留该行,则该集合将永远不会反映在视图中,因为OnCollectionChanged永远不会被提升,通知视图。如果允许创建这些集合而不遇到这些错误,我需要做什么?我猜测问题出在我的OnCollectionChanged函数或AddCollection()类中。

1 个答案:

答案 0 :(得分:2)

如果我理解正确,您在后台线程上操作集合(或嵌套集合),而同一集合(或“父”集合)被用作UI中的项目源。即使您禁用更改通知,这也不安全。还有其他一些事情,例如用户启动的排序,扩展树节点,由于虚拟化导致的容器回收等,可能导致重新获取集合。如果在另一个线程上更新集合时发生这种情况,则行为未定义。例如,您可以在另一个线程上的插入导致基础列表调整大小的同时触发要迭代的集合,从而可能导致读取空或重复的条目。每当您在两个线程之间共享可变数据时,您需要同步读取和写入,并且由于您不控制执行读取的WPF内部,因此您不能认为执行任何类型的并发写入是安全的。这包括从另一个线程修改UI绑定集合中的对象。

如果您需要在后台线程上操作集合,请拍摄原始集合​​的快照,执行您需要的任何修改,然后将自己封送回UI线程以提交更改(通过完全替换原始或清除)并重新收集该集合)。我使用这种技术安全地对具有大型数据集的网格视图进行背景排序,分组和过滤。但是如果这样做,请小心避免修改集合中包含的项目,因为它们可能仍会被UI引用。可能还需要检测UI线程上发生的任何可能使您的后台更新无效的更改,在这种情况下,当您将自己编组回UI线程,拍摄另一个快照并重新开始时,您将需要放弃更改(或者提出一种更精细的方法来协调这两组变化。)