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