隆重推出 AwesomeObservableCollection






3.67/5 (6投票s)
如何为默认的 ObservableCollection 添加一些强大的新功能
引言
让我们从说明显而易见的事情开始这篇博文
ObservableCollections 很棒!
好了,我说完了。
任何进行过 WPF 开发的人都会承认,在 MVVM 模式中使用 ObservableCollection
可以让应用程序开发,以及生活,变得更加轻松。
但是,就像任何控件/数据结构一样,有时您会发现它并没有提供执行某些任务所需的*所有*功能。
现在,如果您是 ObservableCollection
的常规用户,甚至是“非信徒”(目前),这篇博文将简要概述默认 ObservableCollection
提供的功能,并重点介绍我多年来遇到的一些问题。
与其仅仅指出问题,我们还将看看如何“修复”它们,从而改进 ObservableCollection
以便将来使用。
准备好了吗?我们开始吧。
默认 ObservableCollection 概述
所以,对于不知道 ObservableCollection
是什么的人,这是 MSDN 的定义
表示一个动态数据集合,当项目被添加、删除或整个列表被刷新时提供通知。
我认为上面的定义对于 ObservableCollection
提供的功能非常清晰简洁。它提到的通知在 MVVM 和数据绑定方面效果非常好。
这些通知是通过 CollectionChanged
事件触发的,因此每当集合的项目发生更改时,绑定到 ObservableCollection
的属性都会收到通知,UI/其他内容就可以使用新项目进行更新。功能强大。
不过,它并非没有一些缺失的功能。在下一节中,我们将快速浏览一下我认为默认 ObservableCollection
缺失的一些功能。
ObservableCollection 的问题
问题 1:添加/删除多个项目
让我们再次仔细看看那个定义
表示一个动态数据集合,当项目被添加、删除或整个列表被刷新时提供通知。
从定义来看,我们可以安全地假设更改通知将会在以下伪调用中触发吗?
list.Add(someItem)
list.Remove(someItem)
list.AddRange(someItemList)
list.ClearRange()
准备好答案了吗?来了
答案
- 1+2:是
- 3+4:否
您可能会问为什么最后两个调用不会触发,答案很简单:ObservableCollection
不提供一次添加/删除多个列表项的功能。
您最初的反应可能是,“谁在乎?” 为了展示这可能是一个问题,请想象以下并非完全不切实际的场景
您的 UI 已绑定到一个 ObservableCollection
,并且其中包含一些项目。现在我们需要向列表中添加不少于 50 个新项目。很容易,对吧?我们只需要使用 foreach
遍历新项目,然后逐个添加,就像这样
foreach(var item in itemList)
list.Add(item)
是的,这会起作用,但每次将项目添加到列表时都会触发一次更改通知。这意味着您的数据绑定会更新 50 次。这可能导致应用程序出现严重的性能问题。
当然,绕过这个限制可能有一些方法,比如填充一个单独的非数据绑定列表,然后将 ObservableCollection
设置为该新列表,但这并不适用于所有场景。
我们将看到如何在不太麻烦的情况下克服这个限制。
问题 2:仅集合更改触发通知
INotifyPropertyChanged
接口可能是 WPF 应用程序中最知名的接口。它使开发人员能够通过在对象的属性更改时自动触发更改通知来充分利用 WPF 的数据绑定功能。
让我们看一个简单的例子
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get{ return _name;}
set
{
if(_name==value)
return;
_name=value;
NotifyPropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
假设我们已经正确地完成了 UI 数据绑定,每当 Person
对象的 Name
属性更改时,它都会通知 UI 刷新自身以反映新值。
但是,如果我们有一个包含 Person
对象的 ObservableCollection
,当 Person
对象的名称更改时,它会更新 UI 吗?直说吧:不会。
这是因为 ObservableCollection
仅在集合本身发生更改时触发,并且它不监视其包含的对象是否发生更改。
现在我们将解决指出的问题,首先是比较简单的一个。
添加集合支持
在本节中,我们将为 ObservableCollection
提供向集合添加/删除多个项目而不为每个项目引发通知的功能。
实现这一点最简单的方法是扩展 ObservableCollection
。
我们将创建一个新的 ObservableCollection
类型,并简单地将其命名为 RangeObservableCollection
。
新类的逻辑足够简单
- 添加 2 个新方法,即
AddRange
和ClearRange
。 - 重写
OnCollectionChanged
事件,以检查通知是否被抑制,然后再引发更改通知。 - 在我们的新方法中,我们将在利用现有的
Add
和ClearItems
方法的同时抑制通知。 - 完成后重置抑制。
- 修改集合后自行引发通知。
实现后,完成的类将如下所示
public class RangeObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
}
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
_suppressNotification = true;
foreach (T item in list)
Add(item);
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void ClearRange()
{
_suppressNotification = true;
ClearItems();
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
需要注意的一点是:当我们修改集合后触发更改通知时,我们必须使用 NotifyCollectionChangedAction.Reset
值来指示整个列表已更改。默认的 Add
和 Remove
方法分别触发 NotifyCollectionChangedAction.Add
和 NotifyCollectionChangedAction.Remove
操作,用于添加到/从集合中移除的每个项目。我们不能使用这些操作,因为我们现在只发出一次更改通知,因此不能使用为单个项目保留的操作。
到目前为止,这也不是什么高深莫测的东西,所以让我们继续下一个问题。
添加对象更改通知
警告:我们将要实现的解决方案仅在 ObservableCollection
包含实现 INotifyPropertyChanged
接口的项目时才有效。
好的,不要让上面的警告吓跑您。在几乎所有我们希望 ObservableCollection
通知我们项目本身属性更改的情况下,这些对象很可能已经实现了 INotifyPropertyChanged
接口。
好的,让我们开始工作。
与上一节一样,我们将通过继承来扩展 ObservableCollection
类。
让我们创建一个名为 ItemObservableCollection
的新类,它将继承自 ObservableCollection
,与 RangeObservableCollection
相同。
目前,您的新类应该如下所示
public class ItemObservableCollection<T> : ObservableCollection<T>
{
}
由于我们只处理实现 INotifyPropertyChanged
接口的对象,因此我们需要通过如下更改新类定义来限制集合中可以存储的泛型类型
public class ItemObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
}
新类的逻辑将是
- 创建新方法,当项目上的属性更改时引发更改通知
- 更改
CollectionChanged
事件的功能,以便我们可以执行一些自定义逻辑 - 取消订阅集合中所有旧项目对属性更改通知的订阅
- 订阅所有新项目以引发新的属性更改通知
就是这样。完成后,实现后的类应该如下所示
public class ItemObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public ItemObservableCollection()
{
this.CollectionChanged += CollectionChanged_Handler;
}
void CollectionChanged_Handler(object sender, NotifyCollectionChangedEventArgs e)
{
//unsubscribe all old objects
if (e.OldItems != null)
{
foreach (T x in e.OldItems)
x.PropertyChanged -= ItemChanged;
}
//subscribe all new objects
if (e.NewItems != null)
{
foreach (T x in e.NewItems)
x.PropertyChanged += ItemChanged;
}
}
private void ItemChanged(object sender, PropertyChangedEventArgs e)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
好了。这个新类现在会在其某个项目的一个属性发生更改时引发通知。
不错。
将 ObservableCollection 扩展为超赞
现在您可能开始问自己:博文中提到的 AwesomeObservableCollection
在哪里?等等,我们快到了。
我们已经看到了如何通过简单地创建两个新类来轻松地绕过默认 ObservableCollection
的一些限制,这些类提供了一些很棒的附加功能。
那么,如果我们想要一个能够同时处理这两个问题的 ObservableCollection
怎么办?这就是 AwesomeObservableCollection
的用武之地。它只是一个新类,它将在一个地方拥有与前面两个类相同的功能,并进行一些轻微的修改。
当我们创建具有与前面各节中实现*完全相同*的新 AwesomeObservableCollection
类时,它应该如下所示(为清晰起见,已按区域分开)
public class AwesomeObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public AwesomeObservableCollection()
{
base.CollectionChanged += CollectionChanged_Handler;
}
#region RangeObservableCollection
private bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
}
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
_suppressNotification = true;
foreach (T item in list)
{
Add(item);
}
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void ClearRange()
{
_suppressNotification = true;
ClearItems();
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion RangeObservableCollection
#region ItemObservableCollection
void CollectionChanged_Handler(object sender, NotifyCollectionChangedEventArgs e)
{
//unsubscribe all old objects
if (e.OldItems != null)
{
foreach (T x in e.OldItems)
x.PropertyChanged -= ItemChanged;
}
//subscribe all new objects
if (e.NewItems != null)
{
foreach (T x in e.NewItems)
x.PropertyChanged += ItemChanged;
}
}
private void ItemChanged(object sender, PropertyChangedEventArgs e)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion ItemObservableCollection
}
但是,我们还没有完成。事情变得有点棘手。请参阅 RangeObservable
部分中提到的警告,以回顾我们需要处理的另一个问题。
由于我们在添加/清除范围时抑制了更改通知,因此在通过 NotifyCollectionChangedAction.Add
或 NotifyCollectionChangedAction.Remove
操作将项目添加到/从集合中移除后,更改通知不会触发。它只会触发一次 CollectionChanged
处理程序,并带有 NotifyCollectionChangedAction.Reset
参数,正如我们的实现所示。这意味着 CollectionChanged_Handler
中没有 e.OldItems
或 e.NewItems
,因此任何添加/移除的项目都不会被订阅/取消订阅属性更改通知。
现在,我们需要添加代码来处理我们 AddRange
和 ClearRange
方法中的这些情况。修改方法如下
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");</p>
_suppressNotification = true;
foreach (T item in list)
{
Add(item);
item.PropertyChanged += ItemChanged;
}
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void ClearRange()
{
_suppressNotification = true;
foreach (T item in Items)
item.PropertyChanged -= ItemChanged;
ClearItems();
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
从代码中,您应该可以看到我们所做的唯一更改是自己订阅和取消订阅了项目到更改通知。
为了完整起见,并且因为我喜欢你们,所以这里提供了 AwesomeObservableCollection
的最终实现。
public class AwesomeObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public AwesomeObservableCollection()
{
base.CollectionChanged += CollectionChanged_Handler;
}
#region RangeObservableCollection
private bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
}
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
_suppressNotification = true;
foreach (T item in list)
{
Add(item);
item.PropertyChanged += ItemChanged;
}
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void ClearRange()
{
_suppressNotification = true;
foreach (T item in Items)
item.PropertyChanged -= ItemChanged;
ClearItems();
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion RangeObservableCollection
#region ItemObservableCollection
void CollectionChanged_Handler(object sender, NotifyCollectionChangedEventArgs e)
{
//unsubscribe all old objects
if (e.OldItems != null)
{
foreach (T x in e.OldItems)
x.PropertyChanged -= ItemChanged;
}
//subscribe all new objects
if (e.NewItems != null)
{
foreach (T x in e.NewItems)
x.PropertyChanged += ItemChanged;
}
}
private void ItemChanged(object sender, PropertyChangedEventArgs e)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion ItemObservableCollection
}
结论
在这篇博文中,我们已经回顾了默认 ObservableCollection
处理不好的几个场景,并研究了我们可以应对的方法。在此过程中,我们创建了 3 种功能齐全的 ObservableCollections
类型,可以根据您的具体场景使用。
希望您喜欢它,并在过程中有所收获。
请记住投票,并随时评论反馈/建议/赞美。
感谢阅读。