// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Caching; using osu.Framework.Lists; namespace osu.Framework.Bindables { public class BindableList : IBindableList, IBindable, IParseable, IList, IList { /// /// An event which is raised when this changes. /// public event NotifyCollectionChangedEventHandler CollectionChanged; /// /// An event which is raised when 's state has changed (or manually via ). /// public event Action DisabledChanged; private readonly List collection = new List(); private readonly Cached>> weakReferenceCache = new Cached>>(); private WeakReference> weakReference => weakReferenceCache.IsValid ? weakReferenceCache.Value : weakReferenceCache.Value = new WeakReference>(this); private LockedWeakList> bindings; /// /// Creates a new , optionally adding the items of the given collection. /// /// The items that are going to be contained in the newly created . public BindableList(IEnumerable items = null) { if (items != null) collection.AddRange(items); } #region IList /// /// Gets or sets the item at an index in this . /// /// The index of the item. /// Thrown when setting a value while this is . public T this[int index] { get => collection[index]; set => setIndex(index, value, null); } private void setIndex(int index, T item, BindableList caller) { ensureMutationAllowed(); T lastItem = collection[index]; collection[index] = item; if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.setIndex(index, item, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, lastItem, index)); } /// /// Adds a single item to this . /// /// The item to be added. /// Thrown when this is . public void Add(T item) => add(item, null); private void add(T item, BindableList caller) { ensureMutationAllowed(); collection.Add(item); if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.add(item, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, collection.Count - 1)); } /// /// Retrieves the index of an item in this . /// /// The item to retrieve the index of. /// The index of the item, or -1 if the item isn't in this . public int IndexOf(T item) => collection.IndexOf(item); /// /// Inserts an item at the specified index in this . /// /// The index to insert at. /// The item to insert. /// Thrown when this is . public void Insert(int index, T item) => insert(index, item, null); private void insert(int index, T item, BindableList caller) { ensureMutationAllowed(); collection.Insert(index, item); if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.insert(index, item, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); } /// /// Clears the contents of this . /// /// Thrown when this is . public void Clear() => clear(null); private void clear(BindableList caller) { ensureMutationAllowed(); if (collection.Count <= 0) return; // Preserve items for subscribers var clearedItems = collection.ToList(); collection.Clear(); if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.clear(this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, clearedItems, 0)); } /// /// Determines if an item is in this . /// /// The item to locate in this . /// true if this contains the given item. public bool Contains(T item) => collection.Contains(item); /// /// Removes an item from this . /// /// The item to remove from this . /// true if the removal was successful. /// Thrown if this is . public bool Remove(T item) => remove(item, null); private bool remove(T item, BindableList caller) { ensureMutationAllowed(); int index = collection.IndexOf(item); if (index < 0) return false; // Removal may have come from an equality comparison. // Always return the original reference from the list to other bindings and events. var listItem = collection[index]; collection.RemoveAt(index); if (bindings != null) { foreach (var b in bindings) { // prevent re-removing from the callee. // That would result in a . if (b != caller) b.remove(listItem, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, listItem, index)); return true; } /// /// Removes items starting from . /// /// The index to start removing from. /// The count of items to be removed. public void RemoveRange(int index, int count) { removeRange(index, count, null); } private void removeRange(int index, int count, BindableList caller) { ensureMutationAllowed(); var removedItems = collection.GetRange(index, count); collection.RemoveRange(index, count); if (removedItems.Count == 0) return; if (bindings != null) { foreach (var b in bindings) { // Prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.removeRange(index, count, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, index)); } /// /// Removes an item at the specified index from this . /// /// The index of the item to remove. /// Thrown if this is . public void RemoveAt(int index) => removeAt(index, null); private void removeAt(int index, BindableList caller) { ensureMutationAllowed(); T item = collection[index]; collection.RemoveAt(index); if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.removeAt(index, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); } /// /// Removes all items from this that match a predicate. /// /// The predicate. public int RemoveAll(Predicate match) => removeAll(match, null); private int removeAll(Predicate match, BindableList caller) { ensureMutationAllowed(); var removed = collection.FindAll(match); if (removed.Count == 0) return removed.Count; // RemoveAll is internally optimised collection.RemoveAll(match); if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.removeAll(match, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); return removed.Count; } /// /// Copies the contents of this to the given array, starting at the given index. /// /// The array that is the destination of the items copied from this . /// The index at which the copying begins. public void CopyTo(T[] array, int arrayIndex) => collection.CopyTo(array, arrayIndex); /// /// Copies the contents of this to the given array, starting at the given index. /// /// The array that is the destination of the items copied from this . /// The index at which the copying begins. public void CopyTo(Array array, int index) => ((ICollection)collection).CopyTo(array, index); public int BinarySearch(T item) => collection.BinarySearch(item); public int Count => collection.Count; public bool IsSynchronized => ((ICollection)collection).IsSynchronized; public object SyncRoot => ((ICollection)collection).SyncRoot; public bool IsReadOnly => Disabled; #endregion #region IList object IList.this[int index] { get => this[index]; set => this[index] = (T)value; } int IList.Add(object value) { Add((T)value); return Count - 1; } bool IList.Contains(object value) => Contains((T)value); int IList.IndexOf(object value) => IndexOf((T)value); void IList.Insert(int index, object value) => Insert(index, (T)value); void IList.Remove(object value) => Remove((T)value); bool IList.IsFixedSize => false; #endregion #region IParseable /// /// Parse an object into this instance. /// A collection holding items of type can be parsed. Null results in an empty . /// /// The input which is to be parsed. /// Thrown if this is . public void Parse(object input) { ensureMutationAllowed(); switch (input) { case null: Clear(); break; case IEnumerable enumerable: // enumerate once locally before proceeding. var newItems = enumerable.ToList(); if (this.SequenceEqual(newItems)) return; Clear(); AddRange(newItems); break; default: throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(T)}."); } } #endregion #region ICanBeDisabled private bool disabled; /// /// Whether this has been disabled. When disabled, attempting to change the contents of this will result in an . /// public bool Disabled { get => disabled; set { if (value == disabled) return; disabled = value; triggerDisabledChange(); } } public void BindDisabledChanged(Action onChange, bool runOnceImmediately = false) { DisabledChanged += onChange; if (runOnceImmediately) onChange(Disabled); } private void triggerDisabledChange(bool propagateToBindings = true) { // check a bound bindable hasn't changed the value again (it will fire its own event) bool beforePropagation = disabled; if (propagateToBindings && bindings != null) { foreach (var b in bindings) b.Disabled = disabled; } if (beforePropagation == disabled) DisabledChanged?.Invoke(disabled); } #endregion ICanBeDisabled #region IUnbindable public virtual void UnbindEvents() { CollectionChanged = null; DisabledChanged = null; } public void UnbindBindings() { if (bindings == null) return; foreach (var b in bindings) UnbindFrom(b); } public void UnbindAll() { UnbindEvents(); UnbindBindings(); } public virtual void UnbindFrom(IUnbindable them) { if (!(them is BindableList tThem)) throw new InvalidCastException($"Can't unbind a bindable of type {them.GetType()} from a bindable of type {GetType()}."); removeWeakReference(tThem.weakReference); tThem.removeWeakReference(weakReference); } #endregion IUnbindable #region IHasDescription public string Description { get; set; } #endregion IHasDescription #region IBindableCollection /// /// Adds a collection of items to this . /// /// The collection whose items should be added to this collection. /// Thrown if this collection is public void AddRange(IEnumerable items) => addRange(items as IList ?? items.ToArray(), null); private void addRange(IList items, BindableList caller) { ensureMutationAllowed(); collection.AddRange(items.Cast()); if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.addRange(items, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items, collection.Count - items.Count)); } /// /// Moves an item in this collection. /// /// The index of the item to move. /// The index specifying the new location of the item. public void Move(int oldIndex, int newIndex) => move(oldIndex, newIndex, null); private void move(int oldIndex, int newIndex, BindableList caller) { ensureMutationAllowed(); T item = collection[oldIndex]; collection.RemoveAt(oldIndex); collection.Insert(newIndex, item); if (bindings != null) { foreach (var b in bindings) { // prevent re-adding the item back to the callee. // That would result in a . if (b != caller) b.move(oldIndex, newIndex, this); } } notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex)); } void IBindable.BindTo(IBindable them) { if (!(them is BindableList tThem)) throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); BindTo(tThem); } void IBindableList.BindTo(IBindableList them) { if (!(them is BindableList tThem)) throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); BindTo(tThem); } /// /// An alias of provided for use in object initializer scenarios. /// Passes the provided value as the foreign (more permanent) bindable. /// public IBindableList BindTarget { set => ((IBindableList)this).BindTo(value); } /// /// Binds this to another. /// /// The to be bound to. public void BindTo(BindableList them) { if (them == null) throw new ArgumentNullException(nameof(them)); if (bindings?.Contains(weakReference) == true) throw new ArgumentException("An already bound collection can not be bound again."); if (them == this) throw new ArgumentException("A collection can not be bound to itself"); // copy state and content over Parse(them); Disabled = them.Disabled; addWeakReference(them.weakReference); them.addWeakReference(weakReference); } /// /// Bind an action to with the option of running the bound action once immediately /// with an event for the entire contents of this . /// /// The action to perform when this changes. /// Whether the action provided in should be run once immediately. public void BindCollectionChanged(NotifyCollectionChangedEventHandler onChange, bool runOnceImmediately = false) { CollectionChanged += onChange; if (runOnceImmediately) onChange(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, collection)); } private void addWeakReference(WeakReference> weakReference) { bindings ??= new LockedWeakList>(); bindings.Add(weakReference); } private void removeWeakReference(WeakReference> weakReference) => bindings?.Remove(weakReference); IBindable IBindable.CreateInstance() => CreateInstance(); /// protected virtual BindableList CreateInstance() => new BindableList(); IBindable IBindable.GetBoundCopy() => GetBoundCopy(); IBindableList IBindableList.GetBoundCopy() => GetBoundCopy(); /// public BindableList GetBoundCopy() => IBindable.GetBoundCopyImplementation(this); #endregion IBindableCollection #region IEnumerable public List.Enumerator GetEnumerator() => collection.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); #endregion IEnumerable private void notifyCollectionChanged(NotifyCollectionChangedEventArgs args) => CollectionChanged?.Invoke(this, args); private void ensureMutationAllowed() { if (Disabled) throw new InvalidOperationException($"Cannot mutate the {nameof(BindableList)} while it is disabled."); } public bool IsDefault => Count == 0; } }