Skip to content

Commit 0b1e751

Browse files
author
Miguel Cartier
committed
**New**:
- Added *Rebind* functionality to all Observable classes (*ObservableField*, *ObservableList*, *ObservableDictionary*) allowing rebinding to new data sources without losing existing observers - Added *Rebind* methods to all Observable Resolver classes (*ObservableResolverField*, *ObservableResolverList*, *ObservableResolverDictionary*) to rebind to new origin collections and resolver functions - Added new *IObservableResolverField* interface with *Rebind* method for resolver field implementations
1 parent 467bc28 commit 0b1e751

10 files changed

+373
-17
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this package will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## [0.7.0] - 2025-11-03
8+
9+
**New**:
10+
- Added *Rebind* functionality to all Observable classes (*ObservableField*, *ObservableList*, *ObservableDictionary*) allowing rebinding to new data sources without losing existing observers
11+
- Added *Rebind* methods to all Observable Resolver classes (*ObservableResolverField*, *ObservableResolverList*, *ObservableResolverDictionary*) to rebind to new origin collections and resolver functions
12+
- Added new *IObservableResolverField* interface with *Rebind* method for resolver field implementations
13+
714
## [0.6.7] - 2025-04-07
815

916
**New**:

Runtime/ObservableDictionary.cs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,17 @@ public interface IObservableResolverDictionary<TKey, TValue, TKeyOrigin, TValueO
176176
/// Clear's to the origin dictionary
177177
/// </remarks>
178178
void ClearOrigin();
179+
180+
/// <summary>
181+
/// Rebinds this dictionary to a new origin dictionary and resolver functions without losing existing observers.
182+
/// The internal dictionary will be rebuilt from the new origin dictionary using the new resolvers.
183+
/// </summary>
184+
/// <param name="dictionary">The new origin dictionary to bind to</param>
185+
/// <param name="fromOrignResolver">The new function to convert from origin types to this dictionary's types</param>
186+
/// <param name="toOrignResolver">The new function to convert from this dictionary's types to origin types</param>
187+
void Rebind(IDictionary<TKeyOrigin, TValueOrigin> dictionary,
188+
Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> fromOrignResolver,
189+
Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> toOrignResolver);
179190
}
180191

181192
/// <inheritdoc />
@@ -193,7 +204,7 @@ public class ObservableDictionary<TKey, TValue> : IObservableDictionary<TKey, TV
193204
/// <inheritdoc />
194205
public ReadOnlyDictionary<TKey, TValue> ReadOnlyDictionary => new ReadOnlyDictionary<TKey, TValue>(Dictionary);
195206

196-
protected virtual IDictionary<TKey, TValue> Dictionary { get; }
207+
protected virtual IDictionary<TKey, TValue> Dictionary { get; set; }
197208

198209
private ObservableDictionary() { }
199210

@@ -203,6 +214,15 @@ public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
203214
ObservableUpdateFlag = ObservableUpdateFlag.KeyUpdateOnly;
204215
}
205216

217+
/// <summary>
218+
/// Rebinds this dictionary to a new dictionary without losing existing observers.
219+
/// </summary>
220+
/// <param name="dictionary">The new dictionary to bind to</param>
221+
public void Rebind(IDictionary<TKey, TValue> dictionary)
222+
{
223+
Dictionary = dictionary;
224+
}
225+
206226
/// <inheritdoc cref="Dictionary{TKey,TValue}.this" />
207227
public TValue this[TKey key]
208228
{
@@ -468,14 +488,14 @@ private int AdjustIndex(int index, Action<TKey, TValue, TValue, ObservableUpdate
468488
}
469489
}
470490

471-
/// <inheritdoc />
491+
/// <inheritdoc cref="IObservableResolverDictionary{TKey, TValue, TKeyOrigin, TValueOrigin}"/>
472492
public class ObservableResolverDictionary<TKey, TValue, TKeyOrigin, TValueOrigin> :
473493
ObservableDictionary<TKey, TValue>,
474494
IObservableResolverDictionary<TKey, TValue, TKeyOrigin, TValueOrigin>
475495
{
476-
private readonly IDictionary<TKeyOrigin, TValueOrigin> _dictionary;
477-
private readonly Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> _toOrignResolver;
478-
private readonly Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> _fromOrignResolver;
496+
private IDictionary<TKeyOrigin, TValueOrigin> _dictionary;
497+
private Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> _toOrignResolver;
498+
private Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> _fromOrignResolver;
479499

480500
/// <inheritdoc />
481501
public ReadOnlyDictionary<TKeyOrigin, TValueOrigin> OriginDictionary => new ReadOnlyDictionary<TKeyOrigin, TValueOrigin>(_dictionary);
@@ -495,6 +515,23 @@ public ObservableResolverDictionary(IDictionary<TKeyOrigin, TValueOrigin> dictio
495515
}
496516
}
497517

518+
/// <inheritdoc />
519+
public void Rebind(IDictionary<TKeyOrigin, TValueOrigin> dictionary,
520+
Func<KeyValuePair<TKeyOrigin, TValueOrigin>, KeyValuePair<TKey, TValue>> fromOrignResolver,
521+
Func<TKey, TValue, KeyValuePair<TKeyOrigin, TValueOrigin>> toOrignResolver)
522+
{
523+
_dictionary = dictionary;
524+
_toOrignResolver = toOrignResolver;
525+
_fromOrignResolver = fromOrignResolver;
526+
527+
// Rebuild the internal dictionary from the new origin dictionary
528+
Dictionary.Clear();
529+
foreach (var pair in dictionary)
530+
{
531+
Dictionary.Add(fromOrignResolver(pair));
532+
}
533+
}
534+
498535
/// <inheritdoc />
499536
public TValueOrigin GetOriginValue(TKey key)
500537
{

Runtime/ObservableField.cs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ public interface IObservableField<T> : IObservableFieldReader<T>
5050
/// The field value with possibility to be changed
5151
/// </summary>
5252
new T Value { get; set; }
53+
54+
/// <summary>
55+
/// Rebinds this field to a new value without losing existing observers.
56+
/// </summary>
57+
/// <param name="initialValue">The new initial value for the field</param>
58+
void Rebind(T initialValue);
59+
}
60+
61+
/// <inheritdoc />
62+
/// <remarks>
63+
/// A resolver field with the possibility to rebind to new resolver functions
64+
/// </remarks>
65+
public interface IObservableResolverField<T> : IObservableField<T>
66+
{
67+
/// <summary>
68+
/// Rebinds this field to new resolver functions without losing existing observers
69+
/// </summary>
70+
/// <param name="fieldResolver">The new getter function for the field</param>
71+
/// <param name="fieldSetter">The new setter function for the field</param>
72+
void Rebind(Func<T> fieldResolver, Action<T> fieldSetter);
5373
}
5474

5575
/// <inheritdoc />
@@ -84,6 +104,12 @@ public ObservableField(T initialValue)
84104

85105
public static implicit operator T(ObservableField<T> value) => value.Value;
86106

107+
/// <inheritdoc />
108+
public void Rebind(T initialValue)
109+
{
110+
_value = initialValue;
111+
}
112+
87113
/// <inheritdoc />
88114
public void Observe(Action<T, T> onUpdate)
89115
{
@@ -137,11 +163,11 @@ protected void InvokeUpdate(T previousValue)
137163
}
138164
}
139165

140-
/// <inheritdoc />
141-
public class ObservableResolverField<T> : ObservableField<T>
166+
/// <inheritdoc cref="IObservableResolverField{T}"/>
167+
public class ObservableResolverField<T> : ObservableField<T>, IObservableResolverField<T>
142168
{
143-
private readonly Func<T> _fieldResolver;
144-
private readonly Action<T> _fieldSetter;
169+
private Func<T> _fieldResolver;
170+
private Action<T> _fieldSetter;
145171

146172
/// <inheritdoc cref="IObservableField{T}.Value" />
147173
public override T Value
@@ -165,6 +191,17 @@ public ObservableResolverField(Func<T> fieldResolver, Action<T> fieldSetter)
165191
_fieldSetter = fieldSetter;
166192
}
167193

194+
/// <summary>
195+
/// Rebinds this field to new resolver functions without losing existing observers
196+
/// </summary>
197+
/// <param name="fieldResolver">The new getter function for the field</param>
198+
/// <param name="fieldSetter">The new setter function for the field</param>
199+
public void Rebind(Func<T> fieldResolver, Action<T> fieldSetter)
200+
{
201+
_fieldResolver = fieldResolver;
202+
_fieldSetter = fieldSetter;
203+
}
204+
168205
public static implicit operator T(ObservableResolverField<T> value) => value.Value;
169206
}
170207
}

Runtime/ObservableList.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public interface IObservableList<T> : IObservableListReader<T>
9494
/// <remarks>
9595
/// This interface resolves between 2 lists with different types of values
9696
/// </remarks>
97-
public interface IObservableResolverListReader<T, TOrigin> : IObservableListReader<T>
97+
public interface IObservableResolverListReader<T, out TOrigin> : IObservableListReader<T>
9898
{
9999
/// <summary>
100100
/// The Original List that is being resolved across the entire interface
@@ -134,6 +134,15 @@ public interface IObservableResolverList<T, TOrigin> :
134134
/// Clear's to the origin list
135135
/// </remarks>
136136
void ClearOrigin();
137+
138+
/// <summary>
139+
/// Rebinds this list to a new origin list and resolver functions without losing existing observers.
140+
/// The internal list will be rebuilt from the new origin list using the new resolvers.
141+
/// </summary>
142+
/// <param name="originList">The new origin list to bind to</param>
143+
/// <param name="fromOrignResolver">The new function to convert from origin type to this list's type</param>
144+
/// <param name="toOrignResolver">The new function to convert from this list's type to origin type</param>
145+
void Rebind(IList<TOrigin> originList, Func<TOrigin, T> fromOrignResolver, Func<T, TOrigin> toOrignResolver);
137146
}
138147

139148
/// <inheritdoc />
@@ -160,7 +169,7 @@ public T this[int index]
160169
/// <inheritdoc />
161170
public IReadOnlyList<T> ReadOnlyList => new List<T>(List);
162171

163-
protected virtual List<T> List { get; }
172+
protected virtual List<T> List { get; set; }
164173

165174
protected ObservableList() { }
166175

@@ -169,6 +178,15 @@ public ObservableList(IList<T> list)
169178
List = list as List<T> ?? list.ToList();
170179
}
171180

181+
/// <summary>
182+
/// Rebinds this list to a new list without losing existing observers.
183+
/// </summary>
184+
/// <param name="list">The new list to bind to</param>
185+
public void Rebind(IList<T> list)
186+
{
187+
List = list as List<T> ?? list.ToList();
188+
}
189+
172190
/// <inheritdoc cref="List{T}.GetEnumerator"/>
173191
public List<T>.Enumerator GetEnumerator()
174192
{
@@ -332,12 +350,15 @@ private int AdjustIndex(int index, Action<int, T, T, ObservableUpdateType> actio
332350
}
333351
}
334352

335-
/// <inheritdoc />
353+
/// <inheritdoc cref="IObservableResolverList{T, TOrigin}"/>
354+
/// <remarks>
355+
/// This class resolves between 2 lists with different types of values
356+
/// </remarks>
336357
public class ObservableResolverList<T, TOrigin> : ObservableList<T>, IObservableResolverList<T, TOrigin>
337358
{
338-
private readonly IList<TOrigin> _originList;
339-
private readonly Func<TOrigin, T> _fromOrignResolver;
340-
private readonly Func<T, TOrigin> _toOrignResolver;
359+
private IList<TOrigin> _originList;
360+
private Func<TOrigin, T> _fromOrignResolver;
361+
private Func<T, TOrigin> _toOrignResolver;
341362

342363
/// <inheritdoc />
343364
public IReadOnlyList<TOrigin> OriginList => new List<TOrigin>(_originList);
@@ -357,6 +378,23 @@ public ObservableResolverList(IList<TOrigin> originList,
357378
}
358379
}
359380

