A game framework written with osu! in mind.
at master 125 lines 4.3 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.Linq; 7 8namespace osu.Framework.Bindables 9{ 10 /// <summary> 11 /// Combines multiple bindables into one aggregate bindable result. 12 /// </summary> 13 /// <typeparam name="T">The type of values.</typeparam> 14 public class AggregateBindable<T> 15 { 16 private readonly Func<T, T, T> aggregateFunction; 17 18 /// <summary> 19 /// The final result after aggregating all added sources. 20 /// </summary> 21 public IBindable<T> Result => result; 22 23 private readonly Bindable<T> result; 24 25 private readonly T initialValue; 26 27 /// <summary> 28 /// Create a new aggregate bindable. 29 /// </summary> 30 /// <param name="aggregateFunction">The function to be used for aggregation, taking two input <typeparamref name="T"/> values and returning one output.</param> 31 /// <param name="resultBindable">An optional newly constructed bindable to use for <see cref="Result"/>. The initial value of this bindable is used as the initial value for the aggregate.</param> 32 public AggregateBindable(Func<T, T, T> aggregateFunction, Bindable<T> resultBindable = null) 33 { 34 this.aggregateFunction = aggregateFunction; 35 result = resultBindable ?? new Bindable<T>(); 36 initialValue = result.Value; 37 } 38 39 private readonly List<WeakRefPair> sourceMapping = new List<WeakRefPair>(); 40 41 /// <summary> 42 /// Add a new source to be included in aggregation. 43 /// </summary> 44 /// <param name="bindable">The bindable to add.</param> 45 public void AddSource(IBindable<T> bindable) 46 { 47 lock (sourceMapping) 48 { 49 if (findExistingPair(bindable) != null) 50 return; 51 52 var boundCopy = bindable.GetBoundCopy(); 53 sourceMapping.Add(new WeakRefPair(new WeakReference<IBindable<T>>(bindable), boundCopy)); 54 boundCopy.BindValueChanged(recalculateAggregate, true); 55 } 56 } 57 58 /// <summary> 59 /// Remove a source from being included in aggregation. 60 /// </summary> 61 /// <param name="bindable">The bindable to remove.</param> 62 public void RemoveSource(IBindable<T> bindable) 63 { 64 lock (sourceMapping) 65 { 66 var weak = findExistingPair(bindable); 67 68 if (weak != null) 69 { 70 weak.BoundCopy.UnbindAll(); 71 sourceMapping.Remove(weak); 72 } 73 74 recalculateAggregate(); 75 } 76 } 77 78 private WeakRefPair findExistingPair(IBindable<T> bindable) => 79 sourceMapping.FirstOrDefault(p => p.WeakReference.TryGetTarget(out var target) && target == bindable); 80 81 private void recalculateAggregate(ValueChangedEvent<T> obj = null) 82 { 83 T calculated = initialValue; 84 85 lock (sourceMapping) 86 { 87 for (var i = 0; i < sourceMapping.Count; i++) 88 { 89 var pair = sourceMapping[i]; 90 91 if (!pair.WeakReference.TryGetTarget(out _)) 92 sourceMapping.RemoveAt(i--); 93 else 94 calculated = aggregateFunction(calculated, pair.BoundCopy.Value); 95 } 96 } 97 98 result.Value = calculated; 99 } 100 101 public void RemoveAllSources() 102 { 103 lock (sourceMapping) 104 { 105 foreach (var mapping in sourceMapping.ToArray()) 106 { 107 if (mapping.WeakReference.TryGetTarget(out var b)) 108 RemoveSource(b); 109 } 110 } 111 } 112 113 private class WeakRefPair 114 { 115 public readonly WeakReference<IBindable<T>> WeakReference; 116 public readonly IBindable<T> BoundCopy; 117 118 public WeakRefPair(WeakReference<IBindable<T>> weakReference, IBindable<T> boundCopy) 119 { 120 WeakReference = weakReference; 121 BoundCopy = boundCopy; 122 } 123 } 124 } 125}