< Summary

Information
Class: MyNet.UI.ViewModels.List.ListViewModelBase<T>
Assembly: MyNet.UI
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/ViewModels/List/ListViewModelBase.cs
Tag: 323_28699572109
Line coverage
91%
Covered lines: 102
Uncovered lines: 9
Coverable lines: 111
Total lines: 360
Line coverage: 91.8%
Branch coverage
70%
Covered branches: 45
Total branches: 64
Branch coverage: 70.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)78.57%1414100%
.ctor(...)0%110100%
get_TotalCount()100%11100%
get_FilteredCount()100%11100%
set_CurrentFilter(...)100%11100%
set_CurrentSorting(...)100%11100%
set_CurrentGrouping(...)100%11100%
DeferRefresh()100%11100%
RefreshPipeline()100%22100%
RequestPipelineRefresh()100%11100%
ApplyFilter()100%44100%
ApplySorting()83.33%8660%
ApplyGrouping()50%8660%
ApplyPaging()100%22100%
SyncPagingMetadata()100%44100%
HandleFiltersChanged(...)100%11100%
HandleSortingChanged(...)100%11100%
HandleGroupingChanged(...)100%210%
HandlePagingChanged(...)100%11100%
SubscribeToConfigurationEvents()87.5%88100%
UnsubscribeFromConfigurationEvents()87.5%88100%
DisposeManagedResources()100%11100%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/ViewModels/List/ListViewModelBase.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ListViewModelBase.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.Collections.ObjectModel;
 10using System.Diagnostics.CodeAnalysis;
 11using System.Reactive.Concurrency;
 12using System.Reactive.Linq;
 13using MyNet.Observable.Collections;
 14using MyNet.Observable.Collections.Filters;
 15using MyNet.Observable.Collections.Grouping;
 16using MyNet.Observable.Collections.Sorting;
 17using MyNet.UI.ViewModels.List.Factories;
 18using MyNet.UI.ViewModels.List.Filtering;
 19using MyNet.UI.ViewModels.List.Grouping;
 20using MyNet.UI.ViewModels.List.Paging;
 21using MyNet.UI.ViewModels.List.Sorting;
 22using MyNet.Utilities.Deferring;
 23
 24namespace MyNet.UI.ViewModels.List;
 25
 26/// <summary>
 27/// Provides a lightweight list pipeline implementation for ViewModels.
 28/// </summary>
 29/// <typeparam name="T">The item type.</typeparam>
 30public class ListViewModelBase<T> : ViewModelBase, IListViewModel<T>
 31    where T : notnull
 32{
 13233    private readonly ObservableCollection<IGroup<T>> _groups = [];
 34    private readonly DeferredAction _pipelineRefreshDeferrer;
 35
 36    /// <summary>
 37    /// Initializes a new instance of the <see cref="ListViewModelBase{T}"/> class.
 38    /// </summary>
 39    protected ListViewModelBase(IListDataProvider<T> dataProvider, ListViewModelOptions<T>? options)
 040        : this(dataProvider, options?.Filters, options?.Sorting, options?.Grouping, options?.Paging, options?.Scheduler)
 41    {
 042    }
 43
 44    /// <summary>
 45    /// Initializes a new instance of the <see cref="ListViewModelBase{T}"/> class.
 46    /// </summary>
 47    /// <param name="dataProvider">The pipeline data provider.</param>
 48    /// <param name="filters">Optional filtering configuration.</param>
 49    /// <param name="sorting">Optional sorting configuration.</param>
 50    /// <param name="grouping">Optional grouping configuration.</param>
 51    /// <param name="paging">Optional paging configuration.</param>
 52    /// <param name="scheduler">Optional scheduler for managing asynchronous operations.</param>
 13253    protected ListViewModelBase(
 13254        IListDataProvider<T> dataProvider,
 13255        IFiltersViewModel<T>? filters = null,
 13256        ISortingViewModel<T>? sorting = null,
 13257        IGroupingViewModel<T>? grouping = null,
 13258        IPagingViewModel? paging = null,
 13259        IScheduler? scheduler = null)
 60    {
 13261        ArgumentNullException.ThrowIfNull(dataProvider);
 62
 13263        var scheduler1 = scheduler ?? Scheduler.Default;
 13264        DataProvider = dataProvider;
 13265        _pipelineRefreshDeferrer = new(RefreshPipeline);
 66
 13267        Source = DataProvider.Source;
 13268        FilteredItems = DataProvider.FilteredItems;
 13269        Items = DataProvider.Items;
 13270        Groups = new(_groups);
 71
 13272        Filters = filters;
 13273        Sorting = sorting;
 13274        Grouping = grouping;
 13275        Paging = paging;
 76
 13277        CurrentFilter = filters?.CurrentFilter;
 13278        CurrentSorting = sorting?.CurrentSorting ?? [];
 13279        CurrentGrouping = grouping?.CurrentGrouping ?? [];
 80
 13281        RefreshPipeline();
 13282        SubscribeToConfigurationEvents();
 83
 13284        Disposables.Add(
 13285            DataProvider.ConnectGroups()
 13286                .ObserveOn(scheduler1)
 13287                .Subscribe(groups =>
 13288                {
 13289                    _groups.Clear();
 13290                    foreach (var g in groups)
 13291                        _groups.Add(new Group<T>(g.Key, [.. g.Items]));
 13292                }));
 93
 13294        if (Paging is not null)
 95        {
 1296            Disposables.Add(
 1297                DataProvider.ConnectFiltered()
 1298                    .ObserveOn(scheduler1)
 1299                    .Subscribe(_ => SyncPagingMetadata()));
 100        }
 132101    }
 102
 103    /// <summary>
 104    /// Gets the underlying list data provider.
 105    /// </summary>
 106    protected IListDataProvider<T> DataProvider { get; }
 107
 108    /// <inheritdoc />
 109    public ReadOnlyObservableCollection<T> Source { get; }
 110
 111    /// <inheritdoc />
 112    public ReadOnlyObservableCollection<T> FilteredItems { get; }
 113
 114    /// <inheritdoc />
 115    public ReadOnlyObservableCollection<T> Items { get; }
 116
 117    /// <inheritdoc />
 118    public ReadOnlyObservableCollection<IGroup<T>>? Groups { get; }
 119
 120    /// <inheritdoc />
 3121    public int TotalCount => Source.Count;
 122
 123    /// <inheritdoc />
 6124    public int FilteredCount => DataProvider.FilteredCount;
 125
 126    /// <inheritdoc />
 127    public IFiltersViewModel<T>? Filters { get; }
 128
 129    /// <inheritdoc />
 192130    public IFilter<T>? CurrentFilter { get; private set => SetProperty(ref field, value); }
 131
 132    /// <inheritdoc />
 133    public ISortingViewModel<T>? Sorting { get; }
 134
 135    /// <inheritdoc />
 309136    public IReadOnlyList<ISortingProperty<T>> CurrentSorting { get; private set => SetProperty(ref field, value); }
 137
 138    /// <inheritdoc />
 139    public IGroupingViewModel<T>? Grouping { get; }
 140
 141    /// <inheritdoc />
 300142    public IReadOnlyList<IGroupingProperty<T>> CurrentGrouping { get; private set => SetProperty(ref field, value); }
 143
 144    /// <inheritdoc />
 145    public IPagingViewModel? Paging { get; }
 146
 147    /// <summary>
 148    /// Creates a deferral scope that suspends pipeline refreshes until the scope is disposed.
 149    /// Multiple configuration changes (filter, sorting, grouping) occurring within a
 150    /// single scope will trigger exactly ONE pipeline refresh when the scope ends.
 151    /// </summary>
 152    /// <returns>An <see cref="IDisposable"/> scope; dispose it to flush the deferred refresh.</returns>
 153    /// <example>
 154    /// <code>
 155    /// using (vm.DeferRefresh())
 156    /// {
 157    ///     vm.Filters.Apply(...);
 158    ///     vm.Sorting.Apply(...);
 159    ///     vm.Grouping.Apply(...);
 160    /// } // ← single pipeline refresh here
 161    /// </code>
 162    /// </example>
 12163    public IDisposable DeferRefresh() => _pipelineRefreshDeferrer.Defer();
 164
 165    /// <summary>
 166    /// Refreshes the collection pipeline by applying the current filter, sorting, grouping, and paging window.
 167    /// </summary>
 168    private void RefreshPipeline()
 169    {
 168170        ApplyFilter();
 168171        ApplySorting();
 168172        ApplyGrouping();
 168173        ApplyPaging();
 168174        SyncPagingMetadata();
 175
 168176        if (Paging is not null)
 24177            ApplyPaging();
 168178    }
 179
 180    /// <summary>
 181    /// Requests a pipeline refresh using the same debounce/deferral mechanism as configuration changes.
 182    /// Derived classes can use this hook when external actions (for example CRUD operations)
 183    /// require re-applying the pipeline.
 184    /// </summary>
 9185    protected void RequestPipelineRefresh() => _pipelineRefreshDeferrer.Request();
 186
 187    /// <summary>
 188    /// Applies the current filter from the Filters view model to the collection.
 189    /// When no <see cref="Filters"/> view model is configured, the data provider filter is left unchanged
 190    /// so derived classes can manage ad-hoc filters through <see cref="IListDataProvider{T}.SetFilter"/>.
 191    /// </summary>
 192    private void ApplyFilter()
 193    {
 168194        if (Filters is null)
 129195            return;
 196
 39197        CurrentFilter = Filters.CurrentFilter;
 198
 39199        if (CurrentFilter is null)
 36200            DataProvider.ClearFilter();
 201        else
 3202            DataProvider.SetFilter(CurrentFilter);
 3203    }
 204
 205    /// <summary>
 206    /// Applies the current sorting from the Sorting view model to the collection.
 207    /// </summary>
 208    private void ApplySorting()
 209    {
 168210        CurrentSorting = Sorting?.CurrentSorting ?? [];
 211
 168212        if (CurrentSorting.Count == 0)
 168213            DataProvider.ClearSorting();
 214        else
 0215            DataProvider.SetSorting([.. CurrentSorting]);
 0216    }
 217
 218    /// <summary>
 219    /// Applies the current grouping from the Grouping view model to the collection.
 220    /// </summary>
 221    private void ApplyGrouping()
 222    {
 168223        CurrentGrouping = Grouping?.CurrentGrouping ?? [];
 224
 168225        if (CurrentGrouping.Count == 0)
 168226            DataProvider.ClearGrouping();
 227        else
 0228            DataProvider.SetGrouping([.. CurrentGrouping]);
 0229    }
 230
 231    /// <summary>
 232    /// Applies the current paging window to the collection.
 233    /// </summary>
 234    private void ApplyPaging()
 235    {
 195236        if (Paging is null)
 237        {
 144238            DataProvider.ClearPaging();
 144239            return;
 240        }
 241
 51242        DataProvider.SetPaging(Paging.CurrentPage, Paging.PageSize);
 51243    }
 244
 245    /// <summary>
 246    /// Updates paging metadata based on the current filtered count.
 247    /// </summary>
 248    private void SyncPagingMetadata()
 249    {
 192250        if (Paging is null)
 144251            return;
 252
 48253        var previousPage = Paging.CurrentPage;
 48254        var currentPage = Math.Max(1, Paging.CurrentPage);
 48255        Paging.Update(DataProvider.FilteredCount, currentPage);
 256
 48257        if (Paging.CurrentPage != previousPage)
 3258            ApplyPaging();
 48259    }
 260
 261    /// <summary>
 262    /// Handles changes in the filtering configuration.
 263    /// </summary>
 264    private void HandleFiltersChanged(object? sender, FiltersChangedEventArgs<T> e)
 265    {
 21266        CurrentFilter = e.Filter;
 21267        _pipelineRefreshDeferrer.Request();
 21268    }
 269
 270    /// <summary>
 271    /// Handles changes in the sorting configuration.
 272    /// </summary>
 273    private void HandleSortingChanged(object? sender, SortingChangedEventArgs<T> e)
 274    {
 9275        CurrentSorting = e.Sorting;
 9276        _pipelineRefreshDeferrer.Request();
 9277    }
 278
 279    /// <summary>
 280    /// Handles changes in the grouping configuration.
 281    /// </summary>
 282    private void HandleGroupingChanged(object? sender, GroupingChangedEventArgs<T> e)
 283    {
 0284        CurrentGrouping = e.Grouping;
 0285        _pipelineRefreshDeferrer.Request();
 0286    }
 287
 288    /// <summary>
 289    /// Handles changes in the paging configuration.
 290    /// </summary>
 6291    private void HandlePagingChanged(object? sender, PagingChangedEventArgs e) => _pipelineRefreshDeferrer.Request();
 292
 293    /// <summary>
 294    /// Subscribes to configuration events.
 295    /// </summary>
 296    private void SubscribeToConfigurationEvents()
 297    {
 132298        Filters?.FiltersChanged += HandleFiltersChanged;
 132299        Sorting?.SortingChanged += HandleSortingChanged;
 132300        Grouping?.GroupingChanged += HandleGroupingChanged;
 132301        Paging?.PagingChanged += HandlePagingChanged;
 12302    }
 303
 304    /// <summary>
 305    /// Unsubscribes from configuration events.
 306    /// </summary>
 307    private void UnsubscribeFromConfigurationEvents()
 308    {
 111309        Filters?.FiltersChanged -= HandleFiltersChanged;
 111310        Sorting?.SortingChanged -= HandleSortingChanged;
 111311        Grouping?.GroupingChanged -= HandleGroupingChanged;
 111312        Paging?.PagingChanged -= HandlePagingChanged;
 12313    }
 314
 315    /// <inheritdoc />
 316    protected override void DisposeManagedResources()
 317    {
 111318        UnsubscribeFromConfigurationEvents();
 111319        DataProvider.Dispose();
 111320        base.DisposeManagedResources();
 111321    }
 322}
 323
 324/// <summary>
 325/// List view model base that exposes the underlying <see cref="ExtendedCollection{T}"/> for advanced scenarios (e.g. se
 326/// </summary>
 327/// <typeparam name="T">The item type.</typeparam>
 328/// <typeparam name="TCollection">The collection type.</typeparam>
 329public class ListViewModelBase<T, TCollection> : ListViewModelBase<T>
 330    where TCollection : ExtendedCollection<T>
 331    where T : notnull
 332{
 333    /// <summary>
 334    /// Initializes a new instance of the <see cref="ListViewModelBase{T, TCollection}"/> class.
 335    /// </summary>
 336    protected ListViewModelBase(TCollection collection, ListViewModelOptions<T>? options)
 337        : this(collection, options?.Filters, options?.Sorting, options?.Grouping, options?.Paging, options?.Scheduler)
 338    {
 339    }
 340
 341    /// <summary>
 342    /// Initializes a new instance of the <see cref="ListViewModelBase{T, TCollection}"/> class.
 343    /// </summary>
 344    protected ListViewModelBase(
 345        TCollection collection,
 346        IFiltersViewModel<T>? filters = null,
 347        ISortingViewModel<T>? sorting = null,
 348        IGroupingViewModel<T>? grouping = null,
 349        IPagingViewModel? paging = null,
 350        IScheduler? scheduler = null)
 351        : base(new ExtendedCollectionDataProvider<T>(collection), filters, sorting, grouping, paging, scheduler)
 352        => Collection = collection;
 353
 354    /// <summary>
 355    /// Gets the underlying extended collection used by the list pipeline.
 356    /// </summary>
 357    [field: SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed by the sha
 358    protected TCollection Collection { get; }
 359}
 360