// 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; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; namespace CommunityToolkit.Mvvm.Collections; /// /// The extensions methods to simplify the usage of . /// public static class ObservableGroupedCollectionExtensions { /// /// Returns the first group with key. /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group to query. /// The first group matching . /// Thrown if or are . /// The target group does not exist. public static ObservableGroup FirstGroupByKey(this ObservableGroupedCollection source, TKey key) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ObservableGroup? group = source.FirstGroupByKeyOrDefault(key); if (group is null) { [DoesNotReturn] static void ThrowArgumentExceptionForKeyNotFound() { throw new InvalidOperationException("The requested key was not present in the collection."); } ThrowArgumentExceptionForKeyNotFound(); } return group; } /// /// Returns the first group with key or if not found. /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group to query. /// The first group matching or . /// Thrown if or are . public static ObservableGroup? FirstGroupByKeyOrDefault(this ObservableGroupedCollection source, TKey key) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); // This pattern is used extensively in this file, with many of the public APIs having a first loop on the retrieved // list, and then a fallback one sometimes with the same logic, but on the collection itself. This is done as an // optimization: if a list is available, we can iterate on it directly, which will use List.Enumerator and avoid // allocations (the enumerator is a struct), additional indirections (the enumerator wraps the list instead of the // outer collection, and additional overhead (using the value enumerator avoids the interface stub dispatches). // Because of this, duplicate logic below is intentional and not actually duplicate, as it results in different code. if (source.TryGetList(out List>? list)) { foreach (ObservableGroup group in list) { if (EqualityComparer.Default.Equals(group.Key, key)) { return group; } } return null; } [MethodImpl(MethodImplOptions.NoInlining)] static ObservableGroup? FirstGroupByKeyOrDefaultFallback(ObservableGroupedCollection source, TKey key) { return Enumerable.FirstOrDefault>(source, group => EqualityComparer.Default.Equals(group.Key, key)); } return FirstGroupByKeyOrDefaultFallback(source, key); } /// /// Adds a key-value item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group to add. /// The added . /// Thrown if or are . public static ObservableGroup AddGroup(this ObservableGroupedCollection source, TKey key) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ObservableGroup group = new(key); source.Add(group); return group; } /// /// Adds a key-collection item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The group of items to add. /// The added . /// Thrown if or are . public static ObservableGroup AddGroup(this ObservableGroupedCollection source, IGrouping grouping) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(grouping); ObservableGroup group = new(grouping); source.Add(group); return group; } /// /// Adds a key-collection item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group where will be added. /// The collection to add. /// The added . /// Thrown if , or are . public static ObservableGroup AddGroup(this ObservableGroupedCollection source, TKey key, IEnumerable collection) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(collection); ObservableGroup group = new(key, collection); source.Add(group); return group; } /// /// Adds a key-value item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group to add. /// The added . /// Thrown if or are . public static ObservableGroup InsertGroup(this ObservableGroupedCollection source, TKey key) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (Comparer.Default.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key); source.Insert(index, newGroup); return newGroup; } [MethodImpl(MethodImplOptions.NoInlining)] static ObservableGroup InsertGroupFallback(ObservableGroupedCollection source, TKey key) { int index = 0; foreach (ObservableGroup group in source) { if (Comparer.Default.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key); source.Insert(index, newGroup); return newGroup; } return InsertGroupFallback(source, key); } /// /// Adds a key-value item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The group of items to add. /// The added . /// Thrown if or are . public static ObservableGroup InsertGroup(this ObservableGroupedCollection source, IGrouping grouping) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(grouping); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (Comparer.Default.Compare(grouping.Key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(grouping); source.Insert(index, newGroup); return newGroup; } [MethodImpl(MethodImplOptions.NoInlining)] static ObservableGroup InsertGroupFallback(ObservableGroupedCollection source, IGrouping grouping) { int index = 0; foreach (ObservableGroup group in source) { if (Comparer.Default.Compare(grouping.Key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(grouping); source.Insert(index, newGroup); return newGroup; } return InsertGroupFallback(source, grouping); } /// /// Adds a key-value item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group where will be added. /// The collection to add. /// The added . /// Thrown if , or are . public static ObservableGroup InsertGroup(this ObservableGroupedCollection source, TKey key, IEnumerable collection) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(collection); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (Comparer.Default.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key, collection); source.Insert(index, newGroup); return newGroup; } [MethodImpl(MethodImplOptions.NoInlining)] static ObservableGroup InsertGroupFallback(ObservableGroupedCollection source, TKey key, IEnumerable collection) { int index = 0; foreach (ObservableGroup group in source) { if (Comparer.Default.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key, collection); source.Insert(index, newGroup); return newGroup; } return InsertGroupFallback(source, key, collection); } /// /// Adds a key-value item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group to add. /// The instance to insert at the right position. /// The added . /// Thrown if , or are . public static ObservableGroup InsertGroup(this ObservableGroupedCollection source, TKey key, IComparer comparer) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(comparer); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (comparer.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key); source.Insert(index, newGroup); return newGroup; } [MethodImpl(MethodImplOptions.NoInlining)] static ObservableGroup InsertGroupFallback(ObservableGroupedCollection source, TKey key, IComparer comparer) { int index = 0; foreach (ObservableGroup group in source) { if (comparer.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key); source.Insert(index, newGroup); return newGroup; } return InsertGroupFallback(source, key, comparer); } /// /// Adds a key-value item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The group of items to add. /// The instance to insert at the right position. /// The added . /// Thrown if , or are . public static ObservableGroup InsertGroup(this ObservableGroupedCollection source, IGrouping grouping, IComparer comparer) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(grouping); ArgumentNullException.ThrowIfNull(comparer); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (comparer.Compare(grouping.Key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(grouping); source.Insert(index, newGroup); return newGroup; } [MethodImpl(MethodImplOptions.NoInlining)] static ObservableGroup InsertGroupFallback(ObservableGroupedCollection source, IGrouping grouping, IComparer comparer) { int index = 0; foreach (ObservableGroup group in source) { if (comparer.Compare(grouping.Key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(grouping); source.Insert(index, newGroup); return newGroup; } return InsertGroupFallback(source, grouping, comparer); } /// /// Adds a key-value item into a target . /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group where will be added. /// The instance to insert at the right position. /// The collection to add. /// The added . /// Thrown if , , or are . public static ObservableGroup InsertGroup(this ObservableGroupedCollection source, TKey key, IComparer comparer, IEnumerable collection) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(comparer); ArgumentNullException.ThrowIfNull(collection); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (comparer.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key, collection); source.Insert(index, newGroup); return newGroup; } [MethodImpl(MethodImplOptions.NoInlining)] static ObservableGroup InsertGroupFallback(ObservableGroupedCollection source, TKey key, IComparer comparer, IEnumerable collection) { int index = 0; foreach (ObservableGroup group in source) { if (comparer.Compare(key, group.Key) < 0) { break; } index++; } ObservableGroup newGroup = new(key, collection); source.Insert(index, newGroup); return newGroup; } return InsertGroupFallback(source, key, comparer, collection); } /// /// Add into the first group with key. /// If the group does not exist, it will be added. /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group where the should be added. /// The item to add. /// The instance of the which will receive the value. It will either be an existing group or a new group. /// Thrown if or are . public static ObservableGroup AddItem(this ObservableGroupedCollection source, TKey key, TElement item) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ObservableGroup? group = source.FirstGroupByKeyOrDefault(key); if (group is null) { group = new ObservableGroup(key) { item }; source.Add(group); } else { group.Add(item); } return group; } /// /// Insert into the first group with key. /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group where to insert . /// The item to add. /// The instance of the which will receive the value. /// Thrown if or are . public static ObservableGroup InsertItem(this ObservableGroupedCollection source, TKey key, TElement item) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ObservableGroup? group = source.FirstGroupByKeyOrDefault(key); if (group is null) { group = source.InsertGroup(key, new[] { item }); } else if (group.TryGetList(out List? list)) { int index = 0; foreach (TElement element in list) { if (Comparer.Default.Compare(item, element) < 0) { break; } index++; } group.Insert(index, item); } else { [MethodImpl(MethodImplOptions.NoInlining)] static void InsertItemFallback(ObservableCollection source, TElement item) { int index = 0; foreach (TElement element in source) { if (Comparer.Default.Compare(item, element) < 0) { break; } index++; } source.Insert(index, item); } InsertItemFallback(group, item); } return group; } /// /// Insert into the first group with key. /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group where to insert . /// The instance to compare keys. /// The item to add. /// The instance to compare elements. /// The instance of the which will receive the value. /// Thrown if , , or are . public static ObservableGroup InsertItem( this ObservableGroupedCollection source, TKey key, IComparer keyComparer, TElement item, IComparer itemComparer) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(keyComparer); ArgumentNullException.ThrowIfNull(itemComparer); ObservableGroup? group = source.FirstGroupByKeyOrDefault(key); if (group is null) { group = source.InsertGroup(key, keyComparer, new[] { item }); } else if (group.TryGetList(out List? list)) { int index = 0; foreach (TElement element in list) { if (itemComparer.Compare(item, element) < 0) { break; } index++; } group.Insert(index, item); } else { [MethodImpl(MethodImplOptions.NoInlining)] static void InsertItemFallback(ObservableCollection source, TElement item, IComparer comparer) { int index = 0; foreach (TElement element in source) { if (comparer.Compare(item, element) < 0) { break; } index++; } source.Insert(index, item); } InsertItemFallback(group, item, itemComparer); } return group; } /// /// Remove the first occurrence of the group with from the grouped collection. /// It will not do anything if the group does not exist. /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group to remove. /// Thrown if or are . public static void RemoveGroup(this ObservableGroupedCollection source, TKey key) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (EqualityComparer.Default.Equals(group.Key, key)) { source.RemoveAt(index); return; } index++; } } else { [MethodImpl(MethodImplOptions.NoInlining)] static void RemoveGroupFallback(ObservableGroupedCollection source, TKey key) { int index = 0; foreach (ObservableGroup group in source) { if (EqualityComparer.Default.Equals(group.Key, key)) { source.RemoveAt(index); return; } index++; } } RemoveGroupFallback(source, key); } } /// /// Remove the first from the first group with from the grouped collection. /// It will not do anything if the group or the item does not exist. /// /// The type of the group key. /// The type of the items in the collection. /// The source instance. /// The key of the group where the should be removed. /// The item to remove. /// If true (default value), the group will be removed once it becomes empty. /// Thrown if or are . public static void RemoveItem(this ObservableGroupedCollection source, TKey key, TValue item, bool removeGroupIfEmpty = true) where TKey : notnull { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.For.ThrowIfNull(key); if (source.TryGetList(out List>? list)) { int index = 0; foreach (ObservableGroup group in list) { if (EqualityComparer.Default.Equals(group.Key, key)) { if (group.Remove(item) && removeGroupIfEmpty && group.Count == 0) { source.RemoveAt(index); } return; } index++; } } else { [MethodImpl(MethodImplOptions.NoInlining)] static void RemoveItemFallback(ObservableGroupedCollection source, TKey key, TValue item, bool removeGroupIfEmpty) { int index = 0; foreach (ObservableGroup group in source) { if (EqualityComparer.Default.Equals(group.Key, key)) { if (group.Remove(item) && removeGroupIfEmpty && group.Count == 0) { source.RemoveAt(index); } return; } index++; } } RemoveItemFallback(source, key, item, removeGroupIfEmpty); } } }