Skip to content

Commit

Permalink
Enhanced Observable functionality
Browse files Browse the repository at this point in the history
The ObservePropertyChanged and ObservePropertyChanging methods now support multiple property selectors, allowing for nested observation of property changes. Corresponding unit tests have also been added.
  • Loading branch information
michaelstonis committed Mar 4, 2024
1 parent 62ab623 commit 996bc83
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 13 deletions.
160 changes: 147 additions & 13 deletions src/R3/Factories/ObserveProperty.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace R3;
Expand All @@ -24,7 +24,7 @@ public static Observable<TProperty> ObservePropertyChanged<T, TProperty>(this T

/// <summary>
/// Convert INotifyPropertyChanged to Observable.
/// `propertySelector` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`.
/// `propertySelector1` and `propertySelector2` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`.
/// </summary>
public static Observable<TProperty2> ObservePropertyChanged<T, TProperty1, TProperty2>(this T value,
Func<T, TProperty1> propertySelector1,
Expand All @@ -42,16 +42,61 @@ public static Observable<TProperty2> ObservePropertyChanged<T, TProperty1, TProp
var property1Name = propertySelector1Expr!.Substring(propertySelector1Expr.LastIndexOf('.') + 1);
var property2Name = propertySelector2Expr!.Substring(propertySelector2Expr.LastIndexOf('.') + 1);

var firstPropertyChanged = new ObservePropertyChanged<T, TProperty1>(value, propertySelector1, property1Name, pushCurrentValueOnSubscribe, cancellationToken);

return firstPropertyChanged
return new ObservePropertyChanged<T, TProperty1>(value, propertySelector1, property1Name, true, cancellationToken)
.Select(
(propertySelector2, property2Name, pushCurrentValueOnSubscribe, cancellationToken),
(firstPropertyValue, state) =>
(Observable<TProperty2>)new ObservePropertyChanged<TProperty1, TProperty2>(firstPropertyValue, state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe, state.cancellationToken))
firstPropertyValue is not null
? new ObservePropertyChanged<TProperty1, TProperty2>(firstPropertyValue,
state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe,
state.cancellationToken)
: Empty<TProperty2>())
.Switch();
}

/// <summary>
/// Convert INotifyPropertyChanged to Observable.
/// `propertySelector1`, `propertySelector2`, and `propertySelector3` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`.
/// </summary>
public static Observable<TProperty3> ObservePropertyChanged<T, TProperty1, TProperty2, TProperty3>(this T value,
Func<T, TProperty1> propertySelector1,
Func<TProperty1, TProperty2> propertySelector2,
Func<TProperty2, TProperty3> propertySelector3,
bool pushCurrentValueOnSubscribe = true,
CancellationToken cancellationToken = default,
[CallerArgumentExpression(nameof(propertySelector1))] string? propertySelector1Expr = null,
[CallerArgumentExpression(nameof(propertySelector2))] string? propertySelector2Expr = null,
[CallerArgumentExpression(nameof(propertySelector3))] string? propertySelector3Expr = null)
where T : INotifyPropertyChanged
where TProperty1 : INotifyPropertyChanged
where TProperty2 : INotifyPropertyChanged
{
if (propertySelector1Expr == null) throw new ArgumentNullException(propertySelector1Expr);
if (propertySelector2Expr == null) throw new ArgumentNullException(propertySelector2Expr);
if (propertySelector3Expr == null) throw new ArgumentNullException(propertySelector3Expr);

var property1Name = propertySelector1Expr!.Substring(propertySelector1Expr.LastIndexOf('.') + 1);
var property2Name = propertySelector2Expr!.Substring(propertySelector2Expr.LastIndexOf('.') + 1);
var property3Name = propertySelector3Expr!.Substring(propertySelector3Expr.LastIndexOf('.') + 1);

return new ObservePropertyChanged<T, TProperty1>(value, propertySelector1, property1Name, true, cancellationToken)
.Select(
(propertySelector2, property2Name, propertySelector3, property3Name, pushCurrentValueOnSubscribe, cancellationToken),
(firstPropertyValue, state) =>
firstPropertyValue is not null
? new ObservePropertyChanged<TProperty1, TProperty2>(firstPropertyValue, state.propertySelector2, state.property2Name, true, state.cancellationToken)
.Select(
(state.propertySelector3, state.property3Name, pushCurrentValueOnSubscribe, cancellationToken),
(secondPropertyValue, state2) =>
secondPropertyValue is not null
? new ObservePropertyChanged<TProperty2, TProperty3>(secondPropertyValue,
state2.propertySelector3, state2.property3Name, state2.pushCurrentValueOnSubscribe,
state2.cancellationToken)
: Empty<TProperty3>())
.Switch()
: Empty<TProperty3>())
.Switch();
}

