从BindingList中删除PropertyChanged-item会导致列表重置

本文关键字:致列 列表 BindingList 删除 PropertyChanged-item | 更新日期: 2023-09-27 18:13:11

用户自定义类继承自INotifyPropertyChanged.

在用户定义的类中,某些属性广播PropertyChanged事件。

事件期间对象本身从BindingList中移除。

事件继续执行,BindingList获取ListChangedType。重置事件。

如何避免Reset事件?

从BindingList中删除PropertyChanged-item会导致列表重置

(没有看到这个问题的答案,所以决定添加- question和answer)

Google for "BindingList Child_PropertyChanged"

给出如下代码片段:

http://referencesource.microsoft.com//compmod/系统/componentmodel/BindingList.cs e757be5fba0e6000、引用

表示如果事件被BindingList接收,并且给定的项目不在BindingList中,它将触发列表重置。

但是因为PropertyChanged。调用在开始广播之前收集整个事件列表-无论事件是否在列表中,事件都将被调用。

让以下代码片段演示错误:

Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Reflection;
using System.Security;
namespace Eventing
{
    public class ThisItem : MulticastNotifyPropertyChanged
    {
        void Test()
        {
        }
        String _name;
        public String name2
        {
            get {
                return _name;
            }
            set
            {
                _name = value;
                Console.WriteLine("---------------------------");
                Console.WriteLine("Invoke test #2");
                Console.WriteLine("---------------------------");
                Invoke(this, new PropertyChangedEventArgs("name"));
            }
        }
        public String name1
        {
            get {
                return _name;
            }
            set
            {
                _name = value;
#if TESTINVOKE
                Console.WriteLine("---------------------------");
                Console.WriteLine("Invoke test #1");
                Console.WriteLine("---------------------------");
                InvokeFast(this, new PropertyChangedEventArgs("name"));
#endif
            }
        }
    };
    class Program
    {
        static public BindingList<ThisItem> testList;
        static void Main(string[] args)
        {
            testList = new BindingList<ThisItem>();
            ThisItem t = new ThisItem();
            testList.ListChanged += testList_ListChanged;
            t.PropertyChanged += t_PropertyChanged;
            t.PropertyChanged += t_PropertyChanged2;
            testList.Add(t);
            t.name1 = "testing";
            Console.WriteLine("---------------------------");
            t.PropertyChanged -= t_PropertyChanged;
            t.PropertyChanged -= t_PropertyChanged2;
            t.PropertyChanged += t_PropertyChanged;
            testList.Add(t);
            t.PropertyChanged += t_PropertyChanged2;
            t.name2 = "testing";
        }
        static void testList_ListChanged(object sender, ListChangedEventArgs e)
        {
            Console.WriteLine("3) List changed: " + e.ListChangedType.ToString() + ((e.ListChangedType == ListChangedType.Reset) ? " (*** UPS! ***)": ""));
        }
        static void t_PropertyChanged2(object sender, PropertyChangedEventArgs e)
        {
            Console.WriteLine("2) t_PropertyChanged2: " + e.PropertyName);
        }
        static void t_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Console.WriteLine("1) t_PropertyChanged: " + e.PropertyName);
            testList.Remove((ThisItem)sender);
        }
    }
}

MulticastNotifyPropertyChanged.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace System
{
    /// <summary>
    /// class which implements INotifyPropertyChanged in such manner that event can be broadcasted in safe manner - 
    /// even if given item is removed from BindingList, event in BindingList (Child_PropertyChanged) won't be 
    /// triggered.
    /// </summary>
    public class MulticastNotifyPropertyChanged : INotifyPropertyChanged
    {
        /// <summary>
        /// List of all registered events. List can change during event broadcasting.
        /// </summary>
        List<PropertyChangedEventHandler> _PropertyChangedHandlers = new List<PropertyChangedEventHandler>();
        /// <summary>
        /// Next broadcasted event index.
        /// </summary>
        int iFuncToInvoke;
    #if TESTINVOKE
            PropertyChangedEventHandler _PropertyChangedAllInOne;
    #endif
        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add
            {
    #if TESTINVOKE
                    _PropertyChangedAllInOne += value;
    #endif
                _PropertyChangedHandlers.Add(value);
            }
            remove
            {
    #if TESTINVOKE
                    _PropertyChangedAllInOne -= value;
    #endif
                int index = _PropertyChangedHandlers.IndexOf(value);
                if (index == -1)
                    return;
                if (iFuncToInvoke >= index)     //Scroll back event iterator if needed.
                    iFuncToInvoke--;
    #if TESTINVOKE
                    Console.WriteLine("Unregistering event. Iterator value: " + iFuncToInvoke.ToString());
    #endif
                _PropertyChangedHandlers.Remove(value);
            }
        }
        /// <summary>
        /// Just an accessor, so no cast would be required on client side.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add
            {
                ((INotifyPropertyChanged)this).PropertyChanged += value;
            }
            remove
            {
                ((INotifyPropertyChanged)this).PropertyChanged -= value;
            }
        }
        /// <summary>
        /// Same as normal Invoke, except this plays out safe - if item is removed from BindingList during event broadcast -
        /// event won't be fired in removed item direction.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void Invoke(object sender, PropertyChangedEventArgs e)
        {
            for (iFuncToInvoke = 0; iFuncToInvoke < _PropertyChangedHandlers.Count; iFuncToInvoke++)
            {
    #if TESTINVOKE
                    Console.WriteLine("Invoke: " + iFuncToInvoke.ToString());
    #endif
                _PropertyChangedHandlers[iFuncToInvoke].Invoke(sender, e);
            }
        }
    #if TESTINVOKE
            public void InvokeFast( object sender, PropertyChangedEventArgs e )
            {
                _PropertyChangedAllInOne.Invoke(sender, e);
            }
    #endif
    }
} //namespace System

将导致以下执行流程:

3) List changed: ItemAdded
---------------------------
Invoke test #1
---------------------------
1) t_PropertyChanged: name
Unregistering event. Iterator value: 0
3) List changed: ItemDeleted
2) t_PropertyChanged2: name
3) List changed: Reset (*** UPS! ***)
---------------------------
Unregistering event. Iterator value: -1
Unregistering event. Iterator value: -1
3) List changed: ItemAdded
---------------------------
Invoke test #2
---------------------------
Invoke: 0
1) t_PropertyChanged: name
Unregistering event. Iterator value: 0
3) List changed: ItemDeleted
Invoke: 1
2) t_PropertyChanged2: name

这解决了列表事件-但是它可以在事件期间添加到BindingList中的项目产生更多问题。这段代码可能也可以修正为不将事件广播到新添加的项目(像普通的BindingList那样),但如果需要,您可以自己解决。