Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First pass at observing nested property changes #156

Merged
merged 3 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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 @@
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
Loading