< Summary

Information
Class: MyNet.Observable.Collections.ExtendedCollection
Assembly: MyNet.Observable
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Observable/Collections/ExtendedCollection.cs
Tag: 323_28699572109
Line coverage
75%
Covered lines: 3
Uncovered lines: 1
Coverable lines: 4
Total lines: 498
Line coverage: 75%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Create(...)100%11100%
From(...)100%11100%
FromReadOnly(...)100%11100%
FromObservable(...)100%210%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Observable/Collections/ExtendedCollection.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ExtendedCollection.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections;
 9using System.Collections.Generic;
 10using System.Collections.ObjectModel;
 11using System.Collections.Specialized;
 12using System.ComponentModel;
 13using System.Diagnostics;
 14using System.Diagnostics.CodeAnalysis;
 15using System.Reactive.Concurrency;
 16using System.Reactive.Disposables;
 17using System.Reactive.Linq;
 18using System.Runtime.InteropServices;
 19using DynamicData;
 20using MyNet.Observable.Collections.Filters;
 21using MyNet.Observable.Collections.Grouping;
 22using MyNet.Observable.Collections.Paging;
 23using MyNet.Observable.Collections.Sorting;
 24using MyNet.Observable.Collections.Sources;
 25using MyNet.Primitives;
 26
 27namespace MyNet.Observable.Collections;
 28
 29/// <summary>
 30/// Provides factory methods for creating instances of the <see cref="ExtendedCollection{T}"/> class. The <see cref="Ext
 31/// </summary>
 32[SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "This class serves as 
 33public static class ExtendedCollection
 34{
 35    /// <summary>
 36    /// Creates a new instance of the <see cref="ExtendedCollection{T}"/> class with an optional scheduler for observing
 37    /// </summary>
 38    /// <param name="scheduler">The scheduler on which the collection's operations will be performed.</param>
 39    /// <typeparam name="T">The type of items managed by the extended collection.</typeparam>
 40    /// <returns>The created <see cref="ExtendedCollection{T}"/> instance.</returns>
 41    public static ExtendedCollection<T> Create<T>(IScheduler? scheduler = null)
 42        where T : notnull
 3943        => new(SourceEngine<T>.Empty(), scheduler);
 44
 45    /// <summary>
 46    /// Creates a new instance of the <see cref="ExtendedCollection{T}"/> class from an existing enumerable collection o
 47    /// </summary>
 48    /// <param name="items">The enumerable collection of items to initialize the extended collection with.</param>
 49    /// <param name="scheduler">The scheduler on which the collection's operations will be performed.</param>
 50    /// <typeparam name="T">The type of items managed by the extended collection.</typeparam>
 51    /// <returns>The created <see cref="ExtendedCollection{T}"/> instance.</returns>
 52    public static ExtendedCollection<T> From<T>(IEnumerable<T> items, IScheduler? scheduler = null)
 53        where T : notnull
 6954        => new(SourceEngine<T>.From(items, readOnly: false), scheduler);
 55
 56    /// <summary>
 57    /// Creates a new instance of the <see cref="ExtendedCollection{T}"/> class from an existing enumerable collection o
 58    /// </summary>
 59    /// <param name="items">The enumerable collection of items to initialize the extended collection with.</param>
 60    /// <param name="scheduler">The scheduler on which the collection's operations will be performed.</param>
 61    /// <typeparam name="T">The type of items managed by the extended collection.</typeparam>
 62    /// <returns>The created <see cref="ExtendedCollection{T}"/> instance.</returns>
 63    public static ExtendedCollection<T> FromReadOnly<T>(IEnumerable<T> items, IScheduler? scheduler = null)
 64        where T : notnull
 1265        => new(SourceEngine<T>.From(items, readOnly: true), scheduler);
 66
 67    /// <summary>
 68    /// Creates a new instance of the <see cref="ExtendedCollection{T}"/> class from an observable sequence of change se
 69    /// </summary>
 70    /// <param name="source">The observable sequence of change sets to initialize the extended collection with.</param>
 71    /// <param name="scheduler">The scheduler on which the collection's operations will be performed.</param>
 72    /// <typeparam name="T">The type of items managed by the extended collection.</typeparam>
 73    /// <returns>The created <see cref="ExtendedCollection{T}"/> instance.</returns>
 74    public static ExtendedCollection<T> FromObservable<T>(IObservable<IChangeSet<T>> source, IScheduler? scheduler = nul
 75        where T : notnull
 076        => new(SourceEngine<T>.FromObservable(source), scheduler);
 77}
 78
 79/// <summary>
 80/// Represents an extended collection that supports filtering, sorting, and change notifications. This collection is des
 81/// </summary>
 82/// <typeparam name="T">The type of items managed by the extended collection.</typeparam>
 83[ComVisible(false)]
 84[DebuggerDisplay("Count = {Count}/{SourceCount}")]
 85[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed in DisposeManagedReso
 86public class ExtendedCollection<T> : ObservableObject, ICollection<T>, IReadOnlyList<T>, INotifyCollectionChanged
 87    where T : notnull
 88{
 89    private readonly SourceEngine<T> _source;
 90    private readonly FilterEngine<T> _filterEngine = new();
 91    private readonly SortEngine<T> _sortEngine = new();
 92    private readonly GroupingEngine<T> _groupingEngine = new();
 93    private readonly PagingEngine _pagingEngine = new();
 94    private readonly PropertyDependencyExtractor<T> _dependencyExtractor = new();
 95
 96    private readonly IObservable<IChangeSet<T>> _filteredObservable;
 97    private readonly IObservable<IChangeSet<T>> _itemsObservable;
 98    private readonly IObservable<IChangeSet<T>> _sourceObservable;
 99    private readonly ReadOnlyObservableCollection<T> _filteredItems;
 100    private readonly ReadOnlyObservableCollection<T> _items;
 101    private readonly ReadOnlyObservableCollection<T> _sortedSource;
 102    private readonly IObservable<IReadOnlyList<CollectionGroup<T>>> _groupsObservable;
 103    private readonly Dictionary<string, int> _filterPropertyUsage = [];
 104    private readonly Dictionary<string, int> _sortPropertyUsage = [];
 105    private int _count;
 106    private int _sourceCount;
 107
 108    /// <summary>
 109    /// Initializes a new instance of the <see cref="ExtendedCollection{T}"/> class with a specified source list, read-o
 110    /// </summary>
 111    /// <param name="source">The source list to initialize the collection with.</param>
 112    /// <param name="scheduler">The scheduler to use for observing collection changes. If null, the current thread sched
 113    public ExtendedCollection(SourceEngine<T> source, IScheduler? scheduler = null)
 114    {
 115        _source = source;
 116        IsReadOnly = source.IsReadOnly;
 117
 118        (_sourceObservable, _filteredObservable) = PipelineEngine.Build(_source.Connect(), _filterEngine, _sortEngine);
 119        _itemsObservable = PipelineEngine.ApplyPaging(_filteredObservable, _pagingEngine);
 120
 121        // Groups observable: recomputed reactively whenever filtered items or grouping config changes
 122        _groupsObservable = _filteredObservable.ToCollection()
 123            .CombineLatest(
 124                _groupingEngine.Grouping,
 125                (items, grouping) => GroupingEngine<T>.ComputeGroups([.. items], grouping));
 126
 127        Disposables.Add(_sourceObservable.ObserveOnOptional(scheduler).Bind(out _sortedSource).Subscribe(_ => UpdateSour
 128        Disposables.Add(_filteredObservable.ObserveOnOptional(scheduler).Bind(out _filteredItems).Subscribe(_ => UpdateF
 129        Disposables.Add(_itemsObservable.ObserveOnOptional(scheduler).Bind(out _items).Subscribe());
 130        Disposables.Add(_source.Connect().SubscribeMany(SubscribeToItemChanges).Subscribe());
 131        Disposables.Add(ObserveCollectionChanges(_items).Subscribe(HandleCollectionChanged));
 132
 133        UpdateSourceCount();
 134        UpdateFilteredCount();
 135    }
 136
 137    /// <summary>
 138    /// Gets a read-only observable collection of items after applying filters and sorting, before paging.
 139    /// </summary>
 140    public ReadOnlyObservableCollection<T> FilteredItems => _filteredItems;
 141
 142    /// <summary>
 143    /// Gets a read-only observable collection of items that represents the current page after applying filters, sorting
 144    /// </summary>
 145    public ReadOnlyObservableCollection<T> Items => _items;
 146
 147    /// <summary>
 148    /// Gets a read-only observable collection of items that represents the current state of the source collection after
 149    /// </summary>
 150    public ReadOnlyObservableCollection<T> Source => _sortedSource;
 151
 152    /// <summary>
 153    /// Gets the count of items in the source collection after sorting but before filtering. This count reflects the num
 154    /// </summary>
 155    public int SourceCount => _sourceCount;
 156
 157    /// <summary>
 158    /// Gets the collection of sorting properties that are applied to the items in the collection. The Sorting property 
 159    /// </summary>
 160    public ISortingProperty<T>[] CurrentSort => _sortEngine.Current;
 161
 162    /// <summary>
 163    /// Gets the collection of filters that are applied to the items in the collection. The Filters property allows you 
 164    /// </summary>
 165    public IFilter<T>? CurrentFilter => _filterEngine.Current;
 166
 167    /// <summary>
 168    /// Observes item changes after filter/sort operations, before paging is applied.
 169    /// </summary>
 170    public IObservable<IChangeSet<T>> ConnectFiltered() => _filteredObservable;
 171
 172    /// <summary>
 173    /// Connects to the collection and returns an observable sequence of change sets representing the changes to the ite
 174    /// </summary>
 175    public IObservable<IChangeSet<T>> Connect() => _itemsObservable;
 176
 177    /// <summary>
 178    /// Connects to the source collection and returns an observable sequence of change sets representing the changes to 
 179    /// </summary>
 180    public IObservable<IChangeSet<T>> ConnectSource() => _sourceObservable;
 181
 182    /// <summary>
 183    /// Performs cleanup of resources used by the collection, including disposing of the filter engine, sort engine, and
 184    /// </summary>
 185    protected override void DisposeManagedResources()
 186    {
 187        _filterEngine.Dispose();
 188        _sortEngine.Dispose();
 189        _groupingEngine.Dispose();
 190        _pagingEngine.Dispose();
 191        _source.Dispose();
 192
 193        base.DisposeManagedResources();
 194    }
 195
 196    #region Grouping
 197
 198    /// <summary>
 199    /// Gets the currently active grouping properties applied to the collection.
 200    /// </summary>
 201    public IGroupingProperty<T>[] CurrentGrouping => _groupingEngine.Current;
 202
 203    /// <summary>
 204    /// Returns an observable sequence that emits the current groups whenever the items or grouping configuration change
 205    /// Emits an empty list when no grouping is active.
 206    /// </summary>
 207    public IObservable<IReadOnlyList<CollectionGroup<T>>> ConnectGroups() => _groupsObservable;
 208
 209    /// <summary>
 210    /// Sets the grouping properties for the collection. The collection will reactively recompute its groups.
 211    /// </summary>
 212    /// <param name="grouping">The grouping properties to apply.</param>
 213    public void SetGrouping(params IGroupingProperty<T>[] grouping) => _groupingEngine.Set(grouping);
 214
 215    /// <summary>
 216    /// Clears all grouping properties, producing an empty groups list.
 217    /// </summary>
 218    public void ClearGrouping() => _groupingEngine.Clear();
 219
 220    #endregion
 221
 222    #region Filter
 223
 224    /// <summary>
 225    /// Sets the filter for the collection using the specified filter node. This method updates the filter engine with t
 226    /// </summary>
 227    /// <param name="filter">The filter to apply to the collection.</param>
 228    public void SetFilter(IFilter<T> filter)
 229    {
 230        _filterEngine.Set(filter);
 231        RebuildFilterDependencies(filter);
 232    }
 233
 234    /// <summary>
 235    /// Clears the filter from the collection, effectively removing all filtering criteria and including all items in th
 236    /// </summary>
 237    public void ClearFilter()
 238    {
 239        _filterEngine.Clear();
 240        _filterPropertyUsage.Clear();
 241    }
 242
 243    #endregion
 244
 245    #region Sorting
 246
 247    /// <summary>
 248    /// Sets the sorting properties for the collection using the specified array of sorting properties. This method upda
 249    /// </summary>
 250    /// <param name="sorting">The array of sorting properties to apply to the collection.</param>
 251    public void SetSorting(params ISortingProperty<T>[] sorting)
 252    {
 253        _sortEngine.Set(sorting);
 254        RebuildSortDependencies(sorting);
 255    }
 256
 257    /// <summary>
 258    /// Clears the sorting properties from the collection, effectively removing all sorting criteria and returning to th
 259    /// </summary>
 260    public void ClearSorting()
 261    {
 262        _sortEngine.Clear();
 263        _sortPropertyUsage.Clear();
 264    }
 265
 266    #endregion
 267
 268    #region Paging
 269
 270    /// <summary>
 271    /// Sets the active paging window applied to the filtered and sorted items exposed through <see cref="Items"/>.
 272    /// </summary>
 273    /// <param name="page">The one-based page index.</param>
 274    /// <param name="pageSize">The page size.</param>
 275    public void SetPaging(int page, int pageSize) => _pagingEngine.Set(page, pageSize);
 276
 277    /// <summary>
 278    /// Clears paging and exposes the full filtered and sorted collection through <see cref="Items"/>.
 279    /// </summary>
 280    public void ClearPaging() => _pagingEngine.Clear();
 281
 282    #endregion
 283
 284    #region INotifyCollectionChanged
 285
 286    /// <summary>
 287    /// Occurs when the collection changes. This event is raised whenever items are added, removed, or replaced in the c
 288    /// </summary>
 289    [SuppressMessage("Design", "CA1033:Interface methods should be callable by child types", Justification = "Use OnColl
 290    event NotifyCollectionChangedEventHandler? INotifyCollectionChanged.CollectionChanged
 291    {
 292        add => CollectionChanged += value;
 293        remove => CollectionChanged -= value;
 294    }
 295
 296    /// <summary>
 297    /// Raises the CollectionChanged event with the specified event arguments. This method is called internally to notif
 298    /// </summary>
 299    [SuppressMessage("Roslynator", "RCS1159:Use EventHandler<T>", Justification = "Use OnCollectionChanged")]
 300    private event NotifyCollectionChangedEventHandler? CollectionChanged;
 301
 302    /// <summary>
 303    /// Handles the collection changed event by invoking the CollectionChanged event with the provided event arguments. 
 304    /// </summary>
 305    /// <param name="e">The event arguments describing the change that occurred in the collection.</param>
 306    private void HandleCollectionChanged(NotifyCollectionChangedEventArgs e) => CollectionChanged?.Invoke(this, e);
 307
 308    #endregion INotifyCollectionChanged
 309
 310    #region ICollection
 311
 312    /// <summary>
 313    /// Gets the number of items in the collection after filtering and sorting are applied, before paging.
 314    /// </summary>
 315    public int Count => _count;
 316
 317    /// <summary>
 318    /// Gets a value indicating whether the collection is read-only. If true, the collection does not allow modification
 319    /// </summary>
 320    public bool IsReadOnly { get; }
 321
 322    /// <summary>
 323    /// Gets the item at the specified index in the collection after filtering and sorting are applied. This index is ba
 324    /// </summary>
 325    /// <param name="index">The zero-based index of the item to get.</param>
 326    /// <returns>The item at the specified index.</returns>
 327    public T this[int index] => _items[index];
 328
 329    /// <summary>
 330    /// Adds an item to the collection if it is not read-only. This method checks the IsReadOnly property before attempt
 331    /// </summary>
 332    /// <param name="item">The item to add to the collection.</param>
 333    public void Add(T item) => IsReadOnly.IfFalse(() => _source.Add(item));
 334
 335    /// <summary>
 336    /// Removes all items from the collection if it is not read-only. This method checks the IsReadOnly property before 
 337    /// </summary>
 338    public void Clear() => IsReadOnly.IfFalse(() => _source.Clear());
 339
 340    /// <summary>
 341    /// Removes the first occurrence of a specific item from the collection if it is not read-only. This method checks t
 342    /// </summary>
 343    /// <param name="item">The item to remove from the collection.</param>
 344    /// <returns>True if the item was successfully removed; otherwise, false.</returns>
 345    public bool Remove(T item) => !IsReadOnly && _source.Remove(item);
 346
 347    /// <summary>
 348    /// Determines the index of a specific item in the collection after filtering and sorting are applied. This method s
 349    /// </summary>
 350    /// <param name="item">The item to locate in the collection.</param>
 351    /// <returns>The index of the item if found; otherwise, -1.</returns>
 352    public int IndexOf(T item) => _items.IndexOf(item);
 353
 354    /// <summary>
 355    /// Determines whether the collection contains a specific item after filtering is applied. This method checks for th
 356    /// </summary>
 357    /// <param name="item">The item to locate in the collection.</param>
 358    /// <returns>True if the item is found in the collection; otherwise, false.</returns>
 359    public bool Contains(T item) => _items.Contains(item);
 360
 361    /// <summary>
 362    /// Copies the elements of the collection to an array, starting at a particular array index. This method allows you 
 363    /// </summary>
 364    /// <param name="array">The destination array.</param>
 365    /// <param name="arrayIndex">The zero-based index in the array at which copying begins.</param>
 366    public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
 367
 368    /// <summary>
 369    /// Returns an enumerator that iterates through the collection. This method allows you to use foreach loops or LINQ 
 370    /// </summary>
 371    /// <returns>An enumerator that can be used to iterate through the collection.</returns>
 372    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
 373
 374    /// <summary>
 375    /// Returns an enumerator that iterates through the collection. This method is an explicit implementation of the non
 376    /// </summary>
 377    /// <returns>An enumerator that can be used to iterate through the collection.</returns>
 378    IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
 379
 380    #endregion
 381
 382    #region ICollection Extensions
 383
 384    /// <summary>
 385    /// Adds multiple items to the collection if it is not read-only. This method checks the IsReadOnly property before 
 386    /// </summary>
 387    /// <param name="items">The items to add to the collection.</param>
 388    public void AddRange(IEnumerable<T> items) => IsReadOnly.IfFalse(() => _source.AddRange(items));
 389
 390    /// <summary>
 391    /// Removes multiple items from the collection if it is not read-only. This method checks the IsReadOnly property be
 392    /// </summary>
 393    /// <param name="itemsToRemove">The items to remove from the collection.</param>
 394    public void RemoveMany(IEnumerable<T> itemsToRemove) => IsReadOnly.IfFalse(() => _source.RemoveMany(itemsToRemove));
 395
 396    /// <summary>
 397    /// Replaces the current items in the collection with the specified items if the collection is not read-only. This m
 398    /// </summary>
 399    /// <param name="items">The items to set in the collection.</param>
 400    public void Set(IEnumerable<T> items) => IsReadOnly.IfFalse(() => _source.Edit(x =>
 401    {
 402        x.Clear();
 403        x.AddRange(items);
 404    }));
 405
 406    #endregion
 407
 408    #region Observable
 409
 410    /// <summary>
 411    /// Observes collection changes on the specified source collection and returns an observable sequence of collection 
 412    /// </summary>
 413    /// <param name="source">The source collection to observe for changes.</param>
 414    /// <returns>An observable sequence of collection changed event arguments.</returns>
 415    private static IObservable<NotifyCollectionChangedEventArgs> ObserveCollectionChanges(
 416        INotifyCollectionChanged source) =>
 417        System.Reactive.Linq.Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEve
 418                h => source.CollectionChanged += h,
 419                h => source.CollectionChanged -= h)
 420            .Select(x => x.EventArgs);
 421
 422    /// <summary>
 423    /// Synchronizes <see cref="SourceCount"/> with the underlying source.
 424    /// </summary>
 425    private void UpdateSourceCount() =>
 426        SetProperty(ref _sourceCount, _source.Count, nameof(SourceCount));
 427
 428    /// <summary>
 429    /// Synchronizes <see cref="Count"/> with the filtered view.
 430    /// </summary>
 431    private void UpdateFilteredCount() =>
 432        SetProperty(ref _count, _filteredItems.Count, nameof(Count));
 433
 434    /// <summary>
 435    /// Subscribes to property changes of an item in the collection if it implements INotifyPropertyChanged. This method
 436    /// </summary>
 437    /// <param name="item">The item to observe for property changes.</param>
 438    /// <returns>A disposable that can be used to unsubscribe from property change notifications.</returns>
 439    private IDisposable SubscribeToItemChanges(T item) =>
 440        item is not INotifyPropertyChanged npc
 441            ? Disposable.Empty
 442            : System.Reactive.Linq.Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
 443                    h => npc.PropertyChanged += h,
 444                    h => npc.PropertyChanged -= h)
 445                .Subscribe(e =>
 446                {
 447                    var prop = e.EventArgs.PropertyName;
 448                    if (prop is null) return;
 449
 450                    var affectsFilter = _filterPropertyUsage.ContainsKey(prop);
 451                    var affectsSort = _sortPropertyUsage.ContainsKey(prop);
 452
 453                    if (affectsFilter)
 454                        _filterEngine.Invalidate();
 455
 456                    if (affectsSort)
 457                        _sortEngine.Invalidate();
 458                });
 459
 460    #endregion
 461
 462    #region Dependencies
 463
 464    /// <summary>
 465    /// Rebuilds the filter dependencies based on the provided filter. This method extracts the properties used in the f
 466    /// </summary>
 467    /// <param name="filter">The filter for which to rebuild dependencies.</param>
 468    private void RebuildFilterDependencies(IFilter<T> filter)
 469    {
 470        _filterPropertyUsage.Clear();
 471
 472        var expr = filter.ProvideExpression();
 473        var deps = _dependencyExtractor.ExtractFilter(expr);
 474
 475        foreach (var d in deps)
 476            _filterPropertyUsage[d] = 1;
 477    }
 478
 479    /// <summary>
 480    /// Rebuilds the sort dependencies based on the provided sorting properties. This method extracts the properties use
 481    /// </summary>
 482    /// <param name="sorting">The sorting properties for which to rebuild dependencies.</param>
 483    private void RebuildSortDependencies(ISortingProperty<T>[] sorting)
 484    {
 485        _sortPropertyUsage.Clear();
 486
 487        foreach (var s in sorting)
 488        {
 489            var deps = _dependencyExtractor.ExtractSort(s.ProvideExpression());
 490
 491            foreach (var d in deps)
 492                _sortPropertyUsage[d] = 1;
 493        }
 494    }
 495
 496    #endregion
 497}
 498