C# 如何避免在替换所有元素或添加元素集合时多次触发 ObservableCollection.CollectionChanged
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/13302933/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to Avoid Firing ObservableCollection.CollectionChanged Multiple Times When Replacing All Elements Or Adding a Collection of Elements
提问by Peter Lee
I have ObservableCollection<T>collection, and I want to replace all elements with a new collection of elements, I could do:
我有ObservableCollection<T>集合,我想用新的元素集合替换所有元素,我可以这样做:
collection.Clear();
OR:
或者:
collection.ClearItems();
(BTW, what's the difference between these two methods?)
(顺便说一句,这两种方法有什么区别?)
I could also use foreachto collection.Addone by one, but this will fire multiple times
我也foreach可以collection.Add一一使用,但这会触发多次
Same when adding a collection of elements.
添加元素集合时相同。
EDIT:
编辑:
I found a good library here: Enhanced ObservableCollection with ability to delay or disable notificationsbut it seems that it does NOT support silverlight.
我在这里找到了一个很好的库:Enhanced ObservableCollection 能够延迟或禁用通知,但它似乎不支持 Silverlight。
采纳答案by Jehof
ColinE is right with all his informations. I only want to add my subclass of ObservableCollectionthat I use for this specific case.
ColinE 的所有信息都是正确的。我只想添加我ObservableCollection用于此特定情况的子类。
public class SmartCollection<T> : ObservableCollection<T> {
public SmartCollection()
: base() {
}
public SmartCollection(IEnumerable<T> collection)
: base(collection) {
}
public SmartCollection(List<T> list)
: base(list) {
}
public void AddRange(IEnumerable<T> range) {
foreach (var item in range) {
Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void Reset(IEnumerable<T> range) {
this.Items.Clear();
AddRange(range);
}
}
回答by ColinE
You can achieve this by subclassing ObservableCollectionand implementing your own ReplaceAllmethod. The implementation of this methods would replace all the items within the internal Itemsproperty, then fire a CollectionChangedevent. Likewise, you can add an AddRangemethod. For an implementation of this, see the answer to this question:
您可以通过子类化ObservableCollection和实现您自己的ReplaceAll方法来实现这一点。此方法的实现将替换内部Items属性中的所有项目,然后触发CollectionChanged事件。同样,您可以添加一个AddRange方法。有关此问题的实现,请参阅此问题的答案:
ObservableCollection 不支持 AddRange 方法,所以每添加一个项目我都会收到通知,除了 INotifyCollectionChanging 呢?
The difference between Collection.Clearand Collection.ClearItemsis that Clearis a public API method, whereas ClearItemsis protected, it is an extension point that allows your to extend / modify the behaviour of Clear.
Collection.Clear和之间的区别在于Collection.ClearItems它Clear是一个公共 API 方法,而ClearItems受保护,它是一个扩展点,允许您扩展/修改Clear.
回答by Peter Lee
Here is what I implemented for other folks' reference:
这是我为其他人参考的实现:
// http://stackoverflow.com/questions/13302933/how-to-avoid-firing-observablecollection-collectionchanged-multiple-times-when-r
// http://stackoverflow.com/questions/670577/observablecollection-doesnt-support-addrange-method-so-i-get-notified-for-each
public class ObservableCollectionFast<T> : ObservableCollection<T>
{
public ObservableCollectionFast()
: base()
{
}
public ObservableCollectionFast(IEnumerable<T> collection)
: base(collection)
{
}
public ObservableCollectionFast(List<T> list)
: base(list)
{
}
public virtual void AddRange(IEnumerable<T> collection)
{
if (collection.IsNullOrEmpty())
return;
foreach (T item in collection)
{
this.Items.Add(item);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
// Cannot use NotifyCollectionChangedAction.Add, because Constructor supports only the 'Reset' action.
}
public virtual void RemoveRange(IEnumerable<T> collection)
{
if (collection.IsNullOrEmpty())
return;
bool removed = false;
foreach (T item in collection)
{
if (this.Items.Remove(item))
removed = true;
}
if (removed)
{
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
// Cannot use NotifyCollectionChangedAction.Remove, because Constructor supports only the 'Reset' action.
}
}
public virtual void Reset(T item)
{
this.Reset(new List<T>() { item });
}
public virtual void Reset(IEnumerable<T> collection)
{
if (collection.IsNullOrEmpty() && this.Items.IsNullOrEmpty())
return;
// Step 0: Check if collection is exactly same as this.Items
if (IEnumerableUtils.Equals<T>(collection, this.Items))
return;
int count = this.Count;
// Step 1: Clear the old items
this.Items.Clear();
// Step 2: Add new items
if (!collection.IsNullOrEmpty())
{
foreach (T item in collection)
{
this.Items.Add(item);
}
}
// Step 3: Don't forget the event
if (this.Count != count)
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
回答by Heather V.
I can't comment on previous answers yet, so I'm adding here a RemoveRange adaptation of the SmartCollection implementations above that won't throw a C# InvalidOperationException: Collection Was Modified. It uses a predicate to check if the item should be removed which, in my case, is more optimal than creating a subset of items that meet the remove criteria.
我还不能评论以前的答案,所以我在这里添加了上述 SmartCollection 实现的 RemoveRange 改编,它不会抛出 C# InvalidOperationException: Collection Was Modified。它使用谓词来检查是否应该删除项目,在我的情况下,这比创建满足删除条件的项目子集更理想。
public void RemoveRange(Predicate<T> remove)
{
// iterates backwards so can remove multiple items without invalidating indexes
for (var i = Items.Count-1; i > -1; i--) {
if (remove(Items[i]))
Items.RemoveAt(i);
}
this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
Example:
例子:
LogEntries.RemoveRange(i => closeFileIndexes.Contains(i.fileIndex));
回答by too
For the past few years I am using a more generic solution to eliminate too many ObservableCollection notifications by creating a batch change operation and notifying observers with a Reset action:
在过去的几年里,我使用了一种更通用的解决方案,通过创建批量更改操作并使用重置操作通知观察者来消除过多的 ObservableCollection 通知:
public class ExtendedObservableCollection<T>: ObservableCollection<T>
{
public ExtendedObservableCollection()
{
}
public ExtendedObservableCollection(IEnumerable<T> items)
: base(items)
{
}
public void Execute(Action<IList<T>> itemsAction)
{
itemsAction(Items);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Using it is straightforward:
使用它很简单:
var collection = new ExtendedObservableCollection<string>(new[]
{
"Test",
"Items",
"Here"
});
collection.Execute(items => {
items.RemoveAt(1);
items.Insert(1, "Elements");
items.Add("and there");
});
Calling Execute will generate a single notification but with a drawback - list will be updated in UI as a whole, not only modified elements. This makes it perfect for items.Clear() followed by items.AddRange(newItems).
调用 Execute 将生成单个通知,但有一个缺点 - 列表将在 UI 中作为一个整体进行更新,而不仅仅是修改的元素。这使得它非常适合 items.Clear() 后跟 items.AddRange(newItems)。

