| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="FiltersViewModel.cs" company="Stéphane ANDRE"> |
| | | 3 | | // Copyright (c) Stéphane ANDRE. All rights reserved. |
| | | 4 | | // </copyright> |
| | | 5 | | // ----------------------------------------------------------------------- |
| | | 6 | | |
| | | 7 | | using System; |
| | | 8 | | using System.Collections.Specialized; |
| | | 9 | | using System.ComponentModel; |
| | | 10 | | using System.Diagnostics.CodeAnalysis; |
| | | 11 | | using System.Reactive.Disposables; |
| | | 12 | | using System.Reactive.Linq; |
| | | 13 | | using MyNet.Observable; |
| | | 14 | | using MyNet.Observable.Collections.Filters; |
| | | 15 | | using MyNet.Utilities.Deferring; |
| | | 16 | | |
| | | 17 | | namespace MyNet.UI.ViewModels.List.Filtering; |
| | | 18 | | |
| | | 19 | | /// <summary> |
| | | 20 | | /// Represents the view model for managing filters applied to a collection of items of type T. It maintains a tree of fi |
| | | 21 | | /// </summary> |
| | | 22 | | /// <typeparam name="T">The type of items to be filtered.</typeparam> |
| | | 23 | | public class FiltersViewModel<T> : ObservableObject, IFiltersViewModel<T> |
| | | 24 | | { |
| | | 25 | | private readonly DeferredAction _deferredAction; |
| | | 26 | | [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Disposed through the share |
| | 3 | 27 | | private readonly SerialDisposable _treeSubscriptions = new(); |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// Initializes a new instance of the <see cref="FiltersViewModel{T}"/> class with the specified root filter group v |
| | | 31 | | /// </summary> |
| | | 32 | | /// <param name="root">The root filter group view model.</param> |
| | | 33 | | /// <exception cref="ArgumentNullException">Thrown if the root parameter is null.</exception> |
| | 3 | 34 | | public FiltersViewModel(IFilterGroupViewModel<T> root) |
| | | 35 | | { |
| | 3 | 36 | | Root = root ?? throw new ArgumentNullException(nameof(root)); |
| | | 37 | | |
| | 3 | 38 | | _deferredAction = new(HandleFiltersChanged); |
| | 3 | 39 | | Disposables.Add(_treeSubscriptions); |
| | | 40 | | |
| | 3 | 41 | | RebuildSubscriptions(); |
| | 3 | 42 | | } |
| | | 43 | | |
| | | 44 | | /// <summary> |
| | | 45 | | /// Gets the root filter group view model, which represents the full filter tree configured in the UI. This property |
| | | 46 | | /// </summary> |
| | | 47 | | public IFilterGroupViewModel<T> Root { get; } |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// Gets the current filter built from the UI configuration. This property returns an instance of <see cref="IFilter |
| | | 51 | | /// </summary> |
| | 0 | 52 | | public IFilter<T>? CurrentFilter { get; private set => SetProperty(ref field, value); } |
| | | 53 | | |
| | | 54 | | /// <summary> |
| | | 55 | | /// Gets a value indicating whether there are any active filters in the current configuration. This property returns |
| | | 56 | | /// </summary> |
| | 0 | 57 | | public bool HasActiveFilters => CurrentFilter is not null; |
| | | 58 | | |
| | | 59 | | /// <summary> |
| | | 60 | | /// Gets or sets a value indicating whether the filters should be automatically applied when they change. If set to |
| | | 61 | | /// </summary> |
| | 3 | 62 | | public bool AutoApply { get; set => SetProperty(ref field, value); } = true; |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// Gets a value indicating whether the filter configuration has been modified since the last application. This prop |
| | | 66 | | /// </summary> |
| | 0 | 67 | | public bool IsDirty { get; private set => SetProperty(ref field, value); } |
| | | 68 | | |
| | | 69 | | /// <summary> |
| | | 70 | | /// Occurs when the filter configuration changes and a new filter is produced. This event is raised whenever the fil |
| | | 71 | | /// </summary> |
| | | 72 | | public event EventHandler<FiltersChangedEventArgs<T>>? FiltersChanged; |
| | | 73 | | |
| | | 74 | | /// <summary> |
| | | 75 | | /// Observes collection changes on the specified source collection and returns an observable sequence of collection |
| | | 76 | | /// </summary> |
| | | 77 | | /// <param name="source">The source collection to observe for changes.</param> |
| | | 78 | | /// <returns>An observable sequence of collection changed event arguments.</returns> |
| | | 79 | | private static IObservable<NotifyCollectionChangedEventArgs> ObserveCollectionChanges( |
| | | 80 | | INotifyCollectionChanged source) => |
| | 6 | 81 | | System.Reactive.Linq.Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEve |
| | 6 | 82 | | h => source.CollectionChanged += h, |
| | 6 | 83 | | h => source.CollectionChanged -= h) |
| | 6 | 84 | | .Select(x => x.EventArgs); |
| | | 85 | | |
| | | 86 | | /// <summary> |
| | | 87 | | /// Applies the current filter configuration and raises the FiltersChanged event. This method computes the current f |
| | | 88 | | /// </summary> |
| | | 89 | | public void Apply() |
| | | 90 | | { |
| | 0 | 91 | | CurrentFilter = ComputeCurrentFilter(); |
| | 0 | 92 | | IsDirty = false; |
| | | 93 | | |
| | 0 | 94 | | FiltersChanged?.Invoke(this, new(CurrentFilter)); |
| | 0 | 95 | | } |
| | | 96 | | |
| | | 97 | | /// <summary> |
| | | 98 | | /// Clears all filters by setting them to their empty state. This method traverses the filter tree and resets each f |
| | | 99 | | /// </summary> |
| | | 100 | | public void Clear() |
| | | 101 | | { |
| | 0 | 102 | | using (_deferredAction.Defer()) |
| | 0 | 103 | | Root.Clear(); |
| | 0 | 104 | | } |
| | | 105 | | |
| | | 106 | | /// <summary> |
| | | 107 | | /// Resets the filters to their default state. This method traverses the filter tree and resets each filter conditio |
| | | 108 | | /// </summary> |
| | | 109 | | public void Reset() |
| | | 110 | | { |
| | 0 | 111 | | using (_deferredAction.Defer()) |
| | 0 | 112 | | Root.Reset(); |
| | 0 | 113 | | } |
| | | 114 | | |
| | | 115 | | /// <summary> |
| | | 116 | | /// Handles changes to the filter configuration by marking the state as dirty and applying the filters if AutoApply |
| | | 117 | | /// </summary> |
| | | 118 | | private void HandleFiltersChanged() |
| | | 119 | | { |
| | 0 | 120 | | IsDirty = true; |
| | | 121 | | |
| | 0 | 122 | | if (AutoApply) |
| | 0 | 123 | | Apply(); |
| | 0 | 124 | | } |
| | | 125 | | |
| | | 126 | | /// <summary> |
| | | 127 | | /// Computes the current filter based on the active conditions defined in the filter tree. This method calls the Bui |
| | | 128 | | /// </summary> |
| | | 129 | | /// <returns>The current filter based on the active conditions, or null if there are no active filters.</returns> |
| | 0 | 130 | | private ExpressionFilter<T>? ComputeCurrentFilter() => Root.BuildExpression() is { } expr ? new ExpressionFilter<T>( |
| | | 131 | | |
| | | 132 | | /// <summary> |
| | | 133 | | /// Subscribes to property changes and collection changes in the filter tree starting from the specified node. This |
| | | 134 | | /// </summary> |
| | | 135 | | /// <param name="node">The filter node to start subscribing from.</param> |
| | | 136 | | /// <returns>A CompositeDisposable containing all subscriptions for the specified node and its children.</returns> |
| | | 137 | | private CompositeDisposable Subscribe(IFilterNodeViewModel<T> node) |
| | | 138 | | { |
| | 12 | 139 | | var disposable = new CompositeDisposable(); |
| | 12 | 140 | | if (node is INotifyPropertyChanged npc) |
| | | 141 | | { |
| | 12 | 142 | | disposable.Add(System.Reactive.Linq.Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChanged |
| | 12 | 143 | | h => npc.PropertyChanged += h, |
| | 12 | 144 | | h => npc.PropertyChanged -= h) |
| | 12 | 145 | | .Select(x => x.EventArgs) |
| | 12 | 146 | | .Subscribe(_ => _deferredAction.Request())); |
| | | 147 | | } |
| | | 148 | | |
| | 12 | 149 | | if (node is IFilterGroupViewModel<T> group) |
| | | 150 | | { |
| | 6 | 151 | | disposable.Add(ObserveCollectionChanges(group.Children).Subscribe(_ => |
| | 6 | 152 | | { |
| | 6 | 153 | | RebuildSubscriptions(); |
| | 6 | 154 | | _deferredAction.Request(); |
| | 6 | 155 | | })); |
| | | 156 | | |
| | 30 | 157 | | foreach (var child in group.Children) |
| | 9 | 158 | | disposable.Add(Subscribe(child)); |
| | | 159 | | } |
| | | 160 | | |
| | 12 | 161 | | return disposable; |
| | | 162 | | } |
| | | 163 | | |
| | 3 | 164 | | private void RebuildSubscriptions() => _treeSubscriptions.Disposable = Subscribe(Root); |
| | | 165 | | } |
| | | 166 | | |