Skip to content

Commit

Permalink
Merge pull request #210 from runceel/main
Browse files Browse the repository at this point in the history
Release v7.5.1
  • Loading branch information
runceel authored Oct 19, 2020
2 parents 2ff1ac5 + b3ae675 commit fd67265
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ namespace Reactive.Bindings.Internals
{
internal sealed class PropertyObservable<TProperty> : IObservable<TProperty>, IDisposable
{
internal PropertyPathNode RootNode { get; set; }
private PropertyPathNode RootNode { get; set; }
internal void SetRootNode(PropertyPathNode rootNode)
{
RootNode?.SetCallback(null);
rootNode?.SetCallback(RaisePropertyChanged);
RootNode = rootNode;
}
public TProperty GetPropertyPathValue()
{
var value = RootNode?.GetPropertyPathValue();
Expand Down Expand Up @@ -43,22 +49,7 @@ public static PropertyObservable<TProperty> CreateFromPropertySelector<TSubject,
}

var result = new PropertyObservable<TProperty>();
var node = default(PropertyPathNode);
while (current != null)
{
var propertyName = current.Member.Name;
if (node != null)
{
node = node.InsertBefore(propertyName);
}
else
{
node = new PropertyPathNode(propertyName, result.RaisePropertyChanged);
}
current = current.Expression as MemberExpression;
}

result.RootNode = node;
result.SetRootNode(PropertyPathNode.CreateFromPropertySelector(propertySelector));
result.SetSource(subject);
return result;
}
Expand Down
54 changes: 45 additions & 9 deletions Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,26 @@ namespace Reactive.Bindings.Internals
internal class PropertyPathNode : IDisposable
{
private bool _isDisposed = false;
private readonly Action _callback;
private Action _callback;
private Delegate _getAccessor;
private Delegate _setAccessor;

public event EventHandler PropertyChanged;

public PropertyPathNode(string propertyName, Action callback)
public PropertyPathNode(string propertyName)
{
PropertyName = propertyName;
_callback = callback;
}

public string PropertyName { get; }
public INotifyPropertyChanged Source { get; private set; }
public object Source { get; private set; }
public PropertyPathNode Next { get; private set; }
public PropertyPathNode Prev { get; private set; }
public void SetCallback(Action callback)
{
_callback = callback;
Next?.SetCallback(callback);
}

public PropertyPathNode InsertBefore(string propertyName)
{
Expand All @@ -32,12 +36,12 @@ public PropertyPathNode InsertBefore(string propertyName)
Prev.Next = null;
}

Prev = new PropertyPathNode(propertyName, _callback);
Prev = new PropertyPathNode(propertyName);
Prev.Next = this;
return Prev;
}

public void UpdateSource(INotifyPropertyChanged source)
public void UpdateSource(object source)
{
EnsureDispose();
Cleanup();
Expand All @@ -49,8 +53,11 @@ private void StartObservePropertyChanged()
{
EnsureDispose();
if (Source == null) { return; }
Source.PropertyChanged += SourcePropertyChangedEventHandler;
Next?.UpdateSource(GetPropertyValue() as INotifyPropertyChanged);
if (Source is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged += SourcePropertyChangedEventHandler;
}
Next?.UpdateSource(GetPropertyValue());
}

private object GetPropertyValue()
Expand Down Expand Up @@ -111,7 +118,10 @@ private void Cleanup()
{
if (Source != null)
{
Source.PropertyChanged -= SourcePropertyChangedEventHandler;
if (Source is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged -= SourcePropertyChangedEventHandler;
}
Source = null;
}

Expand All @@ -131,5 +141,31 @@ private void EnsureDispose()

private void RaisePropertyChanged() => PropertyChanged?.Invoke(this, EventArgs.Empty);


public static PropertyPathNode CreateFromPropertySelector<TSubject, TProperty>(
Expression<Func<TSubject, TProperty>> propertySelector)
{
if (!(propertySelector.Body is MemberExpression current))
{
throw new ArgumentException();
}

var node = default(PropertyPathNode);
while (current != null)
{
var propertyName = current.Member.Name;
if (node != null)
{
node = node.InsertBefore(propertyName);
}
else
{
node = new PropertyPathNode(propertyName);
}
current = current.Expression as MemberExpression;
}

return node;
}
}
}
65 changes: 48 additions & 17 deletions Source/ReactiveProperty.NETStandard/ReactiveProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,16 +464,31 @@ public static ReactiveProperty<TProperty> FromObject<TTarget, TProperty>(
ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe,
bool ignoreValidationErrorValue = false)
{
// no use
var getter = AccessorCache<TTarget>.LookupGet(propertySelector, out var propertyName);
var setter = AccessorCache<TTarget>.LookupSet(propertySelector, out propertyName);
if (ExpressionTreeUtils.IsNestedPropertyPath(propertySelector))
{
var propertyPath = PropertyPathNode.CreateFromPropertySelector(propertySelector);
propertyPath.UpdateSource(target);

var initialValue = propertyPath.GetPropertyPathValue();
var result = new ReactiveProperty<TProperty>(raiseEventScheduler, initialValue: initialValue == null ? default : (TProperty)initialValue, mode: mode);
result
.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)
.Subscribe(x => propertyPath.SetPropertyPathValue(x), _ => propertyPath.Dispose(), () => propertyPath.Dispose());
return result;
}
else
{
var getter = AccessorCache<TTarget>.LookupGet(propertySelector, out var propertyName);
var setter = AccessorCache<TTarget>.LookupSet(propertySelector, out propertyName);

var result = new ReactiveProperty<TProperty>(raiseEventScheduler, initialValue: getter(target), mode: mode);
result
.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)
.Subscribe(x => setter(target, x));
var result = new ReactiveProperty<TProperty>(raiseEventScheduler, initialValue: getter(target), mode: mode);
result
.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)
.Subscribe(x => setter(target, x));

