A game framework written with osu! in mind.
at master 657 lines 24 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections; 6using System.Collections.Generic; 7using System.Collections.Specialized; 8using System.Linq; 9using osu.Framework.Caching; 10using osu.Framework.Lists; 11 12namespace osu.Framework.Bindables 13{ 14 public class BindableList<T> : IBindableList<T>, IBindable, IParseable, IList<T>, IList 15 { 16 /// <summary> 17 /// An event which is raised when this <see cref="BindableList{T}"/> changes. 18 /// </summary> 19 public event NotifyCollectionChangedEventHandler CollectionChanged; 20 21 /// <summary> 22 /// An event which is raised when <see cref="Disabled"/>'s state has changed (or manually via <see cref="triggerDisabledChange(bool)"/>). 23 /// </summary> 24 public event Action<bool> DisabledChanged; 25 26 private readonly List<T> collection = new List<T>(); 27 28 private readonly Cached<WeakReference<BindableList<T>>> weakReferenceCache = new Cached<WeakReference<BindableList<T>>>(); 29 30 private WeakReference<BindableList<T>> weakReference => weakReferenceCache.IsValid ? weakReferenceCache.Value : weakReferenceCache.Value = new WeakReference<BindableList<T>>(this); 31 32 private LockedWeakList<BindableList<T>> bindings; 33 34 /// <summary> 35 /// Creates a new <see cref="BindableList{T}"/>, optionally adding the items of the given collection. 36 /// </summary> 37 /// <param name="items">The items that are going to be contained in the newly created <see cref="BindableList{T}"/>.</param> 38 public BindableList(IEnumerable<T> items = null) 39 { 40 if (items != null) 41 collection.AddRange(items); 42 } 43 44 #region IList<T> 45 46 /// <summary> 47 /// Gets or sets the item at an index in this <see cref="BindableList{T}"/>. 48 /// </summary> 49 /// <param name="index">The index of the item.</param> 50 /// <exception cref="InvalidOperationException">Thrown when setting a value while this <see cref="BindableList{T}"/> is <see cref="Disabled"/>.</exception> 51 public T this[int index] 52 { 53 get => collection[index]; 54 set => setIndex(index, value, null); 55 } 56 57 private void setIndex(int index, T item, BindableList<T> caller) 58 { 59 ensureMutationAllowed(); 60 61 T lastItem = collection[index]; 62 63 collection[index] = item; 64 65 if (bindings != null) 66 { 67 foreach (var b in bindings) 68 { 69 // prevent re-adding the item back to the callee. 70 // That would result in a <see cref="StackOverflowException"/>. 71 if (b != caller) 72 b.setIndex(index, item, this); 73 } 74 } 75 76 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, lastItem, index)); 77 } 78 79 /// <summary> 80 /// Adds a single item to this <see cref="BindableList{T}"/>. 81 /// </summary> 82 /// <param name="item">The item to be added.</param> 83 /// <exception cref="InvalidOperationException">Thrown when this <see cref="BindableList{T}"/> is <see cref="Disabled"/>.</exception> 84 public void Add(T item) 85 => add(item, null); 86 87 private void add(T item, BindableList<T> caller) 88 { 89 ensureMutationAllowed(); 90 91 collection.Add(item); 92 93 if (bindings != null) 94 { 95 foreach (var b in bindings) 96 { 97 // prevent re-adding the item back to the callee. 98 // That would result in a <see cref="StackOverflowException"/>. 99 if (b != caller) 100 b.add(item, this); 101 } 102 } 103 104 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, collection.Count - 1)); 105 } 106 107 /// <summary> 108 /// Retrieves the index of an item in this <see cref="BindableList{T}"/>. 109 /// </summary> 110 /// <param name="item">The item to retrieve the index of.</param> 111 /// <returns>The index of the item, or -1 if the item isn't in this <see cref="BindableList{T}"/>.</returns> 112 public int IndexOf(T item) => collection.IndexOf(item); 113 114 /// <summary> 115 /// Inserts an item at the specified index in this <see cref="BindableList{T}"/>. 116 /// </summary> 117 /// <param name="index">The index to insert at.</param> 118 /// <param name="item">The item to insert.</param> 119 /// <exception cref="InvalidOperationException">Thrown when this <see cref="BindableList{T}"/> is <see cref="Disabled"/>.</exception> 120 public void Insert(int index, T item) 121 => insert(index, item, null); 122 123 private void insert(int index, T item, BindableList<T> caller) 124 { 125 ensureMutationAllowed(); 126 127 collection.Insert(index, item); 128 129 if (bindings != null) 130 { 131 foreach (var b in bindings) 132 { 133 // prevent re-adding the item back to the callee. 134 // That would result in a <see cref="StackOverflowException"/>. 135 if (b != caller) 136 b.insert(index, item, this); 137 } 138 } 139 140 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); 141 } 142 143 /// <summary> 144 /// Clears the contents of this <see cref="BindableList{T}"/>. 145 /// </summary> 146 /// <exception cref="InvalidOperationException">Thrown when this <see cref="BindableList{T}"/> is <see cref="Disabled"/>.</exception> 147 public void Clear() 148 => clear(null); 149 150 private void clear(BindableList<T> caller) 151 { 152 ensureMutationAllowed(); 153 154 if (collection.Count <= 0) 155 return; 156 157 // Preserve items for subscribers 158 var clearedItems = collection.ToList(); 159 160 collection.Clear(); 161 162 if (bindings != null) 163 { 164 foreach (var b in bindings) 165 { 166 // prevent re-adding the item back to the callee. 167 // That would result in a <see cref="StackOverflowException"/>. 168 if (b != caller) 169 b.clear(this); 170 } 171 } 172 173 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, clearedItems, 0)); 174 } 175 176 /// <summary> 177 /// Determines if an item is in this <see cref="BindableList{T}"/>. 178 /// </summary> 179 /// <param name="item">The item to locate in this <see cref="BindableList{T}"/>.</param> 180 /// <returns><code>true</code> if this <see cref="BindableList{T}"/> contains the given item.</returns> 181 public bool Contains(T item) 182 => collection.Contains(item); 183 184 /// <summary> 185 /// Removes an item from this <see cref="BindableList{T}"/>. 186 /// </summary> 187 /// <param name="item">The item to remove from this <see cref="BindableList{T}"/>.</param> 188 /// <returns><code>true</code> if the removal was successful.</returns> 189 /// <exception cref="InvalidOperationException">Thrown if this <see cref="BindableList{T}"/> is <see cref="Disabled"/>.</exception> 190 public bool Remove(T item) 191 => remove(item, null); 192 193 private bool remove(T item, BindableList<T> caller) 194 { 195 ensureMutationAllowed(); 196 197 int index = collection.IndexOf(item); 198 199 if (index < 0) 200 return false; 201 202 // Removal may have come from an equality comparison. 203 // Always return the original reference from the list to other bindings and events. 204 var listItem = collection[index]; 205 206 collection.RemoveAt(index); 207 208 if (bindings != null) 209 { 210 foreach (var b in bindings) 211 { 212 // prevent re-removing from the callee. 213 // That would result in a <see cref="StackOverflowException"/>. 214 if (b != caller) 215 b.remove(listItem, this); 216 } 217 } 218 219 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, listItem, index)); 220 221 return true; 222 } 223 224 /// <summary> 225 /// Removes <paramref name="count"/> items starting from <paramref name="index"/>. 226 /// </summary> 227 /// <param name="index">The index to start removing from.</param> 228 /// <param name="count">The count of items to be removed.</param> 229 public void RemoveRange(int index, int count) 230 { 231 removeRange(index, count, null); 232 } 233 234 private void removeRange(int index, int count, BindableList<T> caller) 235 { 236 ensureMutationAllowed(); 237 238 var removedItems = collection.GetRange(index, count); 239 240 collection.RemoveRange(index, count); 241 242 if (removedItems.Count == 0) 243 return; 244 245 if (bindings != null) 246 { 247 foreach (var b in bindings) 248 { 249 // Prevent re-adding the item back to the callee. 250 // That would result in a <see cref="StackOverflowException"/>. 251 if (b != caller) 252 b.removeRange(index, count, this); 253 } 254 } 255 256 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index)); 257 } 258 259 /// <summary> 260 /// Removes an item at the specified index from this <see cref="BindableList{T}"/>. 261 /// </summary> 262 /// <param name="index">The index of the item to remove.</param> 263 /// <exception cref="InvalidOperationException">Thrown if this <see cref="BindableList{T}"/> is <see cref="Disabled"/>.</exception> 264 public void RemoveAt(int index) 265 => removeAt(index, null); 266 267 private void removeAt(int index, BindableList<T> caller) 268 { 269 ensureMutationAllowed(); 270 271 T item = collection[index]; 272 273 collection.RemoveAt(index); 274 275 if (bindings != null) 276 { 277 foreach (var b in bindings) 278 { 279 // prevent re-adding the item back to the callee. 280 // That would result in a <see cref="StackOverflowException"/>. 281 if (b != caller) 282 b.removeAt(index, this); 283 } 284 } 285 286 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); 287 } 288 289 /// <summary> 290 /// Removes all items from this <see cref="BindableList{T}"/> that match a predicate. 291 /// </summary> 292 /// <param name="match">The predicate.</param> 293 public int RemoveAll(Predicate<T> match) 294 => removeAll(match, null); 295 296 private int removeAll(Predicate<T> match, BindableList<T> caller) 297 { 298 ensureMutationAllowed(); 299 300 var removed = collection.FindAll(match); 301 302 if (removed.Count == 0) return removed.Count; 303 304 // RemoveAll is internally optimised 305 collection.RemoveAll(match); 306 307 if (bindings != null) 308 { 309 foreach (var b in bindings) 310 { 311 // prevent re-adding the item back to the callee. 312 // That would result in a <see cref="StackOverflowException"/>. 313 if (b != caller) 314 b.removeAll(match, this); 315 } 316 } 317 318 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); 319 320 return removed.Count; 321 } 322 323 /// <summary> 324 /// Copies the contents of this <see cref="BindableList{T}"/> to the given array, starting at the given index. 325 /// </summary> 326 /// <param name="array">The array that is the destination of the items copied from this <see cref="BindableList{T}"/>.</param> 327 /// <param name="arrayIndex">The index at which the copying begins.</param> 328 public void CopyTo(T[] array, int arrayIndex) 329 => collection.CopyTo(array, arrayIndex); 330 331 /// <summary> 332 /// Copies the contents of this <see cref="BindableList{T}"/> to the given array, starting at the given index. 333 /// </summary> 334 /// <param name="array">The array that is the destination of the items copied from this <see cref="BindableList{T}"/>.</param> 335 /// <param name="index">The index at which the copying begins.</param> 336 public void CopyTo(Array array, int index) 337 => ((ICollection)collection).CopyTo(array, index); 338 339 public int BinarySearch(T item) => collection.BinarySearch(item); 340 341 public int Count => collection.Count; 342 public bool IsSynchronized => ((ICollection)collection).IsSynchronized; 343 public object SyncRoot => ((ICollection)collection).SyncRoot; 344 public bool IsReadOnly => Disabled; 345 346 #endregion 347 348 #region IList 349 350 object IList.this[int index] 351 { 352 get => this[index]; 353 set => this[index] = (T)value; 354 } 355 356 int IList.Add(object value) 357 { 358 Add((T)value); 359 return Count - 1; 360 } 361 362 bool IList.Contains(object value) => Contains((T)value); 363 364 int IList.IndexOf(object value) => IndexOf((T)value); 365 366 void IList.Insert(int index, object value) => Insert(index, (T)value); 367 368 void IList.Remove(object value) => Remove((T)value); 369 370 bool IList.IsFixedSize => false; 371 372 #endregion 373 374 #region IParseable 375 376 /// <summary> 377 /// Parse an object into this instance. 378 /// A collection holding items of type <typeparamref name="T"/> can be parsed. Null results in an empty <see cref="BindableList{T}"/>. 379 /// </summary> 380 /// <param name="input">The input which is to be parsed.</param> 381 /// <exception cref="InvalidOperationException">Thrown if this <see cref="BindableList{T}"/> is <see cref="Disabled"/>.</exception> 382 public void Parse(object input) 383 { 384 ensureMutationAllowed(); 385 386 switch (input) 387 { 388 case null: 389 Clear(); 390 break; 391 392 case IEnumerable<T> enumerable: 393 // enumerate once locally before proceeding. 394 var newItems = enumerable.ToList(); 395 396 if (this.SequenceEqual(newItems)) 397 return; 398 399 Clear(); 400 AddRange(newItems); 401 break; 402 403 default: 404 throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(T)}."); 405 } 406 } 407 408 #endregion 409 410 #region ICanBeDisabled 411 412 private bool disabled; 413 414 /// <summary> 415 /// Whether this <see cref="BindableList{T}"/> has been disabled. When disabled, attempting to change the contents of this <see cref="BindableList{T}"/> will result in an <see cref="InvalidOperationException"/>. 416 /// </summary> 417 public bool Disabled 418 { 419 get => disabled; 420 set 421 { 422 if (value == disabled) 423 return; 424 425 disabled = value; 426 427 triggerDisabledChange(); 428 } 429 } 430 431 public void BindDisabledChanged(Action<bool> onChange, bool runOnceImmediately = false) 432 { 433 DisabledChanged += onChange; 434 if (runOnceImmediately) 435 onChange(Disabled); 436 } 437 438 private void triggerDisabledChange(bool propagateToBindings = true) 439 { 440 // check a bound bindable hasn't changed the value again (it will fire its own event) 441 bool beforePropagation = disabled; 442 443 if (propagateToBindings && bindings != null) 444 { 445 foreach (var b in bindings) 446 b.Disabled = disabled; 447 } 448 449 if (beforePropagation == disabled) 450 DisabledChanged?.Invoke(disabled); 451 } 452 453 #endregion ICanBeDisabled 454 455 #region IUnbindable 456 457 public virtual void UnbindEvents() 458 { 459 CollectionChanged = null; 460 DisabledChanged = null; 461 } 462 463 public void UnbindBindings() 464 { 465 if (bindings == null) 466 return; 467 468 foreach (var b in bindings) 469 UnbindFrom(b); 470 } 471 472 public void UnbindAll() 473 { 474 UnbindEvents(); 475 UnbindBindings(); 476 } 477 478 public virtual void UnbindFrom(IUnbindable them) 479 { 480 if (!(them is BindableList<T> tThem)) 481 throw new InvalidCastException($"Can't unbind a bindable of type {them.GetType()} from a bindable of type {GetType()}."); 482 483 removeWeakReference(tThem.weakReference); 484 tThem.removeWeakReference(weakReference); 485 } 486 487 #endregion IUnbindable 488 489 #region IHasDescription 490 491 public string Description { get; set; } 492 493 #endregion IHasDescription 494 495 #region IBindableCollection 496 497 /// <summary> 498 /// Adds a collection of items to this <see cref="BindableList{T}"/>. 499 /// </summary> 500 /// <param name="items">The collection whose items should be added to this collection.</param> 501 /// <exception cref="InvalidOperationException">Thrown if this collection is <see cref="Disabled"/></exception> 502 public void AddRange(IEnumerable<T> items) 503 => addRange(items as IList ?? items.ToArray(), null); 504 505 private void addRange(IList items, BindableList<T> caller) 506 { 507 ensureMutationAllowed(); 508 509 collection.AddRange(items.Cast<T>()); 510 511 if (bindings != null) 512 { 513 foreach (var b in bindings) 514 { 515 // prevent re-adding the item back to the callee. 516 // That would result in a <see cref="StackOverflowException"/>. 517 if (b != caller) 518 b.addRange(items, this); 519 } 520 } 521 522 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items, collection.Count - items.Count)); 523 } 524 525 /// <summary> 526 /// Moves an item in this collection. 527 /// </summary> 528 /// <param name="oldIndex">The index of the item to move.</param> 529 /// <param name="newIndex">The index specifying the new location of the item.</param> 530 public void Move(int oldIndex, int newIndex) 531 => move(oldIndex, newIndex, null); 532 533 private void move(int oldIndex, int newIndex, BindableList<T> caller) 534 { 535 ensureMutationAllowed(); 536 537 T item = collection[oldIndex]; 538 539 collection.RemoveAt(oldIndex); 540 collection.Insert(newIndex, item); 541 542 if (bindings != null) 543 { 544 foreach (var b in bindings) 545 { 546 // prevent re-adding the item back to the callee. 547 // That would result in a <see cref="StackOverflowException"/>. 548 if (b != caller) 549 b.move(oldIndex, newIndex, this); 550 } 551 } 552 553 notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex)); 554 } 555 556 void IBindable.BindTo(IBindable them) 557 { 558 if (!(them is BindableList<T> tThem)) 559 throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); 560 561 BindTo(tThem); 562 } 563 564 void IBindableList<T>.BindTo(IBindableList<T> them) 565 { 566 if (!(them is BindableList<T> tThem)) 567 throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); 568 569 BindTo(tThem); 570 } 571 572 /// <summary> 573 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios. 574 /// Passes the provided value as the foreign (more permanent) bindable. 575 /// </summary> 576 public IBindableList<T> BindTarget 577 { 578 set => ((IBindableList<T>)this).BindTo(value); 579 } 580 581 /// <summary> 582 /// Binds this <see cref="BindableList{T}"/> to another. 583 /// </summary> 584 /// <param name="them">The <see cref="BindableList{T}"/> to be bound to.</param> 585 public void BindTo(BindableList<T> them) 586 { 587 if (them == null) 588 throw new ArgumentNullException(nameof(them)); 589 if (bindings?.Contains(weakReference) == true) 590 throw new ArgumentException("An already bound collection can not be bound again."); 591 if (them == this) 592 throw new ArgumentException("A collection can not be bound to itself"); 593 594 // copy state and content over 595 Parse(them); 596 Disabled = them.Disabled; 597 598 addWeakReference(them.weakReference); 599 them.addWeakReference(weakReference); 600 } 601 602 /// <summary> 603 /// Bind an action to <see cref="CollectionChanged"/> with the option of running the bound action once immediately 604 /// with an <see cref="NotifyCollectionChangedAction.Add"/> event for the entire contents of this <see cref="BindableList{T}"/>. 605 /// </summary> 606 /// <param name="onChange">The action to perform when this <see cref="BindableList{T}"/> changes.</param> 607 /// <param name="runOnceImmediately">Whether the action provided in <paramref name="onChange"/> should be run once immediately.</param> 608 public void BindCollectionChanged(NotifyCollectionChangedEventHandler onChange, bool runOnceImmediately = false) 609 { 610 CollectionChanged += onChange; 611 if (runOnceImmediately) 612 onChange(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection)); 613 } 614 615 private void addWeakReference(WeakReference<BindableList<T>> weakReference) 616 { 617 bindings ??= new LockedWeakList<BindableList<T>>(); 618 bindings.Add(weakReference); 619 } 620 621 private void removeWeakReference(WeakReference<BindableList<T>> weakReference) => bindings?.Remove(weakReference); 622 623 IBindable IBindable.CreateInstance() => CreateInstance(); 624 625 /// <inheritdoc cref="IBindable.CreateInstance"/> 626 protected virtual BindableList<T> CreateInstance() => new BindableList<T>(); 627 628 IBindable IBindable.GetBoundCopy() => GetBoundCopy(); 629 630 IBindableList<T> IBindableList<T>.GetBoundCopy() => GetBoundCopy(); 631 632 /// <inheritdoc cref="IBindableList{T}.GetBoundCopy"/> 633 public BindableList<T> GetBoundCopy() => IBindable.GetBoundCopyImplementation(this); 634 635 #endregion IBindableCollection 636 637 #region IEnumerable 638 639 public List<T>.Enumerator GetEnumerator() => collection.GetEnumerator(); 640 641 IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator(); 642 643 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 644 645 #endregion IEnumerable 646 647 private void notifyCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); 648 649 private void ensureMutationAllowed() 650 { 651 if (Disabled) 652 throw new InvalidOperationException($"Cannot mutate the {nameof(BindableList<T>)} while it is disabled."); 653 } 654 655 public bool IsDefault => Count == 0; 656 } 657}