Browse Source

Merge pull request #1694 from AvaloniaUI/nonrx-expressionnode

Don't use rx for ExpressionNodes.
pull/1714/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
de9644fd0f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  2. 120
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  3. 35
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  4. 11
      src/Avalonia.Base/Data/Core/IndexerNode.cs
  5. 10
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  6. 10
      src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs
  7. 5
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  8. 14
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs
  9. 19
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  10. 16
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  11. 8
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  12. 68
      src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs
  13. 11
      src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs
  14. 29
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  15. 17
      src/Avalonia.Base/Data/Core/StreamNode.cs
  16. 5
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs

5
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

@ -9,10 +9,5 @@ namespace Avalonia.Data.Core
internal class EmptyExpressionNode : ExpressionNode internal class EmptyExpressionNode : ExpressionNode
{ {
public override string Description => "."; public override string Description => ".";
protected override IObservable<object> StartListeningCore(WeakReference reference)
{
return Observable.Return(reference.Target);
}
} }
} }

120
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -2,22 +2,18 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal abstract class ExpressionNode : ISubject<object> internal abstract class ExpressionNode
{ {
private static readonly object CacheInvalid = new object(); private static readonly object CacheInvalid = new object();
protected static readonly WeakReference UnsetReference = protected static readonly WeakReference UnsetReference =
new WeakReference(AvaloniaProperty.UnsetValue); new WeakReference(AvaloniaProperty.UnsetValue);
private WeakReference _target = UnsetReference; private WeakReference _target = UnsetReference;
private IDisposable _valueSubscription; private Action<object> _subscriber;
private IObserver<object> _observer; private bool _listening;
protected WeakReference LastValue { get; private set; } protected WeakReference LastValue { get; private set; }
@ -33,92 +29,66 @@ namespace Avalonia.Data.Core
var oldTarget = _target?.Target; var oldTarget = _target?.Target;
var newTarget = value.Target; var newTarget = value.Target;
var running = _valueSubscription != null;
if (!ReferenceEquals(oldTarget, newTarget)) if (!ReferenceEquals(oldTarget, newTarget))
{ {
_valueSubscription?.Dispose(); if (_listening)
_valueSubscription = null; {
StopListening();
}
_target = value; _target = value;
if (running) if (_subscriber != null)
{ {
_valueSubscription = StartListening(); StartListening();
} }
} }
} }
} }
public IDisposable Subscribe(IObserver<object> observer) public void Subscribe(Action<object> subscriber)
{ {
if (_observer != null) if (_subscriber != null)
{ {
throw new AvaloniaInternalException("ExpressionNode can only be subscribed once."); throw new AvaloniaInternalException("ExpressionNode can only be subscribed once.");
} }
_observer = observer; _subscriber = subscriber;
var nextSubscription = Next?.Subscribe(this); Next?.Subscribe(NextValueChanged);
_valueSubscription = StartListening(); StartListening();
return Disposable.Create(() =>
{
_valueSubscription?.Dispose();
_valueSubscription = null;
LastValue = null;
nextSubscription?.Dispose();
_observer = null;
});
} }
void IObserver<object>.OnCompleted() public void Unsubscribe()
{ {
throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); Next?.Unsubscribe();
}
void IObserver<object>.OnError(Exception error) if (_listening)
{ {
throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); StopListening();
}
LastValue = null;
_subscriber = null;
} }
void IObserver<object>.OnNext(object value) protected virtual void StartListeningCore(WeakReference reference)
{ {
NextValueChanged(value); ValueChanged(reference.Target);
} }
protected virtual IObservable<object> StartListeningCore(WeakReference reference) protected virtual void StopListeningCore()
{ {
return Observable.Return(reference.Target);
} }
protected virtual void NextValueChanged(object value) protected virtual void NextValueChanged(object value)
{ {
var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException; var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
bindingBroken?.AddNode(Description); bindingBroken?.AddNode(Description);
_observer.OnNext(value); _subscriber(value);
}
private IDisposable StartListening()
{
var target = _target.Target;
IObservable<object> source;
if (target == null)
{
source = Observable.Return(TargetNullNotification());
}
else if (target == AvaloniaProperty.UnsetValue)
{
source = Observable.Empty<object>();
}
else
{
source = StartListeningCore(_target);
}
return source.Subscribe(ValueChanged);
} }
private void ValueChanged(object value) protected void ValueChanged(object value)
{ {
var notification = value as BindingNotification; var notification = value as BindingNotification;
@ -131,24 +101,50 @@ namespace Avalonia.Data.Core
} }
else else
{ {
_observer.OnNext(value); _subscriber(value);
} }
} }
else else
{ {
LastValue = new WeakReference(notification.Value); LastValue = new WeakReference(notification.Value);
if (Next != null) if (Next != null)
{ {
Next.Target = new WeakReference(notification.Value); Next.Target = new WeakReference(notification.Value);
} }
if (Next == null || notification.Error != null) if (Next == null || notification.Error != null)
{ {
_observer.OnNext(value); _subscriber(value);
} }
} }
} }
private void StartListening()
{
var target = _target.Target;
if (target == null)
{
ValueChanged(TargetNullNotification());
_listening = false;
}
else if (target != AvaloniaProperty.UnsetValue)
{
StartListeningCore(_target);
_listening = true;
}
else
{
_listening = false;
}
}
private void StopListening()
{
StopListeningCore();
}
private BindingNotification TargetNullNotification() private BindingNotification TargetNullNotification()
{ {
return new BindingNotification( return new BindingNotification(

35
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -14,9 +14,7 @@ namespace Avalonia.Data.Core
/// <summary> /// <summary>
/// Observes and sets the value of an expression on an object. /// Observes and sets the value of an expression on an object.
/// </summary> /// </summary>
public class ExpressionObserver : LightweightObservableBase<object>, public class ExpressionObserver : LightweightObservableBase<object>, IDescription
IDescription,
IObserver<object>
{ {
/// <summary> /// <summary>
/// An ordered collection of property accessor plugins that can be used to customize /// An ordered collection of property accessor plugins that can be used to customize
@ -55,7 +53,6 @@ namespace Avalonia.Data.Core
private static readonly object UninitializedValue = new object(); private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node; private readonly ExpressionNode _node;
private IDisposable _nodeSubscription;
private object _root; private object _root;
private IDisposable _rootSubscription; private IDisposable _rootSubscription;
private WeakReference<object> _value; private WeakReference<object> _value;
@ -202,34 +199,18 @@ namespace Avalonia.Data.Core
} }
} }
void IObserver<object>.OnNext(object value)
{
var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
broken?.Commit(Description);
_value = new WeakReference<object>(value);
PublishNext(value);
}
void IObserver<object>.OnCompleted()
{
}
void IObserver<object>.OnError(Exception error)
{
}
protected override void Initialize() protected override void Initialize()
{ {
_value = null; _value = null;
_nodeSubscription = _node.Subscribe(this); _node.Subscribe(ValueChanged);
StartRoot(); StartRoot();
} }
protected override void Deinitialize() protected override void Deinitialize()
{ {
_rootSubscription?.Dispose(); _rootSubscription?.Dispose();
_nodeSubscription?.Dispose(); _rootSubscription = null;
_rootSubscription = _nodeSubscription = null; _node.Unsubscribe();
} }
protected override void Subscribed(IObserver<object> observer, bool first) protected override void Subscribed(IObserver<object> observer, bool first)
@ -266,5 +247,13 @@ namespace Avalonia.Data.Core
_node.Target = (WeakReference)_root; _node.Target = (WeakReference)_root;
} }
} }
private void ValueChanged(object value)
{
var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException;
broken?.Commit(Description);
_value = new WeakReference<object>(value);
PublishNext(value);
}
} }
} }