return result;
return result;
}
// no use
}

/// <summary>
Expand Down Expand Up @@ -504,17 +519,33 @@ public static ReactiveProperty<TResult> FromObject<TTarget, TProperty, TResult>(
ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe,
bool ignoreValidationErrorValue = false)
{
// no use
var getter = AccessorCache<TTarget>.LookupGet(propertySelector, out var propertyName);
var setter = AccessorCache<TTarget>.LookupSet(propertySelector, out propertyName);
if (ExpressionTreeUtils.IsNestedPropertyPath(propertySelector))
{
var propertyPath = PropertyPathNode.CreateFromPropertySelector(propertySelector);
propertyPath.UpdateSource(target);

var initialValue = propertyPath.GetPropertyPathValue();
var result = new ReactiveProperty<TResult>(raiseEventScheduler, initialValue: convert(initialValue == null ? default : (TProperty)initialValue), mode: mode);
result
.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)
.Select(convertBack)
.Subscribe(x => propertyPath.SetPropertyPathValue(x), _ => propertyPath.Dispose(), () => propertyPath.Dispose());
return result;
}
else
{
// no use
var getter = AccessorCache<TTarget>.LookupGet(propertySelector, out var propertyName);
var setter = AccessorCache<TTarget>.LookupSet(propertySelector, out propertyName);

var result = new ReactiveProperty<TResult>(raiseEventScheduler, initialValue: convert(getter(target)), mode: mode);
result
.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)
.Select(convertBack)
.Subscribe(x => setter(target, x));
var result = new ReactiveProperty<TResult>(raiseEventScheduler, initialValue: convert(getter(target)), mode: mode);
result
.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)
.Select(convertBack)
.Subscribe(x => setter(target, x));

return result;
return result;
}
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Source/SharedProperties.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<RootNamespace>Reactive.Bindings</RootNamespace>
<Version>7.5.0</Version>
<Version>7.5.1</Version>
<Authors>neuecc xin9le okazuki</Authors>
<PackageProjectUrl>https://github.com/runceel/ReactiveProperty</PackageProjectUrl>
<PackageTags>rx mvvm async rx-main reactive</PackageTags>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class ReactivePropertyStaticTest
[TestMethod]
public void FromObject()
{
var homuhomu = new ToaruClass { Name = "homuhomu", Age = 13 };
var homuhomu = new Person { Name = "homuhomu", Age = 13 };
var rxName = Reactive.Bindings.ReactiveProperty.FromObject(homuhomu, x => x.Name);
rxName.Value.Is("homuhomu");
rxName.Value = "mami";
Expand All @@ -21,10 +21,25 @@ public void FromObject()
homuhomu.Age.Is(20);
}

[TestMethod]
public void FromObject_NestedPath()
{
var homuhomu = new Person { Child = new Person { Name = "homuhomu", Age = 13 } };
var rxName = Reactive.Bindings.ReactiveProperty.FromObject(homuhomu, x => x.Child.Name);
rxName.Value.Is("homuhomu");
rxName.Value = "mami";
homuhomu.Child.Name.Is("mami");

var rxAge = Reactive.Bindings.ReactiveProperty.FromObject(homuhomu, x => x.Child.Age);
rxAge.Value.Is(13);
rxAge.Value = 20;
homuhomu.Child.Age.Is(20);
}

[TestMethod]
public void FromObjectIgnoreValidationErrorValue()
{
var homuhomu = new ToaruClass { Name = "homuhomu", Age = 13 };
var homuhomu = new Person { Name = "homuhomu", Age = 13 };
var rxName = Reactive.Bindings.ReactiveProperty
.FromObject(homuhomu, x => x.Name, ignoreValidationErrorValue: true)
.SetValidateNotifyError((string x) => string.IsNullOrEmpty(x) ? "error" : null);
Expand Down Expand Up @@ -52,10 +67,41 @@ public void FromObjectIgnoreValidationErrorValue()
homuhomu.Age.Is(10);
}

