简体   繁体   中英

Can I somehow temporarily disable WPF data binding changes?

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

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

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

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

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM