Skip to content

Commit

Permalink
add read-only variation of BindableReactiveProperty and its tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Funkest committed Feb 28, 2024
1 parent 66c09b8 commit 1e7f4d4
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 31 deletions.
44 changes: 43 additions & 1 deletion src/R3/BindableReactiveProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,57 @@ public interface IBindableReactiveProperty
void OnNext(object? value);
}

public abstract class ReadOnlyBindableReactiveProperty<T> : ReactivePropertyBase<T>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

public ReadOnlyBindableReactiveProperty()
: base()
{
}

public ReadOnlyBindableReactiveProperty(T value)
: base(value)
{
}

public ReadOnlyBindableReactiveProperty(T value, IEqualityComparer<T>? equalityComparer)
: base(value, equalityComparer)
{
}
}

// all operators need to call from UI Thread(not thread-safe)

#if NET6_0_OR_GREATER
[System.Text.Json.Serialization.JsonConverter(typeof(BindableReactivePropertyJsonConverterFactory))]
#endif
public class BindableReactiveProperty<T> : ReactiveProperty<T>, INotifyPropertyChanged, INotifyDataErrorInfo, IBindableReactiveProperty
public class BindableReactiveProperty<T> : ReadOnlyBindableReactiveProperty<T>, INotifyPropertyChanged, INotifyDataErrorInfo, IBindableReactiveProperty
{
IDisposable? subscription;

public T Value
{
get => this.currentValue;
set
{
OnValueChanging(ref value);

if (EqualityComparer != null)
{
if (EqualityComparer.Equals(this.currentValue, value))
{
return;
}
}

this.currentValue = value;
OnValueChanged(value);

OnNextCore(value);
}
}

// ctor

public BindableReactiveProperty()
Expand Down
83 changes: 54 additions & 29 deletions src/R3/ReactiveProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,8 @@ protected virtual void OnReceiveError(Exception exception) { }
#if NET6_0_OR_GREATER
[JsonConverter(typeof(ReactivePropertyJsonConverterFactory))]
#endif
public class ReactiveProperty<T> : ReadOnlyReactiveProperty<T>, ISubject<T>
public class ReactiveProperty<T> : ReactivePropertyBase<T>
{
T currentValue;
IEqualityComparer<T>? equalityComparer;
FreeListCore<Subscription> list; // struct(array, int)
CompleteState completeState; // struct(int, IntPtr)

public IEqualityComparer<T>? EqualityComparer => equalityComparer;

public override T CurrentValue => currentValue;

public bool IsDisposed => completeState.IsDisposed;

public T Value
{
get => this.currentValue;
Expand All @@ -57,16 +46,52 @@ public T Value
}
}

public ReactiveProperty() : this(default!)
public ReactiveProperty() : base()
{
}

public ReactiveProperty(T value)
: this(value, EqualityComparer<T>.Default)
public ReactiveProperty(T value) : base(value)
{
}

public ReactiveProperty(T value, IEqualityComparer<T>? equalityComparer)
: base(value, equalityComparer)
{
}

protected ReactiveProperty(T value, IEqualityComparer<T>? equalityComparer, bool callOnValueChangeInBaseConstructor) : base(value, equalityComparer, callOnValueChangeInBaseConstructor)
{
}
}

// not abstract to json serialization

#if NET6_0_OR_GREATER
[JsonConverter(typeof(ReactivePropertyJsonConverterFactory))]
#endif
public class ReactivePropertyBase<T> : ReadOnlyReactiveProperty<T>, ISubject<T>
{
protected T currentValue;
IEqualityComparer<T>? equalityComparer;
FreeListCore<Subscription> list; // struct(array, int)
CompleteState completeState; // struct(int, IntPtr)

public IEqualityComparer<T>? EqualityComparer => equalityComparer;

public override T CurrentValue => currentValue;

public bool IsDisposed => completeState.IsDisposed;

public ReactivePropertyBase() : this(default!)
{
}

public ReactivePropertyBase(T value)
: this(value, EqualityComparer<T>.Default)
{
}

public ReactivePropertyBase(T value, IEqualityComparer<T>? equalityComparer)
{
this.equalityComparer = equalityComparer;
this.list = new FreeListCore<Subscription>(this); // use self as gate(reduce memory usage), this is slightly dangerous so don't lock this in user.
Expand All @@ -76,7 +101,7 @@ public ReactiveProperty(T value, IEqualityComparer<T>? equalityComparer)
OnValueChanged(value);
}

protected ReactiveProperty(T value, IEqualityComparer<T>? equalityComparer, bool callOnValueChangeInBaseConstructor)
protected ReactivePropertyBase(T value, IEqualityComparer<T>? equalityComparer, bool callOnValueChangeInBaseConstructor)
{
this.equalityComparer = equalityComparer;
this.list = new FreeListCore<Subscription>(this);
Expand All @@ -99,7 +124,7 @@ protected virtual void OnValueChanging(ref T value) { }

public void ForceNotify()
{
OnNext(Value);
OnNext(currentValue);
}

public void OnNext(T value)
Expand All @@ -111,7 +136,7 @@ public void OnNext(T value)
OnNextCore(value);
}

void OnNextCore(T value)
protected void OnNextCore(T value)
{
if (completeState.IsCompleted) return;

Expand Down Expand Up @@ -213,9 +238,9 @@ sealed class Subscription : IDisposable
{
public readonly Observer<T> observer;
readonly int removeKey;
ReactiveProperty<T>? parent;
ReactivePropertyBase<T>? parent;

public Subscription(ReactiveProperty<T> parent, Observer<T> observer)
public Subscription(ReactivePropertyBase<T> parent, Observer<T> observer)
{
this.parent = parent;
this.observer = observer;
Expand Down Expand Up @@ -260,7 +285,7 @@ public override bool CanConvert(Type typeToConvert)
{
while (type != null)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReactiveProperty<>))
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReactivePropertyBase<>))
{
return type;
}
Expand All @@ -274,23 +299,23 @@ public override bool CanConvert(Type typeToConvert)
protected virtual Type GenericConverterType => typeof(ReactivePropertyJsonConverter<>);
}

public class ReactivePropertyJsonConverter<T> : JsonConverter<ReactiveProperty<T>>
public class ReactivePropertyJsonConverter<T> : JsonConverter<ReactivePropertyBase<T>>
{
public override void Write(Utf8JsonWriter writer, ReactiveProperty<T> value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, ReactivePropertyBase<T> value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.Value, options);
JsonSerializer.Serialize(writer, value.CurrentValue, options);
}

public override ReactiveProperty<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override ReactivePropertyBase<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var v = JsonSerializer.Deserialize<T>(ref reader, options);
return CreateReactiveProperty(v!);
}

// allow customize
protected virtual ReactiveProperty<T> CreateReactiveProperty(T value)
protected virtual ReactivePropertyBase<T> CreateReactiveProperty(T value)
{
return new ReactiveProperty<T>(value);
return new ReactivePropertyBase<T>(value);
}

public override bool CanConvert(Type typeToConvert)
Expand All @@ -302,7 +327,7 @@ public override bool CanConvert(Type typeToConvert)
{
while (type != null)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReactiveProperty<>))
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ReactivePropertyBase<>))
{
return type;
}
Expand All @@ -322,7 +347,7 @@ internal class BindableReactivePropertyJsonConverterFactory : ReactivePropertyJs

internal class BindableReactivePropertyJsonConverter<T> : ReactivePropertyJsonConverter<T>
{
protected override ReactiveProperty<T> CreateReactiveProperty(T value)
protected override ReactivePropertyBase<T> CreateReactiveProperty(T value)
{
return new BindableReactiveProperty<T>(value);
}
Expand Down
54 changes: 54 additions & 0 deletions src/R3/ReactivePropertyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ public static BindableReactiveProperty<T> ToBindableReactiveProperty<T>(this Obs
{
return new BindableReactiveProperty<T>(source, initialValue, equalityComparer);
}

public static ReadOnlyBindableReactiveProperty<T> ToReadOnlyBindableReactiveProperty<T>(this Observable<T> source, T initialValue = default!)
{
if (source is ReadOnlyBindableReactiveProperty<T> rrp)
{
return rrp;
}
return source.ToReadOnlyBindableReactiveProperty(EqualityComparer<T>.Default, initialValue);
}

public static ReadOnlyBindableReactiveProperty<T> ToReadOnlyBindableReactiveProperty<T>(this Observable<T> source, IEqualityComparer<T>? equalityComparer, T initialValue = default!)
{
if (source is ReadOnlyBindableReactiveProperty<T> rrp)
{
return rrp;
}
// allow to cast ReactiveProperty<T>
return new ConnectedBindableReactiveProperty<T>(source, initialValue, equalityComparer);
}
}

internal sealed class ConnectedReactiveProperty<T> : ReactiveProperty<T>
Expand Down Expand Up @@ -64,3 +83,38 @@ protected override void OnCompletedCore(Result result)
}
}
}

