Click here to Skip to main content
15,891,529 members
Articles / Programming Languages / C# 3.5

Enhanced ObservableCollection with ability to delay or disable notifications

Rate me:
Please Sign up or sign in to vote.
4.86/5 (42 votes)
29 Jan 2013CPOL7 min read 216K   4.4K   124  
Implements delayed or disabled INotifyCollectionChanged.
//---------------------------------------------------------------------------- 
//
// Description: Implementation of an Collection<T> implementing INotifyCollectionChanged 
//              to notify listeners of dynamic changes of the list. In addition these 
//              notifications can be postponed or disabled.
// 
// See spec at  http://avalon/connecteddata/Specs/Collection%20Interfaces.mht
//
//  Author:     Eugene Sadovoi
//
// History:
//  09/1/2011 : [....] - created 
//
//--------------------------------------------------------------------------- 

using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Reflection;


namespace System.Collections.ObjectModel
{
    /// <summary>
    /// Observable collection with ability to delay or suspend CollectionChanged notifications
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [Serializable]
    public class ObservableCollectionEx<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged,
                                             IDisposable
    {
        //-----------------------------------------------------
        //  Constants
        //-----------------------------------------------------

        #region Constants

        private const string _countString = "Count";

        // This must agree with Binding.IndexerName.  It is declared separately 
        // here so as to avoid a dependency on PresentationFramework.dll. 
        private const string _indexerName = "Item[]";

        /// <summary>
        /// Empty delegate used to initialize <see cref="CollectionChanged"/> event if it is empty
        /// </summary>
        [field: NonSerialized()]
        private static readonly NotifyCollectionChangedEventHandler _emptyDelegate = delegate { };

        #endregion

        //-----------------------------------------------------
        //  Private Fields 
        //----------------------------------------------------- 

        #region Private Fields

        /// <summary>
        /// 
        /// </summary>
        [field: NonSerialized()]
        private ReentryMonitor _monitor = new ReentryMonitor();

        /// <summary>
        /// Placeholder for all data related to delayed 
        /// notifications.
        /// </summary>
        [field: NonSerialized()]
        private NotificationInfo _notifyInfo;

        /// <summary>
        /// Indicates if modification of container allowed during change notification.
        /// </summary>
        [field: NonSerialized()]
        private bool _disableReentry;

        [field: NonSerialized()]
        Action FireCountAndIndexerChanged = delegate { };

        [field: NonSerialized()]
        Action FireIndexerChanged = delegate { };

        #endregion Private Fields

        //-----------------------------------------------------
        //  Protected Fields 
        //----------------------------------------------------- 

        #region Protected Fields

        /// <summary> 
        /// PropertyChanged event <see cref="INotifyPropertyChanged" />.
        /// </summary> 
        [field: NonSerializedAttribute()]
        protected virtual event PropertyChangedEventHandler PropertyChanged;

        /// <summary> 
        /// Occurs when the collection changes, either by adding or removing an item.
        /// </summary>
        /// <remarks>See <seealso cref="INotifyCollectionChanged"/></remarks>
        [field: NonSerialized()]
        protected virtual event NotifyCollectionChangedEventHandler CollectionChanged = _emptyDelegate;

        #endregion Protected Fields

        //-----------------------------------------------------
        //  Constructors
        //-----------------------------------------------------

        #region Constructors

        /// <summary> 
        /// Initializes a new instance of ObservableCollectionEx that is empty and has default initial capacity. 
        /// </summary>
        public ObservableCollectionEx()
            : base()
        {
        }

        /// <summary>
        /// Initializes a new instance of the ObservableCollectionEx class
        /// that contains elements copied from the specified list 
        /// </summary>
        /// <param name="list">The list whose elements are copied to the new list.</param> 
        /// <remarks> 
        /// The elements are copied onto the ObservableCollectionEx in the
        /// same order they are read by the enumerator of the list. 
        /// </remarks>
        /// <exception cref="ArgumentNullException"> list is a null reference </exception>
        public ObservableCollectionEx(List<T> list)
            : base((list != null) ? new List<T>(list.Count) : list)
        {
            foreach (T item in list)
            {
                Items.Add(item);
            }
        }

        /// <summary>
        /// Initializes a new instance of the ObservableCollection class that contains 
        /// elements copied from the specified collection and has sufficient capacity 
        /// to accommodate the number of elements copied.
        /// </summary> 
        /// <param name="collection">The collection whose elements are copied to the new list.</param>
        /// <remarks>
        /// The elements are copied onto the ObservableCollection in the
        /// same order they are read by the enumerator of the collection. 
        /// </remarks>
        /// <exception cref="ArgumentNullException"> collection is a null reference </exception> 
        public ObservableCollectionEx(IEnumerable<T> collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");

            using (IEnumerator<T> enumerator = collection.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    Items.Add(enumerator.Current);
                }
            }
        }

