Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
257250d
Add IReadOnlyObservableGroup<out TKey>
Sergio0694 Mar 12, 2022
186ff01
Remove unnecessary constructor
Sergio0694 Mar 12, 2022
684a650
Improve caching of KeyChangedEventArgs
Sergio0694 Mar 12, 2022
6d2a502
Improve ReadOnlyObservableGroupedCollection<TKey, TValue> constructors
Sergio0694 Mar 12, 2022
2d58242
Improvements to ReadOnlyObservableGroupedCollection logic
Sergio0694 Mar 12, 2022
d08e1b7
Add new interfaces/members to IReadOnlyObservableGroup
Sergio0694 Mar 19, 2022
a3be770
Add IReadOnlyObservableGroup<out TKey, out TElement>
Sergio0694 Mar 19, 2022
b522487
Rename TValue type params to TElement
Sergio0694 Mar 19, 2022
224b334
Add ILookup<,> to observable grouped collections
Sergio0694 Mar 19, 2022
fcdd356
Add [DebuggerDisplay] to ReadOnlyObservableGroup<,>
Sergio0694 Mar 19, 2022
03e85af
Fix build errors in unit tests
Sergio0694 Mar 21, 2022
2712e9b
Seal ObservableGroup<TKey, TElement> for consistency
Sergio0694 Mar 25, 2022
08fca92
Update ObservableGroupedCollectionExtensions with new shape
Sergio0694 Mar 30, 2022
f6a0f3f
Add null checks to public APIs
Sergio0694 Mar 30, 2022
bf31152
Optimize ILookup<TKey, TElement> implementations
Sergio0694 Mar 30, 2022
0e3be7e
Add tests for ArgumentNullException-s in collections
Sergio0694 Apr 5, 2022
274a3c0
Minor code refactoring
Sergio0694 Apr 5, 2022
dd2a72d
Rename unit test methods for consistency
Sergio0694 Apr 5, 2022
d1e75b9
Add ObservableGroupedCollectionExtensions tests, bug fixes
Sergio0694 Apr 5, 2022
ba20320
Add a comment explaining the collection fast paths
Sergio0694 Apr 6, 2022
48deec9
Remove TODOs
Sergio0694 Apr 6, 2022
b897947
Add an extra test case
Sergio0694 Apr 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CommunityToolkit.Mvvm/Collections/IReadOnlyObservableGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
// 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.Collections;
using System.Collections.Specialized;
using System.ComponentModel;

namespace CommunityToolkit.Mvvm.Collections;

