// 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);
}
}
}