/// <summary>
/// Convert INotifyPropertyChanging to Observable.
Expand All @@ -67,11 +112,95 @@ public static Observable<TProperty> ObservePropertyChanging<T, TProperty>(this T
if (expr == null) throw new ArgumentNullException(expr);

var propertyName = expr!.Substring(expr.LastIndexOf('.') + 1);
return new ObservePropertyChanging<T, TProperty>(value, propertySelector, propertyName, pushCurrentValueOnSubscribe, cancellationToken);
return new ObservePropertyChanging<T, TProperty>(value, propertySelector, propertyName,
pushCurrentValueOnSubscribe, cancellationToken);
}

/// <summary>
/// Convert INotifyPropertyChanging to Observable.
/// `propertySelector1` and `propertySelector2` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`.
/// </summary>
public static Observable<TProperty2> ObservePropertyChanging<T, TProperty1, TProperty2>(this T value,
Func<T, TProperty1> propertySelector1,
Func<TProperty1, TProperty2> propertySelector2,
bool pushCurrentValueOnSubscribe = true,
CancellationToken cancellationToken = default,
[CallerArgumentExpression(nameof(propertySelector1))] string? propertySelector1Expr = null,
[CallerArgumentExpression(nameof(propertySelector2))] string? propertySelector2Expr = null)
where T : INotifyPropertyChanged
where TProperty1 : INotifyPropertyChanging
{
if (propertySelector1Expr == null) throw new ArgumentNullException(propertySelector1Expr);
if (propertySelector2Expr == null) throw new ArgumentNullException(propertySelector2Expr);

var property1Name = propertySelector1Expr!.Substring(propertySelector1Expr.LastIndexOf('.') + 1);
var property2Name = propertySelector2Expr!.Substring(propertySelector2Expr.LastIndexOf('.') + 1);


return new ObservePropertyChanged<T, TProperty1>(value, propertySelector1, property1Name, true, cancellationToken)
.Select(
(propertySelector2, property2Name, pushCurrentValueOnSubscribe, cancellationToken),
(firstPropertyValue, state) =>
firstPropertyValue is not null
? new ObservePropertyChanging<TProperty1, TProperty2>(firstPropertyValue,
state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe,
state.cancellationToken)
: Empty<TProperty2>())
.Switch();
}

/// <summary>
/// Convert INotifyPropertyChanging to Observable.
/// `propertySelector1`, `propertySelector2`, and `propertySelector3` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`.
/// </summary>
public static Observable<TProperty3> ObservePropertyChanging<T, TProperty1, TProperty2, TProperty3>(this T value,
Func<T, TProperty1> propertySelector1,
Func<TProperty1, TProperty2> propertySelector2,
Func<TProperty2, TProperty3> propertySelector3,
bool pushCurrentValueOnSubscribe = true,
CancellationToken cancellationToken = default,
[CallerArgumentExpression(nameof(propertySelector1))] string? propertySelector1Expr = null,
[CallerArgumentExpression(nameof(propertySelector2))] string? propertySelector2Expr = null,
[CallerArgumentExpression(nameof(propertySelector3))] string? propertySelector3Expr = null)
where T : INotifyPropertyChanged
where TProperty1 : INotifyPropertyChanged
where TProperty2 : INotifyPropertyChanging
{
if (propertySelector1Expr == null) throw new ArgumentNullException(propertySelector1Expr);
if (propertySelector2Expr == null) throw new ArgumentNullException(propertySelector2Expr);
if (propertySelector3Expr == null) throw new ArgumentNullException(propertySelector3Expr);

var property1Name = propertySelector1Expr!.Substring(propertySelector1Expr.LastIndexOf('.') + 1);
var property2Name = propertySelector2Expr!.Substring(propertySelector2Expr.LastIndexOf('.') + 1);
var property3Name = propertySelector3Expr!.Substring(propertySelector3Expr.LastIndexOf('.') + 1);

return new ObservePropertyChanged<T, TProperty1>(value, propertySelector1, property1Name, pushCurrentValueOnSubscribe, cancellationToken)
.Select(
(propertySelector2, property2Name, propertySelector3, property3Name, pushCurrentValueOnSubscribe, cancellationToken),
(firstPropertyValue, state) =>
(firstPropertyValue is not null
? new ObservePropertyChanged<TProperty1, TProperty2>(
firstPropertyValue, state.propertySelector2, state.property2Name, true, state.cancellationToken)
.Select(
(state.propertySelector3, state.property3Name, pushCurrentValueOnSubscribe, cancellationToken),
(secondPropertyValue, state2) =>
secondPropertyValue is not null
? new ObservePropertyChanging<TProperty2, TProperty3>(secondPropertyValue,
state2.propertySelector3, state2.property3Name, state2.pushCurrentValueOnSubscribe,
state2.cancellationToken)
: Empty<TProperty3>())
.Switch()
: Empty<TProperty3>()))
.Switch();
}
}

internal sealed class ObservePropertyChanged<T, TProperty>(T value, Func<T, TProperty> propertySelector, string propertyName, bool pushCurrentValueOnSubscribe, CancellationToken cancellationToken)
internal sealed class ObservePropertyChanged<T, TProperty>(
T value,
Func<T, TProperty> propertySelector,
string propertyName,
bool pushCurrentValueOnSubscribe,
CancellationToken cancellationToken)
: Observable<TProperty> where T : INotifyPropertyChanged
{
protected override IDisposable SubscribeCore(Observer<TProperty> observer)
Expand Down Expand Up @@ -150,7 +279,12 @@ public void Dispose()
}
}