/// <summary>
/// An interface for a grouped collection of items.
/// </summary>
public interface IReadOnlyObservableGroup : INotifyPropertyChanged
public interface IReadOnlyObservableGroup : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable
{
/// <summary>
/// Gets the key for the current collection.
Expand All @@ -20,4 +22,12 @@ public interface IReadOnlyObservableGroup : INotifyPropertyChanged
/// Gets the number of items currently in the grouped collection.
/// </summary>
int Count { get; }

/// <summary>
/// Gets the element at the specified index in the current collection.
/// </summary>
/// <param name="index">The zero-based index of the element to get.</param>
/// <returns>The element at the specified index in the read-only list.</returns>
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if the index is out of range.</exception>
object? this[int index] { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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.Collections.Generic;
using System.Linq;

namespace CommunityToolkit.Mvvm.Collections;

/// <summary>
/// An interface for a grouped collection of items.
/// </summary>
/// <typeparam name="TKey">The type of the group key.</typeparam>
/// <typeparam name="TElement">The type of elements in the group.</typeparam>
public interface IReadOnlyObservableGroup<out TKey, out TElement> : IReadOnlyObservableGroup<TKey>, IReadOnlyList<TElement>, IGrouping<TKey, TElement>
where TKey : notnull
{
/// <summary>
/// Gets the element at the specified index in the current collection.
/// </summary>
/// <param name="index">The zero-based index of the element to get.</param>
/// <returns>The element at the specified index in the read-only list.</returns>
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if the index is out of range.</exception>
new TElement this[int index] { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.

namespace CommunityToolkit.Mvvm.Collections;

/// <summary>
/// An interface for a grouped collection of items.
/// </summary>
/// <typeparam name="TKey">The type of the group key.</typeparam>
public interface IReadOnlyObservableGroup<out TKey> : IReadOnlyObservableGroup
where TKey : notnull
{
/// <summary>
/// Gets the key for the current collection.
/// </summary>
new TKey Key { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.ComponentModel;

namespace CommunityToolkit.Mvvm.Collections.Internals;

/// <summary>
/// A helper type for the <see cref="ObservableGroup{TKey, TValue}"/> type.
/// </summary>
internal static class ObservableGroupHelper
{
/// <summary>
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IReadOnlyObservableGroup.Key"/>
/// </summary>
public static readonly PropertyChangedEventArgs KeyChangedEventArgs = new(nameof(IReadOnlyObservableGroup.Key));
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ namespace CommunityToolkit.Mvvm.Collections;
/// <summary>
/// An observable list of observable groups.
/// </summary>
/// <typeparam name="TKey">The type of the group key.</typeparam>
/// <typeparam name="TValue">The type of the items in the collection.</typeparam>
public sealed class ObservableGroupedCollection<TKey, TValue> : ObservableCollection<ObservableGroup<TKey, TValue>>
/// <typeparam name="TKey">The type of the group keys.</typeparam>
/// <typeparam name="TElement">The type of elements in the collection.</typeparam>
public sealed class ObservableGroupedCollection<TKey, TElement> : ObservableCollection<ObservableGroup<TKey, TElement>>, ILookup<TKey, TElement>
where TKey : notnull
{
/// <summary>
Expand All @@ -29,21 +29,50 @@ public ObservableGroupedCollection()
/// Initializes a new instance of the <see cref="ObservableGroupedCollection{TKey, TValue}"/> class.
/// </summary>
/// <param name="collection">The initial data to add in the grouped collection.</param>
public ObservableGroupedCollection(IEnumerable<IGrouping<TKey, TValue>> collection)
: base(collection.Select(static c => new ObservableGroup<TKey, TValue>(c)))
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="collection"/> is <see langword="null"/>.</exception>
public ObservableGroupedCollection(IEnumerable<IGrouping<TKey, TElement>> collection)
: base(collection?.Select(static group => new ObservableGroup<TKey, TElement>(group))!)
{
}

/// <inheritdoc/>
IEnumerable<TElement> ILookup<TKey, TElement>.this[TKey key]
{
get
{
IEnumerable<TElement>? result = null;

if (key is not null)
{
result = this.FirstGroupByKeyOrDefault(key);
}

return result ?? Enumerable.Empty<TElement>();
}
}

/// <summary>
/// Tries to get the underlying <see cref="List{T}"/> instance, if present.
/// </summary>
/// <param name="list">The resulting <see cref="List{T}"/>, if one was in use.</param>
/// <returns>Whether or not a <see cref="List{T}"/> instance has been found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetList([NotNullWhen(true)] out List<ObservableGroup<TKey, TValue>>? list)
internal bool TryGetList([NotNullWhen(true)] out List<ObservableGroup<TKey, TElement>>? list)
{
list = Items as List<ObservableGroup<TKey, TValue>>;
list = Items as List<ObservableGroup<TKey, TElement>>;

return list is not null;
}

/// <inheritdoc/>
bool ILookup<TKey, TElement>.Contains(TKey key)
{
return key is not null && this.FirstGroupByKey(key) is not null;
}

/// <inheritdoc/>
IEnumerator<IGrouping<TKey, TElement>> IEnumerable<IGrouping<TKey, TElement>>.GetEnumerator()
{
return GetEnumerator();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.Collections.Internals;

namespace CommunityToolkit.Mvvm.Collections;

Expand All @@ -15,30 +17,29 @@ namespace CommunityToolkit.Mvvm.Collections;
/// It associates a <see cref="Key"/> to an <see cref="ObservableCollection{T}"/>.
/// </summary>
/// <typeparam name="TKey">The type of the group key.</typeparam>
/// <typeparam name="TValue">The type of the items in the collection.</typeparam>
/// <typeparam name="TElement">The type of elements in the group.</typeparam>
[DebuggerDisplay("Key = {Key}, Count = {Count}")]
public class ObservableGroup<TKey, TValue> : ObservableCollection<TValue>, IGrouping<TKey, TValue>, IReadOnlyObservableGroup
public sealed class ObservableGroup<TKey, TElement> : ObservableCollection<TElement>, IReadOnlyObservableGroup<TKey, TElement>
where TKey : notnull
{
/// <summary>
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="Key"/>
/// </summary>
private static readonly PropertyChangedEventArgs KeyChangedEventArgs = new(nameof(Key));

/// <summary>
/// Initializes a new instance of the <see cref="ObservableGroup{TKey, TValue}"/> class.
/// </summary>
/// <param name="key">The key for the group.</param>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="key"/> is <see langword="null"/>.</exception>
public ObservableGroup(TKey key)
{
ArgumentNullException.For<TKey>.ThrowIfNull(key);

this.key = key;
}

/// <summary>
/// Initializes a new instance of the <see cref="ObservableGroup{TKey, TValue}"/> class.
/// </summary>
/// <param name="grouping">The grouping to fill the group.</param>
public ObservableGroup(IGrouping<TKey, TValue> grouping)
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="grouping"/> is <see langword="null"/>.</exception>
public ObservableGroup(IGrouping<TKey, TElement> grouping)
: base(grouping)
{
this.key = grouping.Key;
Expand All @@ -49,9 +50,12 @@ public ObservableGroup(IGrouping<TKey, TValue> grouping)
/// </summary>
/// <param name="key">The key for the group.</param>
/// <param name="collection">The initial collection of data to add to the group.</param>
public ObservableGroup(TKey key, IEnumerable<TValue> collection)
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="key"/> or <paramref name="collection"/> are <see langword="null"/>.</exception>
public ObservableGroup(TKey key, IEnumerable<TElement> collection)
: base(collection)
{
ArgumentNullException.For<TKey>.ThrowIfNull(key);

this.key = key;
}

Expand All @@ -60,20 +64,39 @@ public ObservableGroup(TKey key, IEnumerable<TValue> collection)
/// <summary>
/// Gets or sets the key of the group.
/// </summary>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="value"/> is <see langword="null"/>.</exception>
public TKey Key
{
get => this.key;
set
{
ArgumentNullException.For<TKey>.ThrowIfNull(value);

if (!EqualityComparer<TKey>.Default.Equals(this.key!, value))
{
this.key = value;

OnPropertyChanged(KeyChangedEventArgs);
OnPropertyChanged(ObservableGroupHelper.KeyChangedEventArgs);
}
}
}

/// <summary>
/// Tries to get the underlying <see cref="List{T}"/> instance, if present.
/// </summary>
/// <param name="list">The resulting <see cref="List{T}"/>, if one was in use.</param>
/// <returns>Whether or not a <see cref="List{T}"/> instance has been found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetList([NotNullWhen(true)] out List<TElement>? list)
{
list = Items as List<TElement>;

return list is not null;
}

/// <inheritdoc/>
object IReadOnlyObservableGroup.Key => Key;

/// <inheritdoc/>
object? IReadOnlyObservableGroup.this[int index] => this[index];
}
Loading