当在包含复选框的WPF树视图中使用INotifyPropertyChanged时,防止无限循环

本文关键字:INotifyPropertyChanged 无限循环 复选框 包含 WPF 视图 | 更新日期: 2023-09-27 18:06:51

这是我的第一个问题,如果格式不完美,我很抱歉。

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

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

我现在需要实现与此相反的操作,如果用户单击其中一个子节点,则如果父节点尚未被选中,则应该选中它。

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

如何防止这种情况发生?

下面是我的菜单项代码:
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;
                //}
            }
        }
    }

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

当在包含复选框的WPF树视图中使用INotifyPropertyChanged时,防止无限循环

在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);
       }
    }

我认为您需要一个其他属性来存储,如果其中一个孩子被选中。比如IsChildChecked.

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

Machine Learning的评论让我找到了答案:

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中,而不是在事件中处理它。

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

1计算父节点的is checked属性

这将工作父监听子的PropertyChanged事件,然后如果他们中的任何一个为真返回真为父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");
}

这种方法的好处是可以独立地维护父节点的单击状态

2翻转1轮,计算子节点的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");
}

这样,只要子进程直接调用SetIsChecked方法,那么只有当父进程通过setter

直接设置时,级联才会在a上触发

注意:在你的代码中,你没有处理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");
}

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