< Summary

Information
Class: MyNet.Collections.ObservableRangeCollection<T>
Assembly: MyNet.Collections
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Collections/ObservableRangeCollection.cs
Tag: 323_28699572109
Line coverage
97%
Covered lines: 108
Uncovered lines: 3
Coverable lines: 111
Total lines: 352
Line coverage: 97.2%
Branch coverage
68%
Covered branches: 45
Total branches: 66
Branch coverage: 68.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
get_Capacity()50%22100%
AddRange(...)70%1010100%
InsertRange(...)60%1010100%
Load(...)70%1010100%
RemoveRange(...)75%88100%
RemoveAll(...)83.33%66100%
SuspendCount()100%11100%
SuspendNotifications()100%11100%
OnCollectionChanged(...)100%22100%
OnPropertyChanged(...)100%44100%
OnCountPropertyChanged(...)25%4475%
SetCapacity(...)50%22100%
Dispose()50%4485.71%
Dispose()75%4490%

File(s)

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

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ObservableRangeCollection.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.Collections.Specialized;
 11using System.ComponentModel;
 12using System.Linq;
 13
 14namespace MyNet.Collections;
 15
 16/// <summary>
 17/// An optimized observable collection with batch operations, notification suspension, and improved performance.
 18/// </summary>
 19/// <typeparam name="T">The type of the item.</typeparam>
 20public class ObservableRangeCollection<T> : ObservableCollection<T>, IObservableRangeCollection<T>
 21{
 22    private bool _suspendCount;
 23    private bool _suspendNotifications;
 24
 25    // Track if we need to send a reset after batch operations
 26    private bool _deferredResetPending;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="ObservableRangeCollection{T}"/> class.
 30    /// </summary>
 27631    public ObservableRangeCollection() { }
 32
 33    /// <summary>
 34    /// Initializes a new instance of the <see cref="ObservableRangeCollection{T}"/> class with initial capacity.
 35    /// </summary>
 36    /// <param name="capacity">The initial capacity to pre-allocate.</param>
 37    public ObservableRangeCollection(int capacity)
 938        : base(new(capacity))
 39    {
 940    }
 41
 42    /// <summary>
 43    /// Initializes a new instance of the <see cref="ObservableRangeCollection{T}"/> class that contains elements copied
 44    /// </summary>
 45    /// <param name="list">The list from which the elements are copied.</param>
 46    public ObservableRangeCollection(Collection<T> list)
 347        : base(list)
 48    {
 349    }
 50
 51    /// <summary>
 52    /// Initializes a new instance of the <see cref="ObservableRangeCollection{T}"/> class that contains elements copied
 53    /// </summary>
 54    /// <param name="collection">The collection from which the elements are copied.</param>
 55    public ObservableRangeCollection(IEnumerable<T> collection)
 8156        : base(collection)
 57    {
 8158    }
 59
 60    /// <summary>
 61    /// Gets the current capacity of the underlying list, if supported.
 62    /// </summary>
 963    public int Capacity => Items is List<T> list ? list.Capacity : Count;
 64
 65    /// <summary>
 66    /// Adds the elements of the specified collection to the end of the collection.
 67    /// Optimized to send a single notification for the entire operation.
 68    /// </summary>
 69    /// <param name="items">The collection whose elements should be added.</param>
 70    public virtual void AddRange(IEnumerable<T> items)
 71    {
 10272        ArgumentNullException.ThrowIfNull(items);
 73
 74        // Fast path for ICollection<T>
 10275        var col = items as ICollection<T> ?? [.. items];
 76
 10577        if (col.Count == 0) return;
 78
 9979        CheckReentrancy();
 80
 81        // Pre-allocate if possible (internal List<T>)
 9982        if (Items is List<T> list)
 83        {
 9984            list.Capacity = Math.Max(list.Capacity, list.Count + col.Count);
 85        }
 86
 1425087        foreach (var item in col)
 702688            Items.Add(item);
 89
 90        // Send batch notification if not suspended
 91        // NotifyCollectionChangedAction.Add with multiple items is supported in WPF
 92        // but can cause issues in some bindings, so we use Reset for safety
 9993        OnCountPropertyChanged();
 9994        if (!_suspendNotifications)
 95        {
 96            // NotifyCollectionChangedAction.Add with multiple items is supported in WPF
 97            // but can cause issues in some bindings, so we use Reset for safety
 9998            OnCollectionChanged(new(NotifyCollectionChangedAction.Reset));
 99        }
 99100    }
 101
 102    /// <summary>
 103    /// Inserts the elements of a collection into the collection at the specified index.
 104    /// </summary>
 105    /// <param name="items">The collection whose elements should be inserted.</param>
 106    /// <param name="index">The zero-based index at which the new elements should be inserted.</param>
 107    public virtual void InsertRange(IEnumerable<T> items, int index)
 108    {
 9109        ArgumentNullException.ThrowIfNull(items);
 110
 111        // Materialize to avoid multiple enumerations
 9112        var collection = items as IList<T> ?? [.. items];
 9113        if (collection.Count == 0) return;
 114
 9115        CheckReentrancy();
 116
 117        // Pre-allocate
 9118        if (Items is List<T> list)
 119        {
 9120            list.Capacity = Math.Max(list.Capacity, list.Count + collection.Count);
 121        }
 122
 54123        foreach (var item in collection)
 18124            Items.Insert(index++, item);
 125
 9126        OnCountPropertyChanged();
 127
 9128        if (!_suspendNotifications)
 129        {
 9130            OnCollectionChanged(new(NotifyCollectionChangedAction.Reset));
 131        }
 9132    }
 133
 134    /// <summary>
 135    /// Clears the collection and loads the specified items in one operation.
 136    /// Most efficient way to replace all items.
 137    /// </summary>
 138    /// <param name="items">The items to load.</param>
 139    public virtual void Load(IEnumerable<T> items)
 140    {
 30141        ArgumentNullException.ThrowIfNull(items);
 142
 30143        CheckReentrancy();
 144
 30145        var source = items as ICollection<T> ?? [.. items];
 146
 30147        var oldCount = Count;
 148
 30149        Items.Clear();
 150
 151        // Pre-allocate if we know the size
 30152        if (Items is List<T> list)
 153        {
 30154            list.Capacity = source.Count;
 155        }
 156
 282157        foreach (var item in source)
 111158            Items.Add(item);
 159
 160        // Only notify if count actually changed or if we have listeners
 30161        if (Count != oldCount)
 6162            OnCountPropertyChanged();
 163
 30164        if (!_suspendNotifications)
 165        {
 30166            OnCollectionChanged(new(NotifyCollectionChangedAction.Reset));
 167        }
 30168    }
 169
 170    /// <summary>
 171    /// Removes a range of elements from the collection.
 172    /// </summary>
 173    /// <param name="index">The zero-based starting index of the range of elements to remove.</param>
 174    /// <param name="count">The number of elements to remove.</param>
 175    public virtual void RemoveRange(int index, int count)
 176    {
 15177        ArgumentOutOfRangeException.ThrowIfNegative(index);
 12178        ArgumentOutOfRangeException.ThrowIfNegative(count);
 9179        if (index + count > Count)
 3180            throw new ArgumentException("Index and count do not denote a valid range of elements.");
 181
 6182        if (count == 0) return;
 183
 6184        CheckReentrancy();
 185
 186        // Remove in reverse order to maintain indices
 36187        for (var i = count - 1; i >= 0; i--)
 12188            Items.RemoveAt(index + i);
 189
 6190        OnCountPropertyChanged();
 191
 6192        if (!_suspendNotifications)
 193        {
 6194            OnCollectionChanged(new(NotifyCollectionChangedAction.Reset));
 195        }
 6196    }
 197
 198    /// <summary>
 199    /// Removes all items matching the predicate.
 200    /// </summary>
 201    /// <param name="predicate">The predicate to test items.</param>
 202    /// <returns>The number of items removed.</returns>
 203    public virtual int RemoveAll(Func<T, bool> predicate)
 204    {
 15205        ArgumentNullException.ThrowIfNull(predicate);
 206
 15207        CheckReentrancy();
 208
 15209        var itemsToRemove = Items.Where(predicate).ToList();
 18210        if (itemsToRemove.Count == 0) return 0;
 211
 354212        foreach (var item in itemsToRemove)
 165213            Items.Remove(item);
 214
 12215        OnCountPropertyChanged();
 216
 12217        if (!_suspendNotifications)
 218        {
 12219            OnCollectionChanged(new(NotifyCollectionChangedAction.Reset));
 220        }
 221
 12222        return itemsToRemove.Count;
 223    }
 224
 225    /// <summary>
 226    /// Suspends count notifications.
 227    /// </summary>
 228    /// <returns>A disposable that will resume notifications when disposed.</returns>
 229    public IDisposable SuspendCount()
 230    {
 12231        var count = Count;
 12232        _suspendCount = true;
 233
 12234        return new CountSuspendScope(this, count);
 235    }
 236
 237    /// <summary>
 238    /// Suspends all notifications. When disposed, a reset notification is fired.
 239    /// </summary>
 240    /// <returns>A disposable that will resume notifications when disposed.</returns>
 241    public IDisposable SuspendNotifications()
 242    {
 15243        _suspendCount = true;
 15244        _suspendNotifications = true;
 15245        _deferredResetPending = false;
 246
 15247        return new NotificationSuspendScope(this);
 248    }
 249
 250    /// <summary>
 251    /// Raises the CollectionChanged event.
 252    /// </summary>
 253    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
 254    {
 8868255        if (_suspendNotifications)
 256        {
 24257            _deferredResetPending = true;
 24258            return;
 259        }
 260
 8844261        base.OnCollectionChanged(e);
 8844262    }
 263
 264    /// <summary>
 265    /// Raises the PropertyChanged event.
 266    /// </summary>
 267    protected override void OnPropertyChanged(PropertyChangedEventArgs e)
 268    {
 17544269        ArgumentNullException.ThrowIfNull(e);
 270
 17544271        if (_suspendCount && e.PropertyName == nameof(Count))
 272        {
 42273            _deferredResetPending = true;
 42274            return;
 275        }
 276
 17502277        base.OnPropertyChanged(e);
 17502278    }
 279
 280    /// <summary>
 281    /// Raises the Count property changed event.
 282    /// </summary>
 283    protected virtual void OnCountPropertyChanged(bool sendCollectionReset = false)
 284    {
 156285        OnPropertyChanged(new(nameof(Count)));
 286
 156287        if (sendCollectionReset && !_suspendNotifications)
 0288            OnCollectionChanged(new(NotifyCollectionChangedAction.Reset));
 156289    }
 290
 291    /// <summary>
 292    /// Sets the capacity of the underlying list if supported.
 293    /// Useful to pre-allocate before adding many items.
 294    /// </summary>
 295    /// <param name="capacity">The desired capacity.</param>
 296    public virtual void SetCapacity(int capacity)
 297    {
 15298        ArgumentOutOfRangeException.ThrowIfNegative(capacity);
 299
 12300        if (Items is List<T> list)
 301        {
 12302            list.Capacity = capacity;
 303        }
 12304    }
 305
 306    /// <summary>
 307    /// Internal scope for suspending count notifications.
 308    /// </summary>
 309    private sealed class CountSuspendScope(ObservableRangeCollection<T> owner, int savedCount) : IDisposable
 310    {
 311        private bool _disposed;
 312
 313        public void Dispose()
 314        {
 12315            if (_disposed)
 0316                return;
 317
 12318            _disposed = true;
 12319            owner._suspendCount = false;
 320
 12321            if (owner.Count != savedCount)
 12322                owner.OnCountPropertyChanged();
 12323        }
 324    }
 325
 326    /// <summary>
 327    /// Internal scope for suspending all notifications.
 328    /// </summary>
 329    private sealed class NotificationSuspendScope(ObservableRangeCollection<T> owner) : IDisposable
 330    {
 331        private bool _disposed;
 332
 333        public void Dispose()
 334        {
 15335            if (_disposed)
 0336                return;
 337
 15338            _disposed = true;
 15339            owner._suspendCount = false;
 15340            owner._suspendNotifications = false;
 341
 342            // Send reset notification if there were any changes
 15343            if (owner._deferredResetPending)
 344            {
 12345                owner.OnCountPropertyChanged();
 12346                owner.OnCollectionChanged(new(NotifyCollectionChangedAction.Reset));
 12347                owner._deferredResetPending = false;
 348            }
 15349        }
 350    }
 351}
 352