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

5

60

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.

Khrushchev answered 9/11, 2012 at 6:1 Comment(4)
if you want to replace all items in first collection using other collection, can't just directly assign the new one to an old one?Nolen
If you do so, the collection itself will be changed, in which case you will lose the reference to the collection, i.e., your CollectionChanged event is gone.Khrushchev
reassign that event handler?Nolen
Reassign that event handler? then what's the purpose of CollectionChanged? :-)Khrushchev
M
89

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);
    }
}
Mauriciomaurie answered 9/11, 2012 at 6:34 Comment(13)
+1 for remembering to raise PropertyChanged for Count and Items[] - I'll have to update the code I use in my own projects ;-)Maurist
@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.Cantabile
@AdrianRatnapala its free to use. Enjoy it. See also this and thisMauriciomaurie
Why exactly do you need to raise separately for Count and Items[]? Does the observer not check this on reset?Delciedelcina
@Delciedelcina 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.Mauriciomaurie
@Mauriciomaurie Why use Reset instead of Add?Wallinga
@Wallinga 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.Lamere
DynamicData provides an ObservableCollectionExtended which can suspend change notifications.Jessamyn
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.Valer
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.Palaestra
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 @MauriciomaurieEconomize
@Economize yes SmartCollection is a subclass of ObservableCollection. It only provides the additional methods AddRange and ResetMauriciomaurie
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 couldEconomize
M
12

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.

Maurist answered 9/11, 2012 at 6:17 Comment(0)
K
7

Here is what I implemented for other folks' reference:

// https://mcmap.net/q/326183/-how-to-avoid-firing-observablecollection-collectionchanged-multiple-times-when-replacing-all-elements-or-adding-a-collection-of-elements
// https://mcmap.net/q/134975/-observablecollection-doesn-39-t-support-addrange-method-so-i-get-notified-for-each-item-added-besides-what-about-inotifycollectionchanging
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));
    }
}
Khrushchev answered 12/11, 2012 at 4:54 Comment(1)
I know this is old but I just found it and cannot compile. It's missing any reference to IEnumerableUtils.Liturgy
T
2

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));
Tighe answered 12/8, 2014 at 13:10 Comment(0)
A
2

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

Auto answered 8/7, 2016 at 12:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.