diff --git a/src/R3/Factories/ObserveProperty.cs b/src/R3/Factories/ObserveProperty.cs index 4a643929..2d5fe36b 100644 --- a/src/R3/Factories/ObserveProperty.cs +++ b/src/R3/Factories/ObserveProperty.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Runtime.CompilerServices; namespace R3; @@ -24,7 +24,7 @@ public static Observable ObservePropertyChanged(this T /// /// 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`. /// public static Observable ObservePropertyChanged(this T value, Func propertySelector1, @@ -42,16 +42,61 @@ public static Observable ObservePropertyChanged(value, propertySelector1, property1Name, pushCurrentValueOnSubscribe, cancellationToken); - - return firstPropertyChanged + return new ObservePropertyChanged(value, propertySelector1, property1Name, true, cancellationToken) .Select( (propertySelector2, property2Name, pushCurrentValueOnSubscribe, cancellationToken), (firstPropertyValue, state) => - (Observable)new ObservePropertyChanged(firstPropertyValue, state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe, state.cancellationToken)) + firstPropertyValue is not null + ? new ObservePropertyChanged(firstPropertyValue, + state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe, + state.cancellationToken) + : Empty()) .Switch(); } + /// + /// 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`. + /// + public static Observable ObservePropertyChanged(this T value, + Func propertySelector1, + Func propertySelector2, + Func 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(value, propertySelector1, property1Name, true, cancellationToken) + .Select( + (propertySelector2, property2Name, propertySelector3, property3Name, pushCurrentValueOnSubscribe, cancellationToken), + (firstPropertyValue, state) => + firstPropertyValue is not null + ? new ObservePropertyChanged(firstPropertyValue, state.propertySelector2, state.property2Name, true, state.cancellationToken) + .Select( + (state.propertySelector3, state.property3Name, pushCurrentValueOnSubscribe, cancellationToken), + (secondPropertyValue, state2) => + secondPropertyValue is not null + ? new ObservePropertyChanged(secondPropertyValue, + state2.propertySelector3, state2.property3Name, state2.pushCurrentValueOnSubscribe, + state2.cancellationToken) + : Empty()) + .Switch() + : Empty()) + .Switch(); + } /// /// Convert INotifyPropertyChanging to Observable. @@ -67,11 +112,95 @@ public static Observable ObservePropertyChanging(this T if (expr == null) throw new ArgumentNullException(expr); var propertyName = expr!.Substring(expr.LastIndexOf('.') + 1); - return new ObservePropertyChanging(value, propertySelector, propertyName, pushCurrentValueOnSubscribe, cancellationToken); + return new ObservePropertyChanging(value, propertySelector, propertyName, + pushCurrentValueOnSubscribe, cancellationToken); + } + + /// + /// Convert INotifyPropertyChanging to Observable. + /// `propertySelector1` and `propertySelector2` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`. + /// + public static Observable ObservePropertyChanging(this T value, + Func propertySelector1, + Func 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(value, propertySelector1, property1Name, true, cancellationToken) + .Select( + (propertySelector2, property2Name, pushCurrentValueOnSubscribe, cancellationToken), + (firstPropertyValue, state) => + firstPropertyValue is not null + ? new ObservePropertyChanging(firstPropertyValue, + state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe, + state.cancellationToken) + : Empty()) + .Switch(); + } + + /// + /// 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`. + /// + public static Observable ObservePropertyChanging(this T value, + Func propertySelector1, + Func propertySelector2, + Func 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(value, propertySelector1, property1Name, pushCurrentValueOnSubscribe, cancellationToken) + .Select( + (propertySelector2, property2Name, propertySelector3, property3Name, pushCurrentValueOnSubscribe, cancellationToken), + (firstPropertyValue, state) => + (firstPropertyValue is not null + ? new ObservePropertyChanged( + firstPropertyValue, state.propertySelector2, state.property2Name, true, state.cancellationToken) + .Select( + (state.propertySelector3, state.property3Name, pushCurrentValueOnSubscribe, cancellationToken), + (secondPropertyValue, state2) => + secondPropertyValue is not null + ? new ObservePropertyChanging(secondPropertyValue, + state2.propertySelector3, state2.property3Name, state2.pushCurrentValueOnSubscribe, + state2.cancellationToken) + : Empty()) + .Switch() + : Empty())) + .Switch(); } } -internal sealed class ObservePropertyChanged(T value, Func propertySelector, string propertyName, bool pushCurrentValueOnSubscribe, CancellationToken cancellationToken) +internal sealed class ObservePropertyChanged( + T value, + Func propertySelector, + string propertyName, + bool pushCurrentValueOnSubscribe, + CancellationToken cancellationToken) : Observable where T : INotifyPropertyChanged { protected override IDisposable SubscribeCore(Observer observer) @@ -150,7 +279,12 @@ public void Dispose() } } -internal sealed class ObservePropertyChanging(T value, Func propertySelector, string propertyName, bool pushCurrentValueOnSubscribe, CancellationToken cancellationToken) +internal sealed class ObservePropertyChanging( + T value, + Func propertySelector, + string propertyName, + bool pushCurrentValueOnSubscribe, + CancellationToken cancellationToken) : Observable where T : INotifyPropertyChanging { protected override IDisposable SubscribeCore(Observer observer) @@ -160,10 +294,10 @@ protected override IDisposable SubscribeCore(Observer 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 observer; readonly T value; @@ -172,7 +306,7 @@ sealed class _ObservePropertyChanged : IDisposable PropertyChangingEventHandler? eventHandler; CancellationTokenRegistration cancellationTokenRegistration; - public _ObservePropertyChanged(Observer observer, T value, Func propertySelector, string propertyName, CancellationToken cancellationToken) + public _ObservePropertyChanging(Observer observer, T value, Func propertySelector, string propertyName, CancellationToken cancellationToken) { this.observer = observer; this.value = value; @@ -186,7 +320,7 @@ public _ObservePropertyChanged(Observer observer, T value, Func { - var s = (_ObservePropertyChanged)state!; + var s = (_ObservePropertyChanging)state!; s.CompleteDispose(); }, this); } diff --git a/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs b/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs index 59219e07..13039f93 100644 --- a/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs +++ b/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs @@ -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] @@ -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;