        /// <summary>
        /// Constructor that configures the container to delay or disable notifications.
        /// </summary>
        /// <param name="parent">Reference to an original collection whos events are being postponed</param>
        /// <param name="notify">Specifies if notifications needs to be delayed or disabled</param>
        public ObservableCollectionEx(ObservableCollectionEx<T> parent, bool notify)
            : base(parent.Items)
        {
            _notifyInfo = new NotificationInfo();
            _notifyInfo.RootCollection = parent;

            if (notify)
            {
                CollectionChanged = _notifyInfo.Initialize();
            }
        }

        /// <summary>
        /// Distructor
        /// </summary>
        ~ObservableCollectionEx()
        {
            Dispose(false);
        }

        #endregion Constructors

        //------------------------------------------------------ 
        //  Public Events 
        //------------------------------------------------------

        #region Public Events

        #region INotifyPropertyChanged implementation

        /// <summary> 
        /// PropertyChanged event (per <see cref="INotifyPropertyChanged" />).
        /// </summary>
        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add
            {
                if (null == _notifyInfo)
                {
                    if (null == PropertyChanged)
                    {
                        FireCountAndIndexerChanged = delegate
                        {
                            OnPropertyChanged(new PropertyChangedEventArgs(_countString));
                            OnPropertyChanged(new PropertyChangedEventArgs(_indexerName));
                        };
                        FireIndexerChanged = delegate
                        {
                            OnPropertyChanged(new PropertyChangedEventArgs(_indexerName));
                        };
                    }

                    PropertyChanged += value;
                }
                else
                    _notifyInfo.RootCollection.PropertyChanged += value;
            }

