A game framework written with osu! in mind.
at master 473 lines 18 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 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}