diff --git a/src/MyNet.Utilities/Collections/ExtendedObservableCollection.cs b/src/MyNet.Utilities/Collections/ExtendedObservableCollection.cs new file mode 100644 index 0000000..d044d88 --- /dev/null +++ b/src/MyNet.Utilities/Collections/ExtendedObservableCollection.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using MyNet.Utilities.Deferring; + +namespace MyNet.Utilities.Collections; + +/// +/// An override of observable collection which allows the suspension of notifications. +/// +/// The type of the item. +public class ExtendedObservableCollection : ObservableCollection +{ + private bool _suspendCount; + + private bool _suspendNotifications; + + /// + /// Initializes a new instance of the class. + /// + public ExtendedObservableCollection() + { + } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified list. + /// + /// The list from which the elements are copied.The parameter cannot be null. + public ExtendedObservableCollection(List list) + : base(list) + { + } + + /// + /// Initializes a new instance of the class that contains elements copied from the specified collection. + /// + /// The collection from which the elements are copied.The parameter cannot be null. + public ExtendedObservableCollection(IEnumerable collection) + : base(collection) + { + } + + /// + /// Adds the elements of the specified collection to the end of the collection. + /// + /// The collection whose elements should be added to the end of the List. The collection itself cannot be null, but it can contain elements that are null. + /// is null. + public void AddRange(IEnumerable collection) + { + ArgumentNullException.ThrowIfNull(collection); + + foreach (var item in collection) + { + Add(item); + } + } + + /// + /// Inserts the elements of a collection into the at the specified index. + /// + /// Inserts the items at the specified index. + /// The zero-based index at which the new elements should be inserted. + /// is null. + /// is less than 0.-or- is greater than Count. + public void InsertRange(IEnumerable collection, int index) + { + ArgumentNullException.ThrowIfNull(collection); + + foreach (var item in collection) + { + InsertItem(index++, item); + } + } + + /// + /// Clears the list and Loads the specified items. + /// + /// The items. + public void Load(IEnumerable items) + { + ArgumentNullException.ThrowIfNull(items); + + CheckReentrancy(); + Clear(); + + foreach (var item in items) + { + Add(item); + } + } + + /// + /// 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. is less than 0.-or- is less than 0. and do not denote a valid range of elements in the . + public void RemoveRange(int index, int count) + { + for (var i = 0; i < count; i++) + { + RemoveAt(index); + } + } + + /// + /// Suspends count notifications. + /// + /// A disposable when disposed will reset the count. + public IDisposable SuspendCount() + { + var count = Count; + _suspendCount = true; + return new Deferrer( + () => + { + _suspendCount = false; + + if (Count != count) + { + OnPropertyChanged(new PropertyChangedEventArgs("Count")); + } + }).Defer(); + } + + /// + /// Suspends notifications. When disposed, a reset notification is fired. + /// + /// A disposable when disposed will reset notifications. + public IDisposable SuspendNotifications() + { + _suspendCount = true; + _suspendNotifications = true; + + return new Deferrer( + () => + { + _suspendCount = false; + _suspendNotifications = false; + OnPropertyChanged(new PropertyChangedEventArgs("Count")); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + }).Defer(); + } + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (_suspendNotifications) + { + return; + } + + base.OnCollectionChanged(e); + } + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (_suspendCount && e.PropertyName == "Count") + { + return; + } + + base.OnPropertyChanged(e); + } +} diff --git a/src/MyNet.Utilities/Collections/ObservableKeyedCollection.cs b/src/MyNet.Utilities/Collections/ObservableKeyedCollection.cs new file mode 100644 index 0000000..4d20a6e --- /dev/null +++ b/src/MyNet.Utilities/Collections/ObservableKeyedCollection.cs @@ -0,0 +1,209 @@ +// Copyright (c) Stéphane ANDRE. All Right Reserved. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace MyNet.Utilities.Collections +{ + public abstract class ObservableKeyedCollection : SortableObservableCollection + where TKey : notnull + { + private Dictionary? _dict; + + protected ObservableKeyedCollection(IEnumerable list) : this(list, null) { } + + protected ObservableKeyedCollection() : this([], null) { } + + protected ObservableKeyedCollection(IEqualityComparer comparer) : this([], comparer) { } + + protected ObservableKeyedCollection(Func sortSelector, ListSortDirection direction = ListSortDirection.Ascending) : base(sortSelector, direction) => Comparer = EqualityComparer.Default; + + protected ObservableKeyedCollection(IEnumerable list, IEqualityComparer? comparer) : base(list) + { + comparer ??= EqualityComparer.Default; + + Comparer = comparer; + } + + public IEqualityComparer Comparer { get; } + + public T? this[TKey key] + => key switch + { + null => throw new ArgumentNullException(nameof(key)), + _ => _dict is not null && _dict.TryGetValue(key, out var value) ? value : Items.FirstOrDefault(x => Comparer.Equals(GetKeyForItem(x), key)) + }; + + public bool Contains(TKey key) + => key switch + { + null => throw new ArgumentNullException(nameof(key)), + _ => _dict is not null ? _dict.ContainsKey(key) : Items.Any(x => Comparer.Equals(GetKeyForItem(x), key)) + }; + + private bool ContainsItem(T item) + { + TKey key; + if (_dict is null || (key = GetKeyForItem(item)) is null) + { + return Items.Contains(item); + } + + var exist = _dict.TryGetValue(key, out var itemInDict); + return exist && EqualityComparer.Default.Equals(itemInDict, item); + } + + public bool TryAdd(T item) + { + var key = GetKeyForItem(item); + if (key is null || _dict is null || _dict.ContainsKey(key)) return false; + + Add(item); + + return true; + } + + public bool Remove(TKey key) + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (_dict is not null) + { + return _dict.ContainsKey(key) && Remove(_dict[key]); + } + + for (var i = 0; i < Items.Count; i++) + { + if (Comparer.Equals(GetKeyForItem(Items[i]), key)) + { + RemoveItem(i); + return true; + } + } + return false; + } + + protected IDictionary? Dictionary => _dict; + + protected void ChangeItemKey(T item, TKey newKey) + { + // check if the item exists in the collection + if (!ContainsItem(item)) + { + return; + } + + var oldKey = GetKeyForItem(item); + if (!Comparer.Equals(oldKey, newKey)) + { + if (newKey is not null) + { + AddKey(newKey, item); + } + + if (oldKey is not null) + { + RemoveKey(oldKey); + } + } + } + + protected override void ClearItems() + { + _dict?.Clear(); + + base.ClearItems(); + } + + protected abstract TKey GetKeyForItem(T item); + + protected override void InsertItem(int index, T item) + { + var key = GetKeyForItem(item); + if (key is not null) + { + AddKey(key, item); + } + base.InsertItem(index, item); + } + + protected void InsertItemInItems(int index, T item) => base.InsertItem(index, item); + + protected override void RemoveItem(int index) + { + var key = GetKeyForItem(Items[index]); + if (key is not null) + { + RemoveKey(key); + } + base.RemoveItem(index); + } + + protected override void SetItem(int index, T item) + => ExecuteThreadSafe(() => + { + var newKey = GetKeyForItem(item); + var oldKey = GetKeyForItem(Items[index]); + + if (Comparer.Equals(oldKey, newKey)) + { + if (newKey is not null && _dict is not null) + { + _dict[newKey] = item; + } + } + else + { + if (newKey is not null) + { + AddKey(newKey, item); + } + + if (oldKey is not null) + { + RemoveKey(oldKey); + } + } + base.SetItem(index, item); + }); + + private void AddKey(TKey key, T item) + => ExecuteThreadSafe(() => + { + if (_dict is null) + { + CreateDictionary(); + } + _dict?.Add(key, item); + }); + + private void CreateDictionary() + { + _dict = new Dictionary(Comparer); + foreach (var item in Items) + { + var key = GetKeyForItem(item); + if (key is not null) + { + _dict.Add(key, item); + } + } + } + + private void RemoveKey(TKey key) + => ExecuteThreadSafe(() => + { + if (_dict is not null) + { + _ = _dict.Remove(key); + } + }); + + } +} diff --git a/src/MyNet.Utilities/Collections/ReadOnlyObservableKeyedCollection.cs b/src/MyNet.Utilities/Collections/ReadOnlyObservableKeyedCollection.cs new file mode 100644 index 0000000..5abd044 --- /dev/null +++ b/src/MyNet.Utilities/Collections/ReadOnlyObservableKeyedCollection.cs @@ -0,0 +1,13 @@ +// Copyright (c) Stéphane ANDRE. All Right Reserved. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; + +namespace MyNet.Utilities.Collections +{ + public class ReadOnlyObservableKeyedCollection(ObservableKeyedCollection list) : ReadOnlyObservableCollection(list) + where TKey : notnull + { + public T? this[TKey key] => ((ObservableKeyedCollection)Items)[key]; + } +} diff --git a/src/MyNet.Utilities/Collections/SortableObservableCollection.cs b/src/MyNet.Utilities/Collections/SortableObservableCollection.cs new file mode 100644 index 0000000..666a4e8 --- /dev/null +++ b/src/MyNet.Utilities/Collections/SortableObservableCollection.cs @@ -0,0 +1,56 @@ +// Copyright (c) Stéphane ANDRE. All Right Reserved. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; + +namespace MyNet.Utilities.Collections +{ + public class SortableObservableCollection : ThreadSafeObservableCollection + { + public Func? SortSelector { get; set; } + + public ListSortDirection SortDirection { get; set; } + + public SortableObservableCollection() { } + + public SortableObservableCollection(List list) : base(list) { } + + public SortableObservableCollection(IEnumerable collection) : base(collection) { } + + public SortableObservableCollection(Func sortSelector, ListSortDirection direction = ListSortDirection.Ascending) => (SortSelector, SortDirection) = (sortSelector, direction); + + protected override void InvokeNotifyCollectionChanged(NotifyCollectionChangedEventHandler notifyEventHandler, NotifyCollectionChangedEventArgs e) + { + base.InvokeNotifyCollectionChanged(notifyEventHandler, e); + + if (SortSelector is null + || e.Action == NotifyCollectionChangedAction.Remove + || e.Action == NotifyCollectionChangedAction.Reset) + return; + + Sort(); + } + + public void Sort() + { + if (SortSelector is null) return; + + ExecuteThreadSafe(() => + { + var query = this.Select((x, index) => (Item: x, Index: index)); + + query = SortDirection == ListSortDirection.Ascending ? query.OrderBy(x => SortSelector.Invoke(x.Item)) : query.OrderByDescending(x => SortSelector.Invoke(x.Item)); + + var map = query.Select((x, index) => (OldIndex: x.Index, NewIndex: index)).Where(o => o.OldIndex != o.NewIndex); + + using var enumerator = map.GetEnumerator(); + if (enumerator.MoveNext()) + Move(enumerator.Current.OldIndex, enumerator.Current.NewIndex); + }); + } + } +} diff --git a/src/MyNet.Utilities/Collections/ThreadSafeObservableCollection.cs b/src/MyNet.Utilities/Collections/ThreadSafeObservableCollection.cs new file mode 100644 index 0000000..192a499 --- /dev/null +++ b/src/MyNet.Utilities/Collections/ThreadSafeObservableCollection.cs @@ -0,0 +1,74 @@ +// Copyright (c) Stéphane ANDRE. All Right Reserved. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Threading.Tasks; + +namespace MyNet.Utilities.Collections +{ + public class ThreadSafeObservableCollection : ExtendedObservableCollection + { + private readonly object _localLock = new(); + private readonly Action? _notifyOnUi; + + public ThreadSafeObservableCollection(Action? notifyOnUi = null) => _notifyOnUi = notifyOnUi; + + public ThreadSafeObservableCollection(List list, Action? notifyOnUi = null) : base(list) => _notifyOnUi = notifyOnUi; + + public ThreadSafeObservableCollection(IEnumerable collection, Action? notifyOnUi = null) : base(collection) => _notifyOnUi = notifyOnUi; + + public override event NotifyCollectionChangedEventHandler? CollectionChanged; + + protected override void InsertItem(int index, T item) => ExecuteThreadSafe(() => base.InsertItem(index, item)); + + protected override void MoveItem(int oldIndex, int newIndex) => ExecuteThreadSafe(() => base.MoveItem(oldIndex, newIndex)); + + protected override void RemoveItem(int index) => ExecuteThreadSafe(() => base.RemoveItem(index)); + + protected override void SetItem(int index, T item) => ExecuteThreadSafe(() => base.SetItem(index, item)); + + protected override void ClearItems() => ExecuteThreadSafe(base.ClearItems); + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + using (BlockReentrancy()) + { + var collectionChanged = CollectionChanged; + if (collectionChanged != null) + NotifyCollectionChanged(e, collectionChanged); + } + } + + private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e, NotifyCollectionChangedEventHandler collectionChanged) + { + foreach (var notifyEventHandler in collectionChanged.GetInvocationList().OfType()) + { + try + { + if (_notifyOnUi is not null) + _notifyOnUi(() => InvokeNotifyCollectionChanged(notifyEventHandler, e)); + else + InvokeNotifyCollectionChanged(notifyEventHandler, e); + } + catch (TaskCanceledException) + { + // Opeation has canceled by the system + } + } + } + + protected virtual void InvokeNotifyCollectionChanged(NotifyCollectionChangedEventHandler notifyEventHandler, NotifyCollectionChangedEventArgs e) => notifyEventHandler.Invoke(this, e); + + protected void ExecuteThreadSafe(Action action) + { + lock (_localLock) + { + action(); + } + } + } + +}