// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable enable using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Framework.Caching; using osu.Framework.Lists; namespace osu.Framework.Bindables { public class BindableDictionary : IBindableDictionary, IBindable, IParseable, IDictionary, IDictionary where TKey : notnull { public event NotifyDictionaryChangedEventHandler? CollectionChanged; /// /// An event which is raised when 's state has changed (or manually via ). /// public event Action? DisabledChanged; private readonly Dictionary collection; private readonly Cached>> weakReferenceCache = new Cached>>(); private WeakReference> weakReference => weakReferenceCache.IsValid ? weakReferenceCache.Value : weakReferenceCache.Value = new WeakReference>(this); private LockedWeakList>? bindings; /// public BindableDictionary(IEqualityComparer? comparer = null) : this(0, comparer) { } /// public BindableDictionary(IDictionary dictionary, IEqualityComparer? comparer = null) : this((IEnumerable>)dictionary, comparer) { } /// public BindableDictionary(int capacity, IEqualityComparer? comparer = null) { collection = new Dictionary(capacity, comparer); } /// public BindableDictionary(IEnumerable> collection, IEqualityComparer? comparer = null) { this.collection = new Dictionary(collection, comparer); } #region IDictionary /// /// Thrown when this is . public void Add(TKey key, TValue value) => add(key, value, null); private void add(TKey key, TValue value, BindableDictionary? caller) { ensureMutationAllowed(); collection.Add(key, value); 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(key, value, this); } } notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs(NotifyDictionaryChangedAction.Add, new KeyValuePair(key, value))); } public bool ContainsKey(TKey key) => collection.ContainsKey(key); /// /// Thrown if this is . public bool Remove(TKey key) => remove(key, out _, null); /// /// Thrown if this is . public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) => remove(key, out value, null); private bool remove(TKey key, [MaybeNullWhen(false)] out TValue value, BindableDictionary? caller) { ensureMutationAllowed(); if (!collection.Remove(key, out value)) return false; if (bindings != null) { foreach (var b in bindings) { // prevent re-removing from the callee. // That would result in a . if (b != caller) b.remove(key, out _, this); } } notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs(NotifyDictionaryChangedAction.Remove, new KeyValuePair(key, value))); return true; } #if NETSTANDARD public bool TryGetValue(TKey key, out TValue value) => collection.TryGetValue(key, out value); #else public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => collection.TryGetValue(key, out value); #endif /// /// Thrown when setting an item while this is . public TValue this[TKey key] { get => collection[key]; set => setKey(key, value, null); } private void setKey(TKey key, TValue value, BindableDictionary? caller) { ensureMutationAllowed(); #nullable disable // Todo: Remove after upgrading Resharper version on CI. bool hasPreviousValue = TryGetValue(key, out TValue lastValue); collection[key] = value; 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.setKey(key, value, this); } } #nullable enable notifyDictionaryChanged(hasPreviousValue ? new NotifyDictionaryChangedEventArgs(new KeyValuePair(key, value), new KeyValuePair(key, lastValue!)) : new NotifyDictionaryChangedEventArgs(NotifyDictionaryChangedAction.Add, new KeyValuePair(key, value))); } public ICollection Keys => collection.Keys; public ICollection Values => collection.Values; #endregion #region IDictionary void IDictionary.Add(object key, object? value) => Add((TKey)key, (TValue)(value ?? throw new ArgumentNullException(nameof(value)))); /// /// Thrown when this is . public void Clear() => clear(null); private void clear(BindableDictionary? caller) { ensureMutationAllowed(); if (collection.Count == 0) return; // Preserve items for subscribers var clearedItems = collection.ToArray(); 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); } } notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs(NotifyDictionaryChangedAction.Remove, clearedItems)); } bool IDictionary.Contains(object key) { return ((IDictionary)collection).Contains(key); } void IDictionary.Remove(object key) => Remove((TKey)key); bool IDictionary.IsFixedSize => ((IDictionary)collection).IsFixedSize; public bool IsReadOnly => Disabled; object? IDictionary.this[object key] { get => this[(TKey)key]; set => this[(TKey)key] = (TValue)value!; } ICollection IDictionary.Values => (ICollection)Values; ICollection IDictionary.Keys => (ICollection)Keys; #endregion #region IReadOnlyDictionary IEnumerable IReadOnlyDictionary.Keys => Keys; IEnumerable IReadOnlyDictionary.Values => Values; #endregion #region ICollection> bool ICollection>.Remove(KeyValuePair item) { #nullable disable // Todo: Remove after upgrading Resharper version on CI. if (TryGetValue(item.Key, out TValue value) && EqualityComparer.Default.Equals(value, item.Value)) { Remove(item.Key); return true; } #nullable enable return false; } void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); bool ICollection>.Contains(KeyValuePair item) => ((ICollection>)collection).Contains(item); void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => ((ICollection>)collection).CopyTo(array, arrayIndex); #endregion #region ICollection void ICollection.CopyTo(Array array, int index) => ((ICollection)collection).CopyTo(array, index); bool ICollection.IsSynchronized => ((ICollection)collection).IsSynchronized; object ICollection.SyncRoot => ((ICollection)collection).SyncRoot; #endregion #region IReadOnlyCollection public int Count => collection.Count; #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, null); break; default: throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(KeyValuePair)}."); } } private void addRange(IList items, BindableDictionary? caller) { ensureMutationAllowed(); var typedItems = (IList>)items; foreach (var (key, value) in typedItems) collection.Add(key, value); 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); } } notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs(NotifyDictionaryChangedAction.Add, typedItems)); } #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 void UnbindEvents() { CollectionChanged = null; DisabledChanged = null; } public void UnbindBindings() { if (bindings == null) return; foreach (var b in bindings) b.unbind(this); bindings?.Clear(); } public void UnbindAll() { UnbindEvents(); UnbindBindings(); } public void UnbindFrom(IUnbindable them) { if (!(them is BindableDictionary 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); } private void unbind(BindableDictionary binding) { Debug.Assert(bindings != null); bindings.Remove(binding.weakReference); } #endregion IUnbindable #region IHasDescription public string? Description { get; set; } #endregion IHasDescription #region IBindableCollection void IBindable.BindTo(IBindable them) { if (!(them is BindableDictionary tThem)) throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); BindTo(tThem); } void IBindableDictionary.BindTo(IBindableDictionary them) { if (!(them is BindableDictionary 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 BindableDictionary BindTarget { set => ((IBindableDictionary)this).BindTo(value); } /// /// Binds this to another. /// /// The to be bound to. public void BindTo(BindableDictionary 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(NotifyDictionaryChangedEventHandler onChange, bool runOnceImmediately = false) { CollectionChanged += onChange; if (runOnceImmediately) onChange(this, new NotifyDictionaryChangedEventArgs(NotifyDictionaryChangedAction.Add, collection.ToArray())); } 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 BindableDictionary CreateInstance() => new BindableDictionary(); IBindable IBindable.GetBoundCopy() => GetBoundCopy(); IBindableDictionary IBindableDictionary.GetBoundCopy() => GetBoundCopy(); /// public BindableDictionary GetBoundCopy() => IBindable.GetBoundCopyImplementation(this); #endregion IBindableCollection #region IEnumerable public Dictionary.Enumerator GetEnumerator() => collection.GetEnumerator(); IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IDictionaryEnumerator IDictionary.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); #endregion IEnumerable private void notifyDictionaryChanged(NotifyDictionaryChangedEventArgs args) => CollectionChanged?.Invoke(this, args); private void ensureMutationAllowed() { if (Disabled) throw new InvalidOperationException($"Cannot mutate the {nameof(BindableDictionary)} while it is disabled."); } public bool IsDefault => Count == 0; } }