ObservableCollection更新后WPF ListView未同步问题求助
我实现了继承自ObservableCollection<T>的ObservableCollectionEx类,用来在批量更新集合时抑制通知,直到更新完成后再触发一次Reset通知。现在在一个VSTO Word的CustomTaskPane对应的WPF UserControl中,有两种方式更新集合:一种是通过Rx的Observable.Interval定时触发,另一种是按钮点击触发。调试确认两种方式都成功更新了集合内容,但只有按钮点击能触发ListView的UI刷新。
ObservableCollectionEx 实现代码
public class ObservableCollectionEx<T> : ObservableCollection<T> { private bool _notificationSupressed = false; private bool _supressNotification = false; public bool SupressNotification { get { return _supressNotification; } set { _supressNotification = value; if (_supressNotification == false && _notificationSupressed) { this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); _notificationSupressed = false; } } } protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (SupressNotification) { _notificationSupressed = true; return; } base.OnCollectionChanged(e); } }
定时更新相关代码
public partial class CrossReferenceControl : UserControl, ICrossReferenceControl { private ICrossReferenceControlViewModel referenceControlViewModel; private IOpenDocumentModel OpenDocumentModel; private ICrossReferenceGuy CrossReferenceGuy; private bool isOpen; private static TimeSpan period = TimeSpan.FromSeconds(20); private IObservable<long> observable = Observable.Interval(period); public readonly Subject<bool> OpenDocModelUpdateActionSubject = new Subject<bool>(); public ICrossReferenceControlViewModel ReferenceControlViewModel => referenceControlViewModel; public bool IsOpen { get { return isOpen; } set { isOpen = value; } } public CrossReferenceControl(IOpenDocumentModel openDocumentModel, ICrossReferenceControlViewModel referenceControlViewModel, ICrossReferenceGuy crossReferenceGuy) { InitializeComponent(); this.referenceControlViewModel = referenceControlViewModel; OpenDocumentModel = openDocumentModel; CrossReferenceGuy = crossReferenceGuy; DataContext = referenceControlViewModel; observable.Subscribe(O => OpenDocumentModel.UpdateCaptionsSubject.OnNext(IsOpen)); } }
按钮点击更新代码
private void ButtonRefresh_Click(object sender, RoutedEventArgs e) { OpenDocumentModel.UpdateCaptionsSubject.OnNext(IsOpen); }
问题原因分析
核心问题是Rx的Observable.Interval默认运行在后台线程,而WPF的UI绑定要求INotifyCollectionChanged通知必须在**Dispatcher线程(UI线程)**上触发。按钮点击事件本身就在UI线程执行,所以集合通知能被ListView正常接收;但定时任务的回调是在后台线程,此时更新集合后触发的OnCollectionChanged(包括Reset通知)属于跨线程操作,WPF绑定系统会忽略这类通知,导致UI不刷新。
另外需要确认:UpdateCaptionsSubject的订阅逻辑中,是否在更新ObservableCollectionEx时正确处理了线程上下文?如果订阅代码没有切换到UI线程,集合的更新和通知都会在后台线程执行,同样会导致UI无响应。
解决方案
方案1:在订阅Observable.Interval时切换到UI线程
修改定时任务的订阅代码,使用ObserveOnDispatcher(需引用System.Reactive.Windows.Threading包)将回调切换到UI线程:
observable .ObserveOnDispatcher() // 将后续操作切换到UI线程 .Subscribe(O => OpenDocumentModel.UpdateCaptionsSubject.OnNext(IsOpen));
方案2:在UpdateCaptionsSubject的订阅端切换到UI线程
如果无法修改定时任务的订阅,可在订阅UpdateCaptionsSubject并更新集合的代码中,确保操作在UI线程执行:
// 示例:在ViewModel或OpenDocumentModel的订阅逻辑中 OpenDocumentModel.UpdateCaptionsSubject .ObserveOnDispatcher() .Subscribe(isOpen => { // 此处执行集合更新逻辑,已处于UI线程 UpdateCaptionsCollection(isOpen); });
方案3:修改ObservableCollectionEx,强制在UI线程触发通知
让集合类本身处理线程问题,重写OnCollectionChanged时自动切换到UI线程:
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (SupressNotification) { _notificationSupressed = true; return; } // 检查是否在UI线程,不在则通过Dispatcher调度 if (Application.Current.Dispatcher.CheckAccess()) { base.OnCollectionChanged(e); } else { Application.Current.Dispatcher.Invoke(() => base.OnCollectionChanged(e)); } }
注:此方式需确保Application.Current不为null,VSTO环境下通常有效,非WPF宿主环境需额外处理。
内容的提问来源于stack exchange,提问作者nomacreates