[TestMethod]
public void FromObjectIgnoreValidationErrorValue_NestedProperty()
{
var homuhomu = new Person { Child = new Person { Name = "homuhomu", Age = 13 } };
var rxName = Reactive.Bindings.ReactiveProperty
.FromObject(homuhomu, x => x.Child.Name, ignoreValidationErrorValue: true)
.SetValidateNotifyError((string x) => string.IsNullOrEmpty(x) ? "error" : null);
rxName.Value.Is("homuhomu");
rxName.Value = "mami";
homuhomu.Child.Name.Is("mami");

rxName.Value = null; // validation error
rxName.Value.IsNull();
homuhomu.Child.Name.Is("mami");

var rxAge = Reactive.Bindings.ReactiveProperty
.FromObject(homuhomu, x => x.Child.Age, ignoreValidationErrorValue: true)
.SetValidateNotifyError((int x) => x >= 0 ? null : "error");

rxAge.Value.Is(13);
rxAge.Value = 20;
homuhomu.Child.Age.Is(20);

rxAge.Value = -1; // validation error
rxAge.Value.Is(-1);
homuhomu.Child.Age.Is(20);

rxAge.Value = 10;
homuhomu.Child.Age.Is(10);
}

[TestMethod]
public void FromObjectConverter()
{
var homuhomu = new ToaruClass { Name = "homuhomu", Age = 13 };
var homuhomu = new Person { Name = "homuhomu", Age = 13 };
var rxAge = Reactive.Bindings.ReactiveProperty.FromObject(homuhomu, x => x.Age,
x => Convert.ToString(x, 16), x => Convert.ToInt32(x, 16));

Expand All @@ -64,10 +110,22 @@ public void FromObjectConverter()
homuhomu.Age.Is(63);
}

[TestMethod]
public void FromObjectConverter_NestedProperty()
{
var homuhomu = new Person { Child = new Person { Name = "homuhomu", Age = 13 } };
var rxAge = Reactive.Bindings.ReactiveProperty.FromObject(homuhomu, x => x.Child.Age,
x => Convert.ToString(x, 16), x => Convert.ToInt32(x, 16));

rxAge.Value.Is("d");
rxAge.Value = "3f";
homuhomu.Child.Age.Is(63);
}

[TestMethod]
public void FromObjectConverterIgnoreValidationErrorValue()
{
var homuhomu = new ToaruClass { Name = "homuhomu", Age = 13 };
var homuhomu = new Person { Name = "homuhomu", Age = 13 };
var rxAge = Reactive.Bindings.ReactiveProperty.FromObject(homuhomu, x => x.Age,
x => Convert.ToString(x, 16), x => Convert.ToInt32(x, 16),
ignoreValidationErrorValue: true)
Expand All @@ -93,11 +151,42 @@ public void FromObjectConverterIgnoreValidationErrorValue()
homuhomu.Age.Is(63);
}

private class ToaruClass
[TestMethod]
public void FromObjectConverterIgnoreValidationErrorValue_NestedProperty()
{
var homuhomu = new Person { Child = new Person { Name = "homuhomu", Age = 13 } };
var rxAge = Reactive.Bindings.ReactiveProperty.FromObject(homuhomu, x => x.Child.Age,
x => Convert.ToString(x, 16), x => Convert.ToInt32(x, 16),
ignoreValidationErrorValue: true)
.SetValidateNotifyError((string x) =>
{
try
{
Convert.ToInt32(x, 16);
return null;
}
catch
{
return "error";
}
});

rxAge.Value.Is("d");
rxAge.Value = "3f";
homuhomu.Child.Age.Is(63);

rxAge.Value = "XXX"; // validation error;
rxAge.Value.Is("XXX");
homuhomu.Child.Age.Is(63);
}

private class Person
{
public string Name { get; set; }

public int Age { get; set; }

public Person Child { get; set; }
}
}
}
6 changes: 3 additions & 3 deletions docs/docs/features/Extension-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,10 @@ public static IObservable<Unit> ObserveResetChanged<T>(this ObservableCollection
((INotifyCollectionChanged)source).ObserveResetChanged<T>();
```

## Observe `PropertyChanged` events of `ObservableCollection`'s items
## Observe `PropertyChanged` events of elements of `ObservableCollection` and `IFilteredReadOnlyObservableCollection`

Watch `PropertyChanged` event of `ObservableCollection`'s items.
`ObserveElementProperty` extension method can observe specific property's `PropertyChanged` event.
Watch `PropertyChanged` events of elements of `ObservableCollection` and `IFilteredReadOnlyObservableCollection`.
`ObserveElementProperty` extension method can observe specific property's `PropertyChanged` events.

```csharp
using Reactive.Bindings.Extensions;
Expand Down
Loading

0 comments on commit fd67265

Please sign in to comment.