11
src/Avalonia.Base/Data/Core/IndexerNode.cs

@ -17,6 +17,8 @@ namespace Avalonia.Data.Core
{ {
internal class IndexerNode : SettableNode internal class IndexerNode : SettableNode
{ {
private IDisposable _subscription;
public IndexerNode(IList<string> arguments) public IndexerNode(IList<string> arguments)
{ {
Arguments = arguments; Arguments = arguments;
@ -24,7 +26,7 @@ namespace Avalonia.Data.Core
public override string Description => "[" + string.Join(",", Arguments) + "]"; public override string Description => "[" + string.Join(",", Arguments) + "]";
protected override IObservable<object> StartListeningCore(WeakReference reference) protected override void StartListeningCore(WeakReference reference)
{ {
var target = reference.Target; var target = reference.Target;
var incc = target as INotifyCollectionChanged; var incc = target as INotifyCollectionChanged;
@ -49,7 +51,12 @@ namespace Avalonia.Data.Core
.Select(_ => GetValue(target))); .Select(_ => GetValue(target)));
} }
return Observable.Merge(inputs).StartWith(GetValue(target)); _subscription = Observable.Merge(inputs).StartWith(GetValue(target)).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
{
_subscription.Dispose();
} }
protected override bool SetTargetValueCore(object value, BindingPriority priority) protected override bool SetTargetValueCore(object value, BindingPriority priority)

10
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -145,15 +145,15 @@ namespace Avalonia.Data.Core.Plugins
return false; return false;
} }
protected override void Dispose(bool disposing) protected override void SubscribeCore()
{ {
_subscription?.Dispose(); _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
_subscription = null;
} }
protected override void SubscribeCore(IObserver<object> observer) protected override void UnsubscribeCore()
{ {
_subscription = Instance?.GetObservable(_property).Subscribe(observer); _subscription?.Dispose();
_subscription = null;
} }
} }
} }

