< Summary

Information
Class: MyNet.UI.ViewModels.List.ListViewModelBase<T1, T2>
Assembly: MyNet.UI
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/ViewModels/List/ListViewModelBase.cs
Tag: 323_28699572109
Line coverage
50%
Covered lines: 2
Uncovered lines: 2
Coverable lines: 4
Total lines: 360
Line coverage: 50%
Branch coverage
0%
Covered branches: 0
Total branches: 10
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)0%110100%
.ctor(...)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{
 33    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)
 40        : this(dataProvider, options?.Filters, options?.Sorting, options?.Grouping, options?.Paging, options?.Scheduler)
 41    {
 42    }
 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>
 53    protected ListViewModelBase(
 54        IListDataProvider<T> dataProvider,
 55        IFiltersViewModel<T>? filters = null,
 56        ISortingViewModel<T>? sorting = null,
 57        IGroupingViewModel<T>? grouping = null,
 58        IPagingViewModel? paging = null,
 59        IScheduler? scheduler = null)
 60    {
 61        ArgumentNullException.ThrowIfNull(dataProvider);
 62
 63        var scheduler1 = scheduler ?? Scheduler.Default;
 64        DataProvider = dataProvider;
 65        _pipelineRefreshDeferrer = new(RefreshPipeline);
 66
 67        Source = DataProvider.Source;
 68        FilteredItems = DataProvider.FilteredItems;
 69        Items = DataProvider.Items;
 70        Groups = new(_groups);
 71
 72        Filters = filters;
 73        Sorting = sorting;
 74        Grouping = grouping;
 75        Paging = paging;
 76
 77        CurrentFilter = filters?.CurrentFilter;
 78        CurrentSorting = sorting?.CurrentSorting ?? [];
 79        CurrentGrouping = grouping?.CurrentGrouping ?? [];
 80
 81        RefreshPipeline();
 82        SubscribeToConfigurationEvents();
 83
 84        Disposables.Add(
 85            DataProvider.ConnectGroups()
 86                .ObserveOn(scheduler1)
 87                .Subscribe(groups =>
 88                {
 89                    _groups.Clear();
 90                    foreach (var g in groups)
 91                        _groups.Add(new Group<T>(g.Key, [.. g.Items]));
 92                }));
 93
 94        if (Paging is not null)
 95        {
 96            Disposables.Add(
 97                DataProvider.ConnectFiltered()
 98                    .ObserveOn(scheduler1)
 99                    .Subscribe(_ => SyncPagingMetadata()));
 100        }
 101    }
 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 />
 121    public int TotalCount => Source.Count;
 122
 123    /// <inheritdoc />
 124    public int FilteredCount => DataProvider.FilteredCount;
 125
 126    /// <inheritdoc />
 127    public IFiltersViewModel<T>? Filters { get; }
 128
 129    /// <inheritdoc />
 130    public IFilter<T>? CurrentFilter { get; private set => SetProperty(ref field, value); }
 131
 132    /// <inheritdoc />
 133    public ISortingViewModel<T>? Sorting { get; }
 134
 135    /// <inheritdoc />
 136    public IReadOnlyList<ISortingProperty<T>> CurrentSorting { get; private set => SetProperty(ref field, value); }
 137
 138    /// <inheritdoc />
 139    public IGroupingViewModel<T>? Grouping { get; }
 140
 141    /// <inheritdoc />
 142    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>
 163    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    {
 170        ApplyFilter();
 171        ApplySorting();
 172        ApplyGrouping();
 173        ApplyPaging();
 174        SyncPagingMetadata();
 175
 176        if (Paging is not null)
 177            ApplyPaging();
 178    }
 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>
 185    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    {
 194        if (Filters is null)
 195            return;
 196
 197        CurrentFilter = Filters.CurrentFilter;
 198
 199        if (CurrentFilter is null)
 200            DataProvider.ClearFilter();
 201        else
 202            DataProvider.SetFilter(CurrentFilter);
 203    }
 204
 205    /// <summary>
 206    /// Applies the current sorting from the Sorting view model to the collection.
 207    /// </summary>
 208    private void ApplySorting()
 209    {
 210        CurrentSorting = Sorting?.CurrentSorting ?? [];
 211
 212        if (CurrentSorting.Count == 0)
 213            DataProvider.ClearSorting();
 214        else
 215            DataProvider.SetSorting([.. CurrentSorting]);
 216    }
 217
 218    /// <summary>
 219    /// Applies the current grouping from the Grouping view model to the collection.
 220    /// </summary>
 221    private void ApplyGrouping()
 222    {
 223        CurrentGrouping = Grouping?.CurrentGrouping ?? [];
 224
 225        if (CurrentGrouping.Count == 0)
 226            DataProvider.ClearGrouping();
 227        else
 228            DataProvider.SetGrouping([.. CurrentGrouping]);
 229    }
 230
 231    /// <summary>
 232    /// Applies the current paging window to the collection.
 233    /// </summary>
 234    private void ApplyPaging()
 235    {
 236        if (Paging is null)
 237        {
 238            DataProvider.ClearPaging();
 239            return;
 240        }
 241
 242        DataProvider.SetPaging(Paging.CurrentPage, Paging.PageSize);
 243    }
 244
 245    /// <summary>
 246    /// Updates paging metadata based on the current filtered count.
 247    /// </summary>
 248    private void SyncPagingMetadata()
 249    {
 250        if (Paging is null)
 251            return;
 252
 253        var previousPage = Paging.CurrentPage;
 254        var currentPage = Math.Max(1, Paging.CurrentPage);
 255        Paging.Update(DataProvider.FilteredCount, currentPage);
 256
 257        if (Paging.CurrentPage != previousPage)
 258            ApplyPaging();
 259    }
 260
 261    /// <summary>
 262    /// Handles changes in the filtering configuration.
 263    /// </summary>
 264    private void HandleFiltersChanged(object? sender, FiltersChangedEventArgs<T> e)
 265    {
 266        CurrentFilter = e.Filter;
 267        _pipelineRefreshDeferrer.Request();
 268    }
 269
 270    /// <summary>
 271    /// Handles changes in the sorting configuration.
 272    /// </summary>
 273    private void HandleSortingChanged(object? sender, SortingChangedEventArgs<T> e)
 274    {
 275        CurrentSorting = e.Sorting;
 276        _pipelineRefreshDeferrer.Request();
 277    }
 278
 279    /// <summary>
 280    /// Handles changes in the grouping configuration.
 281    /// </summary>
 282    private void HandleGroupingChanged(object? sender, GroupingChangedEventArgs<T> e)
 283    {
 284        CurrentGrouping = e.Grouping;
 285        _pipelineRefreshDeferrer.Request();
 286    }
 287
 288    /// <summary>
 289    /// Handles changes in the paging configuration.
 290    /// </summary>
 291    private void HandlePagingChanged(object? sender, PagingChangedEventArgs e) => _pipelineRefreshDeferrer.Request();
 292
 293    /// <summary>
 294    /// Subscribes to configuration events.
 295    /// </summary>
 296    private void SubscribeToConfigurationEvents()
 297    {
 298        Filters?.FiltersChanged += HandleFiltersChanged;
 299        Sorting?.SortingChanged += HandleSortingChanged;
 300        Grouping?.GroupingChanged += HandleGroupingChanged;
 301        Paging?.PagingChanged += HandlePagingChanged;
 302    }
 303
 304    /// <summary>
 305    /// Unsubscribes from configuration events.
 306    /// </summary>
 307    private void UnsubscribeFromConfigurationEvents()
 308    {
 309        Filters?.FiltersChanged -= HandleFiltersChanged;
 310        Sorting?.SortingChanged -= HandleSortingChanged;
 311        Grouping?.GroupingChanged -= HandleGroupingChanged;
 312        Paging?.PagingChanged -= HandlePagingChanged;
 313    }
 314
 315    /// <inheritdoc />
 316    protected override void DisposeManagedResources()
 317    {
 318        UnsubscribeFromConfigurationEvents();
 319        DataProvider.Dispose();
 320        base.DisposeManagedResources();
 321    }
 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)
 0337        : this(collection, options?.Filters, options?.Sorting, options?.Grouping, options?.Paging, options?.Scheduler)
 338    {
 0339    }
 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)
 57351        : base(new ExtendedCollectionDataProvider<T>(collection), filters, sorting, grouping, paging, scheduler)
 57352        => 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