跨线程修改嵌套ObservableCollection时抛出异常问题求助
嘿,我来帮你搞定这个WPF TreeView跨线程更新集合的问题!先理清楚你的场景,再给你实用的解决方案~
你现在的情况是:
- TreeView的
ItemsSource绑定到ViewModel里的ObservableCollection<Plane>(命名为Planes) - 用
HierarchicalDataTemplate做层级展示,模板的ItemsSource又绑定到每个Plane对象下的另一个ObservableCollection(比如子节点集合) - 这两个集合都是在非UI线程里动态更新的,但WPF的UI绑定有严格的线程限制,直接跨线程操作肯定会出问题
先补全你没写完的XAML示例(方便后续说明,假设你的实体类是Plane,有子集合ChildrenPlanes和显示名称PlaneName):
<TreeView x:Name="planeView" BorderThickness="0" MaxHeight="500" ItemsSource="{Binding Planes}" SelectedItemChanged="treeview_OnSelectedItemChanged"> <TreeView.Resources> <HierarchicalDataTemplate DataType="{x:Type local:Plane}" ItemsSource="{Binding ChildrenPlanes}"> <TextBlock Text="{Binding PlaneName}" /> </HierarchicalDataTemplate> </TreeView.Resources> </TreeView>
ObservableCollection<T>本身不是线程安全的,而且WPF的绑定系统要求所有UI相关的操作必须在UI线程执行。如果直接在后台线程添加/删除集合元素,一定会抛出InvalidOperationException,提示“调用线程无法访问此对象,因为另一个线程拥有该对象”。
1. 手动用Dispatcher切回UI线程更新
这是最直接的方式,不管是更新顶层的Planes集合,还是每个Plane下的子集合,都通过UI线程的Dispatcher来执行操作。
ViewModel里的示例代码
假设你的ViewModel已经实现了INotifyPropertyChanged,可以这么写:
public class PlaneViewModel : INotifyPropertyChanged { // 拿到UI线程的Dispatcher private readonly Dispatcher _uiDispatcher = Application.Current.Dispatcher; public ObservableCollection<Plane> Planes { get; } = new ObservableCollection<Plane>(); // 后台线程调用这个方法添加顶层节点 public void AddTopPlaneFromBackground(Plane newPlane) { // 检查当前线程是不是UI线程 if (!_uiDispatcher.CheckAccess()) { // 不是的话,委托到UI线程执行 _uiDispatcher.Invoke(() => AddTopPlaneFromBackground(newPlane)); return; } Planes.Add(newPlane); } // 更新子节点集合的方法 public void AddChildPlaneFromBackground(Plane parentPlane, Plane childPlane) { if (!_uiDispatcher.CheckAccess()) { _uiDispatcher.Invoke(() => AddChildPlaneFromBackground(parentPlane, childPlane)); return; } parentPlane.ChildrenPlanes.Add(childPlane); } // INotifyPropertyChanged的实现(这里略写,你应该已经有了) public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
2. 封装一个线程安全的ObservableCollection
如果需要频繁在后台线程更新集合,不如自己写个线程安全的集合子类,内部自动处理Dispatcher切换,这样ViewModel里的代码会清爽很多。
自定义线程安全集合代码
public class ThreadSafeObservableCollection<T> : ObservableCollection<T> { // 直接拿到UI线程的Dispatcher private readonly Dispatcher _uiDispatcher = Application.Current.Dispatcher; // 重写集合变更通知的方法,确保在UI线程触发 protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (_uiDispatcher.CheckAccess()) { base.OnCollectionChanged(e); } else { _uiDispatcher.Invoke(() => base.OnCollectionChanged(e)); } } // 重写属性变更通知的方法 protected override void OnPropertyChanged(PropertyChangedEventArgs e) { if (_uiDispatcher.CheckAccess()) { base.OnPropertyChanged(e); } else { _uiDispatcher.Invoke(() => base.OnPropertyChanged(e)); } } // 重写Add方法,自动切线程 public new void Add(T item) { if (_uiDispatcher.CheckAccess()) { base.Add(item); } else { _uiDispatcher.Invoke(() => base.Add(item)); } } // 同理,把Remove、Clear这些修改集合的方法都重写一遍 public new void Remove(T item) { if (_uiDispatcher.CheckAccess()) { base.Remove(item); } else { _uiDispatcher.Invoke(() => base.Remove(item)); } } }
怎么用这个自定义集合?
ViewModel里直接替换原来的ObservableCollection就行,后台线程可以直接调用Add/Remove,不用手动切线程:
// 用自定义的线程安全集合 public ThreadSafeObservableCollection<Plane> Planes { get; } = new ThreadSafeObservableCollection<Plane>(); // 后台线程直接调用,不用管线程切换 public void AddPlaneFromBackground(Plane newPlane) { Planes.Add(newPlane); }
如果你的后台线程是用Task启动的,也可以用TaskScheduler.FromCurrentSynchronizationContext()指定UI线程调度器,比如:
Task.Run(() => { // 后台干活,比如从服务端获取Plane数据 var newPlane = FetchPlaneFromApi(); // 切回UI线程更新集合 return Task.Factory.StartNew(() => Planes.Add(newPlane), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); });
另外,要是你的Plane类的属性也会在后台线程更新,那属性的PropertyChanged事件同样要通过Dispatcher触发,不然UI也不会实时更新哦~
内容的提问来源于stack exchange,提问作者az-dev

