WPF 解决 ObservableCollection 提示 Cannot change ObservableCollection during a CollectionChanged event 异

2022-08-12 19:56:52 浏览数 (1)

本文告诉大家在使用 ObservableCollection 时,抛出 InvalidOperationException 异常,提示 Cannot change ObservableCollection during a CollectionChanged event 内容,的原因和解决方法

准确来说,这个异常和 WPF 是没有任何关系的。这个异常是 ObservableCollection 类型抛出的,而 ObservableCollection 类型是在 dotnet runtime 定义的,放在 System.ObjectModel 里,而且此异常可以在除 WPF 的其他框架,比如控制台或者 UWP 上复现

想要解决此问题,还请先了解一下此异常抛出的原因

在 ObservableCollection 的设计上,是可以了解列表的变更。而在列表的变更了解,是通过 CollectionChanged 事件实现。然而事件的触发,稍微了解 C# 语法的开发者都知道,是每个方法独立执行。这就让 ObservableCollection 存在一个设计上需要解决的问题,那就是如果事件 CollectionChanged 被加等两次,意味着有两次方法的调用。如果在第一次调用方法时,在此方法内再次修改了 ObservableCollection 列表的元素,那么将会让第二个方法进入的时候,所获取的状态和第一个方法所获取的一定不相同

这个设计上的问题,是很难解决的。既然很难解决,那就不解决了,将问题交给开发者好了,在 ObservableCollection 判断如果 CollectionChanged 事件被加等大于 1 次,同时在事件触发的过程中,进行集合的变更,将会抛出 InvalidOperationException 异常,提示 Cannot change ObservableCollection during a CollectionChanged event 内容

这就是从设计上的原因。那为什么只加等 1 次时不抛出呢?那是因为既然只有一次,那改不改都影响不了当前的进入的方法的状态

由于 CollectionChanged 事件加等的次数决定了 InvalidOperationException 是否抛出,从而让一些开发者拿到错误的结论: 在 CollectionChanged 事件里面修改集合本身是安全的。或者反过来,在 CollectionChanged 事件里面修改集合本身是不安全的

正确的行为是: 当 CollectionChanged 事件加等的委托在 1 个以内时,在 CollectionChanged 事件里面修改集合本身是安全的。如果 CollectionChanged 事件加等的委托大于 1 个时,在 CollectionChanged 事件里面修改集合本身是不安全的

从代码上,在 ObservableCollection 的各个更改集合的函数,例如 InsertItem ClearItems RemoveItem 等,都会调用 CheckReentrancy 方法,判断是否存在重入。在 CheckReentrancy 方法的实现如下

代码语言:javascript复制
        /// <summary> Check and assert for reentrant attempts to change this collection. </summary>
        /// <exception cref="InvalidOperationException"> raised when changing the collection
        /// while another collection change is still being notified to other listeners </exception>
        protected void CheckReentrancy()
        {
            if (_blockReentrancyCount > 0)
            {
                // we can allow changes if there's only one listener - the problem
                // only arises if reentrant changes make the original event args
                // invalid for later listeners.  This keeps existing code working
                // (e.g. Selector.SelectedItems).
                if (CollectionChanged?.GetInvocationList().Length > 1)
                    throw new InvalidOperationException(SR.ObservableCollectionReentrancyNotAllowed);
            }
        }

上面代码的 _blockReentrancyCount 是在 OnCollectionChanged 方法和 BlockReentrancy 方法使用的。在没有重写 ObservableCollection 的情况下,可以认为 _blockReentrancyCount 只有在 OnCollectionChanged 方法更改

代码语言:javascript复制
        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            NotifyCollectionChangedEventHandler? handler = CollectionChanged;
            if (handler != null)
            {
                // Not calling BlockReentrancy() here to avoid the SimpleMonitor allocation.
                _blockReentrancyCount  ;
                try
                {
                    handler(this, e);
                }
                finally
                {
                    _blockReentrancyCount--;
                }
            }
        }

也就是说,在 CollectionChanged 事件触发进入的方法里面,一定是判断 if (_blockReentrancyCount > 0) 通过的。也就说接下来只需要看 if (CollectionChanged?.GetInvocationList().Length > 1) 判断即可。这里的 GetInvocationList 是 CollectionChanged 事件对应的委托的数量,只要超过 1 个就炸

了解了原因,那么解决方法也很简单。要么是在 CollectionChanged 事件里面修改集合时确保只让 CollectionChanged 加等一个委托。要么就是继承 ObservableCollection 类型,重写 OnCollectionChanged 方法,不要修改 _blockReentrancyCount 字段。要么就是等待 CollectionChanged 事件触发完成之后,通过 Dispatcher 的 InvokeAsync 方法调度出去执行

wpf

0 人点赞