A game framework written with osu! in mind.
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}