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();
+ }
+ }
+ }
+
+}