Skip to content

Commit f3bd3d8

Browse files
authored
Merge pull request #169 from CommunityToolkit/dev/improve-collections
Revamp observable collection APIs
2 parents e4a105f + b897947 commit f3bd3d8

21 files changed

+1879
-1221
lines changed

CommunityToolkit.Mvvm/Collections/IReadOnlyObservableGroup.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections;
6+
using System.Collections.Specialized;
57
using System.ComponentModel;
68

79
namespace CommunityToolkit.Mvvm.Collections;
810

911
/// <summary>
1012
/// An interface for a grouped collection of items.
1113
/// </summary>
12-
public interface IReadOnlyObservableGroup : INotifyPropertyChanged
14+
public interface IReadOnlyObservableGroup : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable
1315
{
1416
/// <summary>
1517
/// Gets the key for the current collection.
@@ -20,4 +22,12 @@ public interface IReadOnlyObservableGroup : INotifyPropertyChanged
2022
/// Gets the number of items currently in the grouped collection.
2123
/// </summary>
2224
int Count { get; }
25+
26+
/// <summary>
27+
/// Gets the element at the specified index in the current collection.
28+
/// </summary>
29+
/// <param name="index">The zero-based index of the element to get.</param>
30+
/// <returns>The element at the specified index in the read-only list.</returns>
31+
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if the index is out of range.</exception>
32+
object? this[int index] { get; }
2333
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
8+
namespace CommunityToolkit.Mvvm.Collections;
9+
10+
/// <summary>
11+
/// An interface for a grouped collection of items.
12+
/// </summary>
13+
/// <typeparam name="TKey">The type of the group key.</typeparam>
14+
/// <typeparam name="TElement">The type of elements in the group.</typeparam>
15+
public interface IReadOnlyObservableGroup<out TKey, out TElement> : IReadOnlyObservableGroup<TKey>, IReadOnlyList<TElement>, IGrouping<TKey, TElement>
16+
where TKey : notnull
17+
{
18+
/// <summary>
19+
/// Gets the element at the specified index in the current collection.
20+
/// </summary>
21+
/// <param name="index">The zero-based index of the element to get.</param>
22+
/// <returns>The element at the specified index in the read-only list.</returns>
23+
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if the index is out of range.</exception>
24+
new TElement this[int index] { get; }
25+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace CommunityToolkit.Mvvm.Collections;
6+
7+
/// <summary>
8+
/// An interface for a grouped collection of items.
9+
/// </summary>
10+
/// <typeparam name="TKey">The type of the group key.</typeparam>
11+
public interface IReadOnlyObservableGroup<out TKey> : IReadOnlyObservableGroup
12+
where TKey : notnull
13+
{
14+
/// <summary>
15+
/// Gets the key for the current collection.
16+
/// </summary>
17+
new TKey Key { get; }
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.ComponentModel;
6+
7+
namespace CommunityToolkit.Mvvm.Collections.Internals;
8+
9+
/// <summary>
10+
/// A helper type for the <see cref="ObservableGroup{TKey, TValue}"/> type.
11+
/// </summary>
12+
internal static class ObservableGroupHelper
13+
{
14+
/// <summary>
15+
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IReadOnlyObservableGroup.Key"/>
16+
/// </summary>
17+
public static readonly PropertyChangedEventArgs KeyChangedEventArgs = new(nameof(IReadOnlyObservableGroup.Key));
18+
}

CommunityToolkit.Mvvm/Collections/ObservableGroupedCollectionExtensions.cs

Lines changed: 587 additions & 204 deletions
Large diffs are not rendered by default.

CommunityToolkit.Mvvm/Collections/ObservableGroupedCollection{TKey,TValue}.cs renamed to CommunityToolkit.Mvvm/Collections/ObservableGroupedCollection{TKey,TElement}.cs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ namespace CommunityToolkit.Mvvm.Collections;
1313
/// <summary>
1414
/// An observable list of observable groups.
1515
/// </summary>
16-
/// <typeparam name="TKey">The type of the group key.</typeparam>
17-
/// <typeparam name="TValue">The type of the items in the collection.</typeparam>
18-
public sealed class ObservableGroupedCollection<TKey, TValue> : ObservableCollection<ObservableGroup<TKey, TValue>>
16+
/// <typeparam name="TKey">The type of the group keys.</typeparam>
17+
/// <typeparam name="TElement">The type of elements in the collection.</typeparam>
18+
public sealed class ObservableGroupedCollection<TKey, TElement> : ObservableCollection<ObservableGroup<TKey, TElement>>, ILookup<TKey, TElement>
1919
where TKey : notnull
2020
{
2121
/// <summary>
@@ -29,21 +29,50 @@ public ObservableGroupedCollection()
2929
/// Initializes a new instance of the <see cref="ObservableGroupedCollection{TKey, TValue}"/> class.
3030
/// </summary>
3131
/// <param name="collection">The initial data to add in the grouped collection.</param>
32-
public ObservableGroupedCollection(IEnumerable<IGrouping<TKey, TValue>> collection)
33-
: base(collection.Select(static c => new ObservableGroup<TKey, TValue>(c)))
32+
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="collection"/> is <see langword="null"/>.</exception>
33+
public ObservableGroupedCollection(IEnumerable<IGrouping<TKey, TElement>> collection)
34+
: base(collection?.Select(static group => new ObservableGroup<TKey, TElement>(group))!)
3435
{
3536
}
3637

38+
/// <inheritdoc/>
39+
IEnumerable<TElement> ILookup<TKey, TElement>.this[TKey key]
40+
{
41+
get
42+
{
43+
IEnumerable<TElement>? result = null;
44+
45+
if (key is not null)
46+
{
47+
result = this.FirstGroupByKeyOrDefault(key);
48+
}
49+
50+
return result ?? Enumerable.Empty<TElement>();
51+
}
52+
}
53+
3754
/// <summary>
3855
/// Tries to get the underlying <see cref="List{T}"/> instance, if present.
3956
/// </summary>
4057
/// <param name="list">The resulting <see cref="List{T}"/>, if one was in use.</param>
4158
/// <returns>Whether or not a <see cref="List{T}"/> instance has been found.</returns>
4259
[MethodImpl(MethodImplOptions.AggressiveInlining)]
43-
internal bool TryGetList([NotNullWhen(true)] out List<ObservableGroup<TKey, TValue>>? list)
60+
internal bool TryGetList([NotNullWhen(true)] out List<ObservableGroup<TKey, TElement>>? list)
4461
{
45-
list = Items as List<ObservableGroup<TKey, TValue>>;
62+
list = Items as List<ObservableGroup<TKey, TElement>>;
4663

4764
return list is not null;
4865
}
66+
67+
/// <inheritdoc/>
68+
bool ILookup<TKey, TElement>.Contains(TKey key)
69+
{
70+
return key is not null && this.FirstGroupByKey(key) is not null;
71+
}
72+
73+
/// <inheritdoc/>
74+
IEnumerator<IGrouping<TKey, TElement>> IEnumerable<IGrouping<TKey, TElement>>.GetEnumerator()
75+
{
76+
return GetEnumerator();
77+
}
4978
}

CommunityToolkit.Mvvm/Collections/ObservableGroup{TKey,TValue}.cs renamed to CommunityToolkit.Mvvm/Collections/ObservableGroup{TKey,TElement}.cs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
using System.Collections.Generic;
66
using System.Collections.ObjectModel;
7-
using System.ComponentModel;
87
using System.Diagnostics;
8+
using System.Diagnostics.CodeAnalysis;
99
using System.Linq;
10+
using System.Runtime.CompilerServices;
11+
using CommunityToolkit.Mvvm.Collections.Internals;
1012

1113
namespace CommunityToolkit.Mvvm.Collections;
1214

@@ -15,30 +17,29 @@ namespace CommunityToolkit.Mvvm.Collections;
1517
/// It associates a <see cref="Key"/> to an <see cref="ObservableCollection{T}"/>.
1618
/// </summary>
1719
/// <typeparam name="TKey">The type of the group key.</typeparam>
18-
/// <typeparam name="TValue">The type of the items in the collection.</typeparam>
20+
/// <typeparam name="TElement">The type of elements in the group.</typeparam>
1921
[DebuggerDisplay("Key = {Key}, Count = {Count}")]
20-
public class ObservableGroup<TKey, TValue> : ObservableCollection<TValue>, IGrouping<TKey, TValue>, IReadOnlyObservableGroup
22+
public sealed class ObservableGroup<TKey, TElement> : ObservableCollection<TElement>, IReadOnlyObservableGroup<TKey, TElement>
2123
where TKey : notnull
2224
{
23-
/// <summary>
24-
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="Key"/>
25-
/// </summary>
26-
private static readonly PropertyChangedEventArgs KeyChangedEventArgs = new(nameof(Key));
27-
2825
/// <summary>
2926
/// Initializes a new instance of the <see cref="ObservableGroup{TKey, TValue}"/> class.
3027
/// </summary>
3128
/// <param name="key">The key for the group.</param>
29+
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="key"/> is <see langword="null"/>.</exception>
3230
public ObservableGroup(TKey key)
3331
{
32+
ArgumentNullException.For<TKey>.ThrowIfNull(key);
33+
3434
this.key = key;
3535
}
3636

3737
/// <summary>
3838
/// Initializes a new instance of the <see cref="ObservableGroup{TKey, TValue}"/> class.
3939
/// </summary>
4040
/// <param name="grouping">The grouping to fill the group.</param>
41-
public ObservableGroup(IGrouping<TKey, TValue> grouping)
41+
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="grouping"/> is <see langword="null"/>.</exception>
42+
public ObservableGroup(IGrouping<TKey, TElement> grouping)
4243
: base(grouping)
4344
{
4445
this.key = grouping.Key;
@@ -49,9 +50,12 @@ public ObservableGroup(IGrouping<TKey, TValue> grouping)
4950
/// </summary>
5051
/// <param name="key">The key for the group.</param>
5152
/// <param name="collection">The initial collection of data to add to the group.</param>
52-
public ObservableGroup(TKey key, IEnumerable<TValue> collection)
53+
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="collection"/> are <see langword="null"/>.</exception>
54+
public ObservableGroup(TKey key, IEnumerable<TElement> collection)
5355
: base(collection)
5456
{
57+
ArgumentNullException.For<TKey>.ThrowIfNull(key);
58+
5559
this.key = key;
5660
}
5761

@@ -60,20 +64,39 @@ public ObservableGroup(TKey key, IEnumerable<TValue> collection)
6064
/// <summary>
6165
/// Gets or sets the key of the group.
6266
/// </summary>
67+
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="value"/> is <see langword="null"/>.</exception>
6368
public TKey Key
6469
{
6570
get => this.key;
6671
set
6772
{
73+
ArgumentNullException.For<TKey>.ThrowIfNull(value);
74+
6875
if (!EqualityComparer<TKey>.Default.Equals(this.key!, value))
6976
{
7077
this.key = value;
7178

72-
OnPropertyChanged(KeyChangedEventArgs);
79+
OnPropertyChanged(ObservableGroupHelper.KeyChangedEventArgs);
7380
}
7481
}
7582
}
7683

84+
/// <summary>
85+
/// Tries to get the underlying <see cref="List{T}"/> instance, if present.
86+
/// </summary>
87+
/// <param name="list">The resulting <see cref="List{T}"/>, if one was in use.</param>
88+
/// <returns>Whether or not a <see cref="List{T}"/> instance has been found.</returns>
89+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
90+
internal bool TryGetList([NotNullWhen(true)] out List<TElement>? list)
91+
{
92+
list = Items as List<TElement>;
93+
94+
return list is not null;
95+
}
96+
7797
/// <inheritdoc/>
7898
object IReadOnlyObservableGroup.Key => Key;
99+
100+
/// <inheritdoc/>
101+
object? IReadOnlyObservableGroup.this[int index] => this[index];
79102
}

0 commit comments

Comments
 (0)