381+
/// <inheritdoc />
382+
public void Rebind(IList<TOrigin> originList,
383+
Func<TOrigin, T> fromOrignResolver,
384+
Func<T, TOrigin> toOrignResolver)
385+
{
386+
_originList = originList;
387+
_fromOrignResolver = fromOrignResolver;
388+
_toOrignResolver = toOrignResolver;
389+
390+
// Rebuild the internal list from the new origin list
391+
List.Clear();
392+
for (var i = 0; i < originList.Count; i++)
393+
{
394+
List.Add(fromOrignResolver(originList[i]));
395+
}
396+
}
397+
360398
/// <inheritdoc />
361399
public override void Add(T data)
362400
{

Tests/Editor/OberservableResolverListTest.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,70 @@ public void ClearOrigin_ClearsOriginList()
6666

6767
_mockList.Received().Clear();
6868
}
69+
70+
[Test]
71+
public void Rebind_ChangesOriginList()
72+
{
73+
// Add initial data
74+
_list.AddOrigin("1");
75+
_list.AddOrigin("2");
76+
77+
// Create new origin list and rebind
78+
var newOriginList = new List<string> { "10", "20", "30", "40" };
79+
_list.Rebind(
80+
newOriginList,
81+
origin => int.Parse(origin),
82+
value => value.ToString());
83+
84+
// Verify new list is being used
85+
Assert.AreEqual(4, _list.Count);
86+
Assert.AreEqual(10, _list[0]);
87+
Assert.AreEqual(20, _list[1]);
88+
Assert.AreEqual(30, _list[2]);
89+
Assert.AreEqual(40, _list[3]);
90+
91+
// Verify add operation uses new origin list
92+
_list.Add(50);
93+
Assert.AreEqual("50", newOriginList[4]);
94+
}
95+
96+
[Test]
97+
public void Rebind_KeepsObservers()
98+
{
99+
// Setup observer
100+
var observerCalls = 0;
101+
_list.Observe((index, prev, curr, type) => observerCalls++);
102+
103+
// Create new origin list and rebind
104+
var newOriginList = new List<string> { "100", "200" };
105+
_list.Rebind(
106+
newOriginList,
107+
origin => int.Parse(origin),
108+
value => value.ToString());
109+
110+
// Trigger update and verify observer is still active
111+
_list.Add(300);
112+
Assert.AreEqual(1, observerCalls);
113+
}
114+
115+
[Test]
116+
public void Rebind_ChangesResolvers()
117+
{
118+
// Create new origin list with different format and rebind with new resolvers
119+
var newOriginList = new List<string> { "value:10", "value:20" };
120+
_list.Rebind(
121+
newOriginList,
122+
origin => int.Parse(origin.Split(':')[1]),
123+
value => $"value:{value}");
124+
125+
// Verify new resolvers are being used
126+
Assert.AreEqual(2, _list.Count);
127+
Assert.AreEqual(10, _list[0]);
128+
Assert.AreEqual(20, _list[1]);
129+
130+
// Verify add operation uses new resolver
131+
_list.Add(30);
132+
Assert.AreEqual("value:30", newOriginList[2]);
133+
}
69134
}
70135
}

Tests/Editor/ObservableDictionaryTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,5 +280,37 @@ public void StopObservingAll_NotObserving_DoesNothing()
280280

281281
_caller.DidNotReceive().Call(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<ObservableUpdateType>());
282282
}
283+
284+
[Test]
285+
public void RebindCheck_BaseClass()
286+
{
287+
// Add initial data
288+
_dictionary.Add(1, 100);
289+
_dictionary.Add(2, 200);
290+
291+
// Setup key-specific observer (this works with default KeyUpdateOnly flag)
292+
_dictionary.Observe(40, _caller.Call);
293+
294+
// Create new dictionary and rebind
295+
var newDictionary = new Dictionary<int, int> { { 10, 1000 }, { 20, 2000 }, { 30, 3000 } };
296+
_dictionary.Rebind(newDictionary);
297+
298+
// Verify new dictionary is being used
299+
Assert.AreEqual(3, _dictionary.Count);
300+
Assert.IsTrue(_dictionary.ContainsKey(10));
301+
Assert.IsTrue(_dictionary.ContainsKey(20));
302+
Assert.IsTrue(_dictionary.ContainsKey(30));
303+
Assert.AreEqual(1000, _dictionary[10]);
304+
Assert.AreEqual(2000, _dictionary[20]);
305+
Assert.AreEqual(3000, _dictionary[30]);
306+
307+
// Verify old keys are no longer present
308+
Assert.IsFalse(_dictionary.ContainsKey(1));
309+
Assert.IsFalse(_dictionary.ContainsKey(2));
310+
311+
// Verify observer still works after rebind
312+
_dictionary.Add(40, 4000);
313+
_caller.Received(1).Call(40, 0, 4000, ObservableUpdateType.Added);
314+
}
283315
}
284316
}

0 commit comments

Comments
 (0)