How to Avoid Firing ObservableCollection.CollectionChanged Multiple Times When Replacing All Elements Or Adding a Collection of Elements
Solution 1
ColinE is right with all his informations. I only want to add my subclass of ObservableCollection
that I use for this specific case.
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);
}
}
Solution 2
You can achieve this by subclassing ObservableCollection
and implementing your own ReplaceAll
method. The implementation of this methods would replace all the items within the internal Items
property, then fire a CollectionChanged
event. Likewise, you can add an AddRange
method. For an implementation of this, see the answer to this question:
The difference between Collection.Clear
and Collection.ClearItems
is that Clear
is a public API method, whereas ClearItems
is protected, it is an extension point that allows your to extend / modify the behaviour of Clear
.
Solution 3
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));
}
}
Solution 4
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:
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).
Solution 5
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.
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));
Comments
-
Peter Lee almost 2 years
I have
ObservableCollection<T>
collection, and I want to replace all elements with a new collection of elements, I could do:collection.Clear();
OR:
collection.ClearItems();
(BTW, what's the difference between these two methods?)
I could also use
foreach
tocollection.Add
one by one, but this will fire multiple timesSame when adding a collection of elements.
EDIT:
I found a good library here: Enhanced ObservableCollection with ability to delay or disable notifications but it seems that it does NOT support silverlight.
-
ColinE over 11 years+1 for remembering to raise PropertyChanged for Count and Items[] - I'll have to update the code I use in my own projects ;-)
-
Adrian Ratnapala about 10 years@Jehof: is it OK to just cut and paste your code into a project. Or is it for reference only? I'm asking for permission here.
-
Jehof about 10 years
-
Aaron over 9 yearsWhy exactly do you need to raise separately for Count and Items[]? Does the observer not check this on reset?
-
Jehof over 9 years@Aaron you need to raise the property changes on
Count
andItem[]
also. Some observers may check these properties. Take a look at the implementation of ObservableCollection and you will see that all methods that manipulate the collection will raise the event for these properties. -
Mark13426 almost 8 years@Jehof Why use Reset instead of Add?
-
hidekuro over 6 years@Mark13426
this.Items.Xxxx()
is not firing notifications and this property is only can access fromSmartCollection
(or your subclasses ofObservableCollection
). You got 2 times notifications if you use manually calledClear()
andAdd
. -
Tom Padilla over 5 yearsI know this is old but I just found it and cannot compile. It's missing any reference to IEnumerableUtils.
-
JK82 over 5 yearsDynamicData provides an ObservableCollectionExtended which can suspend change notifications.
-
Nicholas Miller over 5 yearsExcellent! The performance boost of this is staggering. I'm only displaying 1000 entries on a log page. This solution reduces the page navigation time from about 5 seconds to under 1.
-
Thomas about 5 yearsI love this! I was wondering if anyone can expand on why
.Add
isn't being used. I tried it, and it appears the event is not firing at all, but when.Reset
is used, the event fires. Any reason why that would be the case? I'm not clear on @hidekuro's response. -
bobsyauncle about 4 yearsCan this simply be interchanged in place with ObservableCollection? I don't know if I can use AddRange or not so don't know how much difference it would make @Jehof
-
Jehof about 4 years@bobsyauncle yes SmartCollection is a subclass of ObservableCollection. It only provides the additional methods AddRange and Reset
-
bobsyauncle about 4 yearsOk. Looking for an ObservableCollection class that has some form of improvements to how the current one is done. I guess this one doesn't quite do that, but at least if we wanted to add a bunch we could