From d0dc25b35755624789e72f50cf6453019da9671e Mon Sep 17 00:00:00 2001 From: JakenVeina Date: Fri, 7 Jun 2024 21:51:34 -0500 Subject: [PATCH 1/2] Renamed changeset factory methods to eliminate the possibility for ambiguous method resolution. --- .../Distinct/DistinctChangeSet.cs | 60 ++++++------ src/DynamicDataVNext/Keyed/KeyedChangeSet.cs | 92 +++++++++---------- .../Sorted/SortedChangeSet.cs | 90 +++++++++--------- 3 files changed, 121 insertions(+), 121 deletions(-) diff --git a/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs b/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs index 2350e31..4b4d763 100644 --- a/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs +++ b/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs @@ -30,7 +30,7 @@ public static DistinctChangeSet Addition(T item) /// The type of the items being added. /// The items being added. /// A describing the addition of the given items. - public static DistinctChangeSet Addition(IEnumerable items) + public static DistinctChangeSet BulkAddition(IEnumerable items) { if (!items.TryGetNonEnumeratedCount(out var itemsCount)) itemsCount = 0; @@ -47,8 +47,8 @@ public static DistinctChangeSet Addition(IEnumerable items) }; } - /// - public static DistinctChangeSet Addition(ReadOnlySpan items) + /// + public static DistinctChangeSet BulkAddition(ReadOnlySpan items) { var changes = ImmutableArray.CreateBuilder>(initialCapacity: items.Length); @@ -63,12 +63,12 @@ public static DistinctChangeSet Addition(ReadOnlySpan items) } /// - /// Creates a new representing the clearing of a collection of distinct items. + /// Creates a new representing the removal of a range of items. /// /// The type of the items being removed. /// The items being removed. - /// A describing the clearing of the collection. - public static DistinctChangeSet Clear(IEnumerable items) + /// A describing the removal of the given items. + public static DistinctChangeSet BulkRemoval(IEnumerable items) { if (!items.TryGetNonEnumeratedCount(out var itemsCount)) itemsCount = 0; @@ -81,12 +81,12 @@ public static DistinctChangeSet Clear(IEnumerable items) return new() { Changes = changes.MoveToOrCreateImmutable(), - Type = ChangeSetType.Clear + Type = ChangeSetType.Update }; } - /// - public static DistinctChangeSet Clear(ReadOnlySpan items) + /// + public static DistinctChangeSet BulkRemoval(ReadOnlySpan items) { var changes = ImmutableArray.CreateBuilder>(initialCapacity: items.Length); @@ -96,30 +96,17 @@ public static DistinctChangeSet Clear(ReadOnlySpan items) return new() { Changes = changes.MoveToImmutable(), - Type = ChangeSetType.Clear - }; - } - - /// - /// Creates a new representing the removal of a single item. - /// - /// The type of the item being removed. - /// The item being removed. - /// A describing the removal of the given item. - public static DistinctChangeSet Removal(T item) - => new() - { - Changes = ImmutableArray.Create(DistinctChange.Removal(item)), Type = ChangeSetType.Update }; + } /// - /// Creates a new representing the removal of a range of items. + /// Creates a new representing the clearing of a collection of distinct items. /// /// The type of the items being removed. /// The items being removed. - /// A describing the removal of the given items. - public static DistinctChangeSet Removal(IEnumerable items) + /// A describing the clearing of the collection. + public static DistinctChangeSet Clear(IEnumerable items) { if (!items.TryGetNonEnumeratedCount(out var itemsCount)) itemsCount = 0; @@ -132,12 +119,12 @@ public static DistinctChangeSet Removal(IEnumerable items) return new() { Changes = changes.MoveToOrCreateImmutable(), - Type = ChangeSetType.Update + Type = ChangeSetType.Clear }; } - /// - public static DistinctChangeSet Removal(ReadOnlySpan items) + /// + public static DistinctChangeSet Clear(ReadOnlySpan items) { var changes = ImmutableArray.CreateBuilder>(initialCapacity: items.Length); @@ -147,10 +134,23 @@ public static DistinctChangeSet Removal(ReadOnlySpan items) return new() { Changes = changes.MoveToImmutable(), - Type = ChangeSetType.Update + Type = ChangeSetType.Clear }; } + /// + /// Creates a new representing the removal of a single item. + /// + /// The type of the item being removed. + /// The item being removed. + /// A describing the removal of the given item. + public static DistinctChangeSet Removal(T item) + => new() + { + Changes = ImmutableArray.Create(DistinctChange.Removal(item)), + Type = ChangeSetType.Update + }; + /// /// Creates a new representing the resetting of items in a collection of distinct items. /// diff --git a/src/DynamicDataVNext/Keyed/KeyedChangeSet.cs b/src/DynamicDataVNext/Keyed/KeyedChangeSet.cs index cd831a7..a110044 100644 --- a/src/DynamicDataVNext/Keyed/KeyedChangeSet.cs +++ b/src/DynamicDataVNext/Keyed/KeyedChangeSet.cs @@ -36,7 +36,7 @@ public static KeyedChangeSet Addition( /// The type of the items being added. /// The items being added, and their keys. /// A describing the addition of the given items. - public static KeyedChangeSet Addition(IEnumerable> additions) + public static KeyedChangeSet BulkAddition(IEnumerable> additions) { if (!additions.TryGetNonEnumeratedCount(out var additionsCount)) additionsCount = 0; @@ -63,7 +63,7 @@ public static KeyedChangeSet Addition(IEnumerableThe items being added. /// A selector for determining the key for each item being added. /// A describing the addition of the given items. - public static KeyedChangeSet Addition( + public static KeyedChangeSet BulkAddition( IEnumerable items, Func keySelector) { @@ -84,8 +84,8 @@ public static KeyedChangeSet Addition( }; } - /// - public static KeyedChangeSet Addition( + /// + public static KeyedChangeSet BulkAddition( ReadOnlySpan items, Func keySelector) { @@ -104,14 +104,13 @@ public static KeyedChangeSet Addition( } /// - /// Creates a new representing the clearing of a keyed collection. + /// Creates a new representing the removal of a range of items. /// /// The type of the items' keys. /// The type of the items being removed. - /// The type of collection or sequence containing the items being removed. Allows JIT optimizations, depending on the type given. /// The items being removed, and their keys. - /// A describing the clearing of the collection. - public static KeyedChangeSet Clear(IEnumerable> removals) + /// A describing the removal of the given items. + public static KeyedChangeSet BulkRemoval(IEnumerable> removals) { if (!removals.TryGetNonEnumeratedCount(out var removalsCount)) removalsCount = 0; @@ -126,19 +125,19 @@ public static KeyedChangeSet Clear(IEnumerable - /// Creates a new representing the clearing of a keyed collection. + /// Creates a new representing the removal of a range of items. /// /// The type of the items' keys. /// The type of the items being removed. /// The items being removed. /// A selector for determining the key for each item being removed. - /// A describing the clearing of the collection. - public static KeyedChangeSet Clear( + /// A describing the removal of the given items. + public static KeyedChangeSet BulkRemoval( IEnumerable items, Func keySelector) { @@ -155,12 +154,12 @@ public static KeyedChangeSet Clear( return new() { Changes = changes.MoveToOrCreateImmutable(), - Type = ChangeSetType.Clear + Type = ChangeSetType.Update }; } - /// - public static KeyedChangeSet Clear( + /// + public static KeyedChangeSet BulkRemoval( ReadOnlySpan items, Func keySelector) { @@ -174,37 +173,19 @@ public static KeyedChangeSet Clear( return new() { Changes = changes.MoveToImmutable(), - Type = ChangeSetType.Clear - }; - } - - /// - /// Creates a new representing the removal of a single item. - /// - /// The type of the item's key. - /// The type of the item being removed. - /// The item's key. - /// The item being removed. - /// A describing the removal of the given item. - public static KeyedChangeSet Removal( - TKey key, - TItem item) - => new() - { - Changes = ImmutableArray.Create(KeyedChange.Removal( - key: key, - item: item)), Type = ChangeSetType.Update }; + } /// - /// Creates a new representing the removal of a range of items. + /// Creates a new representing the clearing of a keyed collection. /// /// The type of the items' keys. /// The type of the items being removed. + /// The type of collection or sequence containing the items being removed. Allows JIT optimizations, depending on the type given. /// The items being removed, and their keys. - /// A describing the removal of the given items. - public static KeyedChangeSet Removal(IEnumerable> removals) + /// A describing the clearing of the collection. + public static KeyedChangeSet Clear(IEnumerable> removals) { if (!removals.TryGetNonEnumeratedCount(out var removalsCount)) removalsCount = 0; @@ -219,19 +200,19 @@ public static KeyedChangeSet Removal(IEnumerable - /// Creates a new representing the removal of a range of items. + /// Creates a new representing the clearing of a keyed collection. /// /// The type of the items' keys. /// The type of the items being removed. /// The items being removed. /// A selector for determining the key for each item being removed. - /// A describing the removal of the given items. - public static KeyedChangeSet Removal( + /// A describing the clearing of the collection. + public static KeyedChangeSet Clear( IEnumerable items, Func keySelector) { @@ -248,12 +229,12 @@ public static KeyedChangeSet Removal( return new() { Changes = changes.MoveToOrCreateImmutable(), - Type = ChangeSetType.Update + Type = ChangeSetType.Clear }; } - /// - public static KeyedChangeSet Removal( + /// + public static KeyedChangeSet Clear( ReadOnlySpan items, Func keySelector) { @@ -267,10 +248,29 @@ public static KeyedChangeSet Removal( return new() { Changes = changes.MoveToImmutable(), - Type = ChangeSetType.Update + Type = ChangeSetType.Clear }; } + /// + /// Creates a new representing the removal of a single item. + /// + /// The type of the item's key. + /// The type of the item being removed. + /// The item's key. + /// The item being removed. + /// A describing the removal of the given item. + public static KeyedChangeSet Removal( + TKey key, + TItem item) + => new() + { + Changes = ImmutableArray.Create(KeyedChange.Removal( + key: key, + item: item)), + Type = ChangeSetType.Update + }; + /// /// Creates a new representing the replacement of a single item. /// diff --git a/src/DynamicDataVNext/Sorted/SortedChangeSet.cs b/src/DynamicDataVNext/Sorted/SortedChangeSet.cs index f2cd5a8..9797db8 100644 --- a/src/DynamicDataVNext/Sorted/SortedChangeSet.cs +++ b/src/DynamicDataVNext/Sorted/SortedChangeSet.cs @@ -69,6 +69,27 @@ public static SortedChangeSet Insertion( Type = ChangeSetType.Update }; + /// + /// Creates a new representing the movement of a single item. + /// + /// The type of item being moved. + /// The index of the item, before being moved. + /// The index of the item, after being moved.. + /// The item being moved. + /// A describing the movement of the given item. + public static SortedChangeSet Movement( + int oldIndex, + int newIndex, + T item) + => new() + { + Changes = ImmutableArray.Create(SortedChange.Movement( + oldIndex: oldIndex, + newIndex: newIndex, + item: item)), + Type = ChangeSetType.Update + }; + /// /// Creates a new representing the insertion of a range of items. /// @@ -76,7 +97,7 @@ public static SortedChangeSet Insertion( /// The index at which the items are being inserted. /// The items being inserted. /// A describing the insertion of the given items. - public static SortedChangeSet Insertion( + public static SortedChangeSet RangeInsertion( int index, IEnumerable items) { @@ -98,8 +119,8 @@ public static SortedChangeSet Insertion( }; } - /// - public static SortedChangeSet Insertion( + /// + public static SortedChangeSet RangeInsertion( int index, ReadOnlySpan items) { @@ -118,45 +139,6 @@ public static SortedChangeSet Insertion( }; } - /// - /// Creates a new representing the movement of a single item. - /// - /// The type of item being moved. - /// The index of the item, before being moved. - /// The index of the item, after being moved.. - /// The item being moved. - /// A describing the movement of the given item. - public static SortedChangeSet Movement( - int oldIndex, - int newIndex, - T item) - => new() - { - Changes = ImmutableArray.Create(SortedChange.Movement( - oldIndex: oldIndex, - newIndex: newIndex, - item: item)), - Type = ChangeSetType.Update - }; - - /// - /// Creates a new representing the removal of a single item. - /// - /// The type of item being removed. - /// The index of the item being removed. - /// The item being removed. - /// A describing the removal of the given item. - public static SortedChangeSet Removal( - int index, - T item) - => new() - { - Changes = ImmutableArray.Create(SortedChange.Removal( - index: index, - item: item)), - Type = ChangeSetType.Update - }; - /// /// Creates a new representing the removal of a range of items. /// @@ -165,7 +147,7 @@ public static SortedChangeSet Removal( /// The index at which the sequence of removed items begins. /// The items being removed. /// A describing the removal of the given items. - public static SortedChangeSet Removal( + public static SortedChangeSet RangeRemoval( int index, IReadOnlyList items) { @@ -184,8 +166,8 @@ public static SortedChangeSet Removal( }; } - /// - public static SortedChangeSet Removal( + /// + public static SortedChangeSet RangeRemoval( int index, ReadOnlySpan items) { @@ -204,6 +186,24 @@ public static SortedChangeSet Removal( }; } + /// + /// Creates a new representing the removal of a single item. + /// + /// The type of item being removed. + /// The index of the item being removed. + /// The item being removed. + /// A describing the removal of the given item. + public static SortedChangeSet Removal( + int index, + T item) + => new() + { + Changes = ImmutableArray.Create(SortedChange.Removal( + index: index, + item: item)), + Type = ChangeSetType.Update + }; + /// /// Creates a new representing the replacement of a single item. /// From 7cc0d6ff5917e4b614c23af31049eb16cf6054ed Mon Sep 17 00:00:00 2001 From: JakenVeina Date: Fri, 7 Jun 2024 23:58:55 -0500 Subject: [PATCH 2/2] Initial basic implementation for change-tracking and change-publishing collections. --- .../Distinct/ChangeTrackingSet.cs | 495 ++++++++++++++++++ src/DynamicDataVNext/Distinct/IExtendedSet.cs | 37 ++ src/DynamicDataVNext/Distinct/ISubjectSet.cs | 9 +- src/DynamicDataVNext/Distinct/SubjectSet.cs | 299 +++++++++++ .../Keyed/ChangeTrackingCache.cs | 367 +++++++++++++ .../Keyed/ChangeTrackingDictionary.cs | 400 ++++++++++++++ src/DynamicDataVNext/Keyed/ICache.cs | 122 +++++ .../Keyed/IExtendedDictionary.cs | 109 ++++ .../Keyed/IObservableCache.cs | 68 +-- .../Keyed/IObservableDictionary.cs | 5 +- src/DynamicDataVNext/Keyed/IReadOnlyCache.cs | 25 + src/DynamicDataVNext/Keyed/ISubjectCache.cs | 80 +-- .../Keyed/ISubjectDictionary.cs | 41 +- src/DynamicDataVNext/Keyed/SubjectCache.cs | 369 +++++++++++++ .../Keyed/SubjectDictionary.cs | 409 +++++++++++++++ .../Sorted/ChangeTrackingList.cs | 416 +++++++++++++++ src/DynamicDataVNext/Sorted/IExtendedList.cs | 68 +++ .../Sorted/IObservableList.cs | 3 - src/DynamicDataVNext/Sorted/ISubjectList.cs | 42 +- src/DynamicDataVNext/Sorted/SortedMovement.cs | 9 + src/DynamicDataVNext/Sorted/SubjectList.cs | 327 ++++++++++++ 21 files changed, 3465 insertions(+), 235 deletions(-) create mode 100644 src/DynamicDataVNext/Distinct/ChangeTrackingSet.cs create mode 100644 src/DynamicDataVNext/Distinct/IExtendedSet.cs create mode 100644 src/DynamicDataVNext/Distinct/SubjectSet.cs create mode 100644 src/DynamicDataVNext/Keyed/ChangeTrackingCache.cs create mode 100644 src/DynamicDataVNext/Keyed/ChangeTrackingDictionary.cs create mode 100644 src/DynamicDataVNext/Keyed/ICache.cs create mode 100644 src/DynamicDataVNext/Keyed/IExtendedDictionary.cs create mode 100644 src/DynamicDataVNext/Keyed/IReadOnlyCache.cs create mode 100644 src/DynamicDataVNext/Keyed/SubjectCache.cs create mode 100644 src/DynamicDataVNext/Keyed/SubjectDictionary.cs create mode 100644 src/DynamicDataVNext/Sorted/ChangeTrackingList.cs create mode 100644 src/DynamicDataVNext/Sorted/IExtendedList.cs create mode 100644 src/DynamicDataVNext/Sorted/SubjectList.cs diff --git a/src/DynamicDataVNext/Distinct/ChangeTrackingSet.cs b/src/DynamicDataVNext/Distinct/ChangeTrackingSet.cs new file mode 100644 index 0000000..cbd9449 --- /dev/null +++ b/src/DynamicDataVNext/Distinct/ChangeTrackingSet.cs @@ -0,0 +1,495 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace DynamicDataVNext; + +/// +/// A collection of distinct items, which tracks and allows for capturing of changes made to it, as they occur. +/// +/// The type of the items in the collection. +public sealed class ChangeTrackingSet + : IExtendedSet, + IReadOnlySet +{ + private readonly DistinctChangeSet.Builder _changeCollector; + private readonly IEqualityComparer _comparer; + + private bool _isChangeCollectionEnabled; + private bool _isDirty; + private HashSet _items; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for detecting item changes, within the collection, or if should be used. + public ChangeTrackingSet( + int? capacity = null, + IEqualityComparer? comparer = null) + { + _comparer = comparer ?? EqualityComparer.Default; + + _items = (capacity is int givenCapacity) + ? new( + capacity: givenCapacity, + comparer: _comparer) + : new( + comparer: _comparer); + + _changeCollector = new(); + _isChangeCollectionEnabled = true; + } + + /// + /// The comparer to be used for matching items against each other. + /// + public IEqualityComparer Comparer + => _items.Comparer; + + /// + public int Count + => _items.Count; + + /// + /// A flag indicating whether the collection should actually collect changes, to be retrieved by calls to . + /// Defaults to . + /// + /// + /// Note that any changes previously collected, but not captured, when this property is set to will be discarded. Otherwise, it would be possible for to generate a corrupt changes, when next called, after setting this property back to . + /// + public bool IsChangeCollectionEnabled + { + get => _isChangeCollectionEnabled; + set + { + if ((value is false) && (_changeCollector.Count is not 0)) + _changeCollector.Clear(); + + _isChangeCollectionEnabled = value; + } + } + + /// + /// A flag indicating whether changes have been made to the collection since its creation, or since the last call to `. + /// + public bool IsDirty + => _isDirty; + + /// + /// Captures any previously-collected changes made to the collection, and resets the collection to a "clean" state (I.E. sets to ). + /// + /// A containing all changes made to the collection since its construction, the last call to , or since was last changed to . + /// + /// Note that this method will always return an empty changeset, when is . + /// + public DistinctChangeSet CaptureChangesAndClean() + { + _isDirty = false; + return _changeCollector.BuildAndClear(); + } + + /// + public bool Add(T item) + { + var result = _items.Add(item); + + if (result && _isChangeCollectionEnabled) + _changeCollector.AddChange(DistinctChange.Addition(item)); + + _isDirty |= result; + + return result; + } + + /// + public void Clear() + { + if (_items.Count is 0) + return; + + if (_isChangeCollectionEnabled) + { + foreach (var item in _items) + _changeCollector.AddChange(DistinctChange.Removal(item)); + + _changeCollector.OnSourceCleared(); + } + + _items.Clear(); + + _isDirty = true; + } + + /// + public bool Contains(T item) + => _items.Contains(item); + + /// + public void CopyTo( + T[] array, + int arrayIndex) + => _items.CopyTo(array, arrayIndex); + + /// + public void EnsureCapacity(int capacity) + => _items.EnsureCapacity(capacity); + + /// + public void ExceptWith(IEnumerable other) + { + if (_items.Count is 0) + return; + + if (_isChangeCollectionEnabled) + { + if(other.TryGetNonEnumeratedCount(out var otherCount)) + { + if (otherCount is 0) + return; + + _changeCollector.EnsureCapacity(Math.Min(otherCount, _items.Count)); + } + + foreach (var item in other) + if (_items.Remove(item)) + { + _changeCollector.AddChange(DistinctChange.Removal(item)); + _isDirty = true; + } + + if (_items.Count is 0) + _changeCollector.OnSourceCleared(); + } + else + { + var oldCount = _items.Count; + + _items.ExceptWith(other); + + _isDirty |= _items.Count != oldCount; + } + } + + /// + public void ExceptWith(ReadOnlySpan other) + { + if ((other.Length is 0) || (_items.Count is 0)) + return; + + if (_isChangeCollectionEnabled) + { + _changeCollector.EnsureCapacity(Math.Min(other.Length, _items.Count)); + + foreach (var item in other) + if (_items.Remove(item)) + { + _changeCollector.AddChange(DistinctChange.Removal(item)); + _isDirty = true; + } + + if (_items.Count is 0) + _changeCollector.OnSourceCleared(); + } + else + { + foreach (var item in other) + _isDirty |= _items.Remove(item); + } + } + + /// + public void IntersectWith(IEnumerable other) + { + if (_items.Count is 0) + return; + + if (_isChangeCollectionEnabled) + { + if(other.TryGetNonEnumeratedCount(out var otherCount)) + _changeCollector.EnsureCapacity(Math.Min(0, _items.Count - otherCount)); + + var newItems = new HashSet(capacity: Math.Min(otherCount, _items.Count)); + var hasChanges = false; + foreach (var item in other) + { + if (_items.Remove(item)) + newItems.Add(item); + else + { + _changeCollector.AddChange(DistinctChange.Removal(item)); + hasChanges = true; + } + } + + if (hasChanges) + { + _items = newItems; + _isDirty = true; + + if (_items.Count is 0) + _changeCollector.OnSourceCleared(); + } + } + else + { + var oldCount = _items.Count; + + _items.IntersectWith(other); + + _isDirty = _items.Count != oldCount; + } + } + + /// + public void IntersectWith(ReadOnlySpan other) + { + if (_items.Count is 0) + return; + + var newItems = new HashSet(capacity: Math.Min(other.Length, _items.Count)); + var hasChanges = false; + + if (_isChangeCollectionEnabled) + { + _changeCollector.EnsureCapacity(Math.Min(0, _items.Count - other.Length)); + + foreach (var item in other) + { + if (_items.Remove(item)) + newItems.Add(item); + else + { + _changeCollector.AddChange(DistinctChange.Removal(item)); + hasChanges = true; + } + } + } + else + { + foreach (var item in other) + { + if (_items.Remove(item)) + newItems.Add(item); + else + hasChanges = true; + } + } + + if (hasChanges) + { + _items = newItems; + _isDirty = true; + + if (_isChangeCollectionEnabled && (_items.Count is 0)) + _changeCollector.OnSourceCleared(); + } + } + + /// + public HashSet.Enumerator GetEnumerator() + => _items.GetEnumerator(); + + /// + public bool IsProperSubsetOf(IEnumerable other) + => _items.IsProperSubsetOf(other); + + /// + public bool IsProperSupersetOf(IEnumerable other) + => _items.IsProperSupersetOf(other); + + /// + public bool IsSubsetOf(IEnumerable other) + => _items.IsSubsetOf(other); + + /// + public bool IsSupersetOf(IEnumerable other) + => _items.IsSupersetOf(other); + + /// + public bool Overlaps(IEnumerable other) + => _items.Overlaps(other); + + /// + public bool Remove(T item) + { + var result = _items.Remove(item); + + if (result && _isChangeCollectionEnabled) + _changeCollector.AddChange(DistinctChange.Removal(item)); + + _isDirty |= result; + + return result; + } + + /// + public void Reset(IEnumerable items) + { + Clear(); + UnionWith(items); + } + + /// + public void Reset(ReadOnlySpan items) + { + Clear(); + UnionWith(items); + } + + /// + public bool SetEquals(IEnumerable other) + => _items.SetEquals(other); + + /// + public void SymmetricExceptWith(IEnumerable other) + { + if (_isChangeCollectionEnabled) + { + if(other.TryGetNonEnumeratedCount(out var otherCount)) + { + if (otherCount is 0) + return; + + _items.EnsureCapacity(_items.Count + otherCount); + _changeCollector.EnsureCapacity(_items.Count + otherCount); + } + + foreach (var item in other) + { + if (_items.Add(item)) + _changeCollector.AddChange(DistinctChange.Addition(item)); + else + { + _items.Remove(item); + _changeCollector.AddChange(DistinctChange.Removal(item)); + } + + _isDirty = true; + } + + if (_items.Count is 0) + _changeCollector.OnSourceCleared(); + } + else + { + foreach (var item in other) + { + if (!_items.Add(item)) + _items.Remove(item); + + _isDirty = true; + } + } + } + + /// + public void SymmetricExceptWith(ReadOnlySpan other) + { + if (other.Length is 0) + return; + + if (_isChangeCollectionEnabled) + { + _items.EnsureCapacity(_items.Count + other.Length); + _changeCollector.EnsureCapacity(_items.Count + other.Length); + + foreach (var item in other) + { + if (_items.Add(item)) + _changeCollector.AddChange(DistinctChange.Addition(item)); + else + { + _items.Remove(item); + _changeCollector.AddChange(DistinctChange.Removal(item)); + } + + _isDirty = true; + } + + if (_items.Count is 0) + _changeCollector.OnSourceCleared(); + } + else + { + foreach (var item in other) + { + if (!_items.Add(item)) + _items.Remove(item); + + _isDirty = true; + } + } + } + + /// + public void UnionWith(IEnumerable other) + { + if (_isChangeCollectionEnabled) + { + if (other.TryGetNonEnumeratedCount(out var otherCount)) + { + if (otherCount is 0) + return; + + _items.EnsureCapacity(_items.Count + otherCount); + + _changeCollector.EnsureCapacity(otherCount); + } + + foreach(var item in other) + if (_items.Add(item)) + { + _changeCollector.AddChange(DistinctChange.Addition(item)); + _isDirty = true; + } + } + else + { + var oldCount = _items.Count; + + _items.UnionWith(other); + + _isDirty |= oldCount != _items.Count; + } + } + + /// + public void UnionWith(ReadOnlySpan other) + { + if (other.Length is 0) + return; + + _items.EnsureCapacity(_items.Count + other.Length); + + if (_isChangeCollectionEnabled) + { + _changeCollector.EnsureCapacity(other.Length); + + foreach(var item in other) + if (_items.Add(item)) + { + _changeCollector.AddChange(DistinctChange.Addition(item)); + _isDirty = true; + } + } + else + { + foreach(var item in other) + _isDirty |= _items.Add(item); + } + } + + bool ICollection.IsReadOnly + => false; + + void ICollection.Add(T item) + => Add(item); + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); +} diff --git a/src/DynamicDataVNext/Distinct/IExtendedSet.cs b/src/DynamicDataVNext/Distinct/IExtendedSet.cs new file mode 100644 index 0000000..1a7a091 --- /dev/null +++ b/src/DynamicDataVNext/Distinct/IExtendedSet.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace DynamicDataVNext; + +/// +/// Describes an extended version of , supporting range operations. +/// +/// The type of the items in the collection. +public interface IExtendedSet + : ISet +{ + /// . + void ExceptWith(ReadOnlySpan other); + + /// . + void IntersectWith(ReadOnlySpan other); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The set of items with which to populate the collection. + /// Throws for . + void Reset(IEnumerable items); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The set of items with which to populate the collection. + void Reset(ReadOnlySpan items); + + /// . + void SymmetricExceptWith(ReadOnlySpan other); + + /// . + void UnionWith(ReadOnlySpan other); +} diff --git a/src/DynamicDataVNext/Distinct/ISubjectSet.cs b/src/DynamicDataVNext/Distinct/ISubjectSet.cs index a6eeab9..5307c72 100644 --- a/src/DynamicDataVNext/Distinct/ISubjectSet.cs +++ b/src/DynamicDataVNext/Distinct/ISubjectSet.cs @@ -9,7 +9,7 @@ namespace DynamicDataVNext; /// /// The type of the items in the collection. public interface ISubjectSet - : ISet, + : IExtendedSet, IObservable> { /// @@ -17,13 +17,6 @@ public interface ISubjectSet /// IObservable CollectionChanged { get; } - /// - /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. - /// - /// The set of items with which to populate the collection. - /// Throws for . - void Reset(IEnumerable items); - /// /// Temporarily suspends the publication of notifications by the collection, until the returned object is disposed, at which point all mutations made during the suspension will (if any) will be published as one notification. /// diff --git a/src/DynamicDataVNext/Distinct/SubjectSet.cs b/src/DynamicDataVNext/Distinct/SubjectSet.cs new file mode 100644 index 0000000..60a532c --- /dev/null +++ b/src/DynamicDataVNext/Distinct/SubjectSet.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace DynamicDataVNext; + +/// +/// The basic implementation of , providing simple collection and change notification functionality, with no concurrency or thread-safety. +/// +/// The type of the items in the collection. +public sealed class SubjectSet + : ISubjectSet, + IObservableSet, + IDisposable + where T : notnull +{ + private readonly Subject> _changeStream; + private readonly Subject _collectionChanged; + private readonly Subject _notificationsResumed; + private readonly Action _onChangeStreamFinalized; + private readonly ChangeTrackingSet _items; + + private int _notificationSuspensionCount; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for detecting item changes, within the collection, or if should be used. + public SubjectSet( + int? capacity = null, + IEqualityComparer? comparer = null) + { + _collectionChanged = new(); + _changeStream = new(); + _notificationsResumed = new(); + _items = new( + capacity: capacity, + comparer: comparer); + + _onChangeStreamFinalized = () => _items.IsChangeCollectionEnabled = _changeStream.HasObservers; + } + + /// + public IObservable CollectionChanged + => _collectionChanged; + + /// + public IEqualityComparer Comparer + => _items.Comparer; + + /// + public int Count + => _items.Count; + + /// + public bool Add(T item) + { + var result = _items.Add(item); + + PublishPendingNotifications(); + + return result; + } + + /// + public void Clear() + { + _items.Clear(); + + PublishPendingNotifications(); + } + + /// + public bool Contains(T item) + => _items.Contains(item); + + /// + public void CopyTo(T[] array, int arrayIndex) + => _items.CopyTo(array, arrayIndex); + + /// + public void Dispose() + { + _changeStream .OnCompleted(); + _collectionChanged .OnCompleted(); + _notificationsResumed .OnCompleted(); + + _changeStream .Dispose(); + _collectionChanged .Dispose(); + _notificationsResumed .Dispose(); + } + + /// + public void EnsureCapacity(int capacity) + => _items.EnsureCapacity(capacity); + + /// + public void ExceptWith(ReadOnlySpan other) + { + _items.ExceptWith(other); + + PublishPendingNotifications(); + } + + /// + public void ExceptWith(IEnumerable other) + { + _items.ExceptWith(other); + + PublishPendingNotifications(); + } + + /// + public HashSet.Enumerator GetEnumerator() + => _items.GetEnumerator(); + + /// + public void IntersectWith(ReadOnlySpan other) + { + _items.IntersectWith(other); + + PublishPendingNotifications(); + } + + /// + public void IntersectWith(IEnumerable other) + { + _items.IntersectWith(other); + + PublishPendingNotifications(); + } + + /// + public bool IsProperSubsetOf(IEnumerable other) + => _items.IsProperSubsetOf(other); + + /// + public bool IsProperSupersetOf(IEnumerable other) + => _items.IsProperSupersetOf(other); + + /// + public bool IsSubsetOf(IEnumerable other) + => _items.IsSubsetOf(other); + + /// + public bool IsSupersetOf(IEnumerable other) + => _items.IsSupersetOf(other); + + /// + public bool Overlaps(IEnumerable other) + => _items.Overlaps(other); + + /// + public bool Remove(T item) + { + var result = _items.Remove(item); + + PublishPendingNotifications(); + + return result; + } + + /// + public void Reset(IEnumerable items) + { + _items.Reset(items); + + PublishPendingNotifications(); + } + + /// + public void Reset(ReadOnlySpan items) + { + _items.Reset(items); + + PublishPendingNotifications(); + } + /// + public bool SetEquals(IEnumerable other) + => _items.SetEquals(other); + + /// + public IDisposable Subscribe(IObserver> observer) + { + _items.IsChangeCollectionEnabled = true; + + return ((_notificationSuspensionCount is 0) + ? Observable.Empty() + : _notificationsResumed + .Take(1)) + .Select(_ => _changeStream + .Finally(_onChangeStreamFinalized) + .Prepend(DistinctChangeSet.BulkAddition(_items))) + .Switch() + .Subscribe(observer); + } + + /// + public NotificationSuspension SuspendNotifications() + { + ++_notificationSuspensionCount; + return new(this); + } + + /// + public void SymmetricExceptWith(ReadOnlySpan other) + { + _items.SymmetricExceptWith(other); + + PublishPendingNotifications(); + } + + /// + public void SymmetricExceptWith(IEnumerable other) + { + _items.SymmetricExceptWith(other); + + PublishPendingNotifications(); + } + + /// + public void UnionWith(ReadOnlySpan other) + { + _items.UnionWith(other); + + PublishPendingNotifications(); + } + + /// + public void UnionWith(IEnumerable other) + { + _items.UnionWith(other); + + PublishPendingNotifications(); + } + + bool ICollection.IsReadOnly + => false; + + void ICollection.Add(T item) + => Add(item); + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IDisposable ISubjectSet.SuspendNotifications() + => SuspendNotifications(); + + private void OnNotificationSuspensionDisposed() + { + --_notificationSuspensionCount; + if (_notificationSuspensionCount is 0) + { + PublishPendingNotifications(); + + _notificationsResumed.OnNext(Unit.Default); + } + } + + private void PublishPendingNotifications() + { + if ((_notificationSuspensionCount is not 0) || !_items.IsDirty) + return; + + _collectionChanged.OnNext(Unit.Default); + + _changeStream.OnNext(_items.CaptureChangesAndClean()); + } + + /// + /// A value that controls suspension of notifications, for a . Will trigger notifications to be resumed, when disposed. + /// + public struct NotificationSuspension + : IDisposable + { + private SubjectSet? _owner; + + internal NotificationSuspension(SubjectSet owner) + => _owner = owner; + + /// + public void Dispose() + { + if (_owner is not null) + { + _owner.OnNotificationSuspensionDisposed(); + _owner = null; + } + } + } +} diff --git a/src/DynamicDataVNext/Keyed/ChangeTrackingCache.cs b/src/DynamicDataVNext/Keyed/ChangeTrackingCache.cs new file mode 100644 index 0000000..a6facd1 --- /dev/null +++ b/src/DynamicDataVNext/Keyed/ChangeTrackingCache.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http.Headers; + +namespace DynamicDataVNext; + +/// +/// A collection of distinctly-keyed items, which tracks and allows for capturing of changes made to it, as they occur. +/// +/// The type of the key values of items in the collection. +/// The type of the items in the collection. +public sealed class ChangeTrackingCache + : ICache, + IReadOnlyCache + where TKey : notnull +{ + private readonly KeyedChangeSet.Builder _changeCollector; + private readonly IEqualityComparer _itemComparer; + private readonly Dictionary _itemsByKey; + private readonly Func _keySelector; + + private bool _isChangeCollectionEnabled; + private bool _isDirty; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for matching keys against each other, or if should be used. + /// The comparer to be used for detecting item changes, within the collection, or if should be used. + public ChangeTrackingCache( + Func keySelector, + int? capacity = null, + IEqualityComparer? keyComparer = null, + IEqualityComparer? itemComparer = null) + { + _keySelector = keySelector; + + _itemComparer = itemComparer ?? EqualityComparer.Default; + + _itemsByKey = (capacity is int givenCapacity) + ? new( + capacity: givenCapacity, + comparer: keyComparer) + : new( + comparer: keyComparer); + + _changeCollector = new(); + _isChangeCollectionEnabled = true; + } + + /// + public TItem this[TKey key] + => _itemsByKey[key]; + + /// + public int Count + => _itemsByKey.Count; + + /// + /// A flag indicating whether the collection should actually collect changes, to be retrieved by calls to . + /// Defaults to . + /// + /// + /// Note that any changes previously collected, but not captured, when this property is set to will be discarded. Otherwise, it would be possible for to generate a corrupt changes, when next called, after setting this property back to . + /// + public bool IsChangeCollectionEnabled + { + get => _isChangeCollectionEnabled; + set + { + if ((value is false) && (_changeCollector.Count is not 0)) + _changeCollector.Clear(); + + _isChangeCollectionEnabled = value; + } + } + + /// + /// A flag indicating whether changes have been made to the collection since its creation, or since the last call to `. + /// + public bool IsDirty + => _isDirty; + + /// + /// The comparer to be used for detecting value changes, within the collection. + /// + public IEqualityComparer ItemComparer + => _itemComparer; + + /// + /// The comparer to be used for matching key values against each other. + /// + public IEqualityComparer KeyComparer + => _itemsByKey.Comparer; + + /// + /// The function used by the collection to identify the key values of items. + /// + public Func KeySelector + => _keySelector; + + /// + public Dictionary.KeyCollection Keys + => _itemsByKey.Keys; + + /// + public void Add(TItem item) + { + var key = _keySelector.Invoke(item); + + _itemsByKey.Add(key, item); + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(KeyedChange.Addition(key, item)); + + _isDirty = true; + } + + /// + public void AddOrReplace(TItem item) + { + var key = _keySelector.Invoke(item); + + var wasKeyFound = _itemsByKey.TryGetValue(key, out var oldItem); + + if (wasKeyFound && _itemComparer.Equals(oldItem, item)) + return; + + _itemsByKey[key] = item; + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(wasKeyFound + ? KeyedChange.Replacement( + key: key, + oldItem: oldItem!, + newItem: item) + : KeyedChange.Addition(key, item)); + + _isDirty = true; + } + + /// + public void AddOrReplaceRange(IEnumerable values) + { + ArgumentNullException.ThrowIfNull(values, nameof(values)); + + if (values.TryGetNonEnumeratedCount(out var valueCount)) + { + _itemsByKey.EnsureCapacity(_itemsByKey.Count + valueCount); + + if (_isChangeCollectionEnabled) + _changeCollector.EnsureCapacity(_changeCollector.Count + valueCount); + } + + foreach (var value in values) + AddOrReplace(value); + } + + /// + public void AddOrReplaceRange(ReadOnlySpan values) + { + _itemsByKey.EnsureCapacity(_itemsByKey.Count + values.Length); + + if (_isChangeCollectionEnabled) + _changeCollector.EnsureCapacity(_changeCollector.Count + values.Length); + + foreach (var value in values) + AddOrReplace(value); + } + + /// + /// Captures any previously-collected changes made to the collection, and resets the collection to a "clean" state (I.E. sets to ). + /// + /// A containing all changes made to the collection since its construction, the last call to , or since was last changed to . + /// + /// Note that this method will always return an empty changeset, when is . + /// + public KeyedChangeSet CaptureChangesAndClean() + { + _isDirty = false; + return _changeCollector.BuildAndClear(); + } + + /// + public void Clear() + { + if (_itemsByKey.Count is 0) + return; + + if (_isChangeCollectionEnabled) + { + _changeCollector.EnsureCapacity(_itemsByKey.Count); + + foreach (var item in _itemsByKey) + _changeCollector.AddChange(KeyedChange.Removal(item.Key, item.Value)); + + _changeCollector.OnSourceCleared(); + } + + _isDirty = true; + } + + /// + public bool Contains(TItem item) + => _itemsByKey.TryGetValue(_keySelector.Invoke(item), out var existingItem) + && _itemComparer.Equals(item, existingItem); + + /// + public bool ContainsKey(TKey key) + => _itemsByKey.ContainsKey(key); + + /// + public void CopyTo( + TItem[] array, + int arrayIndex) + => _itemsByKey.Values.CopyTo(array, arrayIndex); + + /// + public void EnsureCapacity(int capacity) + => _itemsByKey.EnsureCapacity(capacity); + + /// + public Dictionary.ValueCollection.Enumerator GetEnumerator() + => _itemsByKey.Values.GetEnumerator(); + + /// + public bool Remove(TItem item) + { + var key = _keySelector.Invoke(item); + + if (!_itemsByKey.TryGetValue(key, out var existingItem) + || !_itemComparer.Equals(item, existingItem)) + return false; + + if (_isChangeCollectionEnabled) + { + _changeCollector.AddChange(KeyedChange.Removal(key, item)); + if (_itemsByKey.Count is 0) + _changeCollector.OnSourceCleared(); + } + + _isDirty = true; + return true; + } + + /// + public bool Remove(TKey key) + { + if (!_itemsByKey.TryGetValue(key, out var value)) + return false; + + if (_isChangeCollectionEnabled) + { + _changeCollector.AddChange(KeyedChange.Removal(key, value)); + if (_itemsByKey.Count is 0) + _changeCollector.OnSourceCleared(); + } + + _isDirty = true; + return true; + } + + /// + public void RemoveRange(IEnumerable items) + { + if (_itemsByKey.Count is 0) + return; + + if (_isChangeCollectionEnabled && items.TryGetNonEnumeratedCount(out var itemCount)) + _changeCollector.EnsureCapacity(itemCount); + + var wereItemsRemoved = false; + foreach (var item in items) + wereItemsRemoved |= Remove(item); + + if ((_isChangeCollectionEnabled) && wereItemsRemoved && (_itemsByKey.Count is 0)) + _changeCollector.OnSourceCleared(); + } + + /// + public void RemoveRange(ReadOnlySpan items) + { + if (_itemsByKey.Count is 0) + return; + + _changeCollector.EnsureCapacity(items.Length); + + var wereItemsRemoved = false; + foreach (var item in items) + wereItemsRemoved |= Remove(item); + + if ((_isChangeCollectionEnabled) && wereItemsRemoved && (_itemsByKey.Count is 0)) + _changeCollector.OnSourceCleared(); + } + + /// + public void RemoveRange(IEnumerable keys) + { + if (_itemsByKey.Count is 0) + return; + + if (_isChangeCollectionEnabled && keys.TryGetNonEnumeratedCount(out var keyCount)) + _changeCollector.EnsureCapacity(keyCount); + + var wereKeysRemoved = false; + foreach (var key in keys) + wereKeysRemoved |= Remove(key); + + if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_itemsByKey.Count is 0)) + _changeCollector.OnSourceCleared(); + } + + /// + public void RemoveRange(ReadOnlySpan keys) + { + if (_itemsByKey.Count is 0) + return; + + _changeCollector.EnsureCapacity(keys.Length); + + var wereKeysRemoved = false; + foreach (var key in keys) + wereKeysRemoved |= Remove(key); + + if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_itemsByKey.Count is 0)) + _changeCollector.OnSourceCleared(); + } + + /// + public void Reset(IEnumerable items) + { + Clear(); + AddOrReplaceRange(items); + } + + /// + public void Reset(ReadOnlySpan values) + { + Clear(); + AddOrReplaceRange(values); + } + + /// + public bool TryGetItem( + TKey key, + [MaybeNullWhen(false)] out TItem item) + => _itemsByKey.TryGetValue(key, out item); + + IReadOnlyCollection ICache.Keys + => _itemsByKey.Keys; + + IReadOnlyCollection IReadOnlyCache.Keys + => _itemsByKey.Keys; + + bool ICollection.IsReadOnly + => false; + + IEnumerator IEnumerable.GetEnumerator() + => _itemsByKey.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => _itemsByKey.Values.GetEnumerator(); +} diff --git a/src/DynamicDataVNext/Keyed/ChangeTrackingDictionary.cs b/src/DynamicDataVNext/Keyed/ChangeTrackingDictionary.cs new file mode 100644 index 0000000..82272e0 --- /dev/null +++ b/src/DynamicDataVNext/Keyed/ChangeTrackingDictionary.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace DynamicDataVNext; + +/// +/// A collection of items, with distinct keys, which tracks and allows for capturing of changes made to it, as they occur. +/// +/// The type of the item keys in the collection. +/// The type of the item values in the collection. +public sealed class ChangeTrackingDictionary + : IExtendedDictionary, + IReadOnlyDictionary + where TKey : notnull +{ + private readonly KeyedChangeSet.Builder _changeCollector; + private readonly IEqualityComparer _valueComparer; + private readonly Dictionary _valuesByKey; + + private bool _isChangeCollectionEnabled; + private bool _isDirty; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for matching keys against each other, or if should be used. + /// The comparer to be used for detecting value changes, within the collection, or if should be used. + public ChangeTrackingDictionary( + int? capacity = null, + IEqualityComparer? keyComparer = null, + IEqualityComparer? valueComparer = null) + { + _valueComparer = valueComparer ?? EqualityComparer.Default; + + _valuesByKey = (capacity is int givenCapacity) + ? new( + capacity: givenCapacity, + comparer: keyComparer) + : new( + comparer: keyComparer); + + _changeCollector = new(); + _isChangeCollectionEnabled = true; + } + + /// + public TValue this[TKey key] + { + get => _valuesByKey[key]; + set + { + var wasKeyFound = _valuesByKey.TryGetValue(key, out var oldValue); + + if (wasKeyFound && _valueComparer.Equals(oldValue, value)) + return; + + _valuesByKey[key] = value; + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(wasKeyFound + ? KeyedChange.Replacement( + key: key, + oldItem: oldValue!, + newItem: value) + : KeyedChange.Addition(key, value)); + + _isDirty = true; + } + } + + /// + public int Count + => _valuesByKey.Count; + + /// + /// A flag indicating whether the collection should actually collect changes, to be retrieved by calls to . + /// Defaults to . + /// + /// + /// Note that any changes previously collected, but not captured, when this property is set to will be discarded. Otherwise, it would be possible for to generate a corrupt changes, when next called, after setting this property back to . + /// + public bool IsChangeCollectionEnabled + { + get => _isChangeCollectionEnabled; + set + { + if ((value is false) && (_changeCollector.Count is not 0)) + _changeCollector.Clear(); + + _isChangeCollectionEnabled = value; + } + } + + /// + /// A flag indicating whether changes have been made to the collection since its creation, or since the last call to `. + /// + public bool IsDirty + => _isDirty; + + /// + /// The comparer to be used for matching key values against each other. + /// + public IEqualityComparer KeyComparer + => _valuesByKey.Comparer; + + /// + public Dictionary.KeyCollection Keys + => _valuesByKey.Keys; + + /// + /// The comparer to be used for detecting value changes, within the collection. + /// + public IEqualityComparer ValueComparer + => _valueComparer; + + /// + public Dictionary.ValueCollection Values + => _valuesByKey.Values; + + /// + public void Add( + TKey key, + TValue value) + { + _valuesByKey.Add(key, value); + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(KeyedChange.Addition(key, value)); + + _isDirty = true; + } + + /// + public void Add(KeyValuePair item) + => Add(item.Key, item.Value); + + /// + public void AddOrReplaceRange( + IEnumerable values, + Func keySelector) + { + ArgumentNullException.ThrowIfNull(values, nameof(values)); + ArgumentNullException.ThrowIfNull(keySelector, nameof(keySelector)); + + if (values.TryGetNonEnumeratedCount(out var valueCount)) + { + _valuesByKey.EnsureCapacity(_valuesByKey.Count + valueCount); + + if (_isChangeCollectionEnabled) + _changeCollector.EnsureCapacity(_changeCollector.Count + valueCount); + } + + foreach (var value in values) + { + var key = keySelector.Invoke(value); + + this[key] = value; + } + } + + /// + public void AddOrReplaceRange( + ReadOnlySpan values, + Func keySelector) + { + ArgumentNullException.ThrowIfNull(keySelector, nameof(keySelector)); + + _valuesByKey.EnsureCapacity(_valuesByKey.Count + values.Length); + + if (_isChangeCollectionEnabled) + _changeCollector.EnsureCapacity(_changeCollector.Count + values.Length); + + foreach (var value in values) + { + var key = keySelector.Invoke(value); + + this[key] = value; + } + } + + /// + public void AddOrReplaceRange(IEnumerable> items) + { + ArgumentNullException.ThrowIfNull(items, nameof(items)); + + if (items.TryGetNonEnumeratedCount(out var itemCount)) + { + _valuesByKey.EnsureCapacity(_valuesByKey.Count + itemCount); + + if (_isChangeCollectionEnabled) + _changeCollector.EnsureCapacity(_changeCollector.Count + itemCount); + } + + foreach (var item in items) + this[item.Key] = item.Value; + } + + /// + public void AddOrReplaceRange(ReadOnlySpan> items) + { + _valuesByKey.EnsureCapacity(_valuesByKey.Count + items.Length); + + if (_isChangeCollectionEnabled) + _changeCollector.EnsureCapacity(_changeCollector.Count + items.Length); + + foreach (var item in items) + this[item.Key] = item.Value; + } + + /// + /// Captures any previously-collected changes made to the collection, and resets the collection to a "clean" state (I.E. sets to ). + /// + /// A containing all changes made to the collection since its construction, the last call to , or since was last changed to . + /// + /// Note that this method will always return an empty changeset, when is . + /// + public KeyedChangeSet CaptureChangesAndClean() + { + _isDirty = false; + return _changeCollector.BuildAndClear(); + } + + /// + public void Clear() + { + if (_valuesByKey.Count is 0) + return; + + if (_isChangeCollectionEnabled) + { + _changeCollector.EnsureCapacity(_valuesByKey.Count); + + foreach (var item in _valuesByKey) + _changeCollector.AddChange(KeyedChange.Removal(item.Key, item.Value)); + + _changeCollector.OnSourceCleared(); + } + + _isDirty = true; + } + + /// + public bool Contains(KeyValuePair item) + => _valuesByKey.TryGetValue(item.Key, out var value) + && _valueComparer.Equals(item.Value, value); + + /// + public bool ContainsKey(TKey key) + => _valuesByKey.ContainsKey(key); + + /// + public void CopyTo( + KeyValuePair[] array, + int arrayIndex) + => ((ICollection>)_valuesByKey).CopyTo(array, arrayIndex); + + /// + public void EnsureCapacity(int capacity) + => _valuesByKey.EnsureCapacity(capacity); + + /// + public Dictionary.Enumerator GetEnumerator() + => _valuesByKey.GetEnumerator(); + + /// + public bool Remove(TKey key) + { + if (!_valuesByKey.TryGetValue(key, out var value)) + return false; + + if (_isChangeCollectionEnabled) + { + _changeCollector.AddChange(KeyedChange.Removal(key, value)); + if (_valuesByKey.Count is 0) + _changeCollector.OnSourceCleared(); + } + + _isDirty = true; + return true; + } + + /// + public bool Remove(KeyValuePair item) + { + if (!_valuesByKey.Contains(item)) + return false; + + if (_isChangeCollectionEnabled) + { + _changeCollector.AddChange(KeyedChange.Removal(item.Key, item.Value)); + if (_valuesByKey.Count is 0) + _changeCollector.OnSourceCleared(); + } + + _isDirty = true; + return true; + } + + /// + public void RemoveRange(IEnumerable keys) + { + if (_valuesByKey.Count is 0) + return; + + if (_isChangeCollectionEnabled && keys.TryGetNonEnumeratedCount(out var keyCount)) + _changeCollector.EnsureCapacity(keyCount); + + var wereKeysRemoved = false; + foreach (var key in keys) + wereKeysRemoved |= Remove(key); + + if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_valuesByKey.Count is 0)) + _changeCollector.OnSourceCleared(); + } + + /// + public void RemoveRange(ReadOnlySpan keys) + { + if (_valuesByKey.Count is 0) + return; + + _changeCollector.EnsureCapacity(keys.Length); + + var wereKeysRemoved = false; + foreach (var key in keys) + wereKeysRemoved |= Remove(key); + + if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_valuesByKey.Count is 0)) + _changeCollector.OnSourceCleared(); + } + + /// + public void Reset( + IEnumerable values, + Func keySelector) + { + Clear(); + AddOrReplaceRange(values, keySelector); + } + + /// + public void Reset( + ReadOnlySpan values, + Func keySelector) + { + Clear(); + AddOrReplaceRange(values, keySelector); + } + + /// + public void Reset(IEnumerable> items) + { + Clear(); + AddOrReplaceRange(items); + } + + /// + public void Reset(ReadOnlySpan> items) + { + Clear(); + AddOrReplaceRange(items); + } + + /// + public bool TryGetValue( + TKey key, + [MaybeNullWhen(false)] out TValue value) + => _valuesByKey.TryGetValue(key, out value); + + bool ICollection>.IsReadOnly + => false; + + ICollection IDictionary.Keys + => _valuesByKey.Keys; + + IEnumerable IReadOnlyDictionary.Keys + => _valuesByKey.Keys; + + IReadOnlyCollection IExtendedDictionary.Keys + => _valuesByKey.Keys; + + ICollection IDictionary.Values + => _valuesByKey.Values; + + IEnumerable IReadOnlyDictionary.Values + => _valuesByKey.Values; + + IReadOnlyCollection IExtendedDictionary.Values + => _valuesByKey.Values; + + IEnumerator IEnumerable.GetEnumerator() + => _valuesByKey.GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() + => _valuesByKey.GetEnumerator(); +} diff --git a/src/DynamicDataVNext/Keyed/ICache.cs b/src/DynamicDataVNext/Keyed/ICache.cs new file mode 100644 index 0000000..2f2e299 --- /dev/null +++ b/src/DynamicDataVNext/Keyed/ICache.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace DynamicDataVNext; + +/// +/// Describes a collection of distinctly-keyed items. +/// +/// The type of the key values of items in the collection. +/// The type of the items in the collection. +public interface ICache + : ICollection +{ + /// + /// Retrieves the item in the collection for the given key. + /// + /// The key of the item to be retrieved. + /// Throws when does not exist within the collection. + /// The item in the collection with the given key. + TItem this[TKey key] { get; } + + /// + /// Retrieves the current set of keys present within the collection. + /// + /// + /// Note that the returned collection represents a "snapshot" of the source collection, at the time at which it is created. Changes made to the source collection after a key collection is retrieved are not reflected upon the key collection. + /// + IReadOnlyCollection Keys { get; } + + /// + /// Applies an item to the collection, as a single operation, by either adding or replacing the item, based on its key value. + /// + /// The item to be applied to the collection. + void AddOrReplace(TItem item); + + /// + /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value. + /// + /// The items to applied to the collection. + /// Throws for . + void AddOrReplaceRange(IEnumerable items); + + /// + /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value. + /// + /// The items to applied to the collection. + void AddOrReplaceRange(ReadOnlySpan items); + + /// + /// Checks whether a given key is currently present, within the collection. + /// + /// The key to check for. + /// A flag indicating whether the given key is present in the collection, or not. + bool ContainsKey(TKey key); + + /// + /// Removes an item from the collection, by its key. + /// + /// The key to be removed. + /// A flag indicating whether the given key was present in the collection, and thus removed successfully, or not. + bool Remove(TKey key); + + /// + /// Removes a set of items, from the collection, as a single operation. + /// + /// The set of items to be removed. + /// Throws for . + /// + /// Note that the collection will silently ignore items that are not present in the collection. + /// + void RemoveRange(IEnumerable items); + + /// + /// Removes a set of items, from the collection, as a single operation. + /// + /// The set of items to be removed. + /// + /// Note that the collection will silently ignore items that are not present in the collection. + /// + void RemoveRange(ReadOnlySpan items); + + /// + /// Removes a set of items, by key, from the collection, as a single operation. + /// + /// The set of keys to be removed. + /// Throws for . + /// + /// Note that the collection will silently ignore keys that are not present in the collection. + /// + void RemoveRange(IEnumerable keys); + + /// + /// Removes a set of items, by key, from the collection, as a single operation. + /// + /// The set of keys to be removed. + /// + /// Note that the collection will silently ignore keys that are not present in the collection. + /// + void RemoveRange(ReadOnlySpan keys); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The set of items with which to populate the collection. + /// Throws for . + void Reset(IEnumerable items); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The set of items with which to populate the collection. + void Reset(ReadOnlySpan items); + + /// + /// Attempts to retrieve an item from the collection, by its key. + /// + /// The key whose item is to be retrieved. + /// The item in the collection whose key is . The default value of is assigned, if no such item is present. + /// A flag indicating whether an item with the given key was successfully retrieved, or not. + bool TryGetItem(TKey key, [MaybeNullWhen(false)] out TItem item); +} diff --git a/src/DynamicDataVNext/Keyed/IExtendedDictionary.cs b/src/DynamicDataVNext/Keyed/IExtendedDictionary.cs new file mode 100644 index 0000000..31de7e3 --- /dev/null +++ b/src/DynamicDataVNext/Keyed/IExtendedDictionary.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; + +namespace DynamicDataVNext; + +/// +/// Describes an extended version of , supporting range operations. +/// +/// The type of the item keys in the collection. +/// The type of the item values in the collection. +/// +/// It's worth noting that the lack of an AddRange() method on this interface is intentional. Such a method would intuitively need to follow the pattern of and throw an exception when a key is presented that is already present in the collection, which would then introduce the possibility for an AddRange() operation to only be half-applied to the collection. The most appropriate thing to do would be to roll back any items already added to the collection, when the exception is thrown, which would require quite a lot of additional state tracking and item iteration. The should usually be preferred, for adding batches of items to a collection, and if key validation is desired, it can be implemented on the consumer's end. +/// +public interface IExtendedDictionary + : IDictionary +{ + /// + new IReadOnlyCollection Keys { get; } + + /// + new IReadOnlyCollection Values { get; } + + /// + /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its selected key value. + /// + /// The values to use as for each applied item. + /// A selector to select a value for each applied item. + /// Throws for and . + void AddOrReplaceRange( + IEnumerable values, + Func keySelector); + + /// + /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its selected key value. + /// + /// The values to use as for each applied item. + /// A selector to select a value for each applied item. + /// Throws for . + void AddOrReplaceRange( + ReadOnlySpan values, + Func keySelector); + + /// + /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value. + /// + /// The keys and values to be applied. + /// Throws for . + void AddOrReplaceRange(IEnumerable> items); + + /// + /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value. + /// + /// The keys and values to be applied. + void AddOrReplaceRange(ReadOnlySpan> items); + + /// + /// Removes a set of items, by key, from the collection, as a single operation. + /// + /// The set of keys to be removed. + /// Throws for . + /// + /// Note that the collection will silently ignore keys that are not present in the collection. + /// + void RemoveRange(IEnumerable keys); + + /// + /// Removes a set of items, by key, from the collection, as a single operation. + /// + /// The set of keys to be removed. + /// Throws for . + /// + /// Note that the collection will silently ignore keys that are not present in the collection. + /// + void RemoveRange(ReadOnlySpan keys); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The set of items with which to populate the collection. + /// A selector to select a value for each new item. + /// Throws for and . + void Reset( + IEnumerable values, + Func keySelector); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The set of items with which to populate the collection. + /// A selector to select a value for each new item. + /// Throws for . + void Reset( + ReadOnlySpan values, + Func keySelector); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The keys vand values to be added. + /// Throws for . + void Reset(IEnumerable> items); + + /// + /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. + /// + /// The keys vand values to be added. + /// Throws for . + void Reset(ReadOnlySpan> items); +} diff --git a/src/DynamicDataVNext/Keyed/IObservableCache.cs b/src/DynamicDataVNext/Keyed/IObservableCache.cs index 11652da..877d1a3 100644 --- a/src/DynamicDataVNext/Keyed/IObservableCache.cs +++ b/src/DynamicDataVNext/Keyed/IObservableCache.cs @@ -1,79 +1,21 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Reactive; namespace DynamicDataVNext; /// -/// Describes a collection of distinctly-keyed items, which publishes notifications about its mutations to subscribers, as they occur. +/// Describes a collection of distinctly-keyed items, which may not be mutated by the consumer, and which publishes notifications about its mutations to subscribers, as they occur. /// /// The type of the key values of items in the collection. /// The type of the items in the collection. public interface IObservableCache - : IReadOnlyCollection, - IObservable> + : IReadOnlyCache, + IObservable> { - /// - /// retrieves the item in the collection (if any) for the given key. - /// - /// The key of the item to be retrieved. - /// Throws when does not exist within the collection. - /// The item in the collection with the given key. - TItem this[TKey key] { get; } - - /// - /// Retrieves the current set of keys present within the collection. - /// - /// - /// Note that the returned collection represents a "snapshot" of the source collection, at the time at which it is created. Changes made to the source collection after a key collection is retrieved are not reflected upon the key collection. - /// - IReadOnlyCollection Keys { get; } - - /// - /// Retrieves the current set of items present within the collection. - /// - /// - /// Note that the returned collection represents a "snapshot" of the source collection, at the time at which it is created. Changes made to the source collection after an item collection is retrieved are not reflected upon the item collection. - /// - IReadOnlyCollection Items { get; } - - /// - /// An event that occurs after any mutation of the collection occurs. - /// + /// IObservable CollectionChanged { get; } - /// - /// Checks whether a given key is currently present, within the collection. - /// - /// The key to check for. - /// A flag indicating whether the given key is present in the collection, or not. - bool ContainsKey(TKey key); - - /// - /// Allows subscribers to observe the value of a particular keyed item within the collection, as it changes. - /// - /// The key whose value is to be observed. - /// A stream which will publish the latest value, for the given key, within the collection. - /// - /// The returned stream will always immediately publish the current value for the given key, upon subscription, and will complete if the given key is removed. If the key is not present within the collection upon subscription, the stream will complete immediately. - /// + /// IObservable ObserveValue(TKey key); - - /// - /// Attempts to retrieve an item from the collection, by its key. - /// - /// The key whose item is to be retrieved. - /// The item in the collection whose key is . The default value of is assigned, if no such item is present. - /// A flag indicating whether an item with the given key was successfully retrieved, or not. - bool TryGetItem(TKey key, [MaybeNullWhen(false)] out TItem item); - - /// - /// Temporarily suspends the publication of notifications by the collection, until the returned object is disposed, at which point all mutations made during the suspension will (if any) will be published as one notification. - /// - /// An object that will trigger the resumption of notifications, when disposed. - /// - /// May be called multiple times, in which case no notifications will be published until all outstanding suspensions have been disposed. - /// - IDisposable SuspendNotifications(); } diff --git a/src/DynamicDataVNext/Keyed/IObservableDictionary.cs b/src/DynamicDataVNext/Keyed/IObservableDictionary.cs index 369a7ce..b0e2689 100644 --- a/src/DynamicDataVNext/Keyed/IObservableDictionary.cs +++ b/src/DynamicDataVNext/Keyed/IObservableDictionary.cs @@ -11,7 +11,7 @@ namespace DynamicDataVNext; /// The type of the item values in the collection. public interface IObservableDictionary : IReadOnlyDictionary, - IObservable> + IObservable> { /// IObservable CollectionChanged { get; } @@ -24,7 +24,4 @@ public interface IObservableDictionary /// IObservable ObserveValue(TKey key); - - /// - IDisposable SuspendNotifications(); } diff --git a/src/DynamicDataVNext/Keyed/IReadOnlyCache.cs b/src/DynamicDataVNext/Keyed/IReadOnlyCache.cs new file mode 100644 index 0000000..b36e897 --- /dev/null +++ b/src/DynamicDataVNext/Keyed/IReadOnlyCache.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace DynamicDataVNext; + +/// +/// Describes a collection of distinctly-keyed items, which does not allow public mutation. +/// +/// The type of the key values of items in the collection. +/// The type of the items in the collection. +public interface IReadOnlyCache + : IReadOnlyCollection +{ + /// + TItem this[TKey key] { get; } + + /// + IReadOnlyCollection Keys { get; } + + /// + bool ContainsKey(TKey key); + + /// + bool TryGetItem(TKey key, [MaybeNullWhen(false)] out TItem item); +} diff --git a/src/DynamicDataVNext/Keyed/ISubjectCache.cs b/src/DynamicDataVNext/Keyed/ISubjectCache.cs index 9054b77..1493929 100644 --- a/src/DynamicDataVNext/Keyed/ISubjectCache.cs +++ b/src/DynamicDataVNext/Keyed/ISubjectCache.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Reactive; namespace DynamicDataVNext; @@ -11,56 +9,14 @@ namespace DynamicDataVNext; /// The type of the key values of items in the collection. /// The type of the items in the collection. public interface ISubjectCache - : ICollection, - IObservable> + : ICache, + IObservable> { - /// - /// Accesses the item in the collection (if any) for the given key. - /// - /// The key of the item to be accessed. - /// Throws when does not exist within the collection, during a retrieval. - /// The item in the collection with the given key. - /// - /// When assigning a value to a given key, the key need not be already-present within the collection. - /// - TItem this[TKey key] { get; set; } - - /// - /// Retrieves the current set of keys present within the collection. - /// - /// - /// Note that the returned collection represents a "snapshot" of the source collection, at the time at which it is created. Changes made to the source collection after a key collection is retrieved are not reflected upon the key collection. - /// - IReadOnlyCollection Keys { get; } - - /// - /// Retrieves the current set of items present within the collection. - /// - /// - /// Note that the returned collection represents a "snapshot" of the source collection, at the time at which it is created. Changes made to the source collection after an item collection is retrieved are not reflected upon the item collection. - /// - IReadOnlyCollection Items { get; } - - /// - /// Adds a range of items to the collection, as a single operation. - /// - /// The items to be added. - /// Throws for . - /// Throws if contains an item whose key is already present within the collection. - void AddRange(IEnumerable items); - /// /// An event that occurs after any mutation of the collection occurs. /// IObservable CollectionChanged { get; } - /// - /// Checks whether a given key is currently present, within the collection. - /// - /// The key to check for. - /// A flag indicating whether the given key is present in the collection, or not. - bool ContainsKey(TKey key); - /// /// Allows subscribers to observe the value of a particular keyed item within the collection, as it changes. /// @@ -71,38 +27,6 @@ public interface ISubjectCache /// IObservable ObserveValue(TKey key); - /// - /// Removes an item from the collection, by its key. - /// - /// The key to be removed. - /// A flag indicating whether the given key was present in the collection, and thus removed successfully, or not. - bool Remove(TKey key); - - /// - /// Removes a set of items, by key, from the collection, as a single operation. - /// - /// The set of keys to be removed. - /// Throws for . - /// - /// Note that the collection will silently ignore keys that are not present in the collection. - /// - void RemoveRange(IEnumerable keys); - - /// - /// Attempts to retrieve an item from the collection, by its key. - /// - /// The key whose item is to be retrieved. - /// The item in the collection whose key is . The default value of is assigned, if no such item is present. - /// A flag indicating whether an item with the given key was successfully retrieved, or not. - bool TryGetItem(TKey key, [MaybeNullWhen(false)] out TItem item); - - /// - /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. - /// - /// The set of items with which to populate the collection. - /// Throws for . - void Reset(IEnumerable items); - /// /// Temporarily suspends the publication of notifications by the collection, until the returned object is disposed, at which point all mutations made during the suspension will (if any) will be published as one notification. /// diff --git a/src/DynamicDataVNext/Keyed/ISubjectDictionary.cs b/src/DynamicDataVNext/Keyed/ISubjectDictionary.cs index 04e41f5..0ea508b 100644 --- a/src/DynamicDataVNext/Keyed/ISubjectDictionary.cs +++ b/src/DynamicDataVNext/Keyed/ISubjectDictionary.cs @@ -10,31 +10,14 @@ namespace DynamicDataVNext; /// The type of the item keys in the collection. /// The type of the item values in the collection. public interface ISubjectDictionary - : IDictionary, - IObservable> + : IExtendedDictionary, + IObservable> { /// /// An event that occurs after any mutation of the collection occurs. /// IObservable CollectionChanged { get; } - /// - new IReadOnlyCollection Keys { get; } - - /// - new IReadOnlyCollection Values { get; } - - /// - /// Adds a range of items to the collection, as a single operation. - /// - /// The values to use as for each new item. - /// A selector to select a value for each new item. - /// Throws for and . - /// Throws if returns a key that already exists within the collection. - void AddRange( - IEnumerable values, - Func keySelector); - /// /// Allows subscribers to observe the value of a particular keyed item within the collection, as it changes. /// @@ -45,26 +28,6 @@ void AddRange( /// IObservable ObserveValue(TKey key); - /// - /// Removes a set of items, by key, from the collection, as a single operation. - /// - /// The set of keys to be removed. - /// Throws for . - /// - /// Note that the collection will silently ignore keys that are not present in the collection. - /// - void RemoveRange(IEnumerable keys); - - /// - /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation. - /// - /// The set of items with which to populate the collection. - /// A selector to select a value for each new item. - /// Throws for and . - void Reset( - IEnumerable values, - Func keySelector); - /// /// Temporarily suspends the publication of notifications by the collection, until the returned object is disposed, at which point all mutations made during the suspension will (if any) will be published as one notification. /// diff --git a/src/DynamicDataVNext/Keyed/SubjectCache.cs b/src/DynamicDataVNext/Keyed/SubjectCache.cs new file mode 100644 index 0000000..d7f6777 --- /dev/null +++ b/src/DynamicDataVNext/Keyed/SubjectCache.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace DynamicDataVNext; + +/// +/// The basic implementation of , providing simple collection and change notification functionality, with no concurrency or thread-safety. +/// +/// The type of the items in the collection. +public sealed class SubjectCache + : ISubjectCache, + IObservableCache, + IDisposable + where TKey : notnull +{ + private readonly Subject> _changeStream; + private readonly Subject _collectionChanged; + private readonly Subject _notificationsResumed; + private readonly Action _onChangeStreamFinalized; + private readonly ChangeTrackingCache _items; + + private int _notificationSuspensionCount; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for matching keys against each other, or if should be used. + /// The comparer to be used for detecting item changes, within the collection, or if should be used. + public SubjectCache( + Func keySelector, + int? capacity = null, + IEqualityComparer? keyComparer = null, + IEqualityComparer? itemComparer = null) + { + _collectionChanged = new(); + _changeStream = new(); + _notificationsResumed = new(); + _items = new( + keySelector: keySelector, + capacity: capacity, + keyComparer: keyComparer, + itemComparer: itemComparer); + + _onChangeStreamFinalized = () => _items.IsChangeCollectionEnabled = _changeStream.HasObservers; + } + + /// + public TItem this[TKey key] + => _items[key]; + + /// + public IObservable CollectionChanged + => _collectionChanged; + + /// + public int Count + => _items.Count; + + /// + public IEqualityComparer ItemComparer + => _items.ItemComparer; + + /// + public IEqualityComparer KeyComparer + => _items.KeyComparer; + + /// + public IReadOnlyCollection Keys + => _items.Keys; + + /// + public void Add(TItem value) + { + _items.Add(value); + + PublishPendingNotifications(); + } + + /// + public void AddOrReplace(TItem value) + { + _items.AddOrReplace(value); + + PublishPendingNotifications(); + } + + /// + public void AddOrReplaceRange(IEnumerable values) + { + _items.AddOrReplaceRange(values); + + PublishPendingNotifications(); + } + + /// + public void AddOrReplaceRange(ReadOnlySpan values) + { + _items.AddOrReplaceRange(values); + + PublishPendingNotifications(); + } + + /// + public void Clear() + { + _items.Clear(); + + PublishPendingNotifications(); + } + + /// + public bool Contains(TItem item) + => _items.Contains(item); + + /// + public bool ContainsKey(TKey key) + => _items.ContainsKey(key); + + /// + public void CopyTo( + TItem[] array, + int arrayIndex) + => _items.CopyTo(array, arrayIndex); + + /// + public void Dispose() + { + _changeStream .OnCompleted(); + _collectionChanged .OnCompleted(); + _notificationsResumed .OnCompleted(); + + _changeStream .Dispose(); + _collectionChanged .Dispose(); + _notificationsResumed .Dispose(); + } + + /// + public void EnsureCapacity(int capacity) + => _items.EnsureCapacity(capacity); + + /// + public Dictionary.ValueCollection.Enumerator GetEnumerator() + => _items.GetEnumerator(); + + /// + public IObservable ObserveValue(TKey key) + => ((_notificationSuspensionCount is 0) + ? Observable.Empty() + : Observable.Never() + .TakeUntil(_notificationsResumed)) + .Concat(Observable.Create(observer => + { + _items.IsChangeCollectionEnabled = true; + + if (!_items.TryGetItem(key, out var initialItem)) + { + observer.OnCompleted(); + return Disposable.Empty; + } + + observer.OnNext(initialItem); + return _changeStream + .Finally(_onChangeStreamFinalized) + .SubscribeSafe(Observer.Create>( + onNext: changeSet => + { + switch (changeSet.Type) + { + case ChangeSetType.Clear: + observer.OnCompleted(); + break; + + case ChangeSetType.Reset: + if (_items.TryGetItem(key, out var item)) + observer.OnNext(item); + else + observer.OnCompleted(); + break; + + default: + foreach (var change in changeSet.Changes) + { + switch (change.Type) + { + case KeyedChangeType.Removal: + if (_items.KeyComparer.Equals(key, change.AsRemoval().Key)) + observer.OnCompleted(); + break; + + case KeyedChangeType.Replacement: + var replacement = change.AsReplacement(); + if (_items.KeyComparer.Equals(key, replacement.Key)) + observer.OnNext(replacement.NewItem); + break; + } + } + break; + } + }, + onError: observer.OnError, + onCompleted: observer.OnCompleted)); + })); + + /// + public bool Remove(TItem item) + { + var result = _items.Remove(item); + + PublishPendingNotifications(); + + return result; + } + + /// + public bool Remove(TKey key) + { + var result = _items.Remove(key); + + PublishPendingNotifications(); + + return result; + } + + /// + public void RemoveRange(IEnumerable items) + { + _items.RemoveRange(items); + + PublishPendingNotifications(); + } + + /// + public void RemoveRange(ReadOnlySpan items) + { + _items.RemoveRange(items); + + PublishPendingNotifications(); + } + + /// + public void RemoveRange(IEnumerable keys) + { + _items.RemoveRange(keys); + + PublishPendingNotifications(); + } + + /// + public void RemoveRange(ReadOnlySpan keys) + { + _items.RemoveRange(keys); + + PublishPendingNotifications(); + } + + /// + public void Reset(IEnumerable items) + { + _items.Reset(items); + + PublishPendingNotifications(); + } + + /// + public void Reset(ReadOnlySpan items) + { + _items.Reset(items); + + PublishPendingNotifications(); + } + + /// + public IDisposable Subscribe(IObserver> observer) + { + _items.IsChangeCollectionEnabled = true; + + return ((_notificationSuspensionCount is 0) + ? Observable.Empty() + : _notificationsResumed + .Take(1)) + .Select(_ => _changeStream + .Finally(_onChangeStreamFinalized) + .Prepend(KeyedChangeSet.BulkAddition(_items, _items.KeySelector))) + .Switch() + .Subscribe(observer); + } + + /// + public NotificationSuspension SuspendNotifications() + { + ++_notificationSuspensionCount; + return new(this); + } + + /// + public bool TryGetItem( + TKey key, + [MaybeNullWhen(false)] out TItem item) + => _items.TryGetItem(key, out item); + + bool ICollection.IsReadOnly + => false; + + IReadOnlyCollection ICache.Keys + => _items.Keys; + + IReadOnlyCollection IReadOnlyCache.Keys + => _items.Keys; + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IDisposable ISubjectCache.SuspendNotifications() + => SuspendNotifications(); + + private void OnNotificationSuspensionDisposed() + { + --_notificationSuspensionCount; + if (_notificationSuspensionCount is 0) + { + PublishPendingNotifications(); + + _notificationsResumed.OnNext(Unit.Default); + } + } + + private void PublishPendingNotifications() + { + if ((_notificationSuspensionCount is not 0) || !_items.IsDirty) + return; + + _collectionChanged.OnNext(Unit.Default); + + _changeStream.OnNext(_items.CaptureChangesAndClean()); + } + + /// + /// A value that controls suspension of notifications, for a . Will trigger notifications to be resumed, when disposed. + /// + public struct NotificationSuspension + : IDisposable + { + private SubjectCache? _owner; + + internal NotificationSuspension(SubjectCache owner) + => _owner = owner; + + /// + public void Dispose() + { + if (_owner is not null) + { + _owner.OnNotificationSuspensionDisposed(); + _owner = null; + } + } + } +} diff --git a/src/DynamicDataVNext/Keyed/SubjectDictionary.cs b/src/DynamicDataVNext/Keyed/SubjectDictionary.cs new file mode 100644 index 0000000..f1c55e6 --- /dev/null +++ b/src/DynamicDataVNext/Keyed/SubjectDictionary.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace DynamicDataVNext; + +/// +/// The basic implementation of , providing simple collection and change notification functionality, with no concurrency or thread-safety. +/// +/// The type of the items in the collection. +public sealed class SubjectDictionary + : ISubjectDictionary, + IObservableDictionary, + IDisposable + where TKey : notnull +{ + private readonly Subject> _changeStream; + private readonly Subject _collectionChanged; + private readonly Subject _notificationsResumed; + private readonly Action _onChangeStreamFinalized; + private readonly ChangeTrackingDictionary _valuesByKey; + + private int _notificationSuspensionCount; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for matching keys against each other, or if should be used. + /// The comparer to be used for detecting value changes, within the collection, or if should be used. + public SubjectDictionary( + int? capacity = null, + IEqualityComparer? keyComparer = null, + IEqualityComparer? valueComparer = null) + { + _collectionChanged = new(); + _changeStream = new(); + _notificationsResumed = new(); + _valuesByKey = new( + capacity: capacity, + keyComparer: keyComparer, + valueComparer: valueComparer); + + _onChangeStreamFinalized = () => _valuesByKey.IsChangeCollectionEnabled = _changeStream.HasObservers; + } + + /// + public TValue this[TKey key] + { + get => _valuesByKey[key]; + set + { + _valuesByKey[key] = value; + + PublishPendingNotifications(); + } + } + + /// + public IObservable CollectionChanged + => _collectionChanged; + + /// + public int Count + => _valuesByKey.Count; + + /// + public IEqualityComparer KeyComparer + => _valuesByKey.KeyComparer; + + /// + public IReadOnlyCollection Keys + => _valuesByKey.Keys; + + /// + public IEqualityComparer ValueComparer + => _valuesByKey.ValueComparer; + + /// + public IReadOnlyCollection Values + => _valuesByKey.Values; + + /// + public void Add(TKey key, TValue value) + { + _valuesByKey.Add(key, value); + + PublishPendingNotifications(); + } + + /// + public void Add(KeyValuePair item) + { + _valuesByKey.Add(item); + + PublishPendingNotifications(); + } + + /// + public void AddOrReplaceRange( + IEnumerable values, + Func keySelector) + { + _valuesByKey.AddOrReplaceRange(values, keySelector); + + PublishPendingNotifications(); + } + + /// + public void AddOrReplaceRange( + ReadOnlySpan values, + Func keySelector) + { + _valuesByKey.AddOrReplaceRange(values, keySelector); + + PublishPendingNotifications(); + } + + /// + public void AddOrReplaceRange(IEnumerable> items) + { + _valuesByKey.AddOrReplaceRange(items); + + PublishPendingNotifications(); + } + + /// + public void AddOrReplaceRange(ReadOnlySpan> items) + { + _valuesByKey.AddOrReplaceRange(items); + + PublishPendingNotifications(); + } + + /// + public void Clear() + { + _valuesByKey.Clear(); + + PublishPendingNotifications(); + } + + /// + public bool Contains(KeyValuePair item) + => _valuesByKey.Contains(item); + + /// + public bool ContainsKey(TKey key) + => _valuesByKey.ContainsKey(key); + + /// + public void CopyTo( + KeyValuePair[] array, + int arrayIndex) + => _valuesByKey.CopyTo(array, arrayIndex); + + /// + public void Dispose() + { + _changeStream .OnCompleted(); + _collectionChanged .OnCompleted(); + _notificationsResumed .OnCompleted(); + + _changeStream .Dispose(); + _collectionChanged .Dispose(); + _notificationsResumed .Dispose(); + } + + /// + public void EnsureCapacity(int capacity) + => _valuesByKey.EnsureCapacity(capacity); + + /// + public Dictionary.Enumerator GetEnumerator() + => _valuesByKey.GetEnumerator(); + + /// + public IObservable ObserveValue(TKey key) + => ((_notificationSuspensionCount is 0) + ? Observable.Empty() + : Observable.Never() + .TakeUntil(_notificationsResumed)) + .Concat(Observable.Create(observer => + { + _valuesByKey.IsChangeCollectionEnabled = true; + + if (!_valuesByKey.TryGetValue(key, out var initialValue)) + { + observer.OnCompleted(); + return Disposable.Empty; + } + + observer.OnNext(initialValue); + return _changeStream + .Finally(_onChangeStreamFinalized) + .SubscribeSafe(Observer.Create>( + onNext: changeSet => + { + switch (changeSet.Type) + { + case ChangeSetType.Clear: + observer.OnCompleted(); + break; + + case ChangeSetType.Reset: + if (_valuesByKey.TryGetValue(key, out var value)) + observer.OnNext(value); + else + observer.OnCompleted(); + break; + + default: + foreach (var change in changeSet.Changes) + { + switch (change.Type) + { + case KeyedChangeType.Removal: + if (_valuesByKey.KeyComparer.Equals(key, change.AsRemoval().Key)) + observer.OnCompleted(); + break; + + case KeyedChangeType.Replacement: + var replacement = change.AsReplacement(); + if (_valuesByKey.KeyComparer.Equals(key, replacement.Key)) + observer.OnNext(replacement.NewItem); + break; + } + } + break; + } + }, + onError: observer.OnError, + onCompleted: observer.OnCompleted)); + })); + + /// + public bool Remove(TKey key) + { + var result = _valuesByKey.Remove(key); + + PublishPendingNotifications(); + + return result; + } + + /// + public bool Remove(KeyValuePair item) + { + var result = _valuesByKey.Remove(item); + + PublishPendingNotifications(); + + return result; + } + + /// + public void RemoveRange(IEnumerable keys) + { + _valuesByKey.RemoveRange(keys); + + PublishPendingNotifications(); + } + + /// + public void RemoveRange(ReadOnlySpan keys) + { + _valuesByKey.RemoveRange(keys); + + PublishPendingNotifications(); + } + + /// + public void Reset( + IEnumerable values, + Func keySelector) + { + _valuesByKey.Reset(values, keySelector); + + PublishPendingNotifications(); + } + + /// + public void Reset( + ReadOnlySpan values, + Func keySelector) + { + _valuesByKey.Reset(values, keySelector); + + PublishPendingNotifications(); + } + + /// + public void Reset(IEnumerable> items) + { + _valuesByKey.Reset(items); + + PublishPendingNotifications(); + } + + /// + public void Reset(ReadOnlySpan> items) + { + _valuesByKey.Reset(items); + + PublishPendingNotifications(); + } + + /// + public IDisposable Subscribe(IObserver> observer) + { + _valuesByKey.IsChangeCollectionEnabled = true; + + return ((_notificationSuspensionCount is 0) + ? Observable.Empty() + : _notificationsResumed + .Take(1)) + .Select(_ => _changeStream + .Finally(_onChangeStreamFinalized) + .Prepend(KeyedChangeSet.BulkAddition(_valuesByKey))) + .Switch() + .Subscribe(observer); + } + + /// + public NotificationSuspension SuspendNotifications() + { + ++_notificationSuspensionCount; + return new(this); + } + + /// + public bool TryGetValue( + TKey key, + [MaybeNullWhen(false)] out TValue value) + => _valuesByKey.TryGetValue(key, out value); + + bool ICollection>.IsReadOnly + => false; + + ICollection IDictionary.Keys + => _valuesByKey.Keys; + + IEnumerable IReadOnlyDictionary.Keys + => _valuesByKey.Keys; + + ICollection IDictionary.Values + => _valuesByKey.Values; + + IEnumerable IReadOnlyDictionary.Values + => _valuesByKey.Values; + + IEnumerator IEnumerable.GetEnumerator() + => _valuesByKey.GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() + => _valuesByKey.GetEnumerator(); + + IDisposable ISubjectDictionary.SuspendNotifications() + => SuspendNotifications(); + + private void OnNotificationSuspensionDisposed() + { + --_notificationSuspensionCount; + if (_notificationSuspensionCount is 0) + { + PublishPendingNotifications(); + + _notificationsResumed.OnNext(Unit.Default); + } + } + + private void PublishPendingNotifications() + { + if ((_notificationSuspensionCount is not 0) || !_valuesByKey.IsDirty) + return; + + _collectionChanged.OnNext(Unit.Default); + + _changeStream.OnNext(_valuesByKey.CaptureChangesAndClean()); + } + + /// + /// A value that controls suspension of notifications, for a . Will trigger notifications to be resumed, when disposed. + /// + public struct NotificationSuspension + : IDisposable + { + private SubjectDictionary? _owner; + + internal NotificationSuspension(SubjectDictionary owner) + => _owner = owner; + + /// + public void Dispose() + { + if (_owner is not null) + { + _owner.OnNotificationSuspensionDisposed(); + _owner = null; + } + } + } +} diff --git a/src/DynamicDataVNext/Sorted/ChangeTrackingList.cs b/src/DynamicDataVNext/Sorted/ChangeTrackingList.cs new file mode 100644 index 0000000..3691c29 --- /dev/null +++ b/src/DynamicDataVNext/Sorted/ChangeTrackingList.cs @@ -0,0 +1,416 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace DynamicDataVNext; + +public sealed class ChangeTrackingList + : IExtendedList, + IReadOnlyList +{ + private readonly SortedChangeSet.Builder _changeCollector; + private readonly IEqualityComparer _comparer; + private readonly List _items; + + private bool _isChangeCollectionEnabled; + private bool _isDirty; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for detecting item changes, within the collection, or if should be used. + public ChangeTrackingList( + int? capacity = null, + IEqualityComparer? comparer = null) + { + _comparer = comparer ?? EqualityComparer.Default; + + _items = (capacity is int givenCapacity) + ? new(capacity: givenCapacity) + : new(); + + _changeCollector = new(); + _isChangeCollectionEnabled = true; + } + + /// + public T this[int index] + { + get => _items[index]; + set + { + if (_items.Count > index) + { + var oldValue = _items[index]; + + if (_comparer.Equals(oldValue, value)) + return; + + _items[index] = value; + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(SortedChange.Replacement( + index: index, + oldItem: oldValue, + newItem: value)); + } + else + { + _items.Add(value); + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(SortedChange.Insertion( + index: index, + item: value)); + } + + _isDirty = true; + } + } + + /// + /// The comparer to be used for detecting value changes, within the collection. + /// + public IEqualityComparer Comparer + => _comparer; + + /// + public int Count + => _items.Count; + + /// + /// A flag indicating whether the collection should actually collect changes, to be retrieved by calls to . + /// Defaults to . + /// + /// + /// Note that any changes previously collected, but not captured, when this property is set to will be discarded. Otherwise, it would be possible for to generate a corrupt changes, when next called, after setting this property back to . + /// + public bool IsChangeCollectionEnabled + { + get => _isChangeCollectionEnabled; + set + { + if ((value is false) && (_changeCollector.Count is not 0)) + _changeCollector.Clear(); + + _isChangeCollectionEnabled = value; + } + } + + /// + /// A flag indicating whether changes have been made to the collection since its creation, or since the last call to `. + /// + public bool IsDirty + => _isDirty; + + /// + public void Add(T item) + { + _items.Add(item); + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(SortedChange.Insertion( + index: _items.Count - 1, + item: item)); + + _isDirty = true; + } + + /// + public void AddRange(IEnumerable items) + { + if (_isChangeCollectionEnabled) + { + if (items.TryGetNonEnumeratedCount(out var itemCount)) + { + _items.EnsureCapacity(_items.Count + itemCount); + + _changeCollector.EnsureCapacity(itemCount); + } + + foreach (var item in items) + { + _items.Add(item); + + _changeCollector.AddChange(SortedChange.Insertion( + index: _items.Count - 1, + item: item)); + + _isDirty = true; + } + } + else + { + var oldCount = _items.Count; + + _items.AddRange(items); + + _isDirty = oldCount != _items.Count; + } + } + + /// + public void AddRange(ReadOnlySpan items) + { + if (items.Length is 0) + return; + + if (_isChangeCollectionEnabled) + { + _items.EnsureCapacity(_items.Count + items.Length); + + _changeCollector.EnsureCapacity(items.Length); + + foreach (var item in items) + { + _items.Add(item); + + _changeCollector.AddChange(SortedChange.Insertion( + index: _items.Count - 1, + item: item)); + } + } + else + { + _items.AddRange(items); + } + + _isDirty = true; + } + + /// + /// Captures any previously-collected changes made to the collection, and resets the collection to a "clean" state (I.E. sets to ). + /// + /// A containing all changes made to the collection since its construction, the last call to , or since was last changed to . + /// + /// Note that this method will always return an empty changeset, when is . + /// + public SortedChangeSet CaptureChangesAndClean() + { + _isDirty = false; + return _changeCollector.BuildAndClear(); + } + + /// + public void Clear() + { + if (_items.Count is 0) + return; + + _changeCollector.EnsureCapacity(_items.Count); + + if (_isChangeCollectionEnabled) + { + for (var index = _items.Count - 1; index >= 0; --index) + _changeCollector.AddChange(SortedChange.Removal( + index: index, + item: _items[index])); + + _changeCollector.OnSourceCleared(); + } + + _items.Clear(); + + _isDirty = true; + } + + /// + public bool Contains(T item) + => _items.Contains(item); + + /// + public void CopyTo( + T[] array, + int arrayIndex) + => _items.CopyTo(array, arrayIndex); + + /// + public void EnsureCapacity(int capacity) + => _items.EnsureCapacity(capacity); + + /// + public List.Enumerator GetEnumerator() + => _items.GetEnumerator(); + + /// + public int IndexOf(T item) + => _items.IndexOf(item); + + /// + public void Insert( + int index, + T item) + => _items.Insert(index, item); + + /// + public void InsertRange( + int index, + IEnumerable items) + { + if (_isChangeCollectionEnabled) + { + if (items.TryGetNonEnumeratedCount(out var itemCount)) + { + _items.EnsureCapacity(_items.Count + itemCount); + + _changeCollector.EnsureCapacity(itemCount); + } + + var insertionIndex = index; + foreach (var item in items) + { + _items.Insert( + index: insertionIndex, + item: item); + + _changeCollector.AddChange(SortedChange.Insertion( + index: insertionIndex, + item: item)); + + ++insertionIndex; + _isDirty = true; + } + } + else + { + var oldCount = _items.Count; + + _items.InsertRange(index, items); + + _isDirty = oldCount != _items.Count; + } + } + + /// + public void InsertRange( + int index, + ReadOnlySpan items) + { + if (items.Length is 0) + return; + + if (_isChangeCollectionEnabled) + { + _items.EnsureCapacity(_items.Count + items.Length); + + _changeCollector.EnsureCapacity(items.Length); + + var insertionIndex = index; + foreach (var item in items) + { + _items.Insert( + index: insertionIndex, + item: item); + + _changeCollector.AddChange(SortedChange.Insertion( + index: insertionIndex, + item: item)); + + ++insertionIndex; + } + } + else + { + _items.InsertRange(index, items); + } + + _isDirty = true; + } + + /// + public void Move(int oldIndex, int newIndex) + { + if (oldIndex == newIndex) + return; + + var item = _items[oldIndex]; + + _items.RemoveAt(oldIndex); + _items.Insert( + index: newIndex, + item: item); + + if (_isChangeCollectionEnabled) + _changeCollector.AddChange(SortedChange.Movement(oldIndex, newIndex, item)); + + _isDirty = true; + } + + /// + public bool Remove(T item) + { + if (_isChangeCollectionEnabled) + { + var index = _items.IndexOf(item); + if (index < 0) + return false; + + _items.RemoveAt(index); + + _isDirty = true; + return true; + } + else + { + var result = _items.Remove(item); + + _isDirty |= result; + + return result; + } + } + + /// + public void RemoveAt(int index) + { + if (_isChangeCollectionEnabled) + { + var item = _items[index]; + + _changeCollector.AddChange(SortedChange.Removal(index, item)); + } + + _items.RemoveAt(index); + + _isDirty = true; + } + + /// + public void RemoveRange( + int index, + int count) + { + if (count is 0) + return; + + if (_isChangeCollectionEnabled) + { + for (var i = index + count - 1; i >= index; --i) + { + var item = _items[i]; + + _changeCollector.AddChange(SortedChange.Removal( + index: i, + item: item)); + } + + if (_items.Count == count) + _changeCollector.OnSourceCleared(); + } + + _items.RemoveRange(index, count); + + _isDirty = true; + } + + bool ICollection.IsReadOnly + => false; + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); +} diff --git a/src/DynamicDataVNext/Sorted/IExtendedList.cs b/src/DynamicDataVNext/Sorted/IExtendedList.cs new file mode 100644 index 0000000..a77040a --- /dev/null +++ b/src/DynamicDataVNext/Sorted/IExtendedList.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace DynamicDataVNext; + +/// +/// Describes an extended version of , supporting range and movement operations. +/// +/// The type of the items in the collection. +public interface IExtendedList + : IList +{ + /// + /// Adds a range of items to the end of the list. + /// + /// The items to be added. + /// Throws for . + void AddRange(IEnumerable items); + + /// + /// Adds a range of items to the end of the list. + /// + /// The items to be added. + void AddRange(ReadOnlySpan items); + + /// + /// Inserts a range of items into the list. + /// + /// The index at which the first item in the range should be inserted. + /// The items to be inserted. + /// Throws when does not represent a valid index of an item in the list, or the next available index of the list. + /// Throws for . + void InsertRange( + int index, + IEnumerable items); + + /// + /// Inserts a range of items into the list. + /// + /// The index at which the first item in the range should be inserted. + /// The items to be inserted. + /// Throws when does not represent a valid index of an item in the list, or the next available index of the list. + /// Throws for . + void InsertRange( + int index, + ReadOnlySpan items); + + /// + /// Moves an item within the list. + /// + /// The index of the item to be moved, before the operation. + /// The desired index of the item to be moved, after the operation. + /// Throws when or does not represent a valid index of an item in the list. + void Move( + int oldIndex, + int newIndex); + + /// + /// Removes a range of consecutive items from the list. + /// + /// The index of the first item to be removed. + /// The number of items to be removed. + /// Throws when does not represent a valid index of an item within the list. + /// Throws when and define a range that extends beyond the end of the list. + void RemoveRange( + int index, + int count); +} diff --git a/src/DynamicDataVNext/Sorted/IObservableList.cs b/src/DynamicDataVNext/Sorted/IObservableList.cs index 96a55c0..a495032 100644 --- a/src/DynamicDataVNext/Sorted/IObservableList.cs +++ b/src/DynamicDataVNext/Sorted/IObservableList.cs @@ -17,7 +17,4 @@ public interface IObservableList /// IObservable ObserveValue(int index); - - /// - IDisposable SuspendNotifications(); } diff --git a/src/DynamicDataVNext/Sorted/ISubjectList.cs b/src/DynamicDataVNext/Sorted/ISubjectList.cs index c970732..1fa8f5e 100644 --- a/src/DynamicDataVNext/Sorted/ISubjectList.cs +++ b/src/DynamicDataVNext/Sorted/ISubjectList.cs @@ -9,7 +9,7 @@ namespace DynamicDataVNext; /// /// The type of the items in the collection. public interface ISubjectList - : IList, + : IExtendedList, IObservable> { /// @@ -17,55 +17,17 @@ public interface ISubjectList /// IObservable CollectionChanged { get; } - /// - /// Adds a range of items to the end of the list. - /// - /// The items to be added. - /// Throws for . - void AddRange(IEnumerable items); - - /// - /// Inserts a range of items into the list. - /// - /// The index at which the first item in the range should be inserted. - /// The items to be inserted. - /// Throws when does not represent a valid index of an item in the list, or the next available index of the list. - /// Throws for . - void InsertRange( - int index, - IEnumerable items); - - /// - /// Moves an item within the list. - /// - /// The index of the item to be moved, before the operation. - /// The desired index of the item to be moved, after the operation. - /// Throws when or does not represent a valid index of an item in the list. - void Move( - int oldIndex, - int newIndex); - /// /// Allows subscribers to observe the item in the collection, at a particular index, as it changes. /// /// The index whose item is to be observed. /// A stream which will publish the latest item, for the given index, within the collection. + /// Throws if is negative. /// /// The returned stream will always immediately publish the current item for the given index, upon subscription, and will complete if the given index is removed. If the index is not present within the collection upon subscription, the stream will complete immediately. /// IObservable ObserveValue(int index); - /// - /// Removes a range of consecutive items from the list. - /// - /// The index of the first item to be removed. - /// The number of items to be removed. - /// Throws when does not represent a valid index of an item within the list. - /// Throws when and define a range that extends beyond the end of the list. - void RemoveRange( - int index, - int count); - /// /// Temporarily suspends the publication of notifications by the collection, until the returned object is disposed, at which point all mutations made during the suspension will (if any) will be published as one notification. /// diff --git a/src/DynamicDataVNext/Sorted/SortedMovement.cs b/src/DynamicDataVNext/Sorted/SortedMovement.cs index d242333..814fe63 100644 --- a/src/DynamicDataVNext/Sorted/SortedMovement.cs +++ b/src/DynamicDataVNext/Sorted/SortedMovement.cs @@ -20,4 +20,13 @@ public readonly record struct SortedMovement /// The index of the item within the collection, before being moved. /// public required int OldIndex { get; init; } + + /// + /// Checks whether a given index is affected by this movement change. + /// + /// The index to check. + /// if the given index falls within the range of moved items represented by this change; otherwise. + public bool IsIndexAffected(int index) + => ((index >= NewIndex) && (index <= OldIndex)) + || ((index >= OldIndex) && (index <= NewIndex)); } diff --git a/src/DynamicDataVNext/Sorted/SubjectList.cs b/src/DynamicDataVNext/Sorted/SubjectList.cs new file mode 100644 index 0000000..63a139a --- /dev/null +++ b/src/DynamicDataVNext/Sorted/SubjectList.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace DynamicDataVNext; + +/// +/// The basic implementation of , providing simple collection and change notification functionality, with no concurrency or thread-safety. +/// +/// The type of the items in the collection. +public sealed class SubjectList + : ISubjectList, + IObservableList, + IDisposable + where T : notnull +{ + private readonly Subject> _changeStream; + private readonly Subject _collectionChanged; + private readonly Subject _notificationsResumed; + private readonly Action _onChangeStreamFinalized; + private readonly ChangeTrackingList _items; + + private int _notificationSuspensionCount; + + /// + /// Constructs a new instance of the class. + /// + /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory. + /// The comparer to be used for detecting item changes, within the collection, or if should be used. + public SubjectList( + int? capacity = null, + IEqualityComparer? comparer = null) + { + _collectionChanged = new(); + _changeStream = new(); + _notificationsResumed = new(); + _items = new( + capacity: capacity, + comparer: comparer); + + _onChangeStreamFinalized = () => _items.IsChangeCollectionEnabled = _changeStream.HasObservers; + } + + /// + public T this[int index] + { + get => _items[index]; + set + { + _items[index] = value; + + PublishPendingNotifications(); + } + } + + /// + public IObservable CollectionChanged + => _collectionChanged; + + /// + public int Count + => _items.Count; + + /// + public void Add(T item) + { + _items.Add(item); + + PublishPendingNotifications(); + } + + /// + public void AddRange(IEnumerable items) + { + _items.AddRange(items); + + PublishPendingNotifications(); + } + + /// + public void AddRange(ReadOnlySpan items) + { + _items.AddRange(items); + + PublishPendingNotifications(); + } + + /// + public void Clear() + { + _items.Clear(); + + PublishPendingNotifications(); + } + + /// + public bool Contains(T item) + => _items.Contains(item); + + /// + public void CopyTo(T[] array, int arrayIndex) + => _items.CopyTo(array, arrayIndex); + + /// + public void Dispose() + { + _changeStream .OnCompleted(); + _collectionChanged .OnCompleted(); + _notificationsResumed .OnCompleted(); + + _changeStream .Dispose(); + _collectionChanged .Dispose(); + _notificationsResumed .Dispose(); + } + + /// + public void EnsureCapacity(int capacity) + => _items.EnsureCapacity(capacity); + + /// + public List.Enumerator GetEnumerator() + => _items.GetEnumerator(); + + /// + public int IndexOf(T item) + => _items.IndexOf(item); + + /// + public void Insert(int index, T item) + { + _items.Insert(index, item); + + PublishPendingNotifications(); + } + + /// + public void InsertRange(int index, IEnumerable items) + { + _items.InsertRange(index, items); + + PublishPendingNotifications(); + } + + /// + public void InsertRange(int index, ReadOnlySpan items) + { + _items.InsertRange(index, items); + + PublishPendingNotifications(); + } + + /// + public void Move(int oldIndex, int newIndex) + { + _items.Move(oldIndex, newIndex); + + PublishPendingNotifications(); + } + + /// + public IObservable ObserveValue(int index) + => ((_notificationSuspensionCount is 0) + ? Observable.Empty() + : Observable.Never() + .TakeUntil(_notificationsResumed)) + .Concat(Observable.Create(observer => + { + _items.IsChangeCollectionEnabled = true; + + if (index >= _items.Count) + { + observer.OnCompleted(); + return Disposable.Empty; + } + + var oldItem = _items[index]; + observer.OnNext(oldItem); + return _changeStream + .Finally(_onChangeStreamFinalized) + .SubscribeSafe(Observer.Create>( + onNext: changeSet => + { + switch (changeSet.Type) + { + case ChangeSetType.Clear: + observer.OnCompleted(); + break; + + case ChangeSetType.Reset: + if (index < _items.Count) + { + oldItem = _items[index]; + observer.OnNext(oldItem); + } + else + observer.OnCompleted(); + break; + + default: + if (index < _items.Count) + { + var newItem = _items[index]; + if (_items.Comparer.Equals(oldItem, newItem)) + break; + oldItem = newItem; + observer.OnNext(newItem); + } + else + observer.OnCompleted(); + break; + } + }, + onError: observer.OnError, + onCompleted: observer.OnCompleted)); + })); + + /// + public bool Remove(T item) + { + var result = _items.Remove(item); + + PublishPendingNotifications(); + + return result; + } + + /// + public void RemoveAt(int index) + { + _items.RemoveAt(index); + + PublishPendingNotifications(); + } + + /// + public void RemoveRange(int index, int count) + { + _items.RemoveRange(index, count); + + PublishPendingNotifications(); + } + + /// + public IDisposable Subscribe(IObserver> observer) + { + _items.IsChangeCollectionEnabled = true; + + return ((_notificationSuspensionCount is 0) + ? Observable.Empty() + : _notificationsResumed + .Take(1)) + .Select(_ => _changeStream + .Finally(_onChangeStreamFinalized) + .Prepend(SortedChangeSet.RangeInsertion( + index: 0, + items: _items))) + .Switch() + .Subscribe(observer); + } + + /// + public NotificationSuspension SuspendNotifications() + { + ++_notificationSuspensionCount; + return new(this); + } + + bool ICollection.IsReadOnly + => false; + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => _items.GetEnumerator(); + + IDisposable ISubjectList.SuspendNotifications() + => SuspendNotifications(); + + private void OnNotificationSuspensionDisposed() + { + --_notificationSuspensionCount; + if (_notificationSuspensionCount is 0) + { + PublishPendingNotifications(); + + _notificationsResumed.OnNext(Unit.Default); + } + } + + private void PublishPendingNotifications() + { + if ((_notificationSuspensionCount is not 0) || !_items.IsDirty) + return; + + _collectionChanged.OnNext(Unit.Default); + + _changeStream.OnNext(_items.CaptureChangesAndClean()); + } + + /// + /// A value that controls suspension of notifications, for a . Will trigger notifications to be resumed, when disposed. + /// + public struct NotificationSuspension + : IDisposable + { + private SubjectList? _owner; + + internal NotificationSuspension(SubjectList owner) + => _owner = owner; + + /// + public void Dispose() + { + if (_owner is not null) + { + _owner.OnNotificationSuspensionDisposed(); + _owner = null; + } + } + } +}