using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Threading; // ReSharper disable UnusedMember.Global // ReSharper disable EventNeverSubscribedTo.Global namespace Vanara.Collections { /// A generic list that provides event for changes to the list. This is an alternative to ObservableCollection that provides distinct events for each action (add, insert, remove, changed). /// The type of elements in the collection. [Serializable] public class EventedList : IList, IList where T : INotifyPropertyChanged { private static readonly T[] emptyArray = new T[0]; private T[] internalItems; [NonSerialized] private object syncRoot; private int version; /// Initializes a new instance of the class. public EventedList() { internalItems = emptyArray; } /// Initializes a new instance of the class that contains elements copied from the specified collection. /// The collection from which the elements are copied. public EventedList(IEnumerable collection) { switch (collection) { case null: throw new ArgumentNullException(nameof(collection)); case ICollection is2: var count = is2.Count; internalItems = new T[count]; is2.CopyTo(internalItems, 0); Count = count; break; default: Count = 0; internalItems = new T[4]; using (var enumerator = collection.GetEnumerator()) { while (enumerator.MoveNext()) { Add(enumerator.Current); } } break; } } /// Initializes a new instance of the class providing an initial capacity. /// The capacity. public EventedList(int capacity) { if (capacity < 0) { throw new ArgumentOutOfRangeException(nameof(capacity)); } internalItems = new T[capacity]; } /// Occurs when an item has been added. public event EventHandler> ItemAdded; /// Occurs when an item has changed. public event EventHandler> ItemChanged; /// Occurs when an item has been deleted. public event EventHandler> ItemDeleted; /// Occurs when an item's property value has been changed. public event PropertyChangedEventHandler ItemPropertyChanged; /// Occurs when the list has been reset. public event EventHandler> Reset; /// Gets or sets the capacity. /// The capacity. public int Capacity { get => internalItems.Length; set { if (value == internalItems.Length) return; if (value < Count) { throw new ArgumentOutOfRangeException(nameof(value)); } if (value > 0) { var destinationArray = new T[value]; if (Count > 0) { Array.Copy(internalItems, 0, destinationArray, 0, Count); } internalItems = destinationArray; } else { internalItems = emptyArray; } } } /// Gets the number of elements contained in the . /// The number of elements contained in the . public int Count { get; private set; } /// Gets a value indicating whether the has a fixed size. /// /// true if the has a fixed size; otherwise, false. bool IList.IsFixedSize => false; /// Gets a value indicating whether the is read-only. /// /// true if the is read-only; otherwise, false. bool IList.IsReadOnly => false; /// Gets a value indicating whether the is read-only. /// /// true if the is read-only; otherwise, false. bool ICollection.IsReadOnly => false; /// Gets a value indicating whether access to the is synchronized (thread safe). /// /// true if access to the is synchronized (thread safe); otherwise, false. bool ICollection.IsSynchronized => false; /// Gets an object that can be used to synchronize access to the . /// /// An object that can be used to synchronize access to the . object ICollection.SyncRoot { get { if (syncRoot == null) { Interlocked.CompareExchange(ref syncRoot, new object(), null); } return syncRoot; } } /// Gets or sets the element at the specified index. /// The zero-based index of the element to get or set. /// The element at the specified index. public T this[int index] { get { CheckIndex(index); return internalItems[index]; } set { CheckIndex(index); var oldValue = internalItems[index]; internalItems[index] = value; version++; OnItemChanged(index, oldValue, value); } } /// Gets or sets the at the specified index. /// object IList.this[int index] { get => this[index]; set { VerifyValueType(value); this[index] = (T)value; } } /// Adds an item to the . /// The object to add to the . /// The is read-only. public void Add(T item) { if (Count == internalItems.Length) { EnsureCapacity(Count + 1); } internalItems[Count++] = item; version++; OnItemAdded(Count, item); } /// Adds the range of items to the list. /// The collection of items to add. public void AddRange(IEnumerable collection) { InsertRange(Count, collection); } /// Adds the range of items to the list. /// The items to add. public void AddRange(T[] items) { InsertRange(Count, items); } /// Determines if the collection is read-only. /// public ReadOnlyCollection AsReadOnly() => new ReadOnlyCollection(this); /// /// Searches the entire sorted for an element using the default comparer and returns the zero-based index of the element. /// /// The object to locate. The value can be null for reference types. /// /// The zero-based index of item in the sorted , if item is found; otherwise, a negative number that is the bitwise /// complement of the index of the next element that is larger than item or, if there is no larger element, the bitwise complement of . /// public int BinarySearch(T item) => BinarySearch(0, Count, item, null); /// /// Searches the entire sorted for an element using the specified comparer and returns the zero-based index of the element. /// /// The object to locate. The value can be null for reference types. /// The implementation to use when comparing elements, or null to use the default comparer . /// /// The zero-based index of item in the sorted , if item is found; otherwise, a negative number that is the bitwise /// complement of the index of the next element that is larger than item or, if there is no larger element, the bitwise complement of . /// public int BinarySearch(T item, IComparer comparer) => BinarySearch(0, Count, item, comparer); /// /// Searches a range of elements in the sorted for an element using the specified comparer and returns the zero-based index /// of the element. /// /// The zero-based starting index of the range to search. /// The length of the range to search. /// The object to locate. The value can be null for reference types. /// The implementation to use when comparing elements, or null to use the default comparer . /// /// The zero-based index of item in the sorted , if item is found; otherwise, a negative number that is the bitwise /// complement of the index of the next element that is larger than item or, if there is no larger element, the bitwise complement of . /// public int BinarySearch(int index, int count, T item, IComparer comparer) => Array.BinarySearch(internalItems, index, count, item, comparer); /// Removes all items from the . /// The is read-only. public void Clear() { Array.Clear(internalItems, 0, Count); Count = 0; version++; OnReset(); } /// Determines whether the contains a specific value. /// The object to locate in the . /// true if is found in the ; otherwise, false. public bool Contains(T item) { if (item == null) { for (var j = 0; j < Count; j++) { if (internalItems[j] == null) { return true; } } return false; } var comparer = EqualityComparer.Default; for (var i = 0; i < Count; i++) { if (comparer.Equals(internalItems[i], item)) { return true; } } return false; } /// Converts all. /// The type of the output. /// The converter. /// public EventedList ConvertAll(Converter converter) where TOutput : INotifyPropertyChanged { if (converter == null) { throw new ArgumentNullException(nameof(converter)); } var list = new EventedList { internalItems = Array.ConvertAll(internalItems, converter), Count = Count }; return list; } /// /// Copies the elements of the to an , starting at a particular index. /// /// /// The one-dimensional that is the destination of the elements copied from . The /// must have zero-based indexing. /// /// The zero-based index in at which copying begins. /// is null. /// is less than 0. /// /// is multidimensional.-or- is equal to or greater than the length of .-or-The number of elements in the source is greater than the available space from to the end of the destination .-or-Type T cannot be cast automatically to the type of the /// destination . /// public void CopyTo(T[] array, int arrayIndex = 0) { Array.Copy(internalItems, 0, array, arrayIndex, Count); } /// Copies to. /// The index. /// The array. /// Index of the array. /// The count. public void CopyTo(int index, T[] array, int arrayIndex, int count) { if (Count - index < count) throw new ArgumentOutOfRangeException(nameof(index)); Array.Copy(internalItems, index, array, arrayIndex, count); } /// Determines if an item matches the specified predicate. /// The match. /// public bool Exists(Predicate match) => FindIndex(match) != -1; /// Finds the specified match. /// The match. /// public T Find(Predicate match) => Array.Find(internalItems, match); /// Finds all. /// The match. /// public EventedList FindAll(Predicate match) { if (match == null) throw new ArgumentNullException(nameof(match)); return new EventedList(internalItems.Where(match.Invoke)); } /// Finds the index. /// The match. /// public int FindIndex(Predicate match) => FindIndex(0, Count, match); /// Finds the index. /// The start index. /// The match. /// public int FindIndex(int startIndex, Predicate match) => FindIndex(startIndex, Count - startIndex, match); /// Finds the index. /// The start index. /// The count. /// The match. /// public int FindIndex(int startIndex, int count, Predicate match) { CheckRange(startIndex, count); return Array.FindIndex(internalItems, startIndex, count, match); } /// Finds the last. /// The match. /// public T FindLast(Predicate match) => Array.FindLast(internalItems, match); /// Finds the last index. /// The match. /// public int FindLastIndex(Predicate match) => FindLastIndex(Count - 1, Count, match); /// Finds the last index. /// The start index. /// The match. /// public int FindLastIndex(int startIndex, Predicate match) => FindLastIndex(startIndex, startIndex + 1, match); /// Finds the last index. /// The start index. /// The count. /// The match. /// public int FindLastIndex(int startIndex, int count, Predicate match) { CheckIndex(startIndex, "startIndex"); if (count < 0 || startIndex - count + 1 < 0) throw new ArgumentOutOfRangeException(nameof(count)); return Array.FindLastIndex(internalItems, startIndex, count, match); } /// Performs an action on each item in the collection. /// The action. public void ForEach(Action action) { if (action == null) throw new ArgumentNullException(nameof(action)); for (var i = 0; i < Count; i++) action(internalItems[i]); } /// Gets the range of items and returns then in another list. /// The starting index. /// The count of items to place in the list. /// An with the requested items. public EventedList GetRange(int index, int count) { CheckRange(index, count); var list = new EventedList(count); Array.Copy(internalItems, index, list.internalItems, 0, count); list.Count = count; return list; } /// Determines the index of a specific item in the . /// The object to locate in the . /// The index of if found in the list; otherwise, -1. public int IndexOf(T item) => Array.IndexOf(internalItems, item, 0, Count); /// Indexes the of. /// The item. /// The index. /// public int IndexOf(T item, int index) => Array.IndexOf(internalItems, item, index, Count - index); /// Indexes the of. /// The item. /// The index. /// The count. /// public int IndexOf(T item, int index, int count) => Array.IndexOf(internalItems, item, index, count); /// Inserts an item to the at the specified index. /// The zero-based index at which should be inserted. /// The object to insert into the . /// is not a valid index in the . /// The is read-only. public void Insert(int index, T item) { if (index != Count) CheckIndex(index); if (Count == internalItems.Length) { EnsureCapacity(Count + 1); } if (index < Count) { Array.Copy(internalItems, index, internalItems, index + 1, Count - index); } internalItems[index] = item; Count++; version++; OnItemAdded(index, item); } /// Inserts the range. /// The index. /// The collection. public void InsertRange(int index, IEnumerable collection) { if (collection == null) { throw new ArgumentNullException(nameof(collection)); } if (index != Count) CheckIndex(index); if (collection is ICollection is2) { var count = is2.Count; if (count > 0) { EnsureCapacity(Count + count); if (index < Count) { Array.Copy(internalItems, index, internalItems, index + count, Count - index); } if (Equals(is2)) { Array.Copy(internalItems, 0, internalItems, index, index); Array.Copy(internalItems, index + count, internalItems, index * 2, Count - index); } else { var array = new T[count]; is2.CopyTo(array, 0); array.CopyTo(internalItems, index); } Count += count; for (var i = index; i < index + count; i++) OnItemAdded(i, internalItems[i]); } } else { using (var enumerator = collection.GetEnumerator()) { while (enumerator.MoveNext()) { Insert(index++, enumerator.Current); } } } version++; } /// Lasts the index of. /// The item. /// public int LastIndexOf(T item) => LastIndexOf(item, Count - 1, Count); /// Lasts the index of. /// The item. /// The index. /// public int LastIndexOf(T item, int index) => LastIndexOf(item, index, Count - index + 1); /// Lasts the index of. /// The item. /// The index. /// The count. /// public int LastIndexOf(T item, int index, int count) => Array.LastIndexOf(internalItems, item, index, count); /// Removes the first occurrence of a specific object from the . /// The object to remove from the . /// /// true if was successfully removed from the ; otherwise, false. This method also returns false if /// is not found in the original . /// /// The is read-only. public bool Remove(T item) { var index = IndexOf(item); if (index >= 0) { RemoveAt(index); return true; } return false; } /// Removes all. /// The match. /// public int RemoveAll(Predicate match) { if (match == null) { throw new ArgumentNullException(nameof(match)); } var index = 0; while (index < Count && !match(internalItems[index])) { index++; } if (index >= Count) { return 0; } var num2 = index + 1; while (num2 < Count) { while (num2 < Count && match(internalItems[num2])) { num2++; } if (num2 < Count) { var oldVal = internalItems[index + 1]; internalItems[index++] = internalItems[num2++]; OnItemDeleted(index, oldVal); } } Array.Clear(internalItems, index, Count - index); var num3 = Count - index; Count = index; version++; return num3; } /// Removes the item at the specified index. /// The zero-based index of the item to remove. /// is not a valid index in the . /// The is read-only. public void RemoveAt(int index) { CheckIndex(index); Count--; var oldVal = internalItems[index]; if (index < Count) { Array.Copy(internalItems, index + 1, internalItems, index, Count - index); } internalItems[Count] = default; version++; OnItemDeleted(index, oldVal); } /// Removes the range. /// The index. /// The count. public void RemoveRange(int index, int count) { CheckRange(index, count); if (count > 0) { Count -= count; var array = new T[count]; Array.Copy(internalItems, index, array, 0, count); if (index < Count) { Array.Copy(internalItems, index + count, internalItems, index, Count - index); } Array.Clear(internalItems, Count, count); version++; for (var i = index; i < index + count; i++) OnItemDeleted(i, array[i - index]); } } /// Reverses this instance. public void Reverse() { Reverse(0, Count); } /// Reverses the specified index. /// The index. /// The count. public void Reverse(int index, int count) { CheckRange(index, count); Array.Reverse(internalItems, index, count); version++; } /// Sorts this instance. public void Sort() { Sort(0, Count, null); } /// Sorts the specified comparer. /// The comparer. public void Sort(IComparer comparer) { Sort(0, Count, comparer); } /// Sorts the specified index. /// The index. /// The count. /// The comparer. public void Sort(int index, int count, IComparer comparer) { Array.Sort(internalItems, index, count, comparer); version++; } /// Toes the array. /// public T[] ToArray() { var destinationArray = new T[Count]; Array.Copy(internalItems, 0, destinationArray, 0, Count); return destinationArray; } /// Trims the excess. public void TrimExcess() { var num = (int)(internalItems.Length * 0.9); if (Count < num) { Capacity = Count; } } /// Trues for all. /// The match. /// public bool TrueForAll(Predicate match) { if (match == null) { throw new ArgumentNullException(nameof(match)); } for (var i = 0; i < Count; i++) { if (!match(internalItems[i])) { return false; } } return true; } /// Adds the specified item. /// The item. /// int IList.Add(object item) { VerifyValueType(item); Add((T)item); return Count - 1; } /// Determines whether [contains] [the specified item]. /// The item. /// true if [contains] [the specified item]; otherwise, false. bool IList.Contains(object item) => IsCompatibleObject(item) && Contains((T)item); /// Copies list values to an array. /// The array. /// The index of the array at which to start copying into. void ICollection.CopyTo(Array array, int arrayIndex) { if (array != null && array.Rank != 1) throw new ArgumentException(); try { Array.Copy(internalItems, 0, array, arrayIndex, Count); } catch (ArrayTypeMismatchException) { throw new ArgumentException(); } } /// Returns an enumerator that iterates through the collection. /// A that can be used to iterate through the collection. public IEnumerator GetEnumerator() => new Enumerator(this); /// Indexes the of. /// The item. /// int IList.IndexOf(object item) { if (IsCompatibleObject(item)) { return IndexOf((T)item); } return -1; } /// Inserts the specified index. /// The index. /// The item. void IList.Insert(int index, object item) { VerifyValueType(item); Insert(index, (T)item); } /// Removes the specified item. /// The item. void IList.Remove(object item) { if (IsCompatibleObject(item)) { Remove((T)item); } } /// Returns an enumerator that iterates through a collection. /// An object that can be used to iterate through the collection. [ExcludeFromCodeCoverage] IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// Called when [insert]. /// The index. /// The value. protected virtual void OnItemAdded(int index, T value) { if (value != null) { value.PropertyChanged += OnItemPropertyChanged; ItemAdded?.Invoke(this, new ListChangedEventArgs(ListChangedType.ItemAdded, value, index)); } } /// Called when [set]. /// The index. /// The old value. /// The new value. protected virtual void OnItemChanged(int index, T oldValue, T newValue) { if (oldValue != null && !oldValue.Equals(newValue)) { oldValue.PropertyChanged -= OnItemPropertyChanged; if (newValue != null) newValue.PropertyChanged += OnItemPropertyChanged; } ItemChanged?.Invoke(this, new ListChangedEventArgs(ListChangedType.ItemChanged, newValue, index, oldValue)); } /// Called when [remove]. /// The index. /// The value. protected virtual void OnItemDeleted(int index, T value) { if (value != null) { value.PropertyChanged -= OnItemPropertyChanged; ItemDeleted?.Invoke(this, new ListChangedEventArgs(ListChangedType.ItemDeleted, value, index)); } } /// Called when [item property changed]. /// The sender. /// The instance containing the event data. protected virtual void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e) { ItemPropertyChanged?.Invoke(sender, e); } /// Called when [clear]. protected virtual void OnReset() { ForEach(delegate(T item) { item.PropertyChanged -= OnItemPropertyChanged; }); Reset?.Invoke(this, new ListChangedEventArgs(ListChangedType.Reset)); } /// Determines whether [is compatible object] [the specified value]. /// The value. /// true if [is compatible object] [the specified value]; otherwise, false. private static bool IsCompatibleObject(object value) => value is T || value == null && !typeof(T).IsValueType; /// Verifies the type of the value. /// The value. private static void VerifyValueType(object value) { if (!IsCompatibleObject(value)) { throw new ArgumentException(@"Incompatible object", nameof(value)); } } /// Checks the index to ensure it is valid and in the list. /// The index to validate. /// Name of the variable this is being checked. /// Called with the index is out of range. // ReSharper disable once UnusedParameter.Local private void CheckIndex(int idx, string varName = "index") { if (idx >= Count || idx < 0) throw new ArgumentOutOfRangeException(varName); } /// Checks the range. /// The index. /// The count. private void CheckRange(int index, int count) { if (index >= Count || index < 0) throw new ArgumentOutOfRangeException(nameof(index)); if (count < 0 || Count - index < count) throw new ArgumentOutOfRangeException(nameof(count)); } /// Ensures the capacity. /// The min. private void EnsureCapacity(int min) { if (internalItems.Length < min) { var num = internalItems.Length == 0 ? 4 : internalItems.Length*2; if (num < min) { num = min; } Capacity = num; } } /// Enumerates over the . [Serializable, StructLayout(LayoutKind.Sequential)] private struct Enumerator : IEnumerator { private readonly EventedList list; private int index; private readonly int version; /// Initializes a new instance of the struct. /// The list. internal Enumerator(EventedList list) { this.list = list; index = 0; version = list.version; Current = default; } /// Gets the current. /// The current. public T Current { get; private set; } /// Gets the current. /// The current. object IEnumerator.Current { get { if (index == 0 || index == list.Count + 1) { throw new InvalidOperationException(); } return Current; } } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { } /// Sets the enumerator to its initial position, which is before the first element in the collection. /// The collection was modified after the enumerator was created. void IEnumerator.Reset() { if (version != list.version) { throw new InvalidOperationException(); } index = 0; Current = default; } /// Advances the enumerator to the next element of the collection. /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection. /// The collection was modified after the enumerator was created. public bool MoveNext() { if (version != list.version) { throw new InvalidOperationException(); } if (index < list.Count) { Current = list.internalItems[index]; index++; return true; } index = list.Count + 1; Current = default; return false; } } /// An structure passed to events generated by an . /// #pragma warning disable 693 public class ListChangedEventArgs : EventArgs { /// Initializes a new instance of the class. /// The type of change. public ListChangedEventArgs(ListChangedType type) { ItemIndex = -1; ListChangedType = type; } /// Initializes a new instance of the class. /// The type of change. /// The item that has changed. /// Index of the changed item. public ListChangedEventArgs(ListChangedType type, T item, int itemIndex) { Item = item; ItemIndex = itemIndex; ListChangedType = type; } /// Initializes a new instance of the class. /// The type of change. /// The item that has changed. /// Index of the changed item. /// The old item when an item has changed. public ListChangedEventArgs(ListChangedType type, T item, int itemIndex, T oldItem) : this(type, item, itemIndex) { OldItem = oldItem; } /// Gets the item that has changed. /// The item. public T Item { get; } /// Gets the index of the item. /// The index of the item. public int ItemIndex { get; } /// Gets the type of change for the list. /// The type of change for the list. public ListChangedType ListChangedType { get; } /// Gets the item's previous value. /// The old item. public T OldItem { get; } } #pragma warning restore 693 } }