internal sealed class ObservePropertyChanging<T, TProperty>(T value, Func<T, TProperty> propertySelector, string propertyName, bool pushCurrentValueOnSubscribe, CancellationToken cancellationToken)
internal sealed class ObservePropertyChanging<T, TProperty>(
T value,
Func<T, TProperty> propertySelector,
string propertyName,
bool pushCurrentValueOnSubscribe,
CancellationToken cancellationToken)
: Observable<TProperty> where T : INotifyPropertyChanging
{
protected override IDisposable SubscribeCore(Observer<TProperty> observer)
Expand All @@ -160,10 +294,10 @@ protected override IDisposable SubscribeCore(Observer<TProperty> observer)
observer.OnNext(propertySelector(value));
}

return new _ObservePropertyChanged(observer, value, propertySelector, propertyName, cancellationToken);
return new _ObservePropertyChanging(observer, value, propertySelector, propertyName, cancellationToken);
}

sealed class _ObservePropertyChanged : IDisposable
sealed class _ObservePropertyChanging : IDisposable
{
readonly Observer<TProperty> observer;
readonly T value;
Expand All @@ -172,7 +306,7 @@ sealed class _ObservePropertyChanged : IDisposable
PropertyChangingEventHandler? eventHandler;
CancellationTokenRegistration cancellationTokenRegistration;

public _ObservePropertyChanged(Observer<TProperty> observer, T value, Func<T, TProperty> propertySelector, string propertyName, CancellationToken cancellationToken)
public _ObservePropertyChanging(Observer<TProperty> observer, T value, Func<T, TProperty> propertySelector, string propertyName, CancellationToken cancellationToken)
{
this.observer = observer;
this.value = value;
Expand All @@ -186,7 +320,7 @@ public _ObservePropertyChanged(Observer<TProperty> observer, T value, Func<T, TP
{
this.cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>
{
var s = (_ObservePropertyChanged)state!;
var s = (_ObservePropertyChanging)state!;
s.CompleteDispose();
}, this);
}
Expand Down
80 changes: 80 additions & 0 deletions tests/R3.Tests/FactoryTests/ObservePropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,34 @@ public void NestedPropertyChanged()
propertyChanger.InnerPropertyChanged.Value = 1;

liveList.AssertEqual([0, 1]);

propertyChanger.InnerPropertyChanged.Value = 2;

liveList.AssertEqual([0, 1, 2]);
}

[Fact]
public void DoubleNestedPropertyChanged()
{
ChangesProperty propertyChanger = new();

using var liveList = propertyChanger
.ObservePropertyChanged(x => x.InnerPropertyChanged, x => x.InnerPropertyChanged, x => x.Value)
.ToLiveList();

liveList.AssertEqual([]);

propertyChanger.InnerPropertyChanged = new();

liveList.AssertEqual([]);

propertyChanger.InnerPropertyChanged.InnerPropertyChanged = new();

liveList.AssertEqual([0]);

propertyChanger.InnerPropertyChanged.InnerPropertyChanged.Value = 1;

liveList.AssertEqual([0, 1]);
}

[Fact]
Expand All @@ -57,6 +85,58 @@ public void PropertyChanging()
liveList.AssertEqual([0, 0]);
}

[Fact]
public void NestedPropertyChanging()
{
ChangesProperty propertyChanger = new();

using var liveList = propertyChanger
.ObservePropertyChanging(x => x.InnerPropertyChanged, x => x.Value)
.ToLiveList();

liveList.AssertEqual([]);

propertyChanger.InnerPropertyChanged = new();

liveList.AssertEqual([0]);

propertyChanger.InnerPropertyChanged.Value = 1;

liveList.AssertEqual([0, 0]);

propertyChanger.InnerPropertyChanged.Value = 2;

liveList.AssertEqual([0, 0, 1]);
}

[Fact]
public void DoubleNestedPropertyChanging()
{
ChangesProperty propertyChanger = new();

using var liveList = propertyChanger
.ObservePropertyChanging(x => x.InnerPropertyChanged, x => x.InnerPropertyChanged, x => x.Value)
.ToLiveList();

liveList.AssertEqual([]);

propertyChanger.InnerPropertyChanged = new();

liveList.AssertEqual([]);

propertyChanger.InnerPropertyChanged.InnerPropertyChanged = new();

liveList.AssertEqual([0]);

propertyChanger.InnerPropertyChanged.InnerPropertyChanged.Value = 1;

liveList.AssertEqual([0, 0]);

propertyChanger.InnerPropertyChanged.InnerPropertyChanged.Value = 2;

liveList.AssertEqual([0, 0, 1]);
}

class ChangesProperty : INotifyPropertyChanged, INotifyPropertyChanging
{
private int _value;
Expand Down

0 comments on commit 996bc83

Please sign in to comment.