From 0c078c9dec3086b262dcd1bde391106625e106eb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jun 2018 18:23:08 +0200 Subject: [PATCH 1/2] Don't use rx for ExpressionNodes. `ExpressionNode`s were always single-subscriber and making them use `IObservable<>` meant that we had to have extra allocations in order to return `IDisposable`s. Instead of using `IObservable` use a simpler `Subscribe`/`Unsubscribe` pattern. This saves a bunch more memory. --- .../Data/Core/EmptyExpressionNode.cs | 5 - src/Avalonia.Base/Data/Core/ExpressionNode.cs | 120 +++++++++--------- .../Data/Core/ExpressionObserver.cs | 35 ++--- src/Avalonia.Base/Data/Core/IndexerNode.cs | 11 +- .../Plugins/AvaloniaPropertyAccessorPlugin.cs | 10 +- .../Data/Core/Plugins/DataValidatiorBase.cs | 10 +- .../Core/Plugins/ExceptionValidationPlugin.cs | 5 +- .../Data/Core/Plugins/IPropertyAccessor.cs | 14 +- .../Core/Plugins/IndeiValidationPlugin.cs | 19 ++- .../Plugins/InpcPropertyAccessorPlugin.cs | 16 +-- .../Data/Core/Plugins/MethodAccessorPlugin.cs | 8 +- .../Data/Core/Plugins/PropertyAccessorBase.cs | 68 +++++----- .../Data/Core/Plugins/PropertyError.cs | 11 +- .../Data/Core/PropertyAccessorNode.cs | 23 ++-- src/Avalonia.Base/Data/Core/StreamNode.cs | 17 ++- .../Plugins/IndeiValidationPluginTests.cs | 5 +- 16 files changed, 193 insertions(+), 184 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 93e0d5947a..c4166b44e5 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -9,10 +9,5 @@ namespace Avalonia.Data.Core internal class EmptyExpressionNode : ExpressionNode { public override string Description => "."; - - protected override IObservable StartListeningCore(WeakReference reference) - { - return Observable.Return(reference.Target); - } } } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index ac7e97a4b1..600cd68d60 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/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. using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Data; namespace Avalonia.Data.Core { - internal abstract class ExpressionNode : ISubject + internal abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); private WeakReference _target = UnsetReference; - private IDisposable _valueSubscription; - private IObserver _observer; + private Action _subscriber; + private bool _listening; protected WeakReference LastValue { get; private set; } @@ -33,92 +29,66 @@ namespace Avalonia.Data.Core var oldTarget = _target?.Target; var newTarget = value.Target; - var running = _valueSubscription != null; if (!ReferenceEquals(oldTarget, newTarget)) { - _valueSubscription?.Dispose(); - _valueSubscription = null; + if (_listening) + { + StopListening(); + } + _target = value; - if (running) + if (_subscriber != null) { - _valueSubscription = StartListening(); + StartListening(); } } } } - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action subscriber) { - if (_observer != null) + if (_subscriber != null) { throw new AvaloniaInternalException("ExpressionNode can only be subscribed once."); } - _observer = observer; - var nextSubscription = Next?.Subscribe(this); - _valueSubscription = StartListening(); - - return Disposable.Create(() => - { - _valueSubscription?.Dispose(); - _valueSubscription = null; - LastValue = null; - nextSubscription?.Dispose(); - _observer = null; - }); + _subscriber = subscriber; + Next?.Subscribe(NextValueChanged); + StartListening(); } - void IObserver.OnCompleted() + public void Unsubscribe() { - throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); - } + Next?.Unsubscribe(); - void IObserver.OnError(Exception error) - { - throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); + if (_listening) + { + StopListening(); + } + + LastValue = null; + _subscriber = null; } - void IObserver.OnNext(object value) + protected virtual void StartListeningCore(WeakReference reference) { - NextValueChanged(value); + ValueChanged(reference.Target); } - protected virtual IObservable StartListeningCore(WeakReference reference) + protected virtual void StopListeningCore() { - return Observable.Return(reference.Target); } protected virtual void NextValueChanged(object value) { var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainException; bindingBroken?.AddNode(Description); - _observer.OnNext(value); - } - - private IDisposable StartListening() - { - var target = _target.Target; - IObservable source; - - if (target == null) - { - source = Observable.Return(TargetNullNotification()); - } - else if (target == AvaloniaProperty.UnsetValue) - { - source = Observable.Empty(); - } - else - { - source = StartListeningCore(_target); - } - - return source.Subscribe(ValueChanged); + _subscriber(value); } - private void ValueChanged(object value) + protected void ValueChanged(object value) { var notification = value as BindingNotification; @@ -131,24 +101,50 @@ namespace Avalonia.Data.Core } else { - _observer.OnNext(value); + _subscriber(value); } } else { LastValue = new WeakReference(notification.Value); + if (Next != null) { Next.Target = new WeakReference(notification.Value); } - + 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() { return new BindingNotification( diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 3a25407133..3a061206bf 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -14,9 +14,7 @@ namespace Avalonia.Data.Core /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : LightweightObservableBase, - IDescription, - IObserver + public class ExpressionObserver : LightweightObservableBase, IDescription { /// /// 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 readonly ExpressionNode _node; - private IDisposable _nodeSubscription; private object _root; private IDisposable _rootSubscription; private WeakReference _value; @@ -202,34 +199,18 @@ namespace Avalonia.Data.Core } } - void IObserver.OnNext(object value) - { - var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException; - broken?.Commit(Description); - _value = new WeakReference(value); - PublishNext(value); - } - - void IObserver.OnCompleted() - { - } - - void IObserver.OnError(Exception error) - { - } - protected override void Initialize() { _value = null; - _nodeSubscription = _node.Subscribe(this); + _node.Subscribe(ValueChanged); StartRoot(); } protected override void Deinitialize() { _rootSubscription?.Dispose(); - _nodeSubscription?.Dispose(); - _rootSubscription = _nodeSubscription = null; + _rootSubscription = null; + _node.Unsubscribe(); } protected override void Subscribed(IObserver observer, bool first) @@ -266,5 +247,13 @@ namespace Avalonia.Data.Core _node.Target = (WeakReference)_root; } } + + private void ValueChanged(object value) + { + var broken = BindingNotification.ExtractError(value) as MarkupBindingChainException; + broken?.Commit(Description); + _value = new WeakReference(value); + PublishNext(value); + } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNode.cs b/src/Avalonia.Base/Data/Core/IndexerNode.cs index 633d3558ee..afdfe90c4c 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNode.cs @@ -17,6 +17,8 @@ namespace Avalonia.Data.Core { internal class IndexerNode : SettableNode { + private IDisposable _subscription; + public IndexerNode(IList arguments) { Arguments = arguments; @@ -24,7 +26,7 @@ namespace Avalonia.Data.Core public override string Description => "[" + string.Join(",", Arguments) + "]"; - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { var target = reference.Target; var incc = target as INotifyCollectionChanged; @@ -49,7 +51,12 @@ namespace Avalonia.Data.Core .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) diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 48edb218dc..a163c07f87 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -145,15 +145,15 @@ namespace Avalonia.Data.Core.Plugins return false; } - protected override void Dispose(bool disposing) + protected override void SubscribeCore() { - _subscription?.Dispose(); - _subscription = null; + _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); } - protected override void SubscribeCore(IObserver observer) + protected override void UnsubscribeCore() { - _subscription = Instance?.GetObservable(_property).Subscribe(observer); + _subscription?.Dispose(); + _subscription = null; } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs index bd429f04d6..03ab7712bd 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataValidatiorBase.cs @@ -55,13 +55,13 @@ namespace Avalonia.Data.Core.Plugins /// The value. void IObserver.OnNext(object value) => InnerValueChanged(value); - /// - protected override void Dispose(bool disposing) => _inner.Dispose(); - /// /// Begins listening to the inner . /// - protected override void SubscribeCore(IObserver observer) => _inner.Subscribe(this); + protected override void SubscribeCore() => _inner.Subscribe(InnerValueChanged); + + /// + protected override void UnsubscribeCore() => _inner.Dispose(); /// /// Called when the inner notifies with a new value. @@ -74,7 +74,7 @@ namespace Avalonia.Data.Core.Plugins protected virtual void InnerValueChanged(object value) { var notification = value as BindingNotification ?? new BindingNotification(value); - Observer.OnNext(notification); + PublishValue(notification); } } } \ No newline at end of file diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 35f9f7e59a..4507b32e0c 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -1,7 +1,6 @@ // 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. -using Avalonia.Data; using System; using System.Reflection; @@ -36,11 +35,11 @@ namespace Avalonia.Data.Core.Plugins } catch (TargetInvocationException ex) { - Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); + PublishValue(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); } catch (Exception ex) { - Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); + PublishValue(new BindingNotification(ex, BindingErrorType.DataValidationError)); } return false; diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs index d7dda57a72..33ea5bba08 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessor.cs +++ b/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. using System; -using Avalonia.Data; 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 /// /// - public interface IPropertyAccessor : IObservable, IDisposable + public interface IPropertyAccessor : IDisposable { /// /// 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. /// bool SetValue(object value, BindingPriority priority); + + /// + /// Subscribes to the value of the member. + /// + /// A method that receives the values. + void Subscribe(Action listener); + + /// + /// Unsubscribes to the value of the member. + /// + void Unsubscribe(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 436046f3fa..4d6fc01229 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using Avalonia.Data; using Avalonia.Utilities; namespace Avalonia.Data.Core.Plugins @@ -40,43 +39,43 @@ namespace Avalonia.Data.Core.Plugins { 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; if (target != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Subscribe( target, nameof(target.ErrorsChanged), this); } + + base.SubscribeCore(); } - protected override void SubscribeCore(IObserver observer) + protected override void UnsubscribeCore() { var target = _reference.Target as INotifyDataErrorInfo; if (target != null) { - WeakSubscriptionManager.Subscribe( + WeakSubscriptionManager.Unsubscribe( target, nameof(target.ErrorsChanged), this); } - base.SubscribeCore(observer); + base.UnsubscribeCore(); } protected override void InnerValueChanged(object value) { - base.InnerValueChanged(CreateBindingNotification(value)); + PublishValue(CreateBindingNotification(value)); } private BindingNotification CreateBindingNotification(object value) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index ba4e60eb74..dab32b639a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/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; @@ -116,18 +122,12 @@ namespace Avalonia.Data.Core.Plugins } } - protected override void SubscribeCore(IObserver observer) - { - SendCurrentValue(); - SubscribeToChanges(); - } - private void SendCurrentValue() { try { var value = Value; - Observer.OnNext(value); + PublishValue(value); } catch { } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index b2b3a107fa..cf0abc6f35 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/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; - protected override void SubscribeCore(IObserver observer) + protected override void SubscribeCore() { try { - Observer.OnNext(Value); + PublishValue(Value); } catch { } } + + protected override void UnsubscribeCore() + { + } } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs index 9cc78369a7..e840b2c5c9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyAccessorBase.cs +++ b/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. using System; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { /// /// Defines a default base implementation for a . /// - /// - /// is an observable that will only be subscribed to one time. - /// In addition, the subscription can be disposed by calling on the - /// property accessor itself - this prevents needing to hold two references for a subscription. - /// public abstract class PropertyAccessorBase : IPropertyAccessor { + private Action _listener; + /// public abstract Type PropertyType { get; } /// public abstract object Value { get; } - /// - /// Stops the subscription. - /// - public void Dispose() => Dispose(true); + /// + public void Dispose() + { + if (_listener != null) + { + Unsubscribe(); + } + } /// public abstract bool SetValue(object value, BindingPriority priority); - /// - /// The currently subscribed observer. - /// - protected IObserver Observer { get; private set; } - /// - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action listener) { - Contract.Requires(observer != null); + Contract.Requires(listener != null); - if (Observer != null) + if (_listener != null) { throw new InvalidOperationException( - "A property accessor can be subscribed to only once."); + "A member accessor can be subscribed to only once."); } - Observer = observer; - SubscribeCore(observer); - return this; + _listener = listener; + SubscribeCore(); } + public void Unsubscribe() + { + if (_listener == null) + { + throw new InvalidOperationException( + "The member accessor was not subscribed."); + } + + UnsubscribeCore(); + _listener = null; + } + + /// + /// Publishes a value to the listener. + /// + /// The value. + protected void PublishValue(object value) => _listener?.Invoke(value); + /// - /// Stops listening to the property. + /// When overridden in a derived class, begins listening to the member. /// - /// - /// True if the method was called, false if the object is being - /// finalized. - /// - protected virtual void Dispose(bool disposing) => Observer = null; + protected abstract void SubscribeCore(); /// - /// When overridden in a derived class, begins listening to the property. + /// When overridden in a derived class, stops listening to the member. /// - protected abstract void SubscribeCore(IObserver observer); + protected abstract void UnsubscribeCore(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs index 647adc36cb..eb2400807a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/PropertyError.cs @@ -1,6 +1,4 @@ using System; -using System.Reactive.Disposables; -using Avalonia.Data; namespace Avalonia.Data.Core.Plugins { @@ -37,10 +35,13 @@ namespace Avalonia.Data.Core.Plugins return false; } - public IDisposable Subscribe(IObserver observer) + public void Subscribe(Action listener) + { + listener(_error); + } + + public void Unsubscribe() { - observer.OnNext(_error); - return Disposable.Empty; } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 9d657b3144..2565a34322 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -3,9 +3,7 @@ using System; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; -using Avalonia.Data; using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core @@ -39,7 +37,7 @@ namespace Avalonia.Data.Core return false; } - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); var accessor = plugin?.Start(reference, PropertyName); @@ -55,17 +53,14 @@ namespace Avalonia.Data.Core } } - // Ensure that _accessor is set for the duration of the subscription. - return Observable.Using( - () => - { - _accessor = accessor; - return Disposable.Create(() => - { - _accessor = null; - }); - }, - _ => accessor); + accessor.Subscribe(ValueChanged); + _accessor = accessor; + } + + protected override void StopListeningCore() + { + _accessor.Dispose(); + _accessor = null; } } } diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 187c79af49..415def4d30 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/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. using System; -using System.Globalization; -using Avalonia.Data; using System.Reactive.Linq; namespace Avalonia.Data.Core { internal class StreamNode : ExpressionNode { + private IDisposable _subscription; + public override string Description => "^"; - protected override IObservable StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { - return plugin.Start(reference); + _subscription = plugin.Start(reference).Subscribe(ValueChanged); + return; } } // TODO: Improve error. - return Observable.Return(new BindingNotification( + ValueChanged(new BindingNotification( new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index 45c084014b..383030cb6c 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Data.Core.Plugins; using Xunit; @@ -58,9 +57,9 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); - var sub = validator.Subscribe(_ => { }); + validator.Subscribe(_ => { }); Assert.Equal(1, data.ErrorsChangedSubscriptionCount); - sub.Dispose(); + validator.Unsubscribe(); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); } From 6d0e46134919956ba20a115218b5ba7f43fa933c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Jun 2018 10:46:50 +0200 Subject: [PATCH 2/2] Throw if no matching property accessor found. This shouldn't happen normally as `InpcPropertyAcessorPlugin` matches everything. --- src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 2565a34322..e9831eb047 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -53,6 +53,12 @@ namespace Avalonia.Data.Core } } + if (accessor == null) + { + throw new NotSupportedException( + $"Could not find a matching property accessor for {PropertyName}."); + } + accessor.Subscribe(ValueChanged); _accessor = accessor; }