Skip to content

Commit

Permalink
Merge pull request #156 from michaelstonis/feature/nested-prop-obs
Browse files Browse the repository at this point in the history
First pass at observing nested property changes
  • Loading branch information
neuecc authored Mar 5, 2024
2 parents 8af5962 + e79ad4f commit 7aeb80b
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 10 deletions.
170 changes: 160 additions & 10 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 @@ -13,7 +13,7 @@ public static Observable<TProperty> ObservePropertyChanged<T, TProperty>(this T
Func<T, TProperty> propertySelector,
bool pushCurrentValueOnSubscribe = true,
CancellationToken cancellationToken = default,
[CallerArgumentExpression("propertySelector")] string? expr = null)
[CallerArgumentExpression(nameof(propertySelector))] string? expr = null)
where T : INotifyPropertyChanged
{
if (expr == null) throw new ArgumentNullException(expr);
Expand All @@ -22,6 +22,82 @@ public static Observable<TProperty> ObservePropertyChanged<T, TProperty>(this T
return new ObservePropertyChanged<T, TProperty>(value, propertySelector, propertyName, pushCurrentValueOnSubscribe, cancellationToken);
}

/// <summary>
/// Convert INotifyPropertyChanged 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> ObservePropertyChanged<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 : INotifyPropertyChanged
{
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 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.
/// `propertySelector` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`.
Expand All @@ -30,17 +106,86 @@ public static Observable<TProperty> ObservePropertyChanging<T, TProperty>(this T
Func<T, TProperty> propertySelector,
bool pushCurrentValueOnSubscribe = true,
CancellationToken cancellationToken = default,
[CallerArgumentExpression("propertySelector")] string? expr = null)
[CallerArgumentExpression(nameof(propertySelector))] string? expr = null)
where T : INotifyPropertyChanging
{
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 (propertySelector2Expr == null) throw new ArgumentNullException(propertySelector2Expr);

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

return ObservePropertyChanged(value, propertySelector1, true, cancellationToken, propertySelector1Expr)
.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 (propertySelector3Expr == null) throw new ArgumentNullException(propertySelector3Expr);

var property3Name = propertySelector3Expr!.Substring(propertySelector3Expr.LastIndexOf('.') + 1);

return ObservePropertyChanged(value, propertySelector1, propertySelector2, true, cancellationToken, propertySelector1Expr, propertySelector2Expr)
.Select(
(propertySelector3, property3Name, pushCurrentValueOnSubscribe, cancellationToken),
(secondPropertyValue, state) =>
secondPropertyValue is not null
? new ObservePropertyChanging<TProperty2, TProperty3>(secondPropertyValue,
state.propertySelector3, state.property3Name, state.pushCurrentValueOnSubscribe,
state.cancellationToken)
: 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 @@ -119,7 +264,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 @@ -129,10 +279,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 @@ -141,7 +291,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 @@ -155,7 +305,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
107 changes: 107 additions & 0 deletions tests/R3.Tests/FactoryTests/ObservePropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,54 @@ public void PropertyChanged()
liveList.AssertEqual([0, 1]);
}

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

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

liveList.AssertEqual([]);

propertyChanger.InnerPropertyChanged = new();

liveList.AssertEqual([0]);

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]
public void PropertyChanging()
{
Expand All @@ -37,9 +85,62 @@ 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;
private ChangesProperty _innerPropertyChanged;

Check warning on line 143 in tests/R3.Tests/FactoryTests/ObservePropertyTest.cs

View workflow job for this annotation

GitHub Actions / build-dotnet

Non-nullable field '_innerPropertyChanged' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangingEventHandler? PropertyChanging;
Expand All @@ -50,6 +151,12 @@ public int Value
set => SetField(ref _value, value);
}

public ChangesProperty InnerPropertyChanged
{
get => _innerPropertyChanged;
set => SetField(ref _innerPropertyChanged, value);
}

private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
Expand Down

0 comments on commit 7aeb80b

Please sign in to comment.