| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="ObservableRangeCollection.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.Generic; |
| | | 9 | | using System.Collections.ObjectModel; |
| | | 10 | | using System.Collections.Specialized; |
| | | 11 | | using System.ComponentModel; |
| | | 12 | | using System.Linq; |
| | | 13 | | |
| | | 14 | | namespace 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> |
| | | 20 | | public 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> |
| | 276 | 31 | | 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) |
| | 9 | 38 | | : base(new(capacity)) |
| | | 39 | | { |
| | 9 | 40 | | } |
| | | 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) |
| | 3 | 47 | | : base(list) |
| | | 48 | | { |
| | 3 | 49 | | } |
| | | 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) |
| | 81 | 56 | | : base(collection) |
| | | 57 | | { |
| | 81 | 58 | | } |
| | | 59 | | |
| | | 60 | | /// <summary> |
| | | 61 | | /// Gets the current capacity of the underlying list, if supported. |
| | | 62 | | /// </summary> |
| | 9 | 63 | | 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 | | { |
| | 102 | 72 | | ArgumentNullException.ThrowIfNull(items); |
| | | 73 | | |
| | | 74 | | // Fast path for ICollection<T> |
| | 102 | 75 | | var col = items as ICollection<T> ?? [.. items]; |
| | | 76 | | |
| | 105 | 77 | | if (col.Count == 0) return; |
| | | 78 | | |
| | 99 | 79 | | CheckReentrancy(); |
| | | 80 | | |
| | | 81 | | // Pre-allocate if possible (internal List<T>) |
| | 99 | 82 | | if (Items is List<T> list) |
| | | 83 | | { |
| | 99 | 84 | | list.Capacity = Math.Max(list.Capacity, list.Count + col.Count); |
| | | 85 | | } |
| | | 86 | | |
| | 14250 | 87 | | foreach (var item in col) |
| | 7026 | 88 | | 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 |
| | 99 | 93 | | OnCountPropertyChanged(); |
| | 99 | 94 | | 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 |
| | 99 | 98 | | OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); |
| | | 99 | | } |
| | 99 | 100 | | } |
| | | 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 | | { |
| | 9 | 109 | | ArgumentNullException.ThrowIfNull(items); |
| | | 110 | | |
| | | 111 | | // Materialize to avoid multiple enumerations |
| | 9 | 112 | | var collection = items as IList<T> ?? [.. items]; |
| | 9 | 113 | | if (collection.Count == 0) return; |
| | | 114 | | |
| | 9 | 115 | | CheckReentrancy(); |
| | | 116 | | |
| | | 117 | | // Pre-allocate |
| | 9 | 118 | | if (Items is List<T> list) |
| | | 119 | | { |
| | 9 | 120 | | list.Capacity = Math.Max(list.Capacity, list.Count + collection.Count); |
| | | 121 | | } |
| | | 122 | | |
| | 54 | 123 | | foreach (var item in collection) |
| | 18 | 124 | | Items.Insert(index++, item); |
| | | 125 | | |
| | 9 | 126 | | OnCountPropertyChanged(); |
| | | 127 | | |
| | 9 | 128 | | if (!_suspendNotifications) |
| | | 129 | | { |
| | 9 | 130 | | OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); |
| | | 131 | | } |
| | 9 | 132 | | } |
| | | 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 | | { |
| | 30 | 141 | | ArgumentNullException.ThrowIfNull(items); |
| | | 142 | | |
| | 30 | 143 | | CheckReentrancy(); |
| | | 144 | | |
| | 30 | 145 | | var source = items as ICollection<T> ?? [.. items]; |
| | | 146 | | |
| | 30 | 147 | | var oldCount = Count; |
| | | 148 | | |
| | 30 | 149 | | Items.Clear(); |
| | | 150 | | |
| | | 151 | | // Pre-allocate if we know the size |
| | 30 | 152 | | if (Items is List<T> list) |
| | | 153 | | { |
| | 30 | 154 | | list.Capacity = source.Count; |
| | | 155 | | } |
| | | 156 | | |
| | 282 | 157 | | foreach (var item in source) |
| | 111 | 158 | | Items.Add(item); |
| | | 159 | | |
| | | 160 | | // Only notify if count actually changed or if we have listeners |
| | 30 | 161 | | if (Count != oldCount) |
| | 6 | 162 | | OnCountPropertyChanged(); |
| | | 163 | | |
| | 30 | 164 | | if (!_suspendNotifications) |
| | | 165 | | { |
| | 30 | 166 | | OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); |
| | | 167 | | } |
| | 30 | 168 | | } |
| | | 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 | | { |
| | 15 | 177 | | ArgumentOutOfRangeException.ThrowIfNegative(index); |
| | 12 | 178 | | ArgumentOutOfRangeException.ThrowIfNegative(count); |
| | 9 | 179 | | if (index + count > Count) |
| | 3 | 180 | | throw new ArgumentException("Index and count do not denote a valid range of elements."); |
| | | 181 | | |
| | 6 | 182 | | if (count == 0) return; |
| | | 183 | | |
| | 6 | 184 | | CheckReentrancy(); |
| | | 185 | | |
| | | 186 | | // Remove in reverse order to maintain indices |
| | 36 | 187 | | for (var i = count - 1; i >= 0; i--) |
| | 12 | 188 | | Items.RemoveAt(index + i); |
| | | 189 | | |
| | 6 | 190 | | OnCountPropertyChanged(); |
| | | 191 | | |
| | 6 | 192 | | if (!_suspendNotifications) |
| | | 193 | | { |
| | 6 | 194 | | OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); |
| | | 195 | | } |
| | 6 | 196 | | } |
| | | 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 | | { |
| | 15 | 205 | | ArgumentNullException.ThrowIfNull(predicate); |
| | | 206 | | |
| | 15 | 207 | | CheckReentrancy(); |
| | | 208 | | |
| | 15 | 209 | | var itemsToRemove = Items.Where(predicate).ToList(); |
| | 18 | 210 | | if (itemsToRemove.Count == 0) return 0; |
| | | 211 | | |
| | 354 | 212 | | foreach (var item in itemsToRemove) |
| | 165 | 213 | | Items.Remove(item); |
| | | 214 | | |
| | 12 | 215 | | OnCountPropertyChanged(); |
| | | 216 | | |
| | 12 | 217 | | if (!_suspendNotifications) |
| | | 218 | | { |
| | 12 | 219 | | OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); |
| | | 220 | | } |
| | | 221 | | |
| | 12 | 222 | | 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 | | { |
| | 12 | 231 | | var count = Count; |
| | 12 | 232 | | _suspendCount = true; |
| | | 233 | | |
| | 12 | 234 | | 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 | | { |
| | 15 | 243 | | _suspendCount = true; |
| | 15 | 244 | | _suspendNotifications = true; |
| | 15 | 245 | | _deferredResetPending = false; |
| | | 246 | | |
| | 15 | 247 | | return new NotificationSuspendScope(this); |
| | | 248 | | } |
| | | 249 | | |
| | | 250 | | /// <summary> |
| | | 251 | | /// Raises the CollectionChanged event. |
| | | 252 | | /// </summary> |
| | | 253 | | protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) |
| | | 254 | | { |
| | 8868 | 255 | | if (_suspendNotifications) |
| | | 256 | | { |
| | 24 | 257 | | _deferredResetPending = true; |
| | 24 | 258 | | return; |
| | | 259 | | } |
| | | 260 | | |
| | 8844 | 261 | | base.OnCollectionChanged(e); |
| | 8844 | 262 | | } |
| | | 263 | | |
| | | 264 | | /// <summary> |
| | | 265 | | /// Raises the PropertyChanged event. |
| | | 266 | | /// </summary> |
| | | 267 | | protected override void OnPropertyChanged(PropertyChangedEventArgs e) |
| | | 268 | | { |
| | 17544 | 269 | | ArgumentNullException.ThrowIfNull(e); |
| | | 270 | | |
| | 17544 | 271 | | if (_suspendCount && e.PropertyName == nameof(Count)) |
| | | 272 | | { |
| | 42 | 273 | | _deferredResetPending = true; |
| | 42 | 274 | | return; |
| | | 275 | | } |
| | | 276 | | |
| | 17502 | 277 | | base.OnPropertyChanged(e); |
| | 17502 | 278 | | } |
| | | 279 | | |
| | | 280 | | /// <summary> |
| | | 281 | | /// Raises the Count property changed event. |
| | | 282 | | /// </summary> |
| | | 283 | | protected virtual void OnCountPropertyChanged(bool sendCollectionReset = false) |
| | | 284 | | { |
| | 156 | 285 | | OnPropertyChanged(new(nameof(Count))); |
| | | 286 | | |
| | 156 | 287 | | if (sendCollectionReset && !_suspendNotifications) |
| | 0 | 288 | | OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); |
| | 156 | 289 | | } |
| | | 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 | | { |
| | 15 | 298 | | ArgumentOutOfRangeException.ThrowIfNegative(capacity); |
| | | 299 | | |
| | 12 | 300 | | if (Items is List<T> list) |
| | | 301 | | { |
| | 12 | 302 | | list.Capacity = capacity; |
| | | 303 | | } |
| | 12 | 304 | | } |
| | | 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 | | { |
| | 12 | 315 | | if (_disposed) |
| | 0 | 316 | | return; |
| | | 317 | | |
| | 12 | 318 | | _disposed = true; |
| | 12 | 319 | | owner._suspendCount = false; |
| | | 320 | | |
| | 12 | 321 | | if (owner.Count != savedCount) |
| | 12 | 322 | | owner.OnCountPropertyChanged(); |
| | 12 | 323 | | } |
| | | 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 | | { |
| | 15 | 335 | | if (_disposed) |
| | 0 | 336 | | return; |
| | | 337 | | |
| | 15 | 338 | | _disposed = true; |
| | 15 | 339 | | owner._suspendCount = false; |
| | 15 | 340 | | owner._suspendNotifications = false; |
| | | 341 | | |
| | | 342 | | // Send reset notification if there were any changes |
| | 15 | 343 | | if (owner._deferredResetPending) |
| | | 344 | | { |
| | 12 | 345 | | owner.OnCountPropertyChanged(); |
| | 12 | 346 | | owner.OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); |
| | 12 | 347 | | owner._deferredResetPending = false; |
| | | 348 | | } |
| | 15 | 349 | | } |
| | | 350 | | } |
| | | 351 | | } |
| | | 352 | | |