54

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.

slugster
  • 49,403
  • 14
  • 95
  • 145
Peter Lee
  • 12,931
  • 11
  • 73
  • 100
  • 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? – m4ngl3r Nov 09 '12 at 06:18
  • 3
    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. – Peter Lee Nov 09 '12 at 06:25
  • 1
    reassign that event handler? – m4ngl3r Nov 09 '12 at 06:30
  • 1
    Reassign that event handler? then what's the purpose of `CollectionChanged`? :-) – Peter Lee Nov 09 '12 at 23:07

5 Answers5

81

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);
    }
}
Jehof
  • 34,674
  • 10
  • 123
  • 155
  • 18
    +1 for remembering to raise PropertyChanged for Count and Items[] - I'll have to update the code I use in my own projects ;-) – ColinE Nov 09 '12 at 06:57
  • @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. – Adrian Ratnapala Apr 03 '14 at 07:50
  • 1
    @AdrianRatnapala its free to use. Enjoy it. See also [this](http://creativecommons.org/licenses/by-sa/3.0/) and [this](http://blog.stackoverflow.com/2009/06/attribution-required/) – Jehof Apr 03 '14 at 07:59
  • 1
    Why exactly do you need to raise separately for Count and Items[]? Does the observer not check this on reset? – Aaron Nov 11 '14 at 20:27
  • 2
    @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. – Jehof Nov 12 '14 at 07:02
  • @Jehof Why use Reset instead of Add? – Mark13426 Sep 04 '16 at 16:59
  • @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`. – hidekuro Dec 01 '17 at 12:53
  • DynamicData provides an ObservableCollectionExtended which can suspend change notifications. – JK82 Feb 05 '19 at 12:49
  • 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. – Nicholas Miller Feb 20 '19 at 16:57
  • 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. – Thomas May 03 '19 at 01:43
  • 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 – bobsyauncle May 20 '20 at 20:53
  • @bobsyauncle yes SmartCollection is a subclass of ObservableCollection. It only provides the additional methods AddRange and Reset – Jehof May 22 '20 at 08:55
  • 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 – bobsyauncle May 22 '20 at 09:47
11

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.

Community
  • 1
  • 1
ColinE
  • 68,894
  • 15
  • 164
  • 232
6

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));
    }
}
Peter Lee
  • 12,931
  • 11
  • 73
  • 100
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));
VMAtm
  • 27,943
  • 17
  • 79
  • 125
Heather V.
  • 160
  • 1
  • 10
1

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

too
  • 3,009
  • 4
  • 37
  • 51