internal sealed class ConnectedBindableReactiveProperty<T> : BindableReactiveProperty<T>
{
readonly IDisposable sourceSubscription;

public ConnectedBindableReactiveProperty(Observable<T> source, T initialValue, IEqualityComparer<T>? equalityComparer)
: base(initialValue, equalityComparer)
{
this.sourceSubscription = source.Subscribe(new Observer(this));
}

protected override void DisposeCore()
{
sourceSubscription.Dispose();
}

class Observer(ConnectedBindableReactiveProperty<T> parent) : Observer<T>
{
protected override void OnNextCore(T value)
{
parent.Value = value;
}

protected override void OnErrorResumeCore(Exception error)
{
parent.OnErrorResume(error);
}

protected override void OnCompletedCore(Result result)
{
parent.OnCompleted(result);
}
}
}

100 changes: 100 additions & 0 deletions tests/R3.Tests/BindableReactivePropertyTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.Text.Json;

namespace R3.Tests;

public class BindableReactivePropertyTest
{
[Fact]
public void Test()
{
var rp = new ReactiveProperty<int>(100);
var brp = rp.ToBindableReactiveProperty();

var list = brp.ToLiveList();
list.AssertEqual([100]);

rp.Value = 9999;

var list2 = brp.ToLiveList();
list.AssertEqual([100, 9999]);
list2.AssertEqual([9999]);

rp.Value = 9999;
list.AssertEqual([100, 9999]);
list2.AssertEqual([9999]);

rp.Value = 100;
list.AssertEqual([100, 9999, 100]);
list2.AssertEqual([9999, 100]);

rp.Dispose();

list.AssertIsCompleted();
list2.AssertIsCompleted();
}

[Fact]
public void ReadOnlyTest()
{
var rp = new ReactiveProperty<int>(100);
var rorp = rp.ToReadOnlyReactiveProperty();
var brp0 = rp.ToReadOnlyBindableReactiveProperty();
var brp1 = rorp.ToReadOnlyBindableReactiveProperty();

brp0.CurrentValue.Should().Be(100);
brp1.CurrentValue.Should().Be(100);
var list0 = brp0.ToLiveList();
var list1 = brp1.ToLiveList();
list0.AssertEqual([100]);
list1.AssertEqual([100]);

rp.Value = 9999;

brp0.CurrentValue.Should().Be(9999);
brp1.CurrentValue.Should().Be(9999);
list0.AssertEqual([100, 9999]);
list1.AssertEqual([100, 9999]);

rp.Dispose();
list0.AssertIsCompleted();
list1.AssertIsCompleted();
}

[Fact]
public void DefaultValueTest()
{
using var rp = new ReactiveProperty<int>();
var brp0 = rp.ToBindableReactiveProperty();
var brp1 = rp.ToReadOnlyBindableReactiveProperty();
brp0.Value.Should().Be(default);
brp1.CurrentValue.Should().Be(default);
}

[Fact]
public void SubscribeAfterCompleted()
{
var rp = new ReactiveProperty<string>("foo");
var brp0 = rp.ToBindableReactiveProperty();
var brp1 = rp.ToReadOnlyBindableReactiveProperty();
rp.OnCompleted();

using var list0 = brp0.ToLiveList();
using var list1 = brp1.ToLiveList();

list0.AssertIsCompleted();
list0.AssertEqual(["foo"]);
list1.AssertIsCompleted();
list1.AssertEqual(["foo"]);
}

[Fact]
public void SerializeTest()
{
var rp = new ReactiveProperty<string>("foo");
var brp0 = rp.ToBindableReactiveProperty("");
var brp1 = rp.ToReadOnlyBindableReactiveProperty("");

JsonSerializer.Serialize(brp0).Should().Be("\"foo\"");
JsonSerializer.Serialize(brp1).Should().Be("""{"EqualityComparer":{},"CurrentValue":"foo","IsDisposed":false}""");
}
}
Loading

0 comments on commit 1e7f4d4

Please sign in to comment.