Can I somehow temporarily disable WPF data binding changes?
Asked Answered
T

6

21

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()));
Taxiway answered 13/5, 2012 at 14:27 Comment(0)
D
20

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

Disputant answered 13/5, 2012 at 15:34 Comment(0)
R
26

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);
    }
}
Retaliate answered 28/4, 2015 at 5:8 Comment(4)
Nice Class, I like the soltution that you fire the event after re-setting it to true.Yulandayule
Just in case anyone is wondering, this is what the NotifyCollectionChangedAction.Reset value does and gives an approach for handling it #4496404Crescentia
Nice, clean implementation! Sample test in Visual Studio 2019 with a WPF control bound to an ObservableCollection<T> 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.Sightly
I had to extend this code just a bit. I need to disable notifications while my class using the collection is handling the collection changed event. I had to put in a Boolean flag indicating that the extended code is in the collection changed method so that when setting SupressNotification true it won't fire the collection changed event. Without it, you'll get an exception.Delusive
D
20

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

Disputant answered 13/5, 2012 at 15:34 Comment(0)
K
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

Kinghood answered 13/5, 2012 at 14:32 Comment(8)
Interesting. I wonder why this is not part of ObservableCollection.Taxiway
@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...Kinghood
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));.Taxiway
@rFactor: read the API docs. You need to use the constructor that takes a list of the items that were added.Instructor
@KentBoogaart I see, now after adding collection.ToList() as the second parameter, I get Additional information: Range actions are not supported..Taxiway
@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.Kinghood
@Kinghood it expects an event object, not a string -- what's the equivalent event for those strings?Taxiway
@rFactor: yes, sorry. This is a small wrapper function over the actual notification. Look hereKinghood
N
2

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);
    }
Neoma answered 19/11, 2021 at 0:13 Comment(0)
S
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.

Smile answered 5/4, 2018 at 3:3 Comment(0)
L
0

Check out the Caliburn Micro BindableCollection class. It has IsNotifying property that serves exactly this purpose. It also handles AddRange() and RemoveRange() correctly.

someCollection.IsNotifying = false;

// update

someCollection.IsNotifying = true;
someCollection.Refresh();

https://github.com/Caliburn-Micro/Caliburn.Micro/blob/master/src/Caliburn.Micro.Core/BindableCollection.cs

Leoine answered 25/1 at 9:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.