10
src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs

@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
void IObserver<object>.OnNext(object value) => InnerValueChanged(value); void IObserver<object>.OnNext(object value) => InnerValueChanged(value);
/// <inheritdoc/>
protected override void Dispose(bool disposing) => _inner.Dispose();
/// <summary> /// <summary>
/// Begins listening to the inner <see cref="IPropertyAccessor"/>. /// Begins listening to the inner <see cref="IPropertyAccessor"/>.
/// </summary> /// </summary>
protected override void SubscribeCore(IObserver<object> observer) => _inner.Subscribe(this); protected override void SubscribeCore() => _inner.Subscribe(InnerValueChanged);
/// <inheritdoc/>
protected override void UnsubscribeCore() => _inner.Dispose();
/// <summary> /// <summary>
/// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value. /// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
@ -74,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins
protected virtual void InnerValueChanged(object value) protected virtual void InnerValueChanged(object value)
{ {
var notification = value as BindingNotification ?? new BindingNotification(value); var notification = value as BindingNotification ?? new BindingNotification(value);
Observer.OnNext(notification); PublishValue(notification);
} }
} }
} }

5
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved. // Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Data;
using System; using System;
using System.Reflection; using System.Reflection;
@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins
} }
catch (TargetInvocationException ex) catch (TargetInvocationException ex)
{ {
Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
} }
catch (Exception ex) catch (Exception ex)
{ {
Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError));
} }
return false; return false;

14
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
@ -10,7 +9,7 @@ namespace Avalonia.Data.Core.Plugins
/// Defines an accessor to a property on an object returned by a /// Defines an accessor to a property on an object returned by a
/// <see cref="IPropertyAccessorPlugin"/> /// <see cref="IPropertyAccessorPlugin"/>
/// </summary> /// </summary>
public interface IPropertyAccessor : IObservable<object>, IDisposable public interface IPropertyAccessor : IDisposable
{ {
/// <summary> /// <summary>
/// Gets the type of the property. /// Gets the type of the property.
@ -38,5 +37,16 @@ namespace Avalonia.Data.Core.Plugins
/// True if the property was set; false if the property could not be set. /// True if the property was set; false if the property could not be set.
/// </returns> /// </returns>
bool SetValue(object value, BindingPriority priority); bool SetValue(object value, BindingPriority priority);
/// <summary>
/// Subscribes to the value of the member.
/// </summary>
/// <param name="listener">A method that receives the values.</param>
void Subscribe(Action<object> listener);
/// <summary>
/// Unsubscribes to the value of the member.
/// </summary>
void Unsubscribe();
} }
} }

