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

C#SilverlightObservablecollection

C# Problem Overview


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.

C# Solutions


Solution 1 - C#

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 - C#

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:

https://stackoverflow.com/questions/670577/observablecollection-doesnt-support-addrange-method-so-i-get-notified-for-each

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 - C#

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 - C#

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));

Solution 5 - C#

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).

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPeter LeeView Question on Stackoverflow
Solution 1 - C#JehofView Answer on Stackoverflow
Solution 2 - C#ColinEView Answer on Stackoverflow
Solution 3 - C#Peter LeeView Answer on Stackoverflow
Solution 4 - C#Heather V.View Answer on Stackoverflow
Solution 5 - C#tooView Answer on Stackoverflow