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; /// /// Implementation of a dynamic data collection based on generic Collection<T>, /// 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 /// public class CoreObservableCollection : ObservableCollection { //------------------------------------------------------ // // Private Fields // //------------------------------------------------------ #region Private Fields [NonSerialized] private DeferredEventsCollection? _deferredEvents; [NonSerialized] private SynchronizationContext? _synchronizationContext = SynchronizationContext.Current; #endregion Private Fields //------------------------------------------------------ // // Constructors // //------------------------------------------------------ #region Constructors /// /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity. /// public CoreObservableCollection() { } /// /// 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. /// /// The collection whose elements are copied to the new list. /// /// The elements are copied onto the ObservableCollection in the /// same order they are read by the enumerator of the collection. /// /// collection is a null reference public CoreObservableCollection(IEnumerable collection) : base(collection) { } /// /// Initializes a new instance of the ObservableCollection class /// that contains elements copied from the specified list /// /// The list whose elements are copied to the new list. /// /// The elements are copied onto the ObservableCollection in the /// same order they are read by the enumerator of the list. /// /// list is a null reference public CoreObservableCollection(List list) : base(list) { } #endregion Constructors //------------------------------------------------------ // // Public Properties // //------------------------------------------------------ #region Public Properties EqualityComparer? _Comparer; public EqualityComparer Comparer { get => _Comparer ??= EqualityComparer.Default; private set => _Comparer = value; } /// /// Gets or sets a value indicating whether this collection acts as a , /// disallowing duplicate items, based on . /// 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. /// 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 /// /// Adds the elements of the specified collection to the end of the . /// /// /// The collection whose elements should be added to the end of the . /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type. /// /// is null. public void AddRange(IEnumerable collection) { InsertRange(Count, collection); } /// /// Inserts the elements of a collection into the at the specified index. /// /// The zero-based index at which the new elements should be inserted. /// The collection whose elements should be inserted into the List. /// The collection itself cannot be null, but it can contain elements that are null, if type T is a reference type. /// is null. /// is not in the collection range. public void InsertRange(int index, IEnumerable 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 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)Items; target.InsertRange(index, collection); OnEssentialPropertiesChanged(); if (!(collection is IList list)) list = new List(collection); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, index)); } /// /// Removes the first occurence of each item in the specified collection from the . /// /// The items to remove. /// is null. public void RemoveRange(IEnumerable collection) { if (collection == null) throw new ArgumentNullException(nameof(collection)); if (Count == 0) return; else if (collection is ICollection countable) { if (countable.Count == 0) return; else if (countable.Count == 1) using (IEnumerator enumerator = countable.GetEnumerator()) { enumerator.MoveNext(); Remove(enumerator.Current); return; } } else if (!collection.Any()) return; CheckReentrancy(); var clusters = new Dictionary>(); var lastIndex = -1; List? 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 { item }; } OnEssentialPropertiesChanged(); if (Count == 0) OnCollectionReset(); else foreach (KeyValuePair> cluster in clusters) OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, cluster.Value, cluster.Key)); } /// /// Iterates over the collection and removes all items that satisfy the specified match. /// /// The complexity is O(n). /// /// Returns the number of elements that where /// is null. public int RemoveAll(Predicate match) { return RemoveAll(0, Count, match); } /// /// Iterates over the specified range within the collection and removes all items that satisfy the specified match. /// /// The complexity is O(n). /// The index of where to start performing the search. /// The number of items to iterate on. /// /// Returns the number of elements that where /// is out of range. /// is out of range. /// is null. public int RemoveAll(int index, int count, Predicate 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? 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 { 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; } /// /// Removes a range of elements from the >. /// /// The zero-based starting index of the range of elements to remove. /// The number of elements to remove. /// The specified range is exceeding the collection. 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, see constructors var items = (List)Items; List removedItems = items.GetRange(index, count); CheckReentrancy(); items.RemoveRange(index, count); OnEssentialPropertiesChanged(); if (Count == 0) OnCollectionReset(); else OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index)); } /// /// Clears the current collection and replaces it with the specified collection, /// using . /// /// The items to fill the collection with, after clearing it. /// is null. public void ReplaceRange(IEnumerable collection) { ReplaceRange(0, Count, collection); } /// /// Removes the specified range and inserts the specified collection in its position, leaving equal items in equal positions intact. /// /// The index of where to start the replacement. /// The number of items to be replaced. /// The collection to insert in that location. /// is out of range. /// is out of range. /// is null. /// is null. public void ReplaceRange(int index, int count, IEnumerable 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 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 list)) list = new List(collection); using (BlockReentrancy()) using (DeferEvents()) { var rangeCount = index + count; var addedCount = list.Count; var changesMade = false; List? 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 { @new }; oldCluster = new List { old }; } else { newCluster.Add(@new); oldCluster!.Add(old); } changesMade = true; } } OnRangeReplaced(i, newCluster!, oldCluster!); //exceeding position if (count != addedCount) { var items = (List)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 /// /// Called by base class Collection<T> when the list is being cleared; /// raises a CollectionChanged event to any listeners. /// protected override void ClearItems() { if (Count == 0) return; CheckReentrancy(); base.ClearItems(); OnEssentialPropertiesChanged(); OnCollectionReset(); } /// protected override void InsertItem(int index, T item) { if (!AllowDuplicates && Items.Contains(item)) return; base.InsertItem(index, item); } /// 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); } /// /// Raise CollectionChanged event to any listeners. /// Properties/methods modifying this ObservableCollection will raise /// a collection changed event through this virtual method. /// /// /// When overriding this method, either call its base implementation /// or call to guard against reentrant collection changes. /// 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.Post(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 /// /// Helper to raise Count property and the Indexer property. /// void OnEssentialPropertiesChanged() { OnPropertyChanged(EventArgsCache.CountPropertyChanged); OnIndexerPropertyChanged(); } /// /// /// Helper to raise a PropertyChanged event for the Indexer property /// /// 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.Post(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); } /// /// Helper to raise CollectionChanged event to any listeners /// void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) => OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index)); /// /// Helper to raise CollectionChanged event with action == Reset to any listeners /// void OnCollectionReset() => OnCollectionChanged(EventArgsCache.ResetCollectionChanged); /// /// Helper to raise event for clustered action and clear cluster. /// /// The index of the item following the replacement block. /// /// //TODO should have really been a local method inside ReplaceRange(int index, int count, IEnumerable collection, IEqualityComparer comparer), //move when supported language version updated. void OnRangeReplaced(int followingItemIndex, ICollection newCluster, ICollection oldCluster) { if (oldCluster == null || oldCluster.Count == 0) { Debug.Assert(newCluster == null || newCluster.Count == 0); return; } OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Replace, new List(newCluster), new List(oldCluster), followingItemIndex - oldCluster.Count)); oldCluster.Clear(); newCluster.Clear(); } #endregion Private Methods //------------------------------------------------------ // // Private Types // //------------------------------------------------------ #region Private Types sealed class DeferredEventsCollection : List, IDisposable { readonly CoreObservableCollection _collection; public DeferredEventsCollection(CoreObservableCollection 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 } /// /// To be kept outside , since otherwise, a new instance will be created for each generic type used. /// 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); } }