Skip to content

Commit 2d58242

Browse files
committed
Improvements to ReadOnlyObservableGroupedCollection logic
1 parent 6d2a502 commit 2d58242

File tree

1 file changed

+66
-29
lines changed

1 file changed

+66
-29
lines changed

CommunityToolkit.Mvvm/Collections/ReadOnlyObservableGroupedCollection{TKey,TValue}.cs

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using System;
66
using System.Collections.ObjectModel;
77
using System.Collections.Specialized;
8-
using System.Diagnostics;
8+
using System.Diagnostics.CodeAnalysis;
99
using System.Linq;
1010

1111
namespace CommunityToolkit.Mvvm.Collections;
@@ -37,56 +37,93 @@ public ReadOnlyObservableGroupedCollection(ObservableCollection<ReadOnlyObservab
3737
{
3838
}
3939

40+
/// <summary>
41+
/// Forwards the <see cref="INotifyCollectionChanged.CollectionChanged"/> event whenever it is raised by the wrapped collection.
42+
/// </summary>
43+
/// <param name="sender">The wrapped collection (an <see cref="ObservableCollection{T}"/> of <see cref="ReadOnlyObservableGroup{TKey, TValue}"/> instance).</param>
44+
/// <param name="e">The <see cref="NotifyCollectionChangedEventArgs"/> arguments.</param>
45+
/// <exception cref="NotSupportedException">Thrown if a range operation is requested.</exception>
4046
private void OnSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
4147
{
4248
// Even if NotifyCollectionChangedEventArgs allows multiple items, the actual implementation
43-
// is only reporting the changes one by one. We consider only this case for now.
44-
if (e.OldItems?.Count > 1 || e.NewItems?.Count > 1)
49+
// is only reporting the changes one by one. We consider only this case for now. If this is
50+
// added in a new version of .NET, this type will need to be updated accordingly in a new version.
51+
[DoesNotReturn]
52+
static void ThrowNotSupportedExceptionForRangeOperation()
4553
{
46-
static void ThrowNotSupportedException()
47-
{
48-
throw new NotSupportedException(
49-
"ReadOnlyObservableGroupedCollection<TKey, TValue> doesn't support operations on multiple items at once.\n" +
50-
"If this exception was thrown, it likely means support for batched item updates has been added to the " +
51-
"underlying ObservableCollection<T> type, and this implementation doesn't support that feature yet.\n" +
52-
"Please consider opening an issue in https://aka.ms/windowstoolkit to report this.");
53-
}
54-
55-
ThrowNotSupportedException();
54+
throw new NotSupportedException(
55+
"ReadOnlyObservableGroupedCollection<TKey, TValue> doesn't support operations on multiple items at once.\n" +
56+
"If this exception was thrown, it likely means support for batched item updates has been added to the " +
57+
"underlying ObservableCollection<T> type, and this implementation doesn't support that feature yet.\n" +
58+
"Please consider opening an issue in https://aka.ms/toolkit/dotnet to report this.");
5659
}
5760

61+
// The inner Items list is ObservableCollection<ReadOnlyObservableGroup<TKey, TValue>>, so doing a direct cast here will always succeed
62+
ObservableCollection<ReadOnlyObservableGroup<TKey, TValue>> items = (ObservableCollection<ReadOnlyObservableGroup<TKey, TValue>>)Items;
63+
5864
switch (e.Action)
5965
{
60-
case NotifyCollectionChangedAction.Add or NotifyCollectionChangedAction.Replace:
66+
// Insert a single item for an "Add" operation, fail if multiple items are added
67+
case NotifyCollectionChangedAction.Add:
68+
if (e.NewItems!.Count == 1)
69+
{
70+
ObservableGroup<TKey, TValue> newItem = (ObservableGroup<TKey, TValue>)e.NewItems![0]!;
6171

62-
// We only need to find the new item if the operation is either add or remove. In this
63-
// case we just directly find the first item that was modified, or throw if it's not present.
64-
// This normally never happens anyway - add and replace should always have a target element.
65-
ObservableGroup<TKey, TValue> newItem = e.NewItems!.Cast<ObservableGroup<TKey, TValue>>().First();
72+
items.Insert(e.NewStartingIndex, new ReadOnlyObservableGroup<TKey, TValue>(newItem));
73+
}
74+
else if (e.NewItems!.Count > 1)
75+
{
76+
ThrowNotSupportedExceptionForRangeOperation();
77+
}
6678

67-
if (e.Action == NotifyCollectionChangedAction.Add)
79+
break;
80+
81+
// Remove a single item at offset for a "Remove" operation, fail if multiple items are removed
82+
case NotifyCollectionChangedAction.Remove:
83+
if (e.OldItems!.Count == 1)
6884
{
69-
Items.Insert(e.NewStartingIndex, new ReadOnlyObservableGroup<TKey, TValue>(newItem));
85+
items.RemoveAt(e.OldStartingIndex);
7086
}
71-
else
87+
else if (e.OldItems!.Count > 1)
7288
{
73-
Items[e.OldStartingIndex] = new ReadOnlyObservableGroup<TKey, TValue>(newItem);
89+
ThrowNotSupportedExceptionForRangeOperation();
7490
}
7591

7692
break;
77-
case NotifyCollectionChangedAction.Move:
7893

79-
// Our inner Items list is our own ObservableCollection<ReadOnlyObservableGroup<TKey, TValue>> so we can safely cast Items to its concrete type here.
80-
((ObservableCollection<ReadOnlyObservableGroup<TKey, TValue>>)Items).Move(e.OldStartingIndex, e.NewStartingIndex);
94+
// Replace a single item at offset for a "Replace" operation, fail if multiple items are replaced
95+
case NotifyCollectionChangedAction.Replace:
96+
if (e.OldItems!.Count == 1 && e.NewItems!.Count == 1)
97+
{
98+
ObservableGroup<TKey, TValue> replacedItem = (ObservableGroup<TKey, TValue>)e.NewItems![0]!;
99+
100+
items[e.OldStartingIndex] = new ReadOnlyObservableGroup<TKey, TValue>(replacedItem);
101+
}
102+
else if (e.OldItems!.Count > 1 || e.NewItems!.Count > 1)
103+
{
104+
ThrowNotSupportedExceptionForRangeOperation();
105+
}
106+
81107
break;
82-
case NotifyCollectionChangedAction.Remove:
83-
Items.RemoveAt(e.OldStartingIndex);
108+
109+
// Move a single item between offsets for a "Move" operation, fail if multiple items are moved
110+
case NotifyCollectionChangedAction.Move:
111+
if (e.OldItems!.Count == 1 && e.NewItems!.Count == 1)
112+
{
113+
items.Move(e.OldStartingIndex, e.NewStartingIndex);
114+
}
115+
else if (e.OldItems!.Count > 1 || e.NewItems!.Count > 1)
116+
{
117+
ThrowNotSupportedExceptionForRangeOperation();
118+
}
119+
84120
break;
121+
122+
// A "Reset" operation is just forwarded normally
85123
case NotifyCollectionChangedAction.Reset:
86-
Items.Clear();
124+
items.Clear();
87125
break;
88126
default:
89-
Debug.Fail("unsupported value");
90127
break;
91128
}
92129
}

0 commit comments

Comments
 (0)