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

WinUI 3中PropertyChanged事件未触发:Checkbox绑定异常排查

排查WinUI 3中PropertyChanged事件未捕获的问题

勾选或取消勾选名为DefaultProgramCheckBox的复选框时,ProgramWrapper中DefaultProgram属性的SetValue方法已被调用,但ViewModel中的ProcessPropertyChanged方法未能捕获到对应的PropertyChanged事件。以下是相关代码及问题分析:

UI页面CheckBox代码

<CheckBox x:Name="DefaultProgramCheckBox"
          Content="设为默认程序"
          IsChecked="{x:Bind ViewModel.DesktopWrapper.DefaultProgram, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

ViewModel.cs代码

//主模型
[ObservableProperty]
private ProgramWrapper _desktopWrapper = new(new Program());

public ViewModel()
{
    LoadAsync();

    PropertyChanged += ProcessPropertyChanged;
    DesktopWrapper.PropertyChanged += ProcessPropertyChanged;
}

private void ProcessPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    //勾选/取消勾选复选框时,未捕获到PropertyChanged事件
    if (e.PropertyName == nameof(DesktopWrapper.DefaultProgram)) 
        ShowDefaultProgramMessage();
}

ProgramWrapper.cs代码

public class ProgramWrapper : ModelWrapper<Program>
{
    public ProgramWrapper(Program model) : base(model)
    {
        
    }

    public bool DefaultProgram
    {
        get { return GetValue<bool>(); }
        set { SetValue(value); } //勾选/取消勾选复选框时,此方法被调用
    }
}

ModelWrapper.cs代码

public class ModelWrapper<T> : NotifyDataErrorsInfoBase
{
    public T Model { get; set; }
    public ModelWrapper(T model)
    {
        Model = model;
    }

    protected virtual TValue GetValue<TValue>(
        [CallerMemberName] string propertyName = null)
    {
        return (TValue)typeof(T).GetProperty(propertyName).GetValue(Model);
    }

    protected virtual void SetValue<TValue>(
        TValue value,
        [CallerMemberName] string propertyName = null)
    {
        typeof(T).GetProperty(propertyName).SetValue(Model, value);
        OnPropertyChanged(propertyName);
        ValidatePropertyInternal(propertyName, value);
    }

    private void ValidatePropertyInternal(string propertyName, object currentValue)
    {
        ClearErrors(propertyName);

        ValidateDataAnnotations(propertyName, currentValue);

        ValidateCustomErrors(propertyName);
    }

    private void ValidateCustomErrors(string propertyName)
    {
        var errors = ValidateProperty(propertyName);
        if (errors != null)
        {
            foreach (var error in errors)
            {
                AddError(propertyName, error);
            }
        }
    }

    private void ValidateDataAnnotations(string propertyName, object currentValue)
    {
        var context = new ValidationContext(Model) { MemberName = propertyName };
        var results = new List<ValidationResult>();

        Validator.TryValidateProperty(currentValue, context, results);

        foreach (var result in results)
        {
            AddError(propertyName, result.ErrorMessage);
        }
    }

    protected virtual IEnumerable<string> ValidateProperty(string propertyName)
    {
        return null;
    }
}

NotifyDataErrorsInfoBase.cs代码

public class NotifyDataErrorsInfoBase : ModelBase, INotifyDataErrorInfo
{
    private Dictionary<string, List<string>> _errors =
           new Dictionary<string, List<string>>();
    public bool HasErrors => _errors.Any();

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public IEnumerable GetErrors(string propertyName)
    {
        return _errors.ContainsKey(propertyName) ?
            _errors[propertyName] : null;
    }

    public Dictionary<string, List<string>> GetAllErrors()
    {
        return _errors;
    }

    protected virtual void OnErrorsChanged(string propertyName)
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        base.OnPropertyChanged(nameof(HasErrors));
    }
    protected void AddError(string propertyName, string error)
    {
        if (!_errors.ContainsKey(propertyName))
        {
            _errors[propertyName] = new List<string>();
        }
        if (!_errors[propertyName].Contains(error))
        {
            _errors[propertyName].Add(error);
            OnErrorsChanged(propertyName);
        }
    }
    protected void ClearErrors(string propertyName)
    {
        if (_errors.ContainsKey(propertyName))
        {
            _errors.Remove(propertyName);
            OnErrorsChanged(propertyName);
        }
    }
}

问题根源分析

  1. 核心接口缺失:ModelWrapper继承链中的ModelBase未实现INotifyPropertyChanged接口。虽然ModelWrapper的SetValue方法调用了OnPropertyChanged,但如果ModelBase没有定义PropertyChanged事件及正确实现OnPropertyChanged逻辑,事件根本不会被触发。

  2. 事件订阅失效风险:ViewModel构造函数中直接订阅初始DesktopWrapper实例的PropertyChanged事件,但DesktopWrapper是ObservableProperty生成的属性。如果后续LoadAsync或其他逻辑重新赋值DesktopWrapper,旧实例的订阅会失效,新实例的事件未被订阅,导致无法捕获后续变更。

  3. PropertyName匹配验证:虽然nameof(DesktopWrapper.DefaultProgram)结果是"DefaultProgram",与实际触发的属性名一致,但前提是事件能正常触发。

修复方案

1. 补全INotifyPropertyChanged实现

确保ModelBase实现INotifyPropertyChanged接口:

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

2. 动态订阅属性变更事件

利用ObservableProperty的自动生成的变更回调,在DesktopWrapper实例更换时重新订阅事件:

[ObservableProperty]
private ProgramWrapper _desktopWrapper = new(new Program());

// ObservableProperty会自动生成此部分代码,只需实现方法体
partial void OnDesktopWrapperChanged(ProgramWrapper oldValue, ProgramWrapper newValue)
{
    // 取消旧实例订阅
    if (oldValue != null)
        oldValue.PropertyChanged -= ProcessPropertyChanged;
    // 订阅新实例
    if (newValue != null)
        newValue.PropertyChanged += ProcessPropertyChanged;
}

3. 验证事件触发逻辑

在ModelWrapper的SetValue方法中添加日志或断点,确认OnPropertyChanged被调用时传递的propertyName是否正确为"DefaultProgram"。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 17:53:13