From 558affc1b8a92d4d752aec3b333d2e4bb5241e1b Mon Sep 17 00:00:00 2001 From: Michael Stonis Date: Sat, 2 Mar 2024 12:48:00 -0600 Subject: [PATCH] First pass at observing nested property changes Added a new method to observe changes in nested properties of INotifyPropertyChanged objects. This allows for more granular observation of property changes, particularly useful when dealing with complex objects. Also updated the test suite to cover this new functionality. --- src/R3/Factories/ObserveProperty.cs | 35 +++++++++++++++++-- .../FactoryTests/ObservePropertyTest.cs | 27 ++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/R3/Factories/ObserveProperty.cs b/src/R3/Factories/ObserveProperty.cs index d055fd3b..4a643929 100644 --- a/src/R3/Factories/ObserveProperty.cs +++ b/src/R3/Factories/ObserveProperty.cs @@ -13,7 +13,7 @@ public static Observable ObservePropertyChanged(this T Func 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); @@ -22,6 +22,37 @@ public static Observable ObservePropertyChanged(this T return new ObservePropertyChanged(value, propertySelector, propertyName, pushCurrentValueOnSubscribe, cancellationToken); } + /// + /// Convert INotifyPropertyChanged to Observable. + /// `propertySelector` 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, + 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); + + var firstPropertyChanged = new ObservePropertyChanged(value, propertySelector1, property1Name, pushCurrentValueOnSubscribe, cancellationToken); + + return firstPropertyChanged + .Select( + (propertySelector2, property2Name, pushCurrentValueOnSubscribe, cancellationToken), + (firstPropertyValue, state) => + (Observable)new ObservePropertyChanged(firstPropertyValue, state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe, state.cancellationToken)) + .Switch(); + } + + /// /// Convert INotifyPropertyChanging to Observable. /// `propertySelector` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`. @@ -30,7 +61,7 @@ public static Observable ObservePropertyChanging(this T Func 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); diff --git a/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs b/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs index 31fbbabe..59219e07 100644 --- a/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs +++ b/tests/R3.Tests/FactoryTests/ObservePropertyTest.cs @@ -21,6 +21,26 @@ 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]); + } + [Fact] public void PropertyChanging() { @@ -40,6 +60,7 @@ public void PropertyChanging() class ChangesProperty : INotifyPropertyChanged, INotifyPropertyChanging { private int _value; + private ChangesProperty _innerPropertyChanged; public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangingEventHandler? PropertyChanging; @@ -50,6 +71,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));