19
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Data;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
@ -40,43 +39,43 @@ namespace Avalonia.Data.Core.Plugins
{ {
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{ {
Observer.OnNext(CreateBindingNotification(Value)); PublishValue(CreateBindingNotification(Value));
} }
} }
protected override void Dispose(bool disposing) protected override void SubscribeCore()
{ {
base.Dispose(disposing);
var target = _reference.Target as INotifyDataErrorInfo; var target = _reference.Target as INotifyDataErrorInfo;
if (target != null) if (target != null)
{ {
WeakSubscriptionManager.Unsubscribe( WeakSubscriptionManager.Subscribe(
target, target,
nameof(target.ErrorsChanged), nameof(target.ErrorsChanged),
this); this);
} }
base.SubscribeCore();
} }
protected override void SubscribeCore(IObserver<object> observer) protected override void UnsubscribeCore()
{ {
var target = _reference.Target as INotifyDataErrorInfo; var target = _reference.Target as INotifyDataErrorInfo;
if (target != null) if (target != null)
{ {
WeakSubscriptionManager.Subscribe( WeakSubscriptionManager.Unsubscribe(
target, target,
nameof(target.ErrorsChanged), nameof(target.ErrorsChanged),
this); this);
} }
base.SubscribeCore(observer); base.UnsubscribeCore();
} }
protected override void InnerValueChanged(object value) protected override void InnerValueChanged(object value)
{ {
base.InnerValueChanged(CreateBindingNotification(value)); PublishValue(CreateBindingNotification(value));
} }
private BindingNotification CreateBindingNotification(object value) private BindingNotification CreateBindingNotification(object value)

16
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -103,7 +103,13 @@ namespace Avalonia.Data.Core.Plugins
} }
} }
protected override void Dispose(bool disposing) protected override void SubscribeCore()
{
SendCurrentValue();
SubscribeToChanges();
}
protected override void UnsubscribeCore()
{ {
var inpc = _reference.Target as INotifyPropertyChanged; var inpc = _reference.Target as INotifyPropertyChanged;
@ -116,18 +122,12 @@ namespace Avalonia.Data.Core.Plugins
} }
} }
protected override void SubscribeCore(IObserver<object> observer)
{
SendCurrentValue();
SubscribeToChanges();
}
private void SendCurrentValue() private void SendCurrentValue()
{ {
try try
{ {
var value = Value; var value = Value;
Observer.OnNext(value); PublishValue(value);
} }
catch { } catch { }
} }

8
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -74,14 +74,18 @@ namespace Avalonia.Data.Core.Plugins
public override bool SetValue(object value, BindingPriority priority) => false; public override bool SetValue(object value, BindingPriority priority) => false;
protected override void SubscribeCore(IObserver<object> observer) protected override void SubscribeCore()
{ {
try try
{ {
Observer.OnNext(Value); PublishValue(Value);
} }
catch { } catch { }
} }
protected override void UnsubscribeCore()
{
}
} }
} }
} }

68
src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs

@ -2,67 +2,75 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
/// <summary> /// <summary>
/// Defines a default base implementation for a <see cref="IPropertyAccessor"/>. /// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
/// </summary> /// </summary>
/// <remarks>
/// <see cref="IPropertyAccessor"/> is an observable that will only be subscribed to one time.
/// In addition, the subscription can be disposed by calling <see cref="Dispose()"/> on the
/// property accessor itself - this prevents needing to hold two references for a subscription.
/// </remarks>
public abstract class PropertyAccessorBase : IPropertyAccessor public abstract class PropertyAccessorBase : IPropertyAccessor
{ {
private Action<object> _listener;
/// <inheritdoc/> /// <inheritdoc/>
public abstract Type PropertyType { get; } public abstract Type PropertyType { get; }
/// <inheritdoc/> /// <inheritdoc/>
public abstract object Value { get; } public abstract object Value { get; }
/// <summary> /// <inheritdoc/>
/// Stops the subscription. public void Dispose()
/// </summary> {
public void Dispose() => Dispose(true); if (_listener != null)
{
Unsubscribe();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public abstract bool SetValue(object value, BindingPriority priority); public abstract bool SetValue(object value, BindingPriority priority);
/// <summary>
/// The currently subscribed observer.
/// </summary>
protected IObserver<object> Observer { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer) public void Subscribe(Action<object> listener)
{ {
Contract.Requires<ArgumentNullException>(observer != null); Contract.Requires<ArgumentNullException>(listener != null);
if (Observer != null) if (_listener != null)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"A property accessor can be subscribed to only once."); "A member accessor can be subscribed to only once.");
} }
Observer = observer; _listener = listener;
SubscribeCore(observer); SubscribeCore();
return this;
} }
public void Unsubscribe()
{
if (_listener == null)
{
throw new InvalidOperationException(
"The member accessor was not subscribed.");
}
UnsubscribeCore();
_listener = null;
}
/// <summary>
/// Publishes a value to the listener.
/// </summary>
/// <param name="value">The value.</param>
protected void PublishValue(object value) => _listener?.Invoke(value);
/// <summary> /// <summary>
/// Stops listening to the property. /// When overridden in a derived class, begins listening to the member.
/// </summary> /// </summary>
/// <param name="disposing"> protected abstract void SubscribeCore();
/// True if the <see cref="Dispose()"/> method was called, false if the object is being
/// finalized.
/// </param>
protected virtual void Dispose(bool disposing) => Observer = null;
/// <summary> /// <summary>
/// When overridden in a derived class, begins listening to the property. /// When overridden in a derived class, stops listening to the member.
/// </summary> /// </summary>
protected abstract void SubscribeCore(IObserver<object> observer); protected abstract void UnsubscribeCore();
} }
} }

