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
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}