            remove
            {
                if (null == _notifyInfo)
                {
                    PropertyChanged -= value;

                    if (null == PropertyChanged)
                    {
                        FireCountAndIndexerChanged = delegate { };
                        FireIndexerChanged = delegate { };
                    }
                }
                else
                    _notifyInfo.RootCollection.PropertyChanged -= value;
            }
        }

        #endregion INotifyPropertyChanged implementation

        #region INotifyCollectionChanged implementation

        /// <summary> 
        /// Occurs when the collection changes, either by adding or removing an item.
        /// </summary>
        /// <remarks>
        /// see <seealso cref="INotifyCollectionChanged"/> 
        /// </remarks>
        event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
        {
            add
            {
                if (null == _notifyInfo)
                {
                    // Remove ballast delegate if necessary
                    if (1 == CollectionChanged.GetInvocationList().Length)
                        CollectionChanged -= _emptyDelegate;
                    
                    CollectionChanged += value;
                    _disableReentry = CollectionChanged.GetInvocationList().Length > 1;
                }
                else
                    _notifyInfo.RootCollection.CollectionChanged += value;
            }

            remove
            {
                if (null == _notifyInfo)
                {
                    CollectionChanged -= value;
                    
                    if ((null == CollectionChanged) || (0 == CollectionChanged.GetInvocationList().Length))
                        CollectionChanged += _emptyDelegate;

                    _disableReentry = CollectionChanged.GetInvocationList().Length > 1;
                }
                else
                    _notifyInfo.RootCollection.CollectionChanged -= value;
            }
        }

        #endregion INotifyCollectionChanged implementation

        #endregion Public Events

        //------------------------------------------------------ 
        //  Public Methods
        //-----------------------------------------------------

        #region Public Methods

        /// <summary>
        /// Move item at oldIndex to newIndex. 
        /// </summary> 
        public void Move(int oldIndex, int newIndex)
        {
            MoveItem(oldIndex, newIndex);
        }

        /// <summary>
        /// Returns an instance of <see cref="ObservableCollectionEx<T>"/>
        /// class which manipulates original collection but suppresses notifications
        /// untill this instance has been released and Dispose() method has been called.
        /// To supress notifications it is recommended to use this instance inside 
        /// using() statement:
        /// <code>
        ///         using (var iSuppressed = collection.DelayNotifications()) 
        ///         {
        ///             iSuppressed.Add(x); 
        ///             iSuppressed.Add(y); 
        ///             iSuppressed.Add(z); 
        ///         } 
        /// </code>
        /// Each delayed interface is bound to only one type of operation such as Add, Remove, etc.
        /// Different types of operation on the same delayed interface are not allowed. In order to
        /// do other type of opertaion you can allocate another wrapper by calling .DelayNotifications() on
        /// either original object or any delayed instances.
        /// </summary>
        /// <returns><see cref="ObservableCollectionEx<T>"/></returns>
        public ObservableCollectionEx<T> DelayNotifications()
        {
            return new ObservableCollectionEx<T>((null == _notifyInfo) ? this : _notifyInfo.RootCollection, true);
        }

        /// <summary>
        /// Returns a wrapper instance of an ObservableCollectionEx class.
        /// Calling methods of this instance will modify original collection
        /// but will not generate any notifications.
        /// </summary>
        /// <returns><see cref="ObservableCollectionEx<T>"/></returns>
        public ObservableCollectionEx<T> DisableNotifications()
        {
            return new ObservableCollectionEx<T>((null == _notifyInfo) ? this : _notifyInfo.RootCollection, false);
        }


        #endregion Public Methods

        //-----------------------------------------------------
        //  Protected Methods
        //----------------------------------------------------- 

        #region Protected Methods

        /// <summary>
        /// Called by base class Collection&lt;T&gt; when the list is being cleared;
        /// raises a CollectionChanged event to any listeners. 
        /// </summary>
        protected override void ClearItems()
        {
            CheckReentrancy();
            
            base.ClearItems();

            FireCountAndIndexerChanged();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        /// <summary> 
        /// Called by base class Collection&lt;T&gt; when an item is removed from list; 
        /// raises a CollectionChanged event to any listeners.
        /// </summary> 
        protected override void RemoveItem(int index)
        {
            CheckReentrancy();
            T removedItem = this[index];

            base.RemoveItem(index);

            FireCountAndIndexerChanged();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItem, index));
        }

        /// <summary> 
        /// Called by base class Collection&lt;T&gt; when an item is added to list;
        /// raises a CollectionChanged event to any listeners. 
        /// </summary> 
        protected override void InsertItem(int index, T item)
        {
            CheckReentrancy();
            
            base.InsertItem(index, item);

            FireCountAndIndexerChanged();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }

        /// <summary> 
        /// Called by base class Collection&lt;T&gt; when an item is set in list;
        /// raises a CollectionChanged event to any listeners.
        /// </summary>
        protected override void SetItem(int index, T item)
        {
            CheckReentrancy();
            
            T originalItem = this[index];
            base.SetItem(index, item);

            FireIndexerChanged();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, originalItem, item, index));
        }

        /// <summary>
        /// Called by base class ObservableCollection&lt;T&gt; when an item is to be moved within the list; 
        /// raises a CollectionChanged event to any listeners. 
        /// </summary>
        protected virtual void MoveItem(int oldIndex, int newIndex)
        {
            CheckReentrancy();

            T removedItem = this[oldIndex];
            base.RemoveItem(oldIndex);
            base.InsertItem(newIndex, removedItem);

            FireIndexerChanged();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, removedItem, newIndex, oldIndex));
        }


        /// <summary>
        /// Raises a PropertyChanged event (per <see cref="INotifyPropertyChanged" />). 
        /// </summary> 
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChanged(this, e);
        }

        /// <summary> 
        /// Raise CollectionChanged event to any listeners.
        /// Properties/methods modifying this ObservableCollection will raise 
        /// a collection changed event through this virtual method. 
        /// </summary>
        /// <remarks> 
        /// When overriding this method, either call its base implementation
        /// or call <see cref="BlockReentrancy"/> to guard against reentrant collection changes.
        /// </remarks>
        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            using (BlockReentrancy())
            {
                CollectionChanged(this, e);
            }
        }

        /// <summary> 
        /// Disallow reentrant attempts to change this collection. E.g. a event handler 
        /// of the CollectionChanged event is not allowed to make changes to this collection.
        /// </summary> 
        /// <remarks>
        /// typical usage is to wrap e.g. a OnCollectionChanged call with a using() scope:
        /// <code>
        ///         using (BlockReentrancy()) 
        ///         {
        ///             CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, item, index)); 
        ///         } 
        /// </code>
        /// </remarks> 
        protected IDisposable BlockReentrancy()
        {
            return _monitor.Enter();
        }

        /// <summary> Check and assert for reentrant attempts to change this collection. </summary> 
        /// <exception cref="InvalidOperationException"> raised when changing the collection
        /// while another collection change is still being notified to other listeners </exception> 
        protected void CheckReentrancy()
        {
            // we can allow changes if there's only one listener - the problem
            // only arises if reentrant changes make the original event args 
            // invalid for later listeners.  This keeps existing code working 
            // (e.g. Selector.SelectedItems).
            if ((_disableReentry) && (_monitor.IsNotifying))
            {
                    throw new InvalidOperationException("ObservableCollectionEx Reentrancy Not Allowed");
            }
        }

        #endregion Protected Methods

        //-----------------------------------------------------
        //  IDisposable 
        //------------------------------------------------------ 

        #region IDisposable

        /// <summary>
        /// Called by the application code to fire all delayed notifications.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Fires notification with all accumulated events
        /// </summary>
        /// <param name="reason">True is called by App code. False if called from GC.</param>
        protected virtual void Dispose(bool reason)
        {
            // Fire delayed notifications
            if (null != _notifyInfo)
            {
                if (_notifyInfo.HasEventArgs)
                {
                    if (null != _notifyInfo.RootCollection.PropertyChanged)
                    {
                        if (_notifyInfo.IsCountChanged)
                            _notifyInfo.RootCollection.OnPropertyChanged(new PropertyChangedEventArgs(_countString));

                        _notifyInfo.RootCollection.OnPropertyChanged(new PropertyChangedEventArgs(_indexerName));
                    }

                    using (_notifyInfo.RootCollection.BlockReentrancy())
                    {
                        NotifyCollectionChangedEventArgs args = _notifyInfo.EventArgs;

                        foreach (Delegate delegateItem in _notifyInfo.RootCollection.CollectionChanged.GetInvocationList())
                        {
                            try
                            {
                                delegateItem.DynamicInvoke(new object[] { _notifyInfo.RootCollection, args });
                            }
                            catch (TargetInvocationException e)
                            {
                                if ((e.InnerException is NotSupportedException) && (delegateItem.Target is ICollectionView))
                                {
                                    (delegateItem.Target as ICollectionView).Refresh();
                                }
                                else
                                    throw;
                            }
                        }
                    }

                    // Reset and reuse if necessary
                    CollectionChanged = _notifyInfo.Initialize();
                }
            }
        }

        #endregion

        //-----------------------------------------------------
        //  Private Types 
        //------------------------------------------------------ 

        #region Private Types

        [Serializable()]
        private class ReentryMonitor : IDisposable
        {
            #region Fields

            int _referenceCount;

            #endregion

            #region Methods

            public IDisposable Enter()
            {
                ++_referenceCount;

                return this;
            }

            public void Dispose()
            {
                --_referenceCount;
            }

            public bool IsNotifying { get { return _referenceCount != 0; } }

            #endregion
        }

        private class NotificationInfo
        {
            #region Fields

            private Nullable<NotifyCollectionChangedAction> _action;

            private IList _newItems;

            private IList _oldItems;

            private int _newIndex;

            private int _oldIndex;

            #endregion

            #region Methods

            public NotifyCollectionChangedEventHandler Initialize()
            {
                _action = null;
                _newItems = null;
                _oldItems = null;

                return (sender, args) =>
                {
                    ObservableCollectionEx<T> wrapper = sender as ObservableCollectionEx<T>;
                    Debug.Assert(null != wrapper, "Calling object must be ObservableCollectionEx<T>");
                    Debug.Assert(null != wrapper._notifyInfo, "Calling object must be Delayed wrapper.");

                    // Setup 
                    _action = args.Action;

                    switch (_action)
                    {
                        case NotifyCollectionChangedAction.Add:
                            _newItems = new List<T>();
                            IsCountChanged = true;
                            wrapper.CollectionChanged = (s, e) =>
                            {
                                AssertActionType(e);
                                foreach (T item in e.NewItems)
                                    _newItems.Add(item);
                            };
                            wrapper.CollectionChanged(sender, args);
                            break;

                        case NotifyCollectionChangedAction.Remove:
                            _oldItems = new List<T>();
                            IsCountChanged = true;
                            wrapper.CollectionChanged = (s, e) =>
                            {
                                AssertActionType(e);
                                foreach (T item in e.OldItems)
                                    _oldItems.Add(item);
                            };
                            wrapper.CollectionChanged(sender, args);
                            break;

                        case NotifyCollectionChangedAction.Replace:
                            _newItems = new List<T>();
                            _oldItems = new List<T>();
                            wrapper.CollectionChanged = (s, e) =>
                            {
                                AssertActionType(e);
                                foreach (T item in e.NewItems)
                                    _newItems.Add(item);

                                foreach (T item in e.OldItems)
                                    _oldItems.Add(item);
                            };
                            wrapper.CollectionChanged(sender, args);
                            break;

                        case NotifyCollectionChangedAction.Move:
                            _newIndex = args.NewStartingIndex;
                            _newItems = args.NewItems;
                            _oldIndex = args.OldStartingIndex;
                            _oldItems = args.OldItems;
                            wrapper.CollectionChanged = (s, e) =>
                            {
                                throw new InvalidOperationException(
                                    "Due to design of NotifyCollectionChangedEventArgs combination of multiple Move operations is not possible");
                            };
                            break;

                        case NotifyCollectionChangedAction.Reset:
                            IsCountChanged = true;
                            wrapper.CollectionChanged = (s, e) => { AssertActionType(e); };
                            break;
                    }
                };
            }

            #endregion

            #region Properties

            public ObservableCollectionEx<T> RootCollection { get; set; }

            public bool IsCountChanged { get; private set; }

            public NotifyCollectionChangedEventArgs EventArgs
            {
                get
                {
                    switch (_action)
                    {
                        case NotifyCollectionChangedAction.Reset:
                            return new NotifyCollectionChangedEventArgs(_action.Value);

                        case NotifyCollectionChangedAction.Add:
                            return new NotifyCollectionChangedEventArgs(_action.Value, _newItems);

                        case NotifyCollectionChangedAction.Remove:
                            return new NotifyCollectionChangedEventArgs(_action.Value, _oldItems);

                        case NotifyCollectionChangedAction.Move:
                            return new NotifyCollectionChangedEventArgs(_action.Value, _oldItems[0], _newIndex, _oldIndex);

                        case NotifyCollectionChangedAction.Replace:
                            return new NotifyCollectionChangedEventArgs(_action.Value, _newItems, _oldItems);
                    }

                    return null;
                }
            }

            public bool HasEventArgs
            {
                get { return _action.HasValue; }
            }

            #endregion

            #region Private Helper Methods

            private void AssertActionType(NotifyCollectionChangedEventArgs e)
            {
                if (e.Action != _action)
                {
                    throw new InvalidOperationException(
                        string.Format("Attempting to perform {0} during {1}. Mixed actions on the same delayed interface are not allowed.",
                        e.Action, _action));
                }
            }

            #endregion
        }

        #endregion Private Types
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Senior Software Engineer with over 20+ years of experience in variety of technologies, development tools and programming languages.

Microsoft Certified Specialist programming in C#, JavaScript, HTML, CSS

Comments and Discussions