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

C#中INotifyPropertyChanged引发的竞态条件问题咨询

解决C#中INotifyPropertyChanged引发的ComboBox绑定竞态条件问题

从你描述的场景来看,这个竞态条件的核心原因很明确:当用户快速切换SelectedCostcentre时,前一次异步加载员工的任务还没完成,新的任务已经启动,如果旧任务反而晚于新任务完成,就会把员工列表覆盖成旧成本中心的数据,导致UI显示和当前选中的成本中心不匹配。

下面给你两种实用的解决方案,你可以根据自己的场景选择:


方案1:使用CancellationTokenSource取消旧任务(推荐)

这是最直接且资源友好的方式,通过取消未完成的异步任务,从根源上避免无效的结果覆盖。

修改你的ViewModel代码如下:

public abstract class SelectEmployeeViewModel : ViewModelBase
{
    protected readonly Plant plant;
    private CancellationTokenSource _employeeLoadCts;
    private Costcentre _selectedCostcentre;
    private ObservableCollection<Employee> _employees = new ObservableCollection<Employee>();

    public ObservableCollection<Costcentre> Costcentres { get; protected set; }
    public ObservableCollection<Employee> Employees 
    { 
        get => _employees; 
        protected set => SetProperty(ref _employees, value); 
    }

    public Costcentre SelectedCostcentre
    {
        get => _selectedCostcentre;
        set
        {
            if (SetProperty(ref _selectedCostcentre, value))
            {
                // 取消并清理之前的加载任务
                _employeeLoadCts?.Cancel();
                _employeeLoadCts?.Dispose();
                _employeeLoadCts = new CancellationTokenSource();

                // 异步加载员工,传入取消令牌
                _ = LoadEmployeesAsync(value, _employeeLoadCts.Token);
            }
        }
    }

    private async Task LoadEmployeesAsync(Costcentre selectedCostcentre, CancellationToken token)
    {
        try
        {
            Employees.Clear();

            if (selectedCostcentre == null)
                return;

            // 执行实际的员工检索操作,传入取消令牌
            var fetchedEmployees = await plant.GetEmployeesForCostcentreAsync(selectedCostcentre.Id, token);
            
            // 检查令牌是否已取消,避免后续无效操作
            token.ThrowIfCancellationRequested();

            // 在UI线程更新集合(如果ViewModel不在UI线程上下文)
            await Application.Current.Dispatcher.InvokeAsync(() =>
            {
                foreach (var emp in fetchedEmployees)
                {
                    Employees.Add(emp);
                }
            });
        }
        catch (OperationCanceledException)
        {
            // 任务被取消是预期行为,无需额外处理
        }
        catch (Exception ex)
        {
            // 这里可以添加日志记录或错误提示
            Console.WriteLine($"加载员工失败: {ex.Message}");
        }
        finally
        {
            // 清理当前的令牌源(确保只清理当前任务对应的实例)
            if (_employeeLoadCts?.Token == token)
            {
                _employeeLoadCts.Dispose();
                _employeeLoadCts = null;
            }
        }
    }

    // 构造函数等其他初始化代码...
    protected SelectEmployeeViewModel(Plant plant)
    {
        this.plant = plant;
        Costcentres = new ObservableCollection<Costcentre>();
    }
}

关键要点:

  • 每次变更SelectedCostcentre时,先取消并销毁旧的CancellationTokenSource,再创建新的
  • 异步方法中传入CancellationToken,并在获取结果后检查令牌状态,避免无效的UI更新
  • 捕获OperationCanceledException,这是正常取消场景,不用抛出错误
  • 使用Dispatcher.InvokeAsync确保集合更新在UI线程(WPF中ObservableCollection只能在创建它的线程更新)

方案2:"最新请求获胜"机制(备选)

如果你的异步检索操作不支持取消令牌(比如调用第三方API),可以用这个方法:通过维护一个请求ID,只保留最新请求的结果。

示例代码:

public abstract class SelectEmployeeViewModel : ViewModelBase
{
    protected readonly Plant plant;
    private int _currentRequestId;
    private Costcentre _selectedCostcentre;
    private ObservableCollection<Employee> _employees = new ObservableCollection<Employee>();

    public ObservableCollection<Costcentre> Costcentres { get; protected set; }
    public ObservableCollection<Employee> Employees 
    { 
        get => _employees; 
        protected set => SetProperty(ref _employees, value); 
    }

    public Costcentre SelectedCostcentre
    {
        get => _selectedCostcentre;
        set
        {
            if (SetProperty(ref _selectedCostcentre, value))
            {
                _ = LoadEmployeesAsync(value);
            }
        }
    }

    private async Task LoadEmployeesAsync(Costcentre selectedCostcentre)
    {
        // 线程安全地生成唯一请求ID
        var requestId = Interlocked.Increment(ref _currentRequestId);

        try
        {
            Employees.Clear();

            if (selectedCostcentre == null)
                return;

            var fetchedEmployees = await plant.GetEmployeesForCostcentreAsync(selectedCostcentre.Id);

            // 检查当前请求是否是最新的,不是则忽略结果
            if (requestId != _currentRequestId)
                return;

            await Application.Current.Dispatcher.InvokeAsync(() =>
            {
                foreach (var emp in fetchedEmployees)
                {
                    Employees.Add(emp);
                }
            });
        }
        catch (Exception ex)
        {
            Console.WriteLine($"加载员工失败: {ex.Message}");
        }
    }

    // 构造函数等其他代码...
}

关键要点:

  • 用Interlocked.Increment保证多线程下请求ID的唯一性
  • 异步任务完成后,对比请求ID和当前最新ID,不一致则放弃更新UI
  • 这种方法不会取消旧任务,但能避免旧结果覆盖新结果

两种方案里,优先推荐方案1,因为它能真正终止不必要的异步操作,节省服务器资源和客户端性能;方案2适合无法取消异步操作的场景。

内容的提问来源于stack exchange,提问作者Tommehh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 03:29:23