11
src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs

@ -1,6 +1,4 @@
using System; using System;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.Data.Core.Plugins namespace Avalonia.Data.Core.Plugins
{ {
@ -37,10 +35,13 @@ namespace Avalonia.Data.Core.Plugins
return false; return false;
} }
public IDisposable Subscribe(IObserver<object> observer) public void Subscribe(Action<object> listener)
{
listener(_error);
}
public void Unsubscribe()
{ {
observer.OnNext(_error);
return Disposable.Empty;
} }
} }
} }

29
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -3,9 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
@ -39,7 +37,7 @@ namespace Avalonia.Data.Core
return false; return false;
} }
protected override IObservable<object> StartListeningCore(WeakReference reference) protected override void StartListeningCore(WeakReference reference)
{ {
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName));
var accessor = plugin?.Start(reference, PropertyName); var accessor = plugin?.Start(reference, PropertyName);
@ -55,17 +53,20 @@ namespace Avalonia.Data.Core
} }
} }
// Ensure that _accessor is set for the duration of the subscription. if (accessor == null)
return Observable.Using( {
() => throw new NotSupportedException(
{ $"Could not find a matching property accessor for {PropertyName}.");
_accessor = accessor; }
return Disposable.Create(() =>
{ accessor.Subscribe(ValueChanged);
_accessor = null; _accessor = accessor;
}); }
},
_ => accessor); protected override void StopListeningCore()
{
_accessor.Dispose();
_accessor = null;
} }
} }
} }

17
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -2,30 +2,37 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Globalization;
using Avalonia.Data;
using System.Reactive.Linq; using System.Reactive.Linq;
namespace Avalonia.Data.Core namespace Avalonia.Data.Core
{ {
internal class StreamNode : ExpressionNode internal class StreamNode : ExpressionNode
{ {
private IDisposable _subscription;
public override string Description => "^"; public override string Description => "^";
protected override IObservable<object> StartListeningCore(WeakReference reference) protected override void StartListeningCore(WeakReference reference)
{ {
foreach (var plugin in ExpressionObserver.StreamHandlers) foreach (var plugin in ExpressionObserver.StreamHandlers)
{ {
if (plugin.Match(reference)) if (plugin.Match(reference))
{ {
return plugin.Start(reference); _subscription = plugin.Start(reference).Subscribe(ValueChanged);
return;
} }
} }
// TODO: Improve error. // TODO: Improve error.
return Observable.Return(new BindingNotification( ValueChanged(new BindingNotification(
new MarkupBindingChainException("Stream operator applied to unsupported type", Description), new MarkupBindingChainException("Stream operator applied to unsupported type", Description),
BindingErrorType.Error)); BindingErrorType.Error));
} }
protected override void StopListeningCore()
{
_subscription?.Dispose();
_subscription = null;
}
} }
} }

5
tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs

@ -4,7 +4,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Xunit; using Xunit;
@ -58,9 +57,9 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
Assert.Equal(0, data.ErrorsChangedSubscriptionCount); Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
var sub = validator.Subscribe(_ => { }); validator.Subscribe(_ => { });
Assert.Equal(1, data.ErrorsChangedSubscriptionCount); Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
sub.Dispose(); validator.Unsubscribe();
Assert.Equal(0, data.ErrorsChangedSubscriptionCount); Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
} }

Loading…
Cancel
Save