How to Avoid Firing ObservableCollection.CollectionChanged Multiple Times When Replacing All Elements Or Adding a Collection of Elements

26,504

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:

ObservableCollection Doesn't support AddRange method, so I get notified for each item added, besides what about INotifyCollectionChanging?

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));
Share:
26,504
Peter Lee
Author by

Peter Lee

Peter Lee (Yuefeng Li) owner of leestime.com

Updated on July 09, 2022

Comments

  • Peter Lee
    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 to collection.Add one by one, but this will fire multiple times

    Same 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
    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
    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
    Jehof about 10 years
    @AdrianRatnapala its free to use. Enjoy it. See also this and this
  • Aaron
    Aaron over 9 years
    Why exactly do you need to raise separately for Count and Items[]? Does the observer not check this on reset?
  • Jehof
    Jehof over 9 years
    @Aaron you need to raise the property changes on Count and Item[] 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
    Mark13426 almost 8 years
    @Jehof Why use Reset instead of Add?
  • hidekuro
    hidekuro over 6 years
    @Mark13426 this.Items.Xxxx() is not firing notifications and this property is only can access from SmartCollection (or your subclasses of ObservableCollection). You got 2 times notifications if you use manually called Clear() and Add.
  • Tom Padilla
    Tom Padilla over 5 years
    I know this is old but I just found it and cannot compile. It's missing any reference to IEnumerableUtils.
  • JK82
    JK82 over 5 years
    DynamicData provides an ObservableCollectionExtended which can suspend change notifications.
  • Nicholas Miller
    Nicholas Miller over 5 years
    Excellent! 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
    Thomas about 5 years
    I 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
    bobsyauncle about 4 years
    Can 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
    Jehof about 4 years
    @bobsyauncle yes SmartCollection is a subclass of ObservableCollection. It only provides the additional methods AddRange and Reset
  • bobsyauncle
    bobsyauncle about 4 years
    Ok. 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