A game framework written with osu! in mind.
at master 536 lines 20 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 4#nullable enable 5 6using System; 7using System.Collections; 8using System.Collections.Generic; 9using System.Collections.Specialized; 10using System.Diagnostics; 11using System.Diagnostics.CodeAnalysis; 12using System.Linq; 13using osu.Framework.Caching; 14using osu.Framework.Lists; 15 16namespace osu.Framework.Bindables 17{ 18 public class BindableDictionary<TKey, TValue> : IBindableDictionary<TKey, TValue>, IBindable, IParseable, IDictionary<TKey, TValue>, IDictionary 19 where TKey : notnull 20 { 21 public event NotifyDictionaryChangedEventHandler<TKey, TValue>? CollectionChanged; 22 23 /// <summary> 24 /// An event which is raised when <see cref="Disabled"/>'s state has changed (or manually via <see cref="triggerDisabledChange(bool)"/>). 25 /// </summary> 26 public event Action<bool>? DisabledChanged; 27 28 private readonly Dictionary<TKey, TValue> collection; 29 30 private readonly Cached<WeakReference<BindableDictionary<TKey, TValue>>> weakReferenceCache = new Cached<WeakReference<BindableDictionary<TKey, TValue>>>(); 31 32 private WeakReference<BindableDictionary<TKey, TValue>> weakReference 33 => weakReferenceCache.IsValid ? weakReferenceCache.Value : weakReferenceCache.Value = new WeakReference<BindableDictionary<TKey, TValue>>(this); 34 35 private LockedWeakList<BindableDictionary<TKey, TValue>>? bindings; 36 37 /// <inheritdoc cref="Dictionary{TKey,TValue}(IEqualityComparer{TKey})" /> 38 public BindableDictionary(IEqualityComparer<TKey>? comparer = null) 39 : this(0, comparer) 40 { 41 } 42 43 /// <inheritdoc cref="Dictionary{TKey,TValue}(IDictionary{TKey,TValue},IEqualityComparer{TKey})" /> 44 public BindableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey>? comparer = null) 45 : this((IEnumerable<KeyValuePair<TKey, TValue>>)dictionary, comparer) 46 { 47 } 48 49 /// <inheritdoc cref="Dictionary{TKey,TValue}(int,IEqualityComparer{TKey})" /> 50 public BindableDictionary(int capacity, IEqualityComparer<TKey>? comparer = null) 51 { 52 collection = new Dictionary<TKey, TValue>(capacity, comparer); 53 } 54 55 /// <inheritdoc cref="Dictionary{TKey,TValue}(IEnumerable{KeyValuePair{TKey,TValue}},IEqualityComparer{TKey})" /> 56 public BindableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey>? comparer = null) 57 { 58 this.collection = new Dictionary<TKey, TValue>(collection, comparer); 59 } 60 61 #region IDictionary<TKey, Value> 62 63 /// <inheritdoc /> 64 /// <exception cref="InvalidOperationException">Thrown when this <see cref="BindableDictionary{TKey, TValue}"/> is <see cref="Disabled"/>.</exception> 65 public void Add(TKey key, TValue value) 66 => add(key, value, null); 67 68 private void add(TKey key, TValue value, BindableDictionary<TKey, TValue>? caller) 69 { 70 ensureMutationAllowed(); 71 72 collection.Add(key, value); 73 74 if (bindings != null) 75 { 76 foreach (var b in bindings) 77 { 78 // prevent re-adding the item back to the callee. 79 // That would result in a <see cref="StackOverflowException"/>. 80 if (b != caller) 81 b.add(key, value, this); 82 } 83 } 84 85 notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs<TKey, TValue>(NotifyDictionaryChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value))); 86 } 87 88 public bool ContainsKey(TKey key) => collection.ContainsKey(key); 89 90 /// <inheritdoc /> 91 /// <exception cref="InvalidOperationException">Thrown if this <see cref="BindableDictionary{TKey, TValue}"/> is <see cref="Disabled"/>.</exception> 92 public bool Remove(TKey key) 93 => remove(key, out _, null); 94 95 /// <inheritdoc cref="IDictionary.Remove" /> 96 /// <exception cref="InvalidOperationException">Thrown if this <see cref="BindableDictionary{TKey, TValue}"/> is <see cref="Disabled"/>.</exception> 97 public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) 98 => remove(key, out value, null); 99 100 private bool remove(TKey key, [MaybeNullWhen(false)] out TValue value, BindableDictionary<TKey, TValue>? caller) 101 { 102 ensureMutationAllowed(); 103 104 if (!collection.Remove(key, out value)) 105 return false; 106 107 if (bindings != null) 108 { 109 foreach (var b in bindings) 110 { 111 // prevent re-removing from the callee. 112 // That would result in a <see cref="StackOverflowException"/>. 113 if (b != caller) 114 b.remove(key, out _, this); 115 } 116 } 117 118 notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs<TKey, TValue>(NotifyDictionaryChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value))); 119 120 return true; 121 } 122 123#if NETSTANDARD 124 public bool TryGetValue(TKey key, out TValue value) => collection.TryGetValue(key, out value); 125#else 126 public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => collection.TryGetValue(key, out value); 127#endif 128 129 /// <inheritdoc cref="IDictionary{TKey,TValue}.this" /> 130 /// <exception cref="InvalidOperationException">Thrown when setting an item while this <see cref="BindableDictionary{TKey, TValue}"/> is <see cref="Disabled"/>.</exception> 131 public TValue this[TKey key] 132 { 133 get => collection[key]; 134 set => setKey(key, value, null); 135 } 136 137 private void setKey(TKey key, TValue value, BindableDictionary<TKey, TValue>? caller) 138 { 139 ensureMutationAllowed(); 140 141#nullable disable // Todo: Remove after upgrading Resharper version on CI. 142 bool hasPreviousValue = TryGetValue(key, out TValue lastValue); 143 144 collection[key] = value; 145 146 if (bindings != null) 147 { 148 foreach (var b in bindings) 149 { 150 // prevent re-adding the item back to the callee. 151 // That would result in a <see cref="StackOverflowException"/>. 152 if (b != caller) 153 b.setKey(key, value, this); 154 } 155 } 156#nullable enable 157 158 notifyDictionaryChanged(hasPreviousValue 159 ? new NotifyDictionaryChangedEventArgs<TKey, TValue>(new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, lastValue!)) 160 : new NotifyDictionaryChangedEventArgs<TKey, TValue>(NotifyDictionaryChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value))); 161 } 162 163 public ICollection<TKey> Keys => collection.Keys; 164 165 public ICollection<TValue> Values => collection.Values; 166 167 #endregion 168 169 #region IDictionary 170 171 void IDictionary.Add(object key, object? value) => Add((TKey)key, (TValue)(value ?? throw new ArgumentNullException(nameof(value)))); 172 173 /// <inheritdoc cref="IDictionary.Clear" /> 174 /// <exception cref="InvalidOperationException">Thrown when this <see cref="BindableDictionary{TKey, TValue}"/> is <see cref="Disabled"/>.</exception> 175 public void Clear() 176 => clear(null); 177 178 private void clear(BindableDictionary<TKey, TValue>? caller) 179 { 180 ensureMutationAllowed(); 181 182 if (collection.Count == 0) 183 return; 184 185 // Preserve items for subscribers 186 var clearedItems = collection.ToArray(); 187 188 collection.Clear(); 189 190 if (bindings != null) 191 { 192 foreach (var b in bindings) 193 { 194 // prevent re-adding the item back to the callee. 195 // That would result in a <see cref="StackOverflowException"/>. 196 if (b != caller) 197 b.clear(this); 198 } 199 } 200 201 notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs<TKey, TValue>(NotifyDictionaryChangedAction.Remove, clearedItems)); 202 } 203 204 bool IDictionary.Contains(object key) 205 { 206 return ((IDictionary)collection).Contains(key); 207 } 208 209 void IDictionary.Remove(object key) => Remove((TKey)key); 210 211 bool IDictionary.IsFixedSize => ((IDictionary)collection).IsFixedSize; 212 213 public bool IsReadOnly => Disabled; 214 215 object? IDictionary.this[object key] 216 { 217 get => this[(TKey)key]; 218 set => this[(TKey)key] = (TValue)value!; 219 } 220 221 ICollection IDictionary.Values => (ICollection)Values; 222 223 ICollection IDictionary.Keys => (ICollection)Keys; 224 225 #endregion 226 227 #region IReadOnlyDictionary<TKey, TValue> 228 229 IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => Keys; 230 231 IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => Values; 232 233 #endregion 234 235 #region ICollection<KeyValuePair<TKey, TValue>> 236 237 bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item) 238 { 239#nullable disable // Todo: Remove after upgrading Resharper version on CI. 240 if (TryGetValue(item.Key, out TValue value) && EqualityComparer<TValue>.Default.Equals(value, item.Value)) 241 { 242 Remove(item.Key); 243 return true; 244 } 245#nullable enable 246 247 return false; 248 } 249 250 void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item) 251 => Add(item.Key, item.Value); 252 253 bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item) 254 => ((ICollection<KeyValuePair<TKey, TValue>>)collection).Contains(item); 255 256 void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) 257 => ((ICollection<KeyValuePair<TKey, TValue>>)collection).CopyTo(array, arrayIndex); 258 259 #endregion 260 261 #region ICollection 262 263 void ICollection.CopyTo(Array array, int index) 264 => ((ICollection)collection).CopyTo(array, index); 265 266 bool ICollection.IsSynchronized => ((ICollection)collection).IsSynchronized; 267 268 object ICollection.SyncRoot => ((ICollection)collection).SyncRoot; 269 270 #endregion 271 272 #region IReadOnlyCollection<TKey, TValue> 273 274 public int Count => collection.Count; 275 276 #endregion 277 278 #region IParseable 279 280 /// <summary> 281 /// Parse an object into this instance. 282 /// A collection holding items of type <see cref="KeyValuePair{TKey,TValue}"/> can be parsed. Null results in an empty <see cref="BindableDictionary{TKey, TValue}"/>. 283 /// </summary> 284 /// <param name="input">The input which is to be parsed.</param> 285 /// <exception cref="InvalidOperationException">Thrown if this <see cref="BindableDictionary{TKey, TValue}"/> is <see cref="Disabled"/>.</exception> 286 public void Parse(object? input) 287 { 288 ensureMutationAllowed(); 289 290 switch (input) 291 { 292 case null: 293 Clear(); 294 break; 295 296 case IEnumerable<KeyValuePair<TKey, TValue>> enumerable: 297 // enumerate once locally before proceeding. 298 var newItems = enumerable.ToList(); 299 300 if (this.SequenceEqual(newItems)) 301 return; 302 303 Clear(); 304 addRange(newItems, null); 305 break; 306 307 default: 308 throw new ArgumentException($@"Could not parse provided {input.GetType()} ({input}) to {typeof(KeyValuePair<TKey, TValue>)}."); 309 } 310 } 311 312 private void addRange(IList items, BindableDictionary<TKey, TValue>? caller) 313 { 314 ensureMutationAllowed(); 315 316 var typedItems = (IList<KeyValuePair<TKey, TValue>>)items; 317 318 foreach (var (key, value) in typedItems) 319 collection.Add(key, value); 320 321 if (bindings != null) 322 { 323 foreach (var b in bindings) 324 { 325 // prevent re-adding the item back to the callee. 326 // That would result in a <see cref="StackOverflowException"/>. 327 if (b != caller) 328 b.addRange(items, this); 329 } 330 } 331 332 notifyDictionaryChanged(new NotifyDictionaryChangedEventArgs<TKey, TValue>(NotifyDictionaryChangedAction.Add, typedItems)); 333 } 334 335 #endregion 336 337 #region ICanBeDisabled 338 339 private bool disabled; 340 341 /// <summary> 342 /// Whether this <see cref="BindableDictionary{TKey, TValue}"/> has been disabled. 343 /// When disabled, attempting to change the contents of this <see cref="BindableDictionary{TKey, TValue}"/> will result in an <see cref="InvalidOperationException"/>. 344 /// </summary> 345 public bool Disabled 346 { 347 get => disabled; 348 set 349 { 350 if (value == disabled) 351 return; 352 353 disabled = value; 354 355 triggerDisabledChange(); 356 } 357 } 358 359 public void BindDisabledChanged(Action<bool> onChange, bool runOnceImmediately = false) 360 { 361 DisabledChanged += onChange; 362 if (runOnceImmediately) 363 onChange(Disabled); 364 } 365 366 private void triggerDisabledChange(bool propagateToBindings = true) 367 { 368 // check a bound bindable hasn't changed the value again (it will fire its own event) 369 bool beforePropagation = disabled; 370 371 if (propagateToBindings && bindings != null) 372 { 373 foreach (var b in bindings) 374 b.Disabled = disabled; 375 } 376 377 if (beforePropagation == disabled) 378 DisabledChanged?.Invoke(disabled); 379 } 380 381 #endregion ICanBeDisabled 382 383 #region IUnbindable 384 385 public void UnbindEvents() 386 { 387 CollectionChanged = null; 388 DisabledChanged = null; 389 } 390 391 public void UnbindBindings() 392 { 393 if (bindings == null) 394 return; 395 396 foreach (var b in bindings) 397 b.unbind(this); 398 399 bindings?.Clear(); 400 } 401 402 public void UnbindAll() 403 { 404 UnbindEvents(); 405 UnbindBindings(); 406 } 407 408 public void UnbindFrom(IUnbindable them) 409 { 410 if (!(them is BindableDictionary<TKey, TValue> tThem)) 411 throw new InvalidCastException($"Can't unbind a bindable of type {them.GetType()} from a bindable of type {GetType()}."); 412 413 removeWeakReference(tThem.weakReference); 414 tThem.removeWeakReference(weakReference); 415 } 416 417 private void unbind(BindableDictionary<TKey, TValue> binding) 418 { 419 Debug.Assert(bindings != null); 420 bindings.Remove(binding.weakReference); 421 } 422 423 #endregion IUnbindable 424 425 #region IHasDescription 426 427 public string? Description { get; set; } 428 429 #endregion IHasDescription 430 431 #region IBindableCollection 432 433 void IBindable.BindTo(IBindable them) 434 { 435 if (!(them is BindableDictionary<TKey, TValue> tThem)) 436 throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); 437 438 BindTo(tThem); 439 } 440 441 void IBindableDictionary<TKey, TValue>.BindTo(IBindableDictionary<TKey, TValue> them) 442 { 443 if (!(them is BindableDictionary<TKey, TValue> tThem)) 444 throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); 445 446 BindTo(tThem); 447 } 448 449 /// <summary> 450 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios. 451 /// Passes the provided value as the foreign (more permanent) bindable. 452 /// </summary> 453 public BindableDictionary<TKey, TValue> BindTarget 454 { 455 set => ((IBindableDictionary<TKey, TValue>)this).BindTo(value); 456 } 457 458 /// <summary> 459 /// Binds this <see cref="BindableDictionary{TKey, TValue}"/> to another. 460 /// </summary> 461 /// <param name="them">The <see cref="BindableDictionary{TKey, TValue}"/> to be bound to.</param> 462 public void BindTo(BindableDictionary<TKey, TValue> them) 463 { 464 if (them == null) 465 throw new ArgumentNullException(nameof(them)); 466 if (bindings?.Contains(weakReference) == true) 467 throw new ArgumentException("An already bound collection can not be bound again."); 468 if (them == this) 469 throw new ArgumentException("A collection can not be bound to itself"); 470 471 // copy state and content over 472 Parse(them); 473 Disabled = them.Disabled; 474 475 addWeakReference(them.weakReference); 476 them.addWeakReference(weakReference); 477 } 478 479 /// <summary> 480 /// Bind an action to <see cref="CollectionChanged"/> with the option of running the bound action once immediately 481 /// with an <see cref="NotifyCollectionChangedAction.Add"/> event for the entire contents of this <see cref="BindableDictionary{TKey, TValue}"/>. 482 /// </summary> 483 /// <param name="onChange">The action to perform when this <see cref="BindableDictionary{TKey, TValue}"/> changes.</param> 484 /// <param name="runOnceImmediately">Whether the action provided in <paramref name="onChange"/> should be run once immediately.</param> 485 public void BindCollectionChanged(NotifyDictionaryChangedEventHandler<TKey, TValue> onChange, bool runOnceImmediately = false) 486 { 487 CollectionChanged += onChange; 488 if (runOnceImmediately) 489 onChange(this, new NotifyDictionaryChangedEventArgs<TKey, TValue>(NotifyDictionaryChangedAction.Add, collection.ToArray())); 490 } 491 492 private void addWeakReference(WeakReference<BindableDictionary<TKey, TValue>> weakReference) 493 { 494 bindings ??= new LockedWeakList<BindableDictionary<TKey, TValue>>(); 495 bindings.Add(weakReference); 496 } 497 498 private void removeWeakReference(WeakReference<BindableDictionary<TKey, TValue>> weakReference) => bindings?.Remove(weakReference); 499 500 IBindable IBindable.CreateInstance() => CreateInstance(); 501 502 /// <inheritdoc cref="IBindable.CreateInstance"/> 503 protected virtual BindableDictionary<TKey, TValue> CreateInstance() => new BindableDictionary<TKey, TValue>(); 504 505 IBindable IBindable.GetBoundCopy() => GetBoundCopy(); 506 507 IBindableDictionary<TKey, TValue> IBindableDictionary<TKey, TValue>.GetBoundCopy() => GetBoundCopy(); 508 509 /// <inheritdoc cref="IBindable.GetBoundCopy"/> 510 public BindableDictionary<TKey, TValue> GetBoundCopy() => IBindable.GetBoundCopyImplementation(this); 511 512 #endregion IBindableCollection 513 514 #region IEnumerable 515 516 public Dictionary<TKey, TValue>.Enumerator GetEnumerator() => collection.GetEnumerator(); 517 518 IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => GetEnumerator(); 519 520 IDictionaryEnumerator IDictionary.GetEnumerator() => GetEnumerator(); 521 522 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 523 524 #endregion IEnumerable 525 526 private void notifyDictionaryChanged(NotifyDictionaryChangedEventArgs<TKey, TValue> args) => CollectionChanged?.Invoke(this, args); 527 528 private void ensureMutationAllowed() 529 { 530 if (Disabled) 531 throw new InvalidOperationException($"Cannot mutate the {nameof(BindableDictionary<TKey, TValue>)} while it is disabled."); 532 } 533 534 public bool IsDefault => Count == 0; 535 } 536}