diff --git a/Microsoft.Toolkit/Collections/ObservableGroupedCollectionExtensions.cs b/Microsoft.Toolkit/Collections/ObservableGroupedCollectionExtensions.cs
new file mode 100644
index 00000000000..8be7dacec73
--- /dev/null
+++ b/Microsoft.Toolkit/Collections/ObservableGroupedCollectionExtensions.cs
@@ -0,0 +1,299 @@
+// 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.Linq;
+
+namespace Microsoft.Toolkit.Collections
+{
+ ///
+ /// The extensions methods to simplify the usage of .
+ ///
+ public static class ObservableGroupedCollectionExtensions
+ {
+ ///
+ /// Return 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 .
+ /// The target group does not exist.
+ public static ObservableGroup First(this ObservableGroupedCollection source, TKey key)
+ => source.First(group => GroupKeyPredicate(group, key));
+
+ ///
+ /// Return the first group with key or null 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 null.
+ public static ObservableGroup FirstOrDefault(this ObservableGroupedCollection source, TKey key)
+ => source.FirstOrDefault(group => GroupKeyPredicate(group, key));
+
+ ///
+ /// Return the element at position from 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 index of the item from the targeted group.
+ /// The element.
+ /// The target group does not exist.
+ /// is less than zero or is greater than the group elements' count.
+ public static TValue ElementAt(
+ this ObservableGroupedCollection source,
+ TKey key,
+ int index)
+ => source.First(key)[index];
+
+ ///
+ /// Return the element at position from 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 index of the item from the targeted group.
+ /// The element or default(TValue) if it does not exist.
+ public static TValue ElementAtOrDefault(
+ this ObservableGroupedCollection source,
+ TKey key,
+ int index)
+ {
+ var existingGroup = source.FirstOrDefault(key);
+ if (existingGroup is null)
+ {
+ return default;
+ }
+
+ return existingGroup.ElementAtOrDefault(index);
+ }
+
+ ///
+ /// 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 value to add.
+ /// The added .
+ public static ObservableGroup AddGroup(
+ this ObservableGroupedCollection source,
+ TKey key,
+ TValue value)
+ => AddGroup(source, key, new[] { value });
+
+ ///
+ /// 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 .
+ public static ObservableGroup AddGroup(
+ this ObservableGroupedCollection source,
+ TKey key,
+ params TValue[] collection)
+ => source.AddGroup(key, (IEnumerable)collection);
+
+ ///
+ /// 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 .
+ public static ObservableGroup AddGroup(
+ this ObservableGroupedCollection source,
+ TKey key,
+ IEnumerable collection)
+ {
+ var group = new ObservableGroup(key, collection);
+ source.Add(group);
+
+ return group;
+ }
+
+ ///
+ /// 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.
+ public static ObservableGroup AddItem(
+ this ObservableGroupedCollection source,
+ TKey key,
+ TValue item)
+ {
+ var existingGroup = source.FirstOrDefault(key);
+ if (existingGroup is null)
+ {
+ existingGroup = new ObservableGroup(key);
+ source.Add(existingGroup);
+ }
+
+ existingGroup.Add(item);
+ return existingGroup;
+ }
+
+ ///
+ /// Insert into the first group with key at .
+ ///
+ /// 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 index where to insert .
+ /// The item to add.
+ /// The instance of the which will receive the value.
+ /// The target group does not exist.
+ /// is less than zero or is greater than the group elements' count.
+ public static ObservableGroup InsertItem(
+ this ObservableGroupedCollection source,
+ TKey key,
+ int index,
+ TValue item)
+ {
+ var existingGroup = source.First(key);
+ existingGroup.Insert(index, item);
+ return existingGroup;
+ }
+
+ ///
+ /// Replace the element at with in 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 replace the item.
+ /// The index where to insert .
+ /// The item to add.
+ /// The instance of the which will receive the value.
+ /// The target group does not exist.
+ /// is less than zero or is greater than the group elements' count.
+ public static ObservableGroup SetItem(
+ this ObservableGroupedCollection source,
+ TKey key,
+ int index,
+ TValue item)
+ {
+ var existingGroup = source.First(key);
+ existingGroup[index] = item;
+ return existingGroup;
+ }
+
+ ///
+ /// 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.
+ public static void RemoveGroup(
+ this ObservableGroupedCollection source,
+ TKey key)
+ {
+ var index = 0;
+ foreach (var group in source)
+ {
+ if (GroupKeyPredicate(group, key))
+ {
+ source.RemoveAt(index);
+ return;
+ }
+
+ index++;
+ }
+ }
+
+ ///
+ /// 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.
+ public static void RemoveItem(
+ this ObservableGroupedCollection source,
+ TKey key,
+ TValue item,
+ bool removeGroupIfEmpty = true)
+ {
+ var index = 0;
+ foreach (var group in source)
+ {
+ if (GroupKeyPredicate(group, key))
+ {
+ group.Remove(item);
+
+ if (removeGroupIfEmpty && group.Count == 0)
+ {
+ source.RemoveAt(index);
+ }
+
+ return;
+ }
+
+ index++;
+ }
+ }
+
+ ///
+ /// Remove the item at 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 item at should be removed.
+ /// The index of the item to remove in the group.
+ /// If true (default value), the group will be removed once it becomes empty.
+ public static void RemoveItemAt(
+ this ObservableGroupedCollection source,
+ TKey key,
+ int index,
+ bool removeGroupIfEmpty = true)
+ {
+ var groupIndex = 0;
+ foreach (var group in source)
+ {
+ if (GroupKeyPredicate(group, key))
+ {
+ group.RemoveAt(index);
+
+ if (removeGroupIfEmpty && group.Count == 0)
+ {
+ source.RemoveAt(groupIndex);
+ }
+
+ return;
+ }
+
+ groupIndex++;
+ }
+ }
+
+ private static bool GroupKeyPredicate(ObservableGroup group, TKey expectedKey)
+ => EqualityComparer.Default.Equals(group.Key, expectedKey);
+ }
+}
diff --git a/UnitTests/Collections/ObservableGroupedCollectionExtensionsTests.cs b/UnitTests/Collections/ObservableGroupedCollectionExtensionsTests.cs
new file mode 100644
index 00000000000..0c1128be410
--- /dev/null
+++ b/UnitTests/Collections/ObservableGroupedCollectionExtensionsTests.cs
@@ -0,0 +1,539 @@
+// 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 FluentAssertions;
+using Microsoft.Toolkit.Collections;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Linq;
+
+namespace UnitTests.Collections
+{
+ [TestClass]
+ public class ObservableGroupedCollectionExtensionsTests
+ {
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void First_WhenGroupExists_ShouldReturnFirstGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+ var target = groupedCollection.AddGroup("B", 10);
+ groupedCollection.AddGroup("B", 42);
+
+ var result = groupedCollection.First("B");
+
+ result.Should().BeSameAs(target);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void First_WhenGroupDoesNotExist_ShouldThrow()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+
+ Action action = () => groupedCollection.First("I do not exist");
+
+ action.Should().Throw();
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void FirstOrDefault_WhenGroupExists_ShouldReturnFirstGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+ var target = groupedCollection.AddGroup("B", 10);
+ groupedCollection.AddGroup("B", 42);
+
+ var result = groupedCollection.FirstOrDefault("B");
+
+ result.Should().BeSameAs(target);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void FirstOrDefault_WhenGroupDoesNotExist_ShouldReturnNull()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+
+ var result = groupedCollection.FirstOrDefault("I do not exist");
+
+ result.Should().BeNull();
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void ElementAt_WhenGroupExistsAndIndexInRange_ShouldReturnFirstGroupValue()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+ groupedCollection.AddGroup("B", 10, 11, 12);
+ groupedCollection.AddGroup("B", 42);
+
+ var result = groupedCollection.ElementAt("B", 2);
+
+ result.Should().Be(12);
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(3)]
+ public void ElementAt_WhenGroupExistsAndIndexOutOfRange_ShouldReturnThrow(int index)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+ groupedCollection.AddGroup("B", 10, 11, 12);
+ groupedCollection.AddGroup("B", 42);
+
+ Action action = () => groupedCollection.ElementAt("B", index);
+
+ action.Should().Throw();
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void ElementAt_WhenGroupDoesNotExist_ShouldThrow()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+
+ Action action = () => groupedCollection.ElementAt("I do not exist", 0);
+
+ action.Should().Throw();
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void ElementAtOrDefault_WhenGroupExistsAndIndexInRange_ShouldReturnValue()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+ groupedCollection.AddGroup("B", 10, 11, 12);
+ groupedCollection.AddGroup("B", 42);
+
+ var result = groupedCollection.ElementAt("B", 2);
+
+ result.Should().Be(12);
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(3)]
+ public void ElementAtOrDefault_WhenGroupExistsAndIndexOutOfRange_ShouldReturnDefaultValue(int index)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+ groupedCollection.AddGroup("B", 10, 11, 12);
+ groupedCollection.AddGroup("B", 42);
+
+ var result = groupedCollection.ElementAtOrDefault("B", index);
+
+ result.Should().Be(0);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void ElementAtOrDefault_WhenGroupDoesNotExist_ShouldReturnDefaultValue()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 23);
+
+ var result = groupedCollection.ElementAtOrDefault("I do not exist", 0);
+
+ result.Should().Be(0);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void AddGroup_WithItem_ShouldAddGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+
+ var addedGroup = groupedCollection.AddGroup("new key", 23);
+
+ addedGroup.Should().NotBeNull();
+ addedGroup.Key.Should().Be("new key");
+ addedGroup.Should().ContainSingle();
+ addedGroup.Should().ContainInOrder(23);
+
+ groupedCollection.Should().ContainSingle();
+ groupedCollection.Should().HaveElementAt(0, addedGroup);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void AddGroup_WithCollection_ShouldAddGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+
+ var addedGroup = groupedCollection.AddGroup("new key", new[] { 23, 10, 42 });
+
+ addedGroup.Should().NotBeNull();
+ addedGroup.Key.Should().Be("new key");
+ addedGroup.Should().HaveCount(3);
+ addedGroup.Should().ContainInOrder(23, 10, 42);
+
+ groupedCollection.Should().ContainSingle();
+ groupedCollection.Should().HaveElementAt(0, addedGroup);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void AddGroup_WithParamsCollection_ShouldAddGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+
+ var addedGroup = groupedCollection.AddGroup("new key", 23, 10, 42);
+
+ addedGroup.Should().NotBeNull();
+ addedGroup.Key.Should().Be("new key");
+ addedGroup.Should().HaveCount(3);
+ addedGroup.Should().ContainInOrder(23, 10, 42);
+
+ groupedCollection.Should().ContainSingle();
+ groupedCollection.Should().HaveElementAt(0, addedGroup);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void AddItem_WhenTargetGroupDoesNotExists_ShouldCreateAndAddNewGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+
+ var addedGroup = groupedCollection.AddItem("new key", 23);
+
+ addedGroup.Should().NotBeNull();
+ addedGroup.Key.Should().Be("new key");
+ addedGroup.Should().ContainSingle();
+ addedGroup.Should().ContainInOrder(23);
+
+ groupedCollection.Should().ContainSingle();
+ groupedCollection.Should().HaveElementAt(0, addedGroup);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void AddItem_WhenSingleTargetGroupAlreadyExists_ShouldAddItemToExistingGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ var targetGroup = groupedCollection.AddGroup("B", 4, 5, 6);
+ groupedCollection.AddGroup("C", 7, 8);
+
+ var addedGroup = groupedCollection.AddItem("B", 23);
+
+ addedGroup.Should().BeSameAs(targetGroup);
+ addedGroup.Key.Should().Be("B");
+ addedGroup.Should().HaveCount(4);
+ addedGroup.Should().ContainInOrder(4, 5, 6, 23);
+
+ groupedCollection.Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(4);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(4, 5, 6, 23);
+
+ groupedCollection.ElementAt(2).Key.Should().Be("C");
+ groupedCollection.ElementAt(2).Should().HaveCount(2);
+ groupedCollection.ElementAt(2).Should().ContainInOrder(7, 8);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void AddItem_WhenSeveralTargetGroupsAlreadyExist_ShouldAddItemToFirstExistingGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ var targetGroup = groupedCollection.AddGroup("B", 4, 5, 6);
+ groupedCollection.AddGroup("B", 7, 8, 9);
+ groupedCollection.AddGroup("C", 10, 11);
+
+ var addedGroup = groupedCollection.AddItem("B", 23);
+
+ addedGroup.Should().BeSameAs(targetGroup);
+ addedGroup.Key.Should().Be("B");
+ addedGroup.Should().HaveCount(4);
+ addedGroup.Should().ContainInOrder(4, 5, 6, 23);
+
+ groupedCollection.Should().HaveCount(4);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(4);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(4, 5, 6, 23);
+
+ groupedCollection.ElementAt(2).Key.Should().Be("B");
+ groupedCollection.ElementAt(2).Should().HaveCount(3);
+ groupedCollection.ElementAt(2).Should().ContainInOrder(7, 8, 9);
+
+ groupedCollection.ElementAt(3).Key.Should().Be("C");
+ groupedCollection.ElementAt(3).Should().HaveCount(2);
+ groupedCollection.ElementAt(3).Should().ContainInOrder(10, 11);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void InsertItem_WhenGroupDoesNotExist_ShoudThrow()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+
+ Action action = () => groupedCollection.InsertItem("I do not exist", 0, 23);
+
+ action.Should().Throw();
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(4)]
+ public void InsertItem_WhenIndexOutOfRange_ShoudThrow(int index)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+
+ Action action = () => groupedCollection.InsertItem("A", index, 23);
+
+ action.Should().Throw();
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(0, new[] { 23, 1, 2, 3 })]
+ [DataRow(1, new[] { 1, 23, 2, 3 })]
+ [DataRow(3, new[] { 1, 2, 3, 23 })]
+ public void InsertItem_WithValidIndex_WithSeveralGroups_ShoudInsertItemInFirstGroup(int index, int[] expecteGroupValues)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 4, 5);
+ var targetGroup = groupedCollection.AddGroup("B", 1, 2, 3);
+ groupedCollection.AddGroup("B", 6, 7);
+
+ var group = groupedCollection.InsertItem("B", index, 23);
+
+ group.Should().BeSameAs(targetGroup);
+
+ groupedCollection.Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(2);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(4, 5);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(4);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(expecteGroupValues);
+
+ groupedCollection.ElementAt(2).Key.Should().Be("B");
+ groupedCollection.ElementAt(2).Should().HaveCount(2);
+ groupedCollection.ElementAt(2).Should().ContainInOrder(6, 7);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void SetItem_WhenGroupDoesNotExist_ShoudThrow()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+
+ Action action = () => groupedCollection.SetItem("I do not exist", 0, 23);
+
+ action.Should().Throw();
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(-1)]
+ [DataRow(3)]
+ public void SetItem_WhenIndexOutOfRange_ShoudThrow(int index)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+
+ Action action = () => groupedCollection.SetItem("A", index, 23);
+
+ action.Should().Throw();
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(0, new[] { 23, 2, 3 })]
+ [DataRow(1, new[] { 1, 23, 3 })]
+ [DataRow(2, new[] { 1, 2, 23 })]
+ public void SetItem_WithValidIndex_WithSeveralGroups_ShoudReplaceItemInFirstGroup(int index, int[] expecteGroupValues)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 4, 5);
+ var targetGroup = groupedCollection.AddGroup("B", 1, 2, 3);
+ groupedCollection.AddGroup("B", 6, 7);
+
+ var group = groupedCollection.SetItem("B", index, 23);
+
+ group.Should().BeSameAs(targetGroup);
+
+ groupedCollection.Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(2);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(4, 5);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(3);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(expecteGroupValues);
+
+ groupedCollection.ElementAt(2).Key.Should().Be("B");
+ groupedCollection.ElementAt(2).Should().HaveCount(2);
+ groupedCollection.ElementAt(2).Should().ContainInOrder(6, 7);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void RemoveGroup_WhenGroupDoesNotExists_ShouldDoNothing()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+
+ groupedCollection.RemoveGroup("I do not exist");
+
+ groupedCollection.Should().ContainSingle();
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void RemoveGroup_WhenSingleGroupExists_ShouldRemoveGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ groupedCollection.AddGroup("B", 4, 5, 6);
+
+ groupedCollection.RemoveGroup("B");
+
+ groupedCollection.Should().ContainSingle();
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+ }
+
+ [TestCategory("Collections")]
+ [TestMethod]
+ public void RemoveGroup_WhenSeveralGroupsExist_ShouldRemoveFirstGroup()
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ groupedCollection.AddGroup("B", 4, 5, 6);
+ groupedCollection.AddGroup("B", 7, 8);
+
+ groupedCollection.RemoveGroup("B");
+
+ groupedCollection.Should().HaveCount(2);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(2);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(7, 8);
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public void RemoveItem_WhenGroupDoesNotExist_ShouldDoNothing(bool removeGroupIfEmpty)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ groupedCollection.AddGroup("B", 4, 5, 6);
+
+ groupedCollection.RemoveItem("I do not exist", 8, removeGroupIfEmpty);
+
+ groupedCollection.Should().HaveCount(2);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(3);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(4, 5, 6);
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public void RemoveItem_WhenGroupExistsAndItemDoesNotExist_ShouldDoNothing(bool removeGroupIfEmpty)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ groupedCollection.AddGroup("B", 4, 5, 6);
+
+ groupedCollection.RemoveItem("B", 8, removeGroupIfEmpty);
+
+ groupedCollection.Should().HaveCount(2);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(3);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(4, 5, 6);
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(true)]
+ [DataRow(false)]
+ public void RemoveItem_WhenGroupAndItemExist_ShouldRemoveItemFromGroup(bool removeGroupIfEmpty)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ groupedCollection.AddGroup("B", 4, 5, 6);
+
+ groupedCollection.RemoveItem("B", 5, removeGroupIfEmpty);
+
+ groupedCollection.Should().HaveCount(2);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().HaveCount(2);
+ groupedCollection.ElementAt(1).Should().ContainInOrder(4, 6);
+ }
+
+ [TestCategory("Collections")]
+ [DataTestMethod]
+ [DataRow(true, true)]
+ [DataRow(false, false)]
+ public void RemoveItem_WhenRemovingLastItem_ShouldRemoveGroupIfRequired(bool removeGroupIfEmpty, bool expectGroupRemoved)
+ {
+ var groupedCollection = new ObservableGroupedCollection();
+ groupedCollection.AddGroup("A", 1, 2, 3);
+ groupedCollection.AddGroup("B", 4);
+
+ groupedCollection.RemoveItem("B", 4, removeGroupIfEmpty);
+
+ groupedCollection.Should().HaveCount(expectGroupRemoved ? 1 : 2);
+ groupedCollection.ElementAt(0).Key.Should().Be("A");
+ groupedCollection.ElementAt(0).Should().HaveCount(3);
+ groupedCollection.ElementAt(0).Should().ContainInOrder(1, 2, 3);
+
+ if (!expectGroupRemoved)
+ {
+ groupedCollection.ElementAt(1).Key.Should().Be("B");
+ groupedCollection.ElementAt(1).Should().BeEmpty();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj
index ce3edb2b442..de1d3dd3f60 100644
--- a/UnitTests/UnitTests.csproj
+++ b/UnitTests/UnitTests.csproj
@@ -123,6 +123,7 @@
+