增强的 ObservableCollection,支持延迟或禁用通知






4.86/5 (42投票s)
实现延迟或禁用的 INotifyCollectionChanged。
引言
MSDN 描述 ObservableCollection
是一个动态数据集合,当项被添加、删除或整个列表被刷新时会提供通知。
ObservableCollection
是完全可绑定的。它实现了 INotifyPropertyChanged
和 INotifyCollectionChanged
,因此每当集合发生更改时,都会立即触发相应的通知事件,并通知绑定对象进行更新。
这种情况在大多数情况下都有效,但有时将通知推迟到稍后或暂时完全禁用它们会很有益。例如,在批处理更新完成之前。这种通知延迟可以提高性能,并消除更新视觉效果的屏幕闪烁。不幸的是,ObservableCollection
的默认实现不提供此功能。
ObservableCollectionEx
旨在提供此缺失的功能。ObservableCollectionEx
被设计为 ObservableCollection
的直接替代品,与它完全兼容,并且还提供了一种延迟或禁用通知的方法。
背景
为了推迟通知,我们必须暂时将它们重定向到一个存储区域,并在不再需要延迟时一次性触发它们。同时,我们需要继续为不要求延迟的其他集合使用者提供正常行为和通知。
这可以通过提供操纵同一集合的多个“外壳”来实现。一个外壳实例将包含元素的容器,并作为所有通知事件的宿主,而其他外壳实例将处理禁用和延迟的事件。这些额外的外壳引用相同的容器,但它们不会触发更改事件,而是收集这些事件,并在外壳超出范围时触发它们。
ObservableCollection
的实现基于 Collection 类,该类实现了 ICollection 的操作,而 ObservableCollection
则实现了通知。 Collection 类被实现为 IList
接口的包装。它包含一个对容器的引用,该容器公开 IList
并通过它操纵该容器。Collection 类的一个构造函数接受 List
作为参数,并允许该列表成为该 Collection 的容器。这创建了一种让多个 Collection 实例操纵同一个容器的方法,这完美地满足了我们的目的。
不幸的是,这种能力在 ObservableCollection
的实现中丢失了。它没有将 IList
分配给实例作为容器,而是创建该 List
的副本,并使用该副本存储元素。这个限制阻止我们继承 ObservableCollection
类。
ObservableCollectionEx
基于 Collection 类(与 bservableCollection 相同),并实现了与 ObservableCollection
完全相同的方法和属性。
除了这些成员之外,ObservableCollectionEx
还公开了两个方法来创建容器周围的禁用或延迟通知外壳。DisableNotifications()
创建的外壳方法不会对 INotifyPropertyChanged
或 INotifyCollectionChanged
产生任何通知。
对 DelayNorifications()
创建的外壳方法的调用不会产生通知,直到该实例超出范围或已对其调用 IDisposable.Dispose()
。
工作原理
除了少数性能技巧外,ObservableCollectionEx
的行为与 ObservableCollection
类完全相同。它使用 Collection
来执行其操作,通过 INotifyPropertyChanged
和 INotifyCollectionChanged
通知消费者,并在您将 List
传递给构造函数时创建其副本。
区别在于调用 DelayNotifications()
或 DisableNotifications()
方法时。此方法会创建一个新的 ObservableCollectionEx
对象实例,并将其构造函数传递原始 ObservableCollectionEx
对象的引用,以及指定通知是禁用还是延迟的布尔参数。这个新实例将具有与原始对象相同的接口,相同的元素容器,但没有附加到 CollectionChanged
事件的消费者处理程序。因此,当调用此实例的方法并触发事件时,这些事件都不会去任何地方,而是进入临时存储。
更新完成后,当此实例超出范围或调用了 Dispose()
时,所有收集到的事件将被合并为一个,并在原始对象的 CollectionChanged
和 PropertyChanged
上触发,通知所有消费者有关更改。
实现
通常有两种方法可以实现通知延迟:继承 ObservableCollection
或创建操作常规 ObservableCollection
的 Extension
Method
。创建扩展方法很有吸引力。它允许在不进行大量代码修改的情况下向任何可观察集合添加延迟。
但这样做是有代价的。为了实现所需的行为,我们需要访问 ObservableCollection
类的两个私有成员:List
collection 和 PropertyChanged
事件。使用 Reflection
我们可以访问这些成员,但这并不是一个优雅的解决方案。它也不是最快的方法。
出于这些原因,我选择通过继承 ObservableCollection
来实现延迟。
使用代码
将此类包含到项目中的最简单方法是安装在此 链接 处的 Nuget 包。
ObservableCollectionEx
的使用方式应与 ObservableCollection
完全相同。它可以被实例化并替代 ObservableCollection
使用,或者可以从其派生。无需特殊处理。
为了推迟通知,建议使用 using()
指令。
ObservableCollectionEx<T> target = new ObservableCollectionEx<T>();
using (ObservableCollectionEx<T> iDelayed = target.DelayNotifications())
{
iDelayed.Add(item0);
iDelayed.Add(item0);
iDelayed.Add(item0);
}
由于通知参数的设计,无法将不同的操作组合在一起。例如,无法在同一个延迟实例上 Add
和 Remove
元素,除非在这些调用之间调用了 Dispose()
。调用 Dispose()
将触发之前收集的事件并重新初始化操作。
ObservableCollectionEx<T> target = new ObservableCollectionEx<T>();
using (ObservableCollectionEx<T> iDelayed = target.DelayNotifications())
{
iDelayed.Add(item0);
iDelayed.Add(item0);
}
using (ObservableCollectionEx<T> iDelayed = target.DelayNotifications())
{
iDelayed.Remove(item0);
iDelayed.Remove(item0);
}
using (ObservableCollectionEx<T> iDelayed = target.DelayNotifications())
{
iDelayed.Add(item0);
iDelayed.Add(item0);
iDelayed.Dispose();
iDelayed.Remove(item0);
iDelayed.Remove(item0);
}
性能
总的来说,ObservableCollection
和 ObservableCollectionEx
提供可比较的性能。测试使用了包含 10,000 个唯一对象的数组。ObservableCollection
和 ObservableCollectionEx
都使用此数组进行初始化,以预先分配存储空间,从而不影响计时结果。应用程序运行了大约十几次,以便 JIT 优化可执行文件,然后再收集测试结果。
测试包括 10,000 次 Add、Replace 和 Remove 操作。计时使用了 Stopwatch
类收集,并以毫秒为单位表示。
左侧的值表示完成测试(Add
、Replace
和 Remove
)所花费的毫秒数。底部的值指定了通知订阅者(添加到 CollectionChanged
事件的处理程序)的数量。
从图表中可以看出,禁用通知的接口性能与订阅者数量无关。由于多项性能增强,ObservableCollectionEx
的性能略优于 ObservableCollection
,无论订阅者数量如何,但一旦有多个订阅者,它显然会失去禁用接口的优势。
ObservableCollectionEx
延迟通知时的性能与上述结果有所不同。由于通知只调用一次,这可以节省一些时间,但需要一些额外的处理来解开保存的通知。ObservableCollection
和 ObservableCollectionEx
的通知时间由以下公式描述:
ObservableCollection: overhead = (n * a) + (n * b)
ObservableCollectionEx: overhead = a + c + (n * b)
其中 a
是执行通知所需的常量开销,n
是更改的元素数量,b
是绘制每个单独元素的成本,而 c
是执行延迟通知所需的开销。
左侧的值表示完成通知所需的时间。底部的值指定了更改元素的数量。
在这些方程中,值 a
和 c
是常量,因此性能仅取决于两个元素:b
– 绘制每个单独元素所需的时间,以及 n
– 被通知的元素数量。正如您从微积分中知道的那样,b
控制着图表的上升斜率。因此,当绘制每个元素所需的时间(b
)增加时,这两条线会更快地相交。这意味着需要更少的更改元素来看到性能优势。