You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:52:19