Skip to content

Commit

Permalink
First pass at observing nested property changes
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
michaelstonis committed Mar 2, 2024
1 parent 235b8b2 commit 558affc
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 2 deletions.
35 changes: 33 additions & 2 deletions src/R3/Factories/ObserveProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,37 @@ public static Observable<TProperty> ObservePropertyChanged<T, TProperty>(this T
return new ObservePropertyChanged<T, TProperty>(value, propertySelector, propertyName, pushCurrentValueOnSubscribe, cancellationToken);
}

/// <summary>
/// Convert INotifyPropertyChanged to Observable.
/// `propertySelector` 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);

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

return firstPropertyChanged
.Select(
(propertySelector2, property2Name, pushCurrentValueOnSubscribe, cancellationToken),
(firstPropertyValue, state) =>
(Observable<TProperty2>)new ObservePropertyChanged<TProperty1, TProperty2>(firstPropertyValue, state.propertySelector2, state.property2Name, state.pushCurrentValueOnSubscribe, state.cancellationToken))
.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,7 +61,7 @@ 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);
Expand Down
27 changes: 27 additions & 0 deletions tests/R3.Tests/FactoryTests/ObservePropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -40,6 +60,7 @@ public void PropertyChanging()
class ChangesProperty : INotifyPropertyChanged, INotifyPropertyChanging
{
private int _value;
private ChangesProperty _innerPropertyChanged;

Check warning on line 63 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 +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));
Expand Down

0 comments on commit 558affc

Please sign in to comment.