19

I have a WPF application that uses MVVM data bindings. I am adding items to an ObservableCollection<...> and quite many of them indeed.

Now I am wondering that every time I add one to the collection, does it instantly fire the event and cause unnecessary overhead? If so, can I somehow temporarily disable the event notifications and manually fire it once at the end of my code so that if I add 10k items, it gets only fired once, rather than 10k times?

Update: I tried having this class:

using System;
using System.Linq;
using System.Collections.Specialized;
using System.Collections.Generic;

namespace MyProject
{

    /// <summary> 
    /// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed. 
    /// </summary> 
    /// <typeparam name="T"></typeparam> 
    public class ObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>
    {

        /// <summary> 
        /// Adds the elements of the specified collection to the end of the ObservableCollection(Of T). 
        /// </summary> 
        public void AddRange(IEnumerable<T> collection)
        {
            foreach (var i in collection) Items.Add(i);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection.ToList()));
        }

        /// <summary> 
        /// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). 
        /// </summary> 
        public void RemoveRange(IEnumerable<T> collection)
        {
            foreach (var i in collection) Items.Remove(i);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, collection.ToList()));
        }

        /// <summary> 
        /// Clears the current collection and replaces it with the specified item. 
        /// </summary> 
        public void Replace(T item)
        {
            ReplaceRange(new T[] { item });
        }
        /// <summary> 
        /// Clears the current collection and replaces it with the specified collection. 
        /// </summary> 
        public void ReplaceRange(IEnumerable<T> collection)
        {
            List<T> old = new List<T>(Items);
            Items.Clear();
            foreach (var i in collection) Items.Add(i);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, collection.ToList()));
        }

        /// <summary> 
        /// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class. 
        /// </summary> 
        public ObservableCollection() : base() { }

        /// <summary> 
        /// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection. 
        /// </summary> 
        /// <param name="collection">collection: The collection from which the elements are copied.</param> 
        /// <exception cref="System.ArgumentNullException">The collection parameter cannot be null.</exception> 
        public ObservableCollection(IEnumerable<T> collection) : base(collection) { }
    }
}

I get this error now:

Additional information: Range actions are not supported.

The error comes here:

OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection.ToList()));
Tower
  • 98,741
  • 129
  • 357
  • 507

5 Answers5

25

This extension of ObservableCollection solves the problem easily.

It exposes a public SupressNotification property to allow the user to control when CollectionChanged notification will be suppressed.

It does not offer range insertion/deletion, but if CollectionChanged notification is suppressed, the need to do range operation on the collection diminishes in most of the cases.

This implementation substitutes all suppressed notifications with a Reset notification. This is logically sensible. When the user suppresses the notification, do bulk changes and then re-enable it, it should appropriate to send a Resent notification.

public class ObservableCollectionEx<T> : ObservableCollection<T>
{
    private bool _notificationSupressed = false;
    private bool _supressNotification = false;
    public bool SupressNotification
    {
        get
        {
            return _supressNotification;
        }
        set
        {
            _supressNotification = value;
            if (_supressNotification == false && _notificationSupressed)
            {
                this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                _notificationSupressed = false;
            }
        }
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (SupressNotification)
        {
            _notificationSupressed = true;
            return;
        }
        base.OnCollectionChanged(e);
    }
}
Xiaoguo Ge
  • 2,177
  • 20
  • 26
  • 1
    Nice Class, I like the soltution that you fire the event after re-setting it to true. – Tobias Oct 20 '16 at 06:32
  • 1
    Just in case anyone is wondering, this is what the `NotifyCollectionChangedAction.Reset` value does and gives an approach for handling it https://stackoverflow.com/questions/4495904/what-is-notifycollectionchangedaction-reset-value – Tim Rutter Sep 02 '19 at 10:19
  • Nice, clean implementation! Sample test in Visual Studio 2019 with a WPF control bound to an `ObservableCollection` object with a few dozen rows. Original `ObservableCollection` takes ~90 µs per row to update the UI. The suppressible version here takes ~15 µs per row. – AlainD Jul 22 '21 at 10:16
19

A very quick and easy way is to subclass ObservableCollection and suspend notifications when AddRange is called. See the following blog post for clarification.

Dr. Andrew Burnett-Thompson
  • 20,980
  • 8
  • 88
  • 178
8

There is a kind of "tricky" way, but pretty accurate, in my opinion, to achieve this. Is to write you own ObservableCollection and implement AddRange handling.

In this way you can add all your 10k elements into some "holder collection" and after, one time you finished, use AddRange of your ObservableColleciton to do that.

More on this you can find on this link:

ObservableCollection Doesn't support AddRange method....

or this one too

AddRange and ObservableCollection

Community
  • 1
  • 1
