Browse Source

Added CoreObservableCollection to InABox.Core

frankvandenbos 4 months ago
parent
commit
0aefef8f5d
1 changed files with 743 additions and 0 deletions
  1. 743 0
      InABox.Core/CoreObservableCollection.cs

+ 743 - 0
InABox.Core/CoreObservableCollection.cs

@@ -0,0 +1,743 @@
+using System;
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Threading;
+
+namespace InABox.Core
+{
+    // Licensed to the .NET Foundation under one or more agreements.
+    // The .NET Foundation licenses this file to you under the MIT license.
+    // See the LICENSE file in the project root for more information.
+
+    using System.Collections.Generic;
+    using System.Collections.Specialized;
+    using System.ComponentModel;
+    using System.Diagnostics;
+    using System.Linq;
+
+    /// <summary>
+    /// Implementation of a dynamic data collection based on generic Collection&lt;T&gt;,
+    /// implementing INotifyCollectionChanged to notify listeners
+    /// when items get added, removed or the whole list is refreshed.
+    ///
+    /// Modified to Implement Cross-thread Synchronization of events
+    /// Will trigger event callbacks on creating, rather than current thread.
+    /// Collections should be created on Msin UI thread the to prevent exceptions
+    /// </summary>
+    public class CoreObservableCollection<T> : ObservableCollection<T>
+    {
+        //------------------------------------------------------
+        //
+        //  Private Fields
+        //
+        //------------------------------------------------------
+
+        #region Private Fields
+
+        [NonSerialized] private DeferredEventsCollection? _deferredEvents;
+
+        [NonSerialized] private SynchronizationContext? _synchronizationContext = SynchronizationContext.Current;
+
+        #endregion Private Fields
+
+
+        //------------------------------------------------------
+        //
+        //  Constructors
+        //
+        //------------------------------------------------------
+
+        #region Constructors
+
+        /// <summary>
+        /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity.
+        /// </summary>
+        public CoreObservableCollection()
+        {
+        }
+
+        /// <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 CoreObservableCollection(IEnumerable<T> collection) : base(collection)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the ObservableCollection 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 ObservableCollection in the
+        /// same order they are read by the enumerator of the list.
+        /// </remarks>
+        /// <exception cref="ArgumentNullException"> list is a null reference </exception>
+        public CoreObservableCollection(List<T> list) : base(list)
+        {
+        }
+
+        #endregion Constructors
+
+        //------------------------------------------------------
+        //
+        //  Public Properties
+        //
+        //------------------------------------------------------
+
+        #region Public Properties
+
+        EqualityComparer<T>? _Comparer;
+
+        public EqualityComparer<T> Comparer
+        {
+            get => _Comparer ??= EqualityComparer<T>.Default;
+            private set => _Comparer = value;
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether this collection acts as a <see cref="HashSet{T}"/>,
+        /// disallowing duplicate items, based on <see cref="Comparer"/>.
+        /// This might indeed consume background performance, but in the other hand,
+        /// it will pay off in UI performance as less required UI updates are required.
+        /// </summary>
+        public bool AllowDuplicates { get; set; } = true;
+
+        // If the collection is created on a non-UI thread, but needs to update the UI,
+        // we should be able to set the UI context here to prevent exceptions
+        public SynchronizationContext? SynchronizationContext
+        {
+            get => _synchronizationContext;
+            set => _synchronizationContext = value;
+        }
+
+        #endregion Public Properties
+
+        //------------------------------------------------------
+        //
+        //  Public Methods
+        //
+        //------------------------------------------------------
+
+        #region Public Methods
+
+        /// <summary>
+        /// Adds the elements of the specified collection to the end of the <see cref="ObservableCollection{T}"/>.
+        /// </summary>
+        /// <param name="collection">
+        /// The collection whose elements should be added to the end of the <see cref="ObservableCollection{T}"/>.
+        /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.
+        /// </param>
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        public void AddRange(IEnumerable<T> collection)
+        {
+            InsertRange(Count, collection);
+        }
+
+        /// <summary>
+        /// Inserts the elements of a collection into the <see cref="ObservableCollection{T}"/> at the specified index.
+        /// </summary>
+        /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
+        /// <param name="collection">The collection whose elements should be inserted into the List<T>.
+        /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type.</param>                
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is not in the collection range.</exception>
+        public void InsertRange(int index, IEnumerable<T> collection)
+        {
+            if (collection == null)
+                throw new ArgumentNullException(nameof(collection));
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (index > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
+
+            if (!AllowDuplicates)
+                collection =
+                    collection
+                        .Distinct(Comparer)
+                        .Where(item => !Items.Contains(item, Comparer))
+                        .ToList();
+
+            if (collection is ICollection<T> countable)
+            {
+                if (countable.Count == 0)
+                    return;
+            }
+            else if (!collection.Any())
+                return;
+
+            CheckReentrancy();
+
+            //expand the following couple of lines when adding more constructors.
+            var target = (List<T>)Items;
+            target.InsertRange(index, collection);
+
+            OnEssentialPropertiesChanged();
+
+            if (!(collection is IList list))
+                list = new List<T>(collection);
+
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index));
+        }
+
+
+        /// <summary> 
+        /// Removes the first occurence of each item in the specified collection from the <see cref="ObservableCollection{T}"/>.
+        /// </summary>
+        /// <param name="collection">The items to remove.</param>        
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        public void RemoveRange(IEnumerable<T> collection)
+        {
+            if (collection == null)
+                throw new ArgumentNullException(nameof(collection));
+
+            if (Count == 0)
+                return;
+            else if (collection is ICollection<T> countable)
+            {
+                if (countable.Count == 0)
+                    return;
+                else if (countable.Count == 1)
+                    using (IEnumerator<T> enumerator = countable.GetEnumerator())
+                    {
+                        enumerator.MoveNext();
+                        Remove(enumerator.Current);
+                        return;
+                    }
+            }
+            else if (!collection.Any())
+                return;
+
+            CheckReentrancy();
+
+            var clusters = new Dictionary<int, List<T>>();
+            var lastIndex = -1;
+            List<T>? lastCluster = null;
+            foreach (T item in collection)
+            {
+                var index = IndexOf(item);
+                if (index < 0)
+                    continue;
+
+                Items.RemoveAt(index);
+
+                if (lastIndex == index && lastCluster != null)
+                    lastCluster.Add(item);
+                else
+                    clusters[lastIndex = index] = lastCluster = new List<T> { item };
+            }
+
+            OnEssentialPropertiesChanged();
+
+            if (Count == 0)
+                OnCollectionReset();
+            else
+                foreach (KeyValuePair<int, List<T>> cluster in clusters)
+                    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,
+                        cluster.Value, cluster.Key));
+        }
+
+        /// <summary>
+        /// Iterates over the collection and removes all items that satisfy the specified match.
+        /// </summary>
+        /// <remarks>The complexity is O(n).</remarks>
+        /// <param name="match"></param>
+        /// <returns>Returns the number of elements that where </returns>
+        /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+        public int RemoveAll(Predicate<T> match)
+        {
+            return RemoveAll(0, Count, match);
+        }
+
+        /// <summary>
+        /// Iterates over the specified range within the collection and removes all items that satisfy the specified match.
+        /// </summary>
+        /// <remarks>The complexity is O(n).</remarks>
+        /// <param name="index">The index of where to start performing the search.</param>
+        /// <param name="count">The number of items to iterate on.</param>
+        /// <param name="match"></param>
+        /// <returns>Returns the number of elements that where </returns>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
+        public int RemoveAll(int index, int count, Predicate<T> match)
+        {
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (count < 0)
+                throw new ArgumentOutOfRangeException(nameof(count));
+            if (index + count > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (match == null)
+                throw new ArgumentNullException(nameof(match));
+
+            if (Count == 0)
+                return 0;
+
+            List<T>? cluster = null;
+            var clusterIndex = -1;
+            var removedCount = 0;
+
+            using (BlockReentrancy())
+            using (DeferEvents())
+            {
+                for (var i = 0; i < count; i++, index++)
+                {
+                    T item = Items[index];
+                    if (match(item))
+                    {
+                        Items.RemoveAt(index);
+                        removedCount++;
+
+                        if (clusterIndex == index)
+                        {
+                            Debug.Assert(cluster != null);
+                            cluster!.Add(item);
+                        }
+                        else
+                        {
+                            cluster = new List<T> { item };
+                            clusterIndex = index;
+                        }
+
+                        index--;
+                    }
+                    else if (clusterIndex > -1)
+                    {
+                        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,
+                            cluster, clusterIndex));
+                        clusterIndex = -1;
+                        cluster = null;
+                    }
+                }
+
+                if (clusterIndex > -1)
+                    OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,
+                        cluster, clusterIndex));
+            }
+
+            if (removedCount > 0)
+                OnEssentialPropertiesChanged();
+
+            return removedCount;
+        }
+
+        /// <summary>
+        /// Removes a range of elements from the <see cref="ObservableCollection{T}"/>>.
+        /// </summary>
+        /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
+        /// <param name="count">The number of elements to remove.</param>
+        /// <exception cref="ArgumentOutOfRangeException">The specified range is exceeding the collection.</exception>
+        public void RemoveRange(int index, int count)
+        {
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (count < 0)
+                throw new ArgumentOutOfRangeException(nameof(count));
+            if (index + count > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
+
+            if (count == 0)
+                return;
+
+            if (count == 1)
+            {
+                RemoveItem(index);
+                return;
+            }
+
+            //Items will always be List<T>, see constructors
+            var items = (List<T>)Items;
+            List<T> removedItems = items.GetRange(index, count);
+
+            CheckReentrancy();
+
+            items.RemoveRange(index, count);
+
+            OnEssentialPropertiesChanged();
+
+            if (Count == 0)
+                OnCollectionReset();
+            else
+                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,
+                    removedItems, index));
+        }
+
+        /// <summary> 
+        /// Clears the current collection and replaces it with the specified collection,
+        /// using <see cref="Comparer"/>.
+        /// </summary>             
+        /// <param name="collection">The items to fill the collection with, after clearing it.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        public void ReplaceRange(IEnumerable<T> collection)
+        {
+            ReplaceRange(0, Count, collection);
+        }
+
+        /// <summary>
+        /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact.
+        /// </summary>
+        /// <param name="index">The index of where to start the replacement.</param>
+        /// <param name="count">The number of items to be replaced.</param>
+        /// <param name="collection">The collection to insert in that location.</param>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is out of range.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is out of range.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="collection"/> is null.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null.</exception>
+        public void ReplaceRange(int index, int count, IEnumerable<T> collection)
+        {
+            if (index < 0)
+                throw new ArgumentOutOfRangeException(nameof(index));
+            if (count < 0)
+                throw new ArgumentOutOfRangeException(nameof(count));
+            if (index + count > Count)
+                throw new ArgumentOutOfRangeException(nameof(index));
+
+            if (collection == null)
+                throw new ArgumentNullException(nameof(collection));
+
+            if (!AllowDuplicates)
+                collection =
+                    collection
+                        .Distinct(Comparer)
+                        .ToList();
+
+            if (collection is ICollection<T> countable)
+            {
+                if (countable.Count == 0)
+                {
+                    RemoveRange(index, count);
+                    return;
+                }
+            }
+            else if (!collection.Any())
+            {
+                RemoveRange(index, count);
+                return;
+            }
+
+            if (index + count == 0)
+            {
+                InsertRange(0, collection);
+                return;
+            }
+
+            if (!(collection is IList<T> list))
+                list = new List<T>(collection);
+
+            using (BlockReentrancy())
+            using (DeferEvents())
+            {
+                var rangeCount = index + count;
+                var addedCount = list.Count;
+
+                var changesMade = false;
+                List<T>?
+                    newCluster = null,
+                    oldCluster = null;
+
+
+                int i = index;
+                for (; i < rangeCount && i - index < addedCount; i++)
+                {
+                    //parallel position
+                    T old = this[i], @new = list[i - index];
+                    if (Comparer.Equals(old, @new))
+                    {
+                        OnRangeReplaced(i, newCluster!, oldCluster!);
+                        continue;
+                    }
+                    else
+                    {
+                        Items[i] = @new;
+
+                        if (newCluster == null)
+                        {
+                            Debug.Assert(oldCluster == null);
+                            newCluster = new List<T> { @new };
+                            oldCluster = new List<T> { old };
+                        }
+                        else
+                        {
+                            newCluster.Add(@new);
+                            oldCluster!.Add(old);
+                        }
+
+                        changesMade = true;
+                    }
+                }
+
+                OnRangeReplaced(i, newCluster!, oldCluster!);
+
+                //exceeding position
+                if (count != addedCount)
+                {
+                    var items = (List<T>)Items;
+                    if (count > addedCount)
+                    {
+                        var removedCount = rangeCount - addedCount;
+                        T[] removed = new T[removedCount];
+                        items.CopyTo(i, removed, 0, removed.Length);
+                        items.RemoveRange(i, removedCount);
+                        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,
+                            removed, i));
+                    }
+                    else
+                    {
+                        var k = i - index;
+                        T[] added = new T[addedCount - k];
+                        for (int j = k; j < addedCount; j++)
+                        {
+                            T @new = list[j];
+                            added[j - k] = @new;
+                        }
+
+                        items.InsertRange(i, added);
+                        OnCollectionChanged(
+                            new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, added, i));
+                    }
+
+                    OnEssentialPropertiesChanged();
+                }
+                else if (changesMade)
+                {
+                    OnIndexerPropertyChanged();
+                }
+            }
+        }
+
+        #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()
+        {
+            if (Count == 0)
+                return;
+
+            CheckReentrancy();
+            base.ClearItems();
+            OnEssentialPropertiesChanged();
+            OnCollectionReset();
+        }
+
+        /// <inheritdoc/>
+        protected override void InsertItem(int index, T item)
+        {
+            if (!AllowDuplicates && Items.Contains(item))
+                return;
+
+            base.InsertItem(index, item);
+        }
+
+        /// <inheritdoc/>
+        protected override void SetItem(int index, T item)
+        {
+            if (AllowDuplicates)
+            {
+                if (Comparer.Equals(this[index], item))
+                    return;
+            }
+            else if (Items.Contains(item, Comparer))
+                return;
+
+            CheckReentrancy();
+            T oldItem = this[index];
+            base.SetItem(index, item);
+
+            OnIndexerPropertyChanged();
+            OnCollectionChanged(NotifyCollectionChangedAction.Replace, oldItem!, item!, index);
+        }
+
+        /// <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 override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+        {
+            if (_synchronizationContext == null || SynchronizationContext.Current == _synchronizationContext)
+            {
+                // Execute the CollectionChanged event on the current thread
+                RaiseCollectionChanged(e);
+            }
+            else
+            {
+                // Raises the CollectionChanged event on the creator thread
+                _synchronizationContext.Send(RaiseCollectionChanged, e);
+            }
+        }
+
+        private void RaiseCollectionChanged(object? param)
+        {
+            // We are in the creator thread, call the base implementation directly
+            if (param is NotifyCollectionChangedEventArgs args)
+            {
+                if (_deferredEvents != null)
+                {
+                    _deferredEvents.Add(args);
+                    return;
+                }
+
+                base.OnCollectionChanged(args);
+            }
+        }
+
+        protected virtual IDisposable DeferEvents() => new DeferredEventsCollection(this);
+
+        #endregion Protected Methods
+
+
+        //------------------------------------------------------
+        //
+        //  Private Methods
+        //
+        //------------------------------------------------------
+
+        #region Private Methods
+
+        /// <summary>
+        /// Helper to raise Count property and the Indexer property.
+        /// </summary>
+        void OnEssentialPropertiesChanged()
+        {
+            OnPropertyChanged(EventArgsCache.CountPropertyChanged);
+            OnIndexerPropertyChanged();
+        }
+
+        /// <summary>
+        /// /// Helper to raise a PropertyChanged event for the Indexer property
+        /// /// </summary>
+        void OnIndexerPropertyChanged() =>
+            OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+
+        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+            if (_synchronizationContext == null || SynchronizationContext.Current == _synchronizationContext)
+            {
+                // Execute the PropertyChanged event on the current thread
+                RaisePropertyChanged(e);
+            }
+            else
+            {
+                // Raises the PropertyChanged event on the creator thread
+                _synchronizationContext.Send(RaisePropertyChanged, e);
+            }
+        }
+
+        private void RaisePropertyChanged(object? param)
+        {
+            // We are in the creator thread, call the base implementation directly
+            if (param is PropertyChangedEventArgs args)
+                base.OnPropertyChanged(args);
+        }
+
+        /// <summary>
+        /// Helper to raise CollectionChanged event to any listeners
+        /// </summary>
+        void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) =>
+            OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
+
+        /// <summary>
+        /// Helper to raise CollectionChanged event with action == Reset to any listeners
+        /// </summary>
+        void OnCollectionReset() =>
+            OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+
+        /// <summary>
+        /// Helper to raise event for clustered action and clear cluster.
+        /// </summary>
+        /// <param name="followingItemIndex">The index of the item following the replacement block.</param>
+        /// <param name="newCluster"></param>
+        /// <param name="oldCluster"></param>
+        //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable<T> collection, IEqualityComparer<T> comparer),
+        //move when supported language version updated.
+        void OnRangeReplaced(int followingItemIndex, ICollection<T> newCluster, ICollection<T> oldCluster)
+        {
+            if (oldCluster == null || oldCluster.Count == 0)
+            {
+                Debug.Assert(newCluster == null || newCluster.Count == 0);
+                return;
+            }
+
+            OnCollectionChanged(
+                new NotifyCollectionChangedEventArgs(
+                    NotifyCollectionChangedAction.Replace,
+                    new List<T>(newCluster),
+                    new List<T>(oldCluster),
+                    followingItemIndex - oldCluster.Count));
+
+            oldCluster.Clear();
+            newCluster.Clear();
+        }
+
+        #endregion Private Methods
+
+        //------------------------------------------------------
+        //
+        //  Private Types
+        //
+        //------------------------------------------------------
+
+        #region Private Types
+
+        sealed class DeferredEventsCollection : List<NotifyCollectionChangedEventArgs>, IDisposable
+        {
+            readonly CoreObservableCollection<T> _collection;
+
+            public DeferredEventsCollection(CoreObservableCollection<T> collection)
+            {
+                Debug.Assert(collection != null);
+                Debug.Assert(collection._deferredEvents == null);
+                _collection = collection;
+                _collection._deferredEvents = this;
+            }
+
+            public void Dispose()
+            {
+                _collection._deferredEvents = null;
+                foreach (var args in this)
+                    _collection.OnCollectionChanged(args);
+            }
+        }
+
+        #endregion Private Types
+    }
+
+    /// <remarks>
+    /// To be kept outside <see cref="ObservableCollection{T}"/>, since otherwise, a new instance will be created for each generic type used.
+    /// </remarks>
+    internal static class EventArgsCache
+    {
+        internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count");
+
+        internal static readonly PropertyChangedEventArgs IndexerPropertyChanged =
+            new PropertyChangedEventArgs("Item[]");
+
+        internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged =
+            new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+    }
+}