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