将 INotifyPropertyChanged 与包含复选框的 WPF 树视图一起使用时防止无限循环

Preventing infinite loops when using INotifyPropertyChanged with a WPF treeview containing checkboxes

这是我的第一个问题,如果格式不正确,我深表歉意。

我是 WPF 和 MVVM 的新手,我 运行 遇到了一个我似乎无法弄清楚的问题。

我有一个树视图,它显示一个 MenuItem 层次结构,每个 MenuItem 都有一个复选框,用于父节点和子节点。当前的解决方案允许用户单击父节点,并根据需要选中/取消选中所有子项。

我现在需要执行相反的操作,如果用户单击其中一个子节点,则应该选择父节点(如果尚未选择)。

我目前遇到的问题是,以编程方式检查父节点会触发父节点的 INotifiedPropertyChanged 事件,该事件会重新检查我的子节点。

如何防止这种情况发生?

这是我的 MenuItem 代码:​​

public class MenuItem : INotifyPropertyChanged
    {
        string _name;
        List<MenuItem> _subItems = new List<MenuItem>();
        bool _isChecked;
        MenuItem _parent;

        public List<MenuItem> SubItems
        {
            get { return _subItems; }
            set
            {
                _subItems = value;
                RaisePropertyChanged("SubItems");
            }
        }

        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                RaisePropertyChanged("Name");
            }
        }

        public bool IsChecked
        {
            get { return _isChecked; }
            set
            {
                _isChecked = value;
                RaisePropertyChanged("IsChecked");
            }
        }

        public MenuItem Parent
        {
            get { return _parent; }
            set
            {
                _parent = value;
                RaisePropertyChanged("Parent");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

            if (propertyName == "IsChecked")
            {
                if (Parent == null)
                {
                    foreach (MenuItem Child in _subItems)
                        Child.IsChecked = this.IsChecked;
                }

                //if (Parent != null)
                //{
                //    Parent.IsChecked = IsChecked ? true :Parent.IsChecked;
                //}
            }
        }
    }

上面的注释代码是我遇到错误的地方。

任何指导将不胜感激。

我认为如果 children 中的一个被选中,您需要另一个 属性 来存储。像 IsChildChecked 这样的东西。

在 UI 中,您可以使用 MultiBinding 将这两个属性(IsChecked 和 IsChildChecked)绑定到节点的 IsChecked。使用转换器设置它。

机器学习的评论让我找到了答案:

public bool IsChecked
        {
            get { return _isChecked; }
            set
            {
                _isChecked = value;

                if (_parent == null)
                {
                    foreach (MenuItem Child in _subItems)
                    {
                        Child._isChecked = this._isChecked;
                        Child.RaisePropertyChanged("IsChecked");
                    }
                }

                if (_parent != null)
                {
                     _parent._isChecked = _isChecked ? true : _parent._isChecked;
                    _parent.RaisePropertyChanged("IsChecked");
                }

                RaisePropertyChanged("IsChecked");
            }
        }

将代码移动到 setter 而不是在事件中处理它对我有用。

只是根据 OP

已经写的答案做了更详尽的回答
    public bool IsChecked
    {
        get { return _isChecked; }
        set
        {
            _isChecked = value;

            if (_parent == null)
            {
                foreach (MenuItem Child in _subItems)
                {
                    Child._isChecked = this._isChecked;
                    Child.RaisePropertyChanged("IsChecked");
                }
            }

            if (_parent != null)
            {
                _parent.NotifyChecked(_isChecked);
            }

            RaisePropertyChanged("IsChecked");
        }
    }
    public void NotifyChecked(bool childChecked) 
    { 
       _isChecked = childChecked;
        RaisePropertyChanged("IsChecked"); 
       if (_parent != null)
       {
           _parent.NotifyChecked(_isChecked);
       }
    }

您可以采用几种不同的方法,

1 计算parent的被勾选属性

这将通过 parent 侦听 child 的 PropertyChanged 事件来实现,然后如果其中任何一个为真,则为 parent 的 IsChecked

返回真
private bool isChecked;
public bool IsChecked
{
    get{ return isChecked || Children.Any(c=>IsChecked);}
    set
    {
        isChecked = value;
        RaisePropertyChanged("IsChecked");
        foreach(var child in Children)child.IsChecked
    }
}
public void Child_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
    if(e.PropertyName == "IsChecked")
        RaisePropertyChanged("IsChecked");
}

这种方法的好处是可以独立维护 parent 的点击状态

2 翻转 1 轮并计算 child 的 IsChecked 属性

private bool isChecked;
public bool IsChecked
{
    get{ return isChecked || Parent.IsChecked;}
    set
    {
        isChecked = value;
        RaisePropertyChanged("IsChecked");
    }
}
public void Parent_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
    if(e.PropertyName == "IsChecked")
        RaisePropertyChanged("IsChecked");
}

3 创建第二个路由来改变状态而不触发级联

private bool isChecked;
public bool IsChecked
{
    get{ return isChecked;}
    set
    {
        SetIsChecked( value);
        foreach(var child in Children)Parent.SetIsChecked(isChecked)
    }
}
public void SetIsChecked(bool value)
{
    isChecked = value;
    RaisePropertyChanged("IsChecked");
}

这种方式只要children直接调用SetIsChecked方法,那么级联只会在parent通过[=42=直接设置时触发]

注意:在您的代码中您没有处理 PropertyChanged 事件,您只是在引发它

处理看起来像这样

public MenuItem Parent
{
    get { return _parent; }
    set
    {
        //remove old handler
        // this stops listening to the old parent if there is one
        if(_parent != null)
            _parent.PropertyChange-=Parent_PropertyChanged;

        //notice that the value of _parent changes here so _parent above is not the same as _parent used below
        _parent = value;

        //add new handler
        // this starts listening to the new parent if there is one
        if(_parent != null)
            _parent.PropertyChange+=Parent_PropertyChanged;

        RaisePropertyChanged("Parent");
    }
}
//handler
public void Parent_PropertyChanged(object sender,PropertyChangedEventArgs e)
{
    if(e.PropertyName == "IsChecked")
        RaisePropertyChanged("IsChecked");
}

也可以通过在进行任何更改之前检查当前值是否已更改来改进上述所有内容