Skip to content

Commit

Permalink
Merge pull request #151 from Cysharp/observe-property
Browse files Browse the repository at this point in the history
Add ObservePropertyChanged, ObservePropertyChanging for `INotifyPropertyChanged`, `INotifyPropertyChanging`
  • Loading branch information
neuecc authored Mar 1, 2024
2 parents f8aa64f + 8e05aff commit a6ef207
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 11 deletions.
39 changes: 28 additions & 11 deletions sandbox/ConsoleApp1/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Time.Testing;
using R3;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reactive.Concurrency;
using System.Runtime.CompilerServices;
Expand All @@ -22,19 +23,35 @@
//t.Wait();


Observable.Interval(TimeSpan.FromSeconds(1))
.Index()
.Chunk(async (_, ct) =>
{
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 5)), ct);
})
.Subscribe(x =>
{
Console.WriteLine(string.Join(", ", x));
});
var p = new Person { Name = "aiueo" };


p.ObservePropertyChanged(x => x.Name).Subscribe(x => Console.WriteLine($"Changed:{x}"));

p.Name = "kakikukeko";
p.Name = "sasisuseso";



public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

string name = default!;

Console.ReadLine();
public required string Name
{
get
{
return name;
}
set
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
}


internal static class ChannelUtility
Expand Down
199 changes: 199 additions & 0 deletions src/R3/Factories/ObserveProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace R3;

public static partial class Observable
{
/// <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<TProperty> ObservePropertyChanged<T, TProperty>(this T value,
Func<T, TProperty> propertySelector,
bool pushCurrentValueOnSubscribe = true,
CancellationToken cancellationToken = default,
[CallerArgumentExpression("propertySelector")] string? expr = null)
where T : INotifyPropertyChanged
{
if (expr == null) throw new ArgumentNullException(expr);

var propertyName = expr!.Substring(expr.LastIndexOf('.') + 1);
return new ObservePropertyChanged<T, TProperty>(value, propertySelector, propertyName, pushCurrentValueOnSubscribe, cancellationToken);
}

/// <summary>
/// Convert INotifyPropertyChanging to Observable.
/// `propertySelector` must be a Func specifying a simple property. For example, it extracts "Foo" from `x => x.Foo`.
/// </summary>
public static Observable<TProperty> ObservePropertyChanging<T, TProperty>(this T value,
Func<T, TProperty> propertySelector,
bool pushCurrentValueOnSubscribe = true,
CancellationToken cancellationToken = default,
[CallerArgumentExpression("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);
}
}

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)
{
if (pushCurrentValueOnSubscribe)
{
observer.OnNext(propertySelector(value));
}

return new _ObservePropertyChanged(observer, value, propertySelector, propertyName, cancellationToken);
}

sealed class _ObservePropertyChanged : IDisposable
{
readonly Observer<TProperty> observer;
readonly T value;
readonly Func<T, TProperty> propertySelector;
readonly string propertyName;
PropertyChangedEventHandler? eventHandler;
CancellationTokenRegistration cancellationTokenRegistration;

public _ObservePropertyChanged(Observer<TProperty> observer, T value, Func<T, TProperty> propertySelector, string propertyName, CancellationToken cancellationToken)
{
this.observer = observer;
this.value = value;
this.propertySelector = propertySelector;
this.propertyName = propertyName;
this.eventHandler = PublishOnNext;

value.PropertyChanged += eventHandler;

if (cancellationToken.CanBeCanceled)
{
this.cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>
{
var s = (_ObservePropertyChanged)state!;
s.CompleteDispose();
}, this);
}
}

void PublishOnNext(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == propertyName)
{
TProperty prop;
try
{
prop = propertySelector(value);
}
catch (Exception ex)
{
observer.OnErrorResume(ex);
return;
}

observer.OnNext(prop);
}
}

void CompleteDispose()
{
observer.OnCompleted();
Dispose();
}

public void Dispose()
{
var handler = Interlocked.Exchange(ref eventHandler, null);
if (handler != null)
{
cancellationTokenRegistration.Dispose();
value.PropertyChanged -= eventHandler;
}
}
}
}

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)
{
if (pushCurrentValueOnSubscribe)
{
observer.OnNext(propertySelector(value));
}

return new _ObservePropertyChanged(observer, value, propertySelector, propertyName, cancellationToken);
}

sealed class _ObservePropertyChanged : IDisposable
{
readonly Observer<TProperty> observer;
readonly T value;
readonly Func<T, TProperty> propertySelector;
readonly string propertyName;
PropertyChangingEventHandler? eventHandler;
CancellationTokenRegistration cancellationTokenRegistration;

public _ObservePropertyChanged(Observer<TProperty> observer, T value, Func<T, TProperty> propertySelector, string propertyName, CancellationToken cancellationToken)
{
this.observer = observer;
this.value = value;
this.propertySelector = propertySelector;
this.propertyName = propertyName;
this.eventHandler = PublishOnNext;

value.PropertyChanging += eventHandler;

if (cancellationToken.CanBeCanceled)
{
this.cancellationTokenRegistration = cancellationToken.UnsafeRegister(static state =>
{
var s = (_ObservePropertyChanged)state!;
s.CompleteDispose();
}, this);
}
}

void PublishOnNext(object? sender, PropertyChangingEventArgs e)
{
if (e.PropertyName == propertyName)
{
TProperty prop;
try
{
prop = propertySelector(value);
}
catch (Exception ex)
{
observer.OnErrorResume(ex);
return;
}

observer.OnNext(prop);
}
}

void CompleteDispose()
{
observer.OnCompleted();
Dispose();
}

public void Dispose()
{
var handler = Interlocked.Exchange(ref eventHandler, null);
if (handler != null)
{
cancellationTokenRegistration.Dispose();
value.PropertyChanging -= eventHandler;
}
}
}
}
77 changes: 77 additions & 0 deletions tests/R3.Tests/FactoryTests/ObservePropertyTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace R3.Tests.FactoryTests;

public class ObservePropertyTest
{
[Fact]
public void PropertyChanged()
{
ChangesProperty propertyChanger = new();

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

liveList.AssertEqual([0]);

propertyChanger.Value = 1;

liveList.AssertEqual([0, 1]);
}

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

using var liveList = propertyChanger
.ObservePropertyChanging(x => x.Value)
.ToLiveList();

liveList.AssertEqual([0]);

propertyChanger.Value = 1;

liveList.AssertEqual([0, 0]);
}

class ChangesProperty : INotifyPropertyChanged, INotifyPropertyChanging
{
private int _value;

public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangingEventHandler? PropertyChanging;

public int Value
{
get => _value;
set => SetField(ref _value, value);
}

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

private void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}

private bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}

OnPropertyChanging(propertyName);
field = value;
OnPropertyChanged(propertyName);
return true;
}

}
}

0 comments on commit a6ef207

Please sign in to comment.