目录

WPF-IDispatcherService和BindingOperations.EnableCollectionSynchronization

  • IDispatcherService:这不是 .NET 自带的类,而是一种设计模式(或者说是 MVVM 框架里常用的套路)。
  • BindingOperations.EnableCollectionSynchronization:这是 WPF 原生自带的一个静态方法。

1. IDispatcherService (为了解耦和单元测试)

你以前写代码可能习惯这样:

// ViewModel 中
Application.Current.Dispatcher.Invoke(() => { ... });

这就导致你的 ViewModel 深度依赖了 View (WPF UI)
如果你想给这个 ViewModel 写单元测试(Unit Test),你会发现跑不起来,因为单元测试环境里没有 Application.Current,直接报空引用错误。

怎么解决?用接口(Interface)!

第一步:定义一个接口
我们不直接用 Dispatcher,而是定义一个“发令官”的抽象标准。

public interface IDispatcherService
{
    void Invoke(Action action);
    // 也可以加上 InvokeAsync
}

第二步:写一个 WPF 的实现类
这个类专门给 WPF 运行的时候用。

public class WpfDispatcherService : IDispatcherService
{
    public void Invoke(Action action)
    {
        Application.Current.Dispatcher.Invoke(action);
    }
}

第三步:ViewModel 依赖接口,而不是具体实现

public class MainViewModel
{
    private readonly IDispatcherService _dispatcher;

    // 构造函数注入
    public MainViewModel(IDispatcherService dispatcher)
    {
        _dispatcher = dispatcher;
    }

    public void OnBackgroundDataReceived()
    {
        // 这里的代码不认识 WPF,它只认识接口
        // 这样写,即便是在后台线程,通过接口也能切回 UI 线程
        _dispatcher.Invoke(() => 
        {
            this.Status = "数据已更新";
        });
    }
}

为什么这么做?

  • WPF 运行时:注入 WpfDispatcherService,程序正常跑。
  • 单元测试时:注入一个 MockDispatcherService(假的实现类,直接执行 action()),测试就能跑通了!

注:像 Prism, MVVM Light, CommunityToolkit.Mvvm 这些框架通常都内置了类似的机制(比如 MainThread DispatcherHelper),原理都是一样的。

2.BindingOperations.EnableCollectionSynchronization (列表的神器)

这是 WPF 原生提供的一个黑科技,专门解决 ObservableCollection 在多线程下的痛点。

痛点场景
你有一个列表 ObservableCollection<string> Logs 绑定在 ListView 上。
你在后台线程(Task.Run)里收到一条日志,想 Logs.Add("新日志")

  • 如果不处理:报错 NotSupportedException(该类型的 CollectionView 不支持从与 Dispatcher 线程不同的线程对其 SourceCollection 进行的更改)。

传统做法

Application.Current.Dispatcher.Invoke(() => Logs.Add("新日志"));

如果你的日志来得特别快(1秒1000条),你就要 Invoke 1000次,频繁切换线程,性能极差,界面会卡顿。

神技做法:EnableCollectionSynchronization

它的作用是告诉 WPF:“只要我往这个集合里加数据,请你自动帮我把‘通知界面更新’这一步操作搬运到 UI 线程去,不用我自己写 Dispatcher。

代码示例

public class MainViewModel
{
    // 数据源
    public ObservableCollection<string> Logs { get; set; }
    
    // 必须准备一把锁(任意对象)
    private object _logLock = new object();

    public MainViewModel()
    {
        Logs = new ObservableCollection<string>();

        // 🌟 关键就是这一句!只需要在构造函数写一次 🌟
        // 告诉 WPF:对于 Logs 这个集合,如果在后台线程修改它,
        // 请使用 _logLock 这把锁来保证线程安全,并自动帮我更新 UI。
        BindingOperations.EnableCollectionSynchronization(Logs, _logLock);
    }

    public void StartHeavyTask()
    {
        Task.Run(() => 
        {
            // --- 这里是后台线程 ---
            
            // 以前:必须用 Dispatcher.Invoke(...)
            // 现在:直接 Add,像在主线程一样!
            // WPF 会在底层自动处理线程封送,性能比手动 Invoke 更好
            
            lock (_logLock) // 配合锁使用,防止多线程并发写入冲突
            {
                Logs.Add($"后台日志 {DateTime.Now}");
            }
        });
    }
}

使用注意

  1. 必须在 UI 线程启用EnableCollectionSynchronization 这行代码必须在 UI 线程(通常是构造函数)里执行。
  2. 锁的作用:虽然 WPF 帮你解决了 UI 更新的线程问题,但 ObservableCollection 本身并不是线程安全的。如果多个后台线程同时 Add,内部数组会坏掉。所以你在后台 Add 的时候,最好还是 lock(_lockObj) 一下。

总结

  1. IDispatcherService

    • 解决什么:代码结构洁癖,解耦,为了方便写单元测试。
    • 本质:一种设计模式(接口封装)。
  2. BindingOperations.EnableCollectionSynchronization

    • 解决什么:高频更新列表(List/Grid)时的线程切换繁琐和性能问题。
    • 本质:WPF 原生提供的多线程列表更新补丁。