WPF+C# MVVM模式下FileSystemWatcher跨线程更新集合求助
解决WPF MVVM中FileSystemWatcher跨线程更新ObservableCollection的问题
我太懂你碰到的这个坑了——FileSystemWatcher的事件回调是在后台线程触发的,你之前用Dispatcher.CurrentDispatcher其实拿到的是那个后台线程的Dispatcher,根本不是UI线程的!所以调用Invoke之后还是在后台线程操作ObservableCollection,自然会炸出跨线程异常。
下面给你几个实用的解决方案,完全贴合WPF和MVVM的特性:
1. 快速修复:直接用UI线程的Dispatcher
最简单的改法就是换成Application.Current.Dispatcher,它指向的就是UI线程的Dispatcher。修改你的OnCreated方法:
public void OnCreated(object sender, FileSystemEventArgs e) { string name = e.FullPath; // 直接调用UI线程的Dispatcher执行操作 Application.Current.Dispatcher.Invoke(() => GetFile(name)); }
这样GetFile就会在UI线程跑,往ObservableCollection里加元素就不会报错了。
2. MVVM优雅解耦:注入Dispatcher(方便测试)
如果不想让ViewModel直接依赖Application.Current(毕竟单元测试时没UI环境),可以在ViewModel构造时传入UI线程的Dispatcher:
ViewModel代码
private readonly Dispatcher _uiDispatcher; private ObservableCollection<TestFile> destinationFiles = new ObservableCollection<TestFile>(); public ObservableCollection<TestFile> DestinationFiles { get => destinationFiles; private set { destinationFiles = value; RaisePropertyChanged(); } } public MainWindowViewModel(Dispatcher uiDispatcher) { _uiDispatcher = uiDispatcher; // 初始化FileSystemWatcher watcher.NotifyFilter = NotifyFilters.LastWrite; watcher.Created += OnCreated; watcher.Changed += OnChanged; } private void OnCreated(object sender, FileSystemEventArgs e) { string filePath = e.FullPath; // 用注入的UI Dispatcher执行操作 _uiDispatcher.Invoke(() => { var testFile = new TestFile(filePath); DestinationFiles.Add(testFile); // 移动文件的逻辑,一定要处理文件被占用的情况! try { // 先等文件完全写入磁盘,避免移动时报错 if (WaitForFileReady(filePath)) { string destPath = Path.Combine(DestinationFolder, Path.GetFileName(filePath)); File.Move(filePath, destPath); } } catch (Exception ex) { // 这里可以加日志或者给用户弹提示 } }); } // 辅助方法:等待文件可被访问(解决文件未完全写入的问题) private bool WaitForFileReady(string filePath, int timeoutMs = 5000) { var startTime = DateTime.Now; while (true) { try { using (var stream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) { return true; } } catch (IOException) { if (DateTime.Now.Subtract(startTime).TotalMilliseconds > timeoutMs) { return false; } Thread.Sleep(100); } } }
View后台代码(MainWindow.xaml.cs)
初始化ViewModel时把当前UI的Dispatcher传进去:
public MainWindow() { InitializeComponent(); DataContext = new MainWindowViewModel(Dispatcher); }
3. 多线程频繁更新场景:启用集合同步
如果需要多个线程同时操作集合,还可以用BindingOperations.EnableCollectionSynchronization开启同步,配合锁使用:
在ViewModel构造时添加:
private readonly object _collectionLock = new object(); public MainWindowViewModel() { // ...其他初始化代码 BindingOperations.EnableCollectionSynchronization(destinationFiles, _collectionLock); }
这样后续在后台线程直接操作DestinationFiles也不会报错,但要注意锁的使用,避免死锁。不过这种方式不如前两种符合MVVM的单一职责原则,适合复杂的多线程场景。
必看注意点
- FileSystemWatcher的事件时机坑:Created事件可能在文件还没完全写入磁盘时就触发,直接移动会抛出
IOException,所以一定要加等待文件就绪的逻辑(比如上面的WaitForFileReady)。 - ObservableCollection的线程安全性:它本身不是线程安全的,要么在UI线程修改,要么开启集合同步。
内容的提问来源于stack exchange,提问作者ScottinTexas
相关产品推荐
相关产品推荐