Tigran
  • 61,654
  • 8
  • 86
  • 123
  • 1
    Interesting. I wonder why this is not part of `ObservableCollection`. – Tower May 13 '12 at 14:34
  • @rFactor: honeslty, have no idea. It would be very nice to have it like built-in, but... may be, lke Eric Lippert sometimes says: because none it implemented... – Tigran May 13 '12 at 14:34
  • I'm unable to get them to work, I get: `Additional information: Constructor supports only the 'Reset' action.` when the code calls `OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add));`. – Tower May 13 '12 at 14:56
  • @rFactor: read the API docs. You need to use the constructor that takes a list of the items that were added. – Kent Boogaart May 13 '12 at 14:59
  • @KentBoogaart I see, now after adding `collection.ToList()` as the second parameter, I get `Additional information: Range actions are not supported.`. – Tower May 13 '12 at 15:06
  • @rFactor: did you try to use: `this.OnPropertyChanged("Count"); this.OnPropertyChanged("Item[]");` calls couple ? In this way you notify about changing `Count` and `Items` collection. – Tigran May 13 '12 at 15:55
  • @Tigran it expects an event object, not a string -- what's the equivalent event for those strings? – Tower May 13 '12 at 16:00
  • @rFactor: yes, sorry. This is a small wrapper function over the actual notification. Look [here](http://msdn.microsoft.com/en-us/library/ms743695.aspx) – Tigran May 13 '12 at 16:03
1

I found it was necessary to expand upon Xiaoguo Ge's answer. My code is the same as in that answer, except:

  1. I added an override of method OnPropertyChanged in order to suppress PropertyChanged event from being published.
  2. In the property setter, made the two calls to OnPropertyChanged
  3. I renamed the fields and property for a little clarity

My ObservableCollection was the ItemsSource of a DataGrid, where I had cases of replacing several thousand items. Without implementing #1, I found I was not getting the performance gain that I needed (it was substantial!). I am not sure how important #2 may be, but it is shown in another StackOverflow page that takes a slightly different approach to the same problem. I am guessing that the fact that suppressing PropertyChanged events improved my performance is evidence that the DataGrid was subscribed to the event, and therefore it may be important to publish the events when notification suppression is turned off.

One little note is that I believe it is unnecessary to set _havePendingNotifications = true from method OnPropertyChanged, but you could consider adding that if you find differently.

    /// <summary>
    /// If this property is set to true, then CollectionChanged and PropertyChanged
    /// events are not published. Furthermore, if collection changes occur while this property is set
    /// to true, then subsequently setting the property to false will cause a CollectionChanged event
    /// to be published with Action=Reset.  This is designed for faster performance in cases where a
    /// large number of items are to be added or removed from the collection, especially including cases
    /// where the entire collection is to be replaced.  The caller should follow this pattern:
    ///   1) Set NotificationSuppressed to true
    ///   2) Do a number of Add, Insert, and/or Remove calls
    ///   3) Set NotificationSuppressed to false
    /// </summary>
    public Boolean NotificationSuppressed
    {
        get { return _notificationSuppressed; }
        set
        {
            _notificationSuppressed = value;
            if (_notificationSuppressed == false && _havePendingNotifications)
            {
                OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
                OnPropertyChanged(new PropertyChangedEventArgs("Count"));
                OnCollectionChanged(
                           new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                _havePendingNotifications = false;
            }
        }
    }
    /// <summary> This field is backing store for public property NotificationSuppressed </summary>
    protected Boolean _notificationSuppressed = false;
    /// <summary>
    /// This field indicates whether there have been notifications that have been suppressed due to the
    /// NotificationSuppressed property having value of true.  If this field is true, then when
    /// NotificationSuppressed is next set to false, a CollectionChanged event is published with
    /// Action=Reset, and the field is reset to false.
    /// </summary>
    protected Boolean _havePendingNotifications = false;
    /// <summary>
    /// This method publishes the CollectionChanged event with the provided arguments.
    /// </summary>
    /// <param name="e">container for arguments of the event that is published</param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (NotificationSuppressed)
        {
            _havePendingNotifications = true;
            return;
        }
        base.OnCollectionChanged(e);
    }
    /// <summary>
    /// This method publishes the PropertyChanged event with the provided arguments.
    /// </summary>
    /// <param name="e">container for arguments of the event that is published</param>
    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (NotificationSuppressed) return;
        base.OnPropertyChanged(e);
    }
Tony Pulokas
  • 465
  • 5
  • 12
0

Sorry, I wanted to post this as a comment because I wont provide the full implementation details, but it's a bit too long.

About the "Range actions not supported", this comes from the ListCollectionView that WPF is using for the binding, which indeed does not support range actions. However, the normal CollectionView does.

WPF choose to use ListCollectionView when the bound collection implements the non-generic IList interface. So basically to have the AddRange solution working you need to fully reimplement ObservableCollection (rather than interiting it), but without the non-generic interfaces:

public class MyObservableCollection<T> :
    IList<T>,
    IReadOnlyList<T>,
    INotifyCollectionChanged,
    INotifyPropertyChanged
{
   // ...
}

With the help of dotPeek or equivalent tools, it shouldn't take long to implement this. Note that you're probably loosing some optimization from the fact that you will use a CollectionView instead of a ListCollectionView, but from my own experience using such a class globally totally improved the performances.

Benlitz
  • 1,952
  • 1
  • 17
  • 30