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.Generic;
6using System.Globalization;
7using System.Linq;
8using JetBrains.Annotations;
9using Newtonsoft.Json;
10using osu.Framework.Extensions.TypeExtensions;
11using osu.Framework.IO.Serialization;
12using osu.Framework.Lists;
13
14namespace osu.Framework.Bindables
15{
16 /// <summary>
17 /// A generic implementation of a <see cref="IBindable"/>
18 /// </summary>
19 /// <typeparam name="T">The type of our stored <see cref="Value"/>.</typeparam>
20 public class Bindable<T> : IBindable<T>, IBindable, IParseable, ISerializableBindable
21 {
22 /// <summary>
23 /// An event which is raised when <see cref="Value"/> has changed (or manually via <see cref="TriggerValueChange"/>).
24 /// </summary>
25 public event Action<ValueChangedEvent<T>> ValueChanged;
26
27 /// <summary>
28 /// An event which is raised when <see cref="Disabled"/> has changed (or manually via <see cref="TriggerDisabledChange"/>).
29 /// </summary>
30 public event Action<bool> DisabledChanged;
31
32 /// <summary>
33 /// An event which is raised when <see cref="Default"/> has changed (or manually via <see cref="TriggerDefaultChange"/>).
34 /// </summary>
35 public event Action<ValueChangedEvent<T>> DefaultChanged;
36
37 private T value;
38
39 private T defaultValue;
40
41 private bool disabled;
42
43 /// <summary>
44 /// Whether this bindable has been disabled. When disabled, attempting to change the <see cref="Value"/> will result in an <see cref="InvalidOperationException"/>.
45 /// </summary>
46 public virtual bool Disabled
47 {
48 get => disabled;
49 set
50 {
51 // if a lease is active, disabled can *only* be changed by that leased bindable.
52 throwIfLeased();
53
54 if (disabled == value) return;
55
56 SetDisabled(value);
57 }
58 }
59
60 internal void SetDisabled(bool value, bool bypassChecks = false, Bindable<T> source = null)
61 {
62 if (!bypassChecks)
63 throwIfLeased();
64
65 disabled = value;
66 TriggerDisabledChange(source ?? this, true, bypassChecks);
67 }
68
69 /// <summary>
70 /// Check whether the current <see cref="Value"/> is equal to <see cref="Default"/>.
71 /// </summary>
72 public virtual bool IsDefault => EqualityComparer<T>.Default.Equals(value, Default);
73
74 /// <summary>
75 /// Revert the current <see cref="Value"/> to the defined <see cref="Default"/>.
76 /// </summary>
77 public void SetDefault() => Value = Default;
78
79 /// <summary>
80 /// The current value of this bindable.
81 /// </summary>
82 public virtual T Value
83 {
84 get => value;
85 set
86 {
87 // intentionally don't have throwIfLeased() here.
88 // if the leased bindable decides to disable exclusive access (by setting Disabled = false) then anything will be able to write to Value.
89
90 if (Disabled)
91 throw new InvalidOperationException($"Can not set value to \"{value.ToString()}\" as bindable is disabled.");
92
93 if (EqualityComparer<T>.Default.Equals(this.value, value)) return;
94
95 SetValue(this.value, value);
96 }
97 }
98
99 internal void SetValue(T previousValue, T value, bool bypassChecks = false, Bindable<T> source = null)
100 {
101 this.value = value;
102 TriggerValueChange(previousValue, source ?? this, true, bypassChecks);
103 }
104
105 /// <summary>
106 /// The default value of this bindable. Used when calling <see cref="SetDefault"/> or querying <see cref="IsDefault"/>.
107 /// </summary>
108 public virtual T Default
109 {
110 get => defaultValue;
111 set
112 {
113 // intentionally don't have throwIfLeased() here.
114 // if the leased bindable decides to disable exclusive access (by setting Disabled = false) then anything will be able to write to Default.
115
116 if (Disabled)
117 throw new InvalidOperationException($"Can not set default value to \"{value.ToString()}\" as bindable is disabled.");
118
119 if (EqualityComparer<T>.Default.Equals(defaultValue, value)) return;
120
121 SetDefaultValue(defaultValue, value);
122 }
123 }
124
125 internal void SetDefaultValue(T previousValue, T value, bool bypassChecks = false, Bindable<T> source = null)
126 {
127 defaultValue = value;
128 TriggerDefaultChange(previousValue, source ?? this, true, bypassChecks);
129 }
130
131 private WeakReference<Bindable<T>> weakReferenceInstance;
132
133 private WeakReference<Bindable<T>> weakReference => weakReferenceInstance ??= new WeakReference<Bindable<T>>(this);
134
135 /// <summary>
136 /// Creates a new bindable instance. This is used for deserialization of bindables.
137 /// </summary>
138 [UsedImplicitly]
139 private Bindable()
140 : this(default)
141 {
142 }
143
144 /// <summary>
145 /// Creates a new bindable instance initialised with a default value.
146 /// </summary>
147 /// <param name="defaultValue">The initial and default value for this bindable.</param>
148 public Bindable(T defaultValue = default)
149 {
150 value = Default = defaultValue;
151 }
152
153 protected LockedWeakList<Bindable<T>> Bindings { get; private set; }
154
155 void IBindable.BindTo(IBindable them)
156 {
157 if (!(them is Bindable<T> tThem))
158 throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}.");
159
160 BindTo(tThem);
161 }
162
163 void IBindable<T>.BindTo(IBindable<T> them)
164 {
165 if (!(them is Bindable<T> tThem))
166 throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}.");
167
168 BindTo(tThem);
169 }
170
171 /// <summary>
172 /// An alias of <see cref="BindTo"/> provided for use in object initializer scenarios.
173 /// Passes the provided value as the foreign (more permanent) bindable.
174 /// </summary>
175 public IBindable<T> BindTarget
176 {
177 set => ((IBindable<T>)this).BindTo(value);
178 }
179
180 /// <summary>
181 /// Binds this bindable to another such that bi-directional updates are propagated.
182 /// This will adopt any values and value limitations of the bindable bound to.
183 /// </summary>
184 /// <param name="them">The foreign bindable. This should always be the most permanent end of the bind (ie. a ConfigManager).</param>
185 /// <exception cref="InvalidOperationException">Thrown when attempting to bind to an already bound object.</exception>
186 public virtual void BindTo(Bindable<T> them)
187 {
188 if (Bindings?.Contains(them) == true)
189 throw new InvalidOperationException($"This bindable is already bound to the requested bindable ({them}).");
190
191 Value = them.Value;
192 Default = them.Default;
193 Disabled = them.Disabled;
194
195 addWeakReference(them.weakReference);
196 them.addWeakReference(weakReference);
197 }
198
199 /// <summary>
200 /// Bind an action to <see cref="ValueChanged"/> with the option of running the bound action once immediately.
201 /// </summary>
202 /// <param name="onChange">The action to perform when <see cref="Value"/> changes.</param>
203 /// <param name="runOnceImmediately">Whether the action provided in <paramref name="onChange"/> should be run once immediately.</param>
204 public void BindValueChanged(Action<ValueChangedEvent<T>> onChange, bool runOnceImmediately = false)
205 {
206 ValueChanged += onChange;
207 if (runOnceImmediately)
208 onChange(new ValueChangedEvent<T>(Value, Value));
209 }
210
211 /// <summary>
212 /// Bind an action to <see cref="DisabledChanged"/> with the option of running the bound action once immediately.
213 /// </summary>
214 /// <param name="onChange">The action to perform when <see cref="Disabled"/> changes.</param>
215 /// <param name="runOnceImmediately">Whether the action provided in <paramref name="onChange"/> should be run once immediately.</param>
216 public void BindDisabledChanged(Action<bool> onChange, bool runOnceImmediately = false)
217 {
218 DisabledChanged += onChange;
219 if (runOnceImmediately)
220 onChange(Disabled);
221 }
222
223 private void addWeakReference(WeakReference<Bindable<T>> weakReference)
224 {
225 Bindings ??= new LockedWeakList<Bindable<T>>();
226 Bindings.Add(weakReference);
227 }
228
229 private void removeWeakReference(WeakReference<Bindable<T>> weakReference) => Bindings?.Remove(weakReference);
230
231 /// <summary>
232 /// Parse an object into this instance.
233 /// An object deriving T can be parsed, or a string can be parsed if T is an enum type.
234 /// </summary>
235 /// <param name="input">The input which is to be parsed.</param>
236 public virtual void Parse(object input)
237 {
238 Type underlyingType = typeof(T).GetUnderlyingNullableType() ?? typeof(T);
239
240 switch (input)
241 {
242 case T t:
243 Value = t;
244 break;
245
246 case IBindable _:
247 if (!(input is IBindable<T> bindable))
248 throw new ArgumentException($"Expected bindable of type {nameof(IBindable)}<{typeof(T)}>, got {input.GetType()}", nameof(input));
249
250 Value = bindable.Value;
251 break;
252
253 default:
254 if (underlyingType.IsEnum)
255 Value = (T)Enum.Parse(underlyingType, input.ToString());
256 else
257 Value = (T)Convert.ChangeType(input, underlyingType, CultureInfo.InvariantCulture);
258
259 break;
260 }
261 }
262
263 /// <summary>
264 /// Raise <see cref="ValueChanged"/> and <see cref="DisabledChanged"/> once, without any changes actually occurring.
265 /// This does not propagate to any outward bound bindables.
266 /// </summary>
267 public virtual void TriggerChange()
268 {
269 TriggerValueChange(value, this, false);
270 TriggerDisabledChange(this, false);
271 }
272
273 protected void TriggerValueChange(T previousValue, Bindable<T> source, bool propagateToBindings = true, bool bypassChecks = false)
274 {
275 // check a bound bindable hasn't changed the value again (it will fire its own event)
276 T beforePropagation = value;
277
278 if (propagateToBindings && Bindings != null)
279 {
280 foreach (var b in Bindings)
281 {
282 if (b == source) continue;
283
284 b.SetValue(previousValue, value, bypassChecks, this);
285 }
286 }
287
288 if (EqualityComparer<T>.Default.Equals(beforePropagation, value))
289 ValueChanged?.Invoke(new ValueChangedEvent<T>(previousValue, value));
290 }
291
292 protected void TriggerDefaultChange(T previousValue, Bindable<T> source, bool propagateToBindings = true, bool bypassChecks = false)
293 {
294 // check a bound bindable hasn't changed the value again (it will fire its own event)
295 T beforePropagation = defaultValue;
296
297 if (propagateToBindings && Bindings != null)
298 {
299 foreach (var b in Bindings)
300 {
301 if (b == source) continue;
302
303 b.SetDefaultValue(previousValue, defaultValue, bypassChecks, this);
304 }
305 }
306
307 if (EqualityComparer<T>.Default.Equals(beforePropagation, defaultValue))
308 DefaultChanged?.Invoke(new ValueChangedEvent<T>(previousValue, defaultValue));
309 }
310
311 protected void TriggerDisabledChange(Bindable<T> source, bool propagateToBindings = true, bool bypassChecks = false)
312 {
313 // check a bound bindable hasn't changed the value again (it will fire its own event)
314 bool beforePropagation = disabled;
315
316 if (propagateToBindings && Bindings != null)
317 {
318 foreach (var b in Bindings)
319 {
320 if (b == source) continue;
321
322 b.SetDisabled(disabled, bypassChecks, this);
323 }
324 }
325
326 if (beforePropagation == disabled)
327 DisabledChanged?.Invoke(disabled);
328 }
329
330 /// <summary>
331 /// Unbinds any actions bound to the value changed events.
332 /// </summary>
333 public virtual void UnbindEvents()
334 {
335 ValueChanged = null;
336 DefaultChanged = null;
337 DisabledChanged = null;
338 }
339
340 /// <summary>
341 /// Remove all bound <see cref="Bindable{T}"/>s via <see cref="GetBoundCopy"/> or <see cref="BindTo"/>.
342 /// </summary>
343 public void UnbindBindings()
344 {
345 if (Bindings == null)
346 return;
347
348 // ToArray required as this may be called from an async disposal thread.
349 // This can lead to deadlocks since each child is also enumerating its Bindings.
350 foreach (var b in Bindings.ToArray())
351 UnbindFrom(b);
352 }
353
354 /// <summary>
355 /// Calls <see cref="UnbindEvents"/> and <see cref="UnbindBindings"/>.
356 /// Also returns any active lease.
357 /// </summary>
358 public void UnbindAll() => UnbindAllInternal();
359
360 internal virtual void UnbindAllInternal()
361 {
362 if (isLeased)
363 leasedBindable.Return();
364
365 UnbindEvents();
366 UnbindBindings();
367 }
368
369 public virtual void UnbindFrom(IUnbindable them)
370 {
371 if (!(them is Bindable<T> tThem))
372 throw new InvalidCastException($"Can't unbind a bindable of type {them.GetType()} from a bindable of type {GetType()}.");
373
374 removeWeakReference(tThem.weakReference);
375 tThem.removeWeakReference(weakReference);
376 }
377
378 public string Description { get; set; }
379
380 public override string ToString() => value?.ToString() ?? string.Empty;
381
382 /// <summary>
383 /// Create an unbound clone of this bindable.
384 /// </summary>
385 public Bindable<T> GetUnboundCopy()
386 {
387 var clone = GetBoundCopy();
388 clone.UnbindAll();
389 return clone;
390 }
391
392 IBindable IBindable.CreateInstance() => CreateInstance();
393
394 /// <inheritdoc cref="IBindable.CreateInstance"/>
395 protected virtual Bindable<T> CreateInstance() => new Bindable<T>();
396
397 IBindable IBindable.GetBoundCopy() => GetBoundCopy();
398
399 IBindable<T> IBindable<T>.GetBoundCopy() => GetBoundCopy();
400
401 /// <inheritdoc cref="IBindable{T}.GetBoundCopy"/>
402 public Bindable<T> GetBoundCopy() => IBindable.GetBoundCopyImplementation(this);
403
404 void ISerializableBindable.SerializeTo(JsonWriter writer, JsonSerializer serializer)
405 {
406 serializer.Serialize(writer, Value);
407 }
408
409 void ISerializableBindable.DeserializeFrom(JsonReader reader, JsonSerializer serializer)
410 {
411 Value = serializer.Deserialize<T>(reader);
412 }
413
414 private LeasedBindable<T> leasedBindable;
415
416 private bool isLeased => leasedBindable != null;
417
418 /// <summary>
419 /// Takes out a mutually exclusive lease on this bindable.
420 /// During a lease, the bindable will be set to <see cref="Disabled"/>, but changes can still be applied via the <see cref="LeasedBindable{T}"/> returned by this call.
421 /// You should end a lease by calling <see cref="LeasedBindable{T}.Return"/> when done.
422 /// </summary>
423 /// <param name="revertValueOnReturn">Whether the <see cref="Value"/> when <see cref="BeginLease"/> was called should be restored when the lease ends.</param>
424 /// <returns>A bindable with a lease.</returns>
425 public LeasedBindable<T> BeginLease(bool revertValueOnReturn)
426 {
427 if (checkForLease(this))
428 throw new InvalidOperationException("Attempted to lease a bindable that is already in a leased state.");
429
430 return leasedBindable = new LeasedBindable<T>(this, revertValueOnReturn);
431 }
432
433 private bool checkForLease(Bindable<T> source)
434 {
435 if (isLeased)
436 return true;
437
438 if (Bindings == null)
439 return false;
440
441 bool found = false;
442
443 foreach (var b in Bindings)
444 {
445 if (b != source)
446 found |= b.checkForLease(this);
447 }
448
449 return found;
450 }
451
452 /// <summary>
453 /// Called internally by a <see cref="LeasedBindable{T}"/> to end a lease.
454 /// </summary>
455 /// <param name="returnedBindable">The <see cref="ILeasedBindable{T}"/> that was provided as a return of a <see cref="BeginLease"/> call.</param>
456 internal void EndLease(ILeasedBindable<T> returnedBindable)
457 {
458 if (!isLeased)
459 throw new InvalidOperationException("Attempted to end a lease without beginning one.");
460
461 if (returnedBindable != leasedBindable)
462 throw new InvalidOperationException("Attempted to end a lease but returned a different bindable to the one used to start the lease.");
463
464 leasedBindable = null;
465 }
466
467 private void throwIfLeased()
468 {
469 if (isLeased)
470 throw new InvalidOperationException($"Cannot perform this operation on a {nameof(Bindable<T>)} that is currently in a leased state.");
471 }
472 }
473}