1
0
Эх сурвалжийг харах

Reimplemented data validation using BindingNotifications.

Steven Kirk 9 жил өмнө
parent
commit
4906a472b0
34 өөрчлөгдсөн 789 нэмэгдсэн , 565 устгасан
  1. 0 2
      src/Avalonia.Base/Avalonia.Base.csproj
  2. 2 26
      src/Avalonia.Base/AvaloniaObject.cs
  3. 9 5
      src/Avalonia.Base/Data/BindingNotification.cs
  4. 0 17
      src/Avalonia.Base/Data/IValidationStatus.cs
  5. 0 44
      src/Avalonia.Base/Data/ObjectValidationStatus.cs
  6. 3 3
      src/Avalonia.Controls/Control.cs
  7. 1 1
      src/Avalonia.Controls/TextBox.cs
  8. 3 2
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  9. 0 13
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  10. 16 15
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  11. 1 1
      src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
  12. 20 35
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  13. 80 0
      src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs
  14. 13 30
      src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
  15. 36 0
      src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs
  16. 4 1
      src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs
  17. 1 4
      src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  18. 0 35
      src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs
  19. 67 40
      src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
  20. 60 54
      src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  21. 68 0
      src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs
  22. 7 0
      src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs
  23. 0 46
      src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs
  24. 31 24
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  25. 2 2
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  26. 18 15
      tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs
  27. 3 2
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  28. 0 93
      tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
  29. 65 6
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
  30. 1 1
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  31. 64 45
      tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
  32. 71 0
      tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs
  33. 138 0
      tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs
  34. 5 3
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

+ 0 - 2
src/Avalonia.Base/Avalonia.Base.csproj

@@ -45,8 +45,6 @@
     </Compile>
     <Compile Include="Data\BindingNotification.cs" />
     <Compile Include="Data\IndexerBinding.cs" />
-    <Compile Include="Data\IValidationStatus.cs" />
-    <Compile Include="Data\ObjectValidationStatus.cs" />
     <Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
     <Compile Include="Data\AssignBindingAttribute.cs" />
     <Compile Include="Data\BindingOperations.cs" />

+ 2 - 26
src/Avalonia.Base/AvaloniaObject.cs

@@ -50,29 +50,6 @@ namespace Avalonia
         /// </summary>
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
 
-        /// <summary>
-        /// Defines the <see cref="ValidationStatus"/> property.
-        /// </summary>
-        public static readonly DirectProperty<AvaloniaObject, ObjectValidationStatus> ValidationStatusProperty =
-            AvaloniaProperty.RegisterDirect<AvaloniaObject, ObjectValidationStatus>(nameof(ValidationStatus), c => c.ValidationStatus);
-
-        private ObjectValidationStatus validationStatus;
-
-        /// <summary>
-        /// The current validation status of the control.
-        /// </summary>
-        public ObjectValidationStatus ValidationStatus
-        {
-            get
-            {
-                return validationStatus;
-            }
-            private set
-            {
-                SetAndRaise(ValidationStatusProperty, ref validationStatus, value);
-            }
-        }
-
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
         /// </summary>
@@ -497,7 +474,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="property">The property whose validation state changed.</param>
         /// <param name="status">The new validation state.</param>
-        protected virtual void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+        protected virtual void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
         {
         }
 
@@ -505,9 +482,8 @@ namespace Avalonia
         /// Updates the validation status of the current object.
         /// </summary>
         /// <param name="status">The new validation status.</param>
-        protected void UpdateValidationState(IValidationStatus status)
+        protected void UpdateValidationState(BindingNotification status)
         {
-            ValidationStatus = ValidationStatus.UpdateValidationStatus(status);
         }
 
         /// <inheritdoc/>

+ 9 - 5
src/Avalonia.Base/Data/BindingNotification.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Data
     /// Represents a binding notification that can be a valid binding value, or a binding or
     /// data validation error.
     /// </summary>
-    public class BindingNotification : IValidationStatus
+    public class BindingNotification
     {
         /// <summary>
         /// A binding notification representing the null value.
@@ -77,7 +77,7 @@ namespace Avalonia.Data
         /// <param name="errorType">The type of the binding error.</param>
         /// <param name="fallbackValue">The fallback value.</param>
         public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue)
-            : this(error)
+            : this(error, errorType)
         {
             Value = fallbackValue;
             HasValue = true;
@@ -104,8 +104,6 @@ namespace Avalonia.Data
         /// </summary>
         public BindingErrorType ErrorType { get; }
 
-        bool IValidationStatus.IsValid => ErrorType == BindingErrorType.None;
-
         public static bool operator ==(BindingNotification a, BindingNotification b)
         {
             if (object.ReferenceEquals(a, b))
@@ -121,7 +119,7 @@ namespace Avalonia.Data
             return a.HasValue == b.HasValue &&
                    a.ErrorType == b.ErrorType &&
                    (!a.HasValue || object.Equals(a.Value, b.Value)) &&
-                   (a.ErrorType == BindingErrorType.None || object.Equals(a.Error, b.Error));
+                   (a.ErrorType == BindingErrorType.None || ExceptionEquals(a.Error, b.Error));
         }
 
         public static bool operator !=(BindingNotification a, BindingNotification b)
@@ -165,5 +163,11 @@ namespace Avalonia.Data
                 return new BindingNotification(e, BindingErrorType.Error, Value);
             }
         }
+
+        private static bool ExceptionEquals(Exception a, Exception b)
+        {
+            return a?.GetType() == b?.GetType() &&
+                   a.Message == b.Message;
+        }
     }
 }

+ 0 - 17
src/Avalonia.Base/Data/IValidationStatus.cs

@@ -1,17 +0,0 @@
-// 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.
-
-namespace Avalonia.Data
-{
-    /// <summary>
-    /// Contains information on if the current object passed validation.
-    /// Subclasses of this class contain additional information depending on the method of validation checking. 
-    /// </summary>
-    public interface IValidationStatus
-    {
-        /// <summary>
-        /// True when the data passes validation; otherwise, false.
-        /// </summary>
-        bool IsValid { get; }
-    }
-}

+ 0 - 44
src/Avalonia.Base/Data/ObjectValidationStatus.cs

@@ -1,44 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Avalonia.Data
-{
-    /// <summary>
-    /// An immutable struct that contains validation information for a <see cref="AvaloniaObject"/> that validates a single property.
-    /// </summary>
-    public struct ObjectValidationStatus : IValidationStatus
-    {
-        private Dictionary<Type, IValidationStatus> currentValidationStatus;
-
-        public bool IsValid => currentValidationStatus?.Values.All(status => status.IsValid) ?? true;
-
-        /// <summary>
-        /// Constructs the structure with the given validation information.
-        /// </summary>
-        /// <param name="validations">The validation information</param>
-        public ObjectValidationStatus(Dictionary<Type, IValidationStatus> validations)
-            :this()
-        {
-            currentValidationStatus = validations;
-        }
-
-        /// <summary>
-        /// Creates a new status with the updated information.
-        /// </summary>
-        /// <param name="status">The updated status information.</param>
-        /// <returns>The new validation status.</returns>
-        public ObjectValidationStatus UpdateValidationStatus(IValidationStatus status)
-        {
-            var newStatus = new Dictionary<Type, IValidationStatus>(currentValidationStatus ??
-                                                                new Dictionary<Type, IValidationStatus>());
-            newStatus[status.GetType()] = status;
-            return new ObjectValidationStatus(newStatus);
-        }
-
-        public IEnumerable<IValidationStatus> StatusInformation => currentValidationStatus.Values;
-    }
-}

+ 3 - 3
src/Avalonia.Controls/Control.cs

@@ -108,7 +108,7 @@ namespace Avalonia.Controls
             PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
             PseudoClass(IsFocusedProperty, ":focus");
             PseudoClass(IsPointerOverProperty, ":pointerover");
-            PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid");
+            ////PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid");
         }
 
         /// <summary>
@@ -401,10 +401,10 @@ namespace Avalonia.Controls
         protected IPseudoClasses PseudoClasses => Classes;
 
         /// <inheritdoc/>
-        protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+        protected override void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
         {
             base.DataValidationChanged(property, status);
-            ValidationStatus.UpdateValidationStatus(status);
+            ////ValidationStatus.UpdateValidationStatus(status);
         }
 
         /// <summary>

+ 1 - 1
src/Avalonia.Controls/TextBox.cs

@@ -235,7 +235,7 @@ namespace Avalonia.Controls
             HandleTextInput(e.Text);
         }
 
-        protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+        protected override void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
         {
             if (property == TextProperty)
             {

+ 3 - 2
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@@ -48,7 +48,7 @@
     <Compile Include="ControlLocator.cs" />
     <Compile Include="Data\Plugins\ExceptionValidationPlugin.cs" />
     <Compile Include="Data\Plugins\IndeiValidationPlugin.cs" />
-    <Compile Include="Data\Plugins\IValidationPlugin.cs" />
+    <Compile Include="Data\Plugins\IDataValidationPlugin.cs" />
     <Compile Include="Data\Plugins\AvaloniaPropertyAccessorPlugin.cs" />
     <Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
     <Compile Include="Data\Plugins\IPropertyAccessor.cs" />
@@ -59,8 +59,9 @@
     <Compile Include="Data\Parsers\IdentifierParser.cs" />
     <Compile Include="Data\Parsers\ExpressionParser.cs" />
     <Compile Include="Data\Parsers\Reader.cs" />
+    <Compile Include="Data\Plugins\PropertyAccessorBase.cs" />
     <Compile Include="Data\Plugins\PropertyError.cs" />
-    <Compile Include="Data\Plugins\ValidatingPropertyAccessorBase.cs" />
+    <Compile Include="Data\Plugins\DataValidatiorBase.cs" />
     <Compile Include="Data\PropertyAccessorNode.cs" />
     <Compile Include="Data\ExpressionNode.cs" />
     <Compile Include="Data\ExpressionObserver.cs" />

+ 0 - 13
src/Markup/Avalonia.Markup/Data/ExpressionNode.cs

@@ -105,19 +105,6 @@ namespace Avalonia.Markup.Data
             CurrentValue = reference;
         }
 
-        protected virtual void SendValidationStatus(IValidationStatus status)
-        {
-            //Even if elements only bound to sub-values, send validation changes along so they will be surfaced to the UI level.
-            //if (_subject != null)
-            //{
-            //    _subject.OnNext(status);
-            //}
-            //else
-            //{
-            //    Next?.SendValidationStatus(status);
-            //}
-        }
-
         protected virtual void Unsubscribe(object target)
         {
         }

+ 16 - 15
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@@ -31,10 +31,11 @@ namespace Avalonia.Markup.Data
         /// An ordered collection of validation checker plugins that can be used to customize
         /// the validation of view model and model data.
         /// </summary>
-        public static readonly IList<IValidationPlugin> ValidationCheckers =
-            new List<IValidationPlugin>
+        public static readonly IList<IDataValidationPlugin> DataValidators =
+            new List<IDataValidationPlugin>
             {
                 new IndeiValidationPlugin(),
+                ExceptionValidationPlugin.Instance,
             };
 
         private readonly WeakReference _root;
@@ -45,24 +46,24 @@ namespace Avalonia.Markup.Data
         private IDisposable _updateSubscription;
         private int _count;
         private readonly ExpressionNode _node;
-        private bool _enableValidation;
+        private bool _enableDataValidation;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// </summary>
         /// <param name="root">The root object.</param>
         /// <param name="expression">The expression.</param>
-        /// <param name="enableValidation">Whether property validation should be enabled.</param>
-        public ExpressionObserver(object root, string expression, bool enableValidation = false)
+        /// <param name="enableDataValidation">Whether data validation should be enabled.</param>
+        public ExpressionObserver(object root, string expression, bool enableDataValidation = false)
         {
             Contract.Requires<ArgumentNullException>(expression != null);
 
             _root = new WeakReference(root);
-            _enableValidation = enableValidation;
+            _enableDataValidation = enableDataValidation;
 
             if (!string.IsNullOrWhiteSpace(expression))
             {
-                _node = ExpressionNodeBuilder.Build(expression, enableValidation);
+                _node = ExpressionNodeBuilder.Build(expression, enableDataValidation);
             }
 
             Expression = expression;
@@ -73,21 +74,21 @@ namespace Avalonia.Markup.Data
         /// </summary>
         /// <param name="rootObservable">An observable which provides the root object.</param>
         /// <param name="expression">The expression.</param>
-        /// <param name="enableValidation">Whether property validation should be enabled.</param>
+        /// <param name="enableDataValidation">Whether data validation should be enabled.</param>
         public ExpressionObserver(
             IObservable<object> rootObservable,
             string expression,
-            bool enableValidation = false)
+            bool enableDataValidation = false)
         {
             Contract.Requires<ArgumentNullException>(rootObservable != null);
             Contract.Requires<ArgumentNullException>(expression != null);
 
             _rootObservable = rootObservable;
-            _enableValidation = enableValidation;
+            _enableDataValidation = enableDataValidation;
 
             if (!string.IsNullOrWhiteSpace(expression))
             {
-                _node = ExpressionNodeBuilder.Build(expression, enableValidation);
+                _node = ExpressionNodeBuilder.Build(expression, enableDataValidation);
             }
 
             Expression = expression;
@@ -99,12 +100,12 @@ namespace Avalonia.Markup.Data
         /// <param name="rootGetter">A function which gets the root object.</param>
         /// <param name="expression">The expression.</param>
         /// <param name="update">An observable which triggers a re-read of the getter.</param>
-        /// <param name="enableValidation">Whether property validation should be enabled.</param>
+        /// <param name="enableDataValidation">Whether data validation should be enabled.</param>
         public ExpressionObserver(
             Func<object> rootGetter,
             string expression,
             IObservable<Unit> update,
-            bool enableValidation = false)
+            bool enableDataValidation = false)
         {
             Contract.Requires<ArgumentNullException>(rootGetter != null);
             Contract.Requires<ArgumentNullException>(expression != null);
@@ -112,11 +113,11 @@ namespace Avalonia.Markup.Data
 
             _rootGetter = rootGetter;
             _update = update;
-            _enableValidation = enableValidation;
+            _enableDataValidation = enableDataValidation;
 
             if (!string.IsNullOrWhiteSpace(expression))
             {
-                _node = ExpressionNodeBuilder.Build(expression, enableValidation);
+                _node = ExpressionNodeBuilder.Build(expression, enableDataValidation);
             }
 
             Expression = expression;

+ 1 - 1
src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs

@@ -175,7 +175,7 @@ namespace Avalonia.Markup.Data
         {
             var converted = 
                 value as BindingNotification ??
-                value as IValidationStatus ??
+                ////value as IValidationStatus ??
                 Converter.Convert(
                     value,
                     _targetType,

+ 20 - 35
src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs

@@ -4,7 +4,6 @@
 using System;
 using System.Reactive.Linq;
 using Avalonia.Data;
-using Avalonia.Logging;
 
 namespace Avalonia.Markup.Data.Plugins
 {
@@ -13,36 +12,22 @@ namespace Avalonia.Markup.Data.Plugins
     /// </summary>
     public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
     {
-        /// <summary>
-        /// Checks whether this plugin can handle accessing the properties of the specified object.
-        /// </summary>
-        /// <param name="reference">A weak reference to the object.</param>
-        /// <returns>True if the plugin can handle the object; otherwise false.</returns>
-        public bool Match(WeakReference reference)
-        {
-            Contract.Requires<ArgumentNullException>(reference != null);
-
-            return reference.Target is AvaloniaObject;
-        }
+        /// <inheritdoc/>
+        public bool Match(WeakReference reference) => reference.Target is AvaloniaObject;
 
         /// <summary>
         /// Starts monitoring the value of a property on an object.
         /// </summary>
         /// <param name="reference">A weak reference to the object.</param>
         /// <param name="propertyName">The property name.</param>
-        /// <param name="changed">A function to call when the property changes.</param>
         /// <returns>
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.
         /// </returns>
-        public IPropertyAccessor Start(
-            WeakReference reference, 
-            string propertyName, 
-            Action<object> changed)
+        public IPropertyAccessor Start(WeakReference reference, string propertyName)
         {
             Contract.Requires<ArgumentNullException>(reference != null);
             Contract.Requires<ArgumentNullException>(propertyName != null);
-            Contract.Requires<ArgumentNullException>(changed != null);
 
             var instance = reference.Target;
             var o = (AvaloniaObject)instance;
@@ -50,7 +35,7 @@ namespace Avalonia.Markup.Data.Plugins
 
             if (p != null)
             {
-                return new Accessor(new WeakReference<AvaloniaObject>(o), p, changed);
+                return new Accessor(new WeakReference<AvaloniaObject>(o), p);
             }
             else if (instance != AvaloniaProperty.UnsetValue)
             {
@@ -64,23 +49,19 @@ namespace Avalonia.Markup.Data.Plugins
             }
         }
 
-        private class Accessor : IPropertyAccessor
+        private class Accessor : PropertyAccessorBase
         {
             private readonly WeakReference<AvaloniaObject> _reference;
             private readonly AvaloniaProperty _property;
             private IDisposable _subscription;
 
-            public Accessor(
-                WeakReference<AvaloniaObject> reference, 
-                AvaloniaProperty property, 
-                Action<object> changed)
+            public Accessor(WeakReference<AvaloniaObject> reference, AvaloniaProperty property)
             {
                 Contract.Requires<ArgumentNullException>(reference != null);
                 Contract.Requires<ArgumentNullException>(property != null);
 
                 _reference = reference;
                 _property = property;
-                _subscription = Instance.GetWeakObservable(property).Skip(1).Subscribe(changed);
             }
 
             public AvaloniaObject Instance
@@ -93,17 +74,10 @@ namespace Avalonia.Markup.Data.Plugins
                 }
             }
 
-            public Type PropertyType => _property.PropertyType;
-
-            public object Value => Instance.GetValue(_property);
+            public override Type PropertyType => _property.PropertyType;
+            public override object Value => Instance?.GetValue(_property);
 
-            public void Dispose()
-            {
-                _subscription?.Dispose();
-                _subscription = null;
-            }
-
-            public bool SetValue(object value, BindingPriority priority)
+            public override bool SetValue(object value, BindingPriority priority)
             {
                 if (!_property.IsReadOnly)
                 {
@@ -113,6 +87,17 @@ namespace Avalonia.Markup.Data.Plugins
 
                 return false;
             }
+
+            protected override void Dispose(bool disposing)
+            {
+                _subscription?.Dispose();
+                _subscription = null;
+            }
+
+            protected override void SubscribeCore(IObserver<object> observer)
+            {
+                _subscription = Instance.GetWeakObservable(_property).Subscribe(observer);
+            }
         }
     }
 }

+ 80 - 0
src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs

@@ -0,0 +1,80 @@
+// 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 System;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+    /// <summary>
+    /// Base class for data validators.
+    /// </summary>
+    /// <remarks>
+    /// Data validators are <see cref="IPropertyAccessor"/>s that are returned from an 
+    /// <see cref="IDataValidationPlugin"/>. They wrap an inner <see cref="IPropertyAccessor"/>
+    /// and convert any values received from the inner property accessor into
+    /// <see cref="BindingNotification"/>s.
+    /// </remarks>
+    public abstract class DataValidatiorBase : PropertyAccessorBase, IObserver<object>
+    {
+        private readonly IPropertyAccessor _inner;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DataValidatiorBase"/> class.
+        /// </summary>
+        /// <param name="inner">The inner property accessor.</param>
+        protected DataValidatiorBase(IPropertyAccessor inner)
+        {
+            _inner = inner;
+        }
+
+        /// <inheritdoc/>
+        public override Type PropertyType => _inner.PropertyType;
+
+        /// <inheritdoc/>
+        public override object Value => _inner.Value;
+
+        /// <inheritdoc/>
+        public override bool SetValue(object value, BindingPriority priority) => _inner.SetValue(value, priority);
+
+        /// <summary>
+        /// Should never be called: the inner <see cref="IPropertyAccessor"/> should never notify
+        /// completion.
+        /// </summary>
+        void IObserver<object>.OnCompleted() { }
+
+        /// <summary>
+        /// Should never be called: the inner <see cref="IPropertyAccessor"/> should never notify
+        /// an error.
+        /// </summary>
+        void IObserver<object>.OnError(Exception error) { }
+
+        /// <summary>
+        /// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        void IObserver<object>.OnNext(object value) => InnerValueChanged(value);
+
+        /// <inheritdoc/>
+        protected override void Dispose(bool disposing) => _inner.Dispose();
+
+        /// <summary>
+        /// Begins listening to the inner <see cref="IPropertyAccessor"/>.
+        /// </summary>
+        protected override void SubscribeCore(IObserver<object> observer) => _inner.Subscribe(this);
+
+        /// <summary>
+        /// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <remarks>
+        /// Notifies the observer that the value has changed. The value will be wrapped in a
+        /// <see cref="BindingNotification"/> if it is not already a binding notification.
+        /// </remarks>
+        protected virtual void InnerValueChanged(object value)
+        {
+            var notification = value as BindingNotification ?? new BindingNotification(value);
+            Observer.OnNext(notification);
+        }
+    }
+}

+ 13 - 30
src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs

@@ -10,23 +10,26 @@ namespace Avalonia.Markup.Data.Plugins
     /// <summary>
     /// Validates properties that report errors by throwing exceptions.
     /// </summary>
-    public class ExceptionValidationPlugin : IValidationPlugin
+    public class ExceptionValidationPlugin : IDataValidationPlugin
     {
+        /// <summary>
+        /// Gets the default instance of the <see cref="ExceptionValidationPlugin"/>/
+        /// </summary>
         public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin();
 
         /// <inheritdoc/>
         public bool Match(WeakReference reference) => true;
 
         /// <inheritdoc/>
-        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
+        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
         {
-            return new ExceptionValidationChecker(reference, name, accessor, callback);
+            return new Validator(reference, name, inner);
         }
 
-        private class ExceptionValidationChecker : ValidatingPropertyAccessorBase
+        private class Validator : DataValidatiorBase
         {
-            public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
-                : base(reference, name, accessor, callback)
+            public Validator(WeakReference reference, string name, IPropertyAccessor inner)
+                : base(inner)
             {
             }
 
@@ -34,39 +37,19 @@ namespace Avalonia.Markup.Data.Plugins
             {
                 try
                 {
-                    var success = base.SetValue(value, priority);
-                    SendValidationCallback(new ExceptionValidationStatus(null));
-                    return success;
+                    return base.SetValue(value, priority);
                 }
                 catch (TargetInvocationException ex)
                 {
-                    SendValidationCallback(new ExceptionValidationStatus(ex.InnerException));
+                    Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
                 }
                 catch (Exception ex)
                 {
-                    SendValidationCallback(new ExceptionValidationStatus(ex));
+                    Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError));
                 }
-                return false;
-            }
-        }
 
-        /// <summary>
-        /// Describes the current validation status after setting a property value.
-        /// </summary>
-        public class ExceptionValidationStatus : IValidationStatus
-        {
-            internal ExceptionValidationStatus(Exception exception)
-            {
-                Exception = exception;
+                return false;
             }
-
-            /// <summary>
-            /// The thrown exception. If there was no thrown exception, null.
-            /// </summary>
-            public Exception Exception { get; }
-            
-            /// <inheritdoc/>
-            public bool IsValid => Exception == null;
         }
     }
 }

+ 36 - 0
src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs

@@ -0,0 +1,36 @@
+// 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 System;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+    /// <summary>
+    /// Defines how data validation is observed by an <see cref="ExpressionObserver"/>.
+    /// </summary>
+    public interface IDataValidationPlugin
+    {
+        /// <summary>
+        /// Checks whether this plugin can handle data validation on the specified object.
+        /// </summary>
+        /// <param name="reference">A weak reference to the object.</param>
+        /// <returns>True if the plugin can handle the object; otherwise false.</returns>
+        bool Match(WeakReference reference);
+
+        /// <summary>
+        /// Starts monitoring the data validation state of a property on an object.
+        /// </summary>
+        /// <param name="reference">A weak reference to the object.</param>
+        /// <param name="propertyName">The property name.</param>
+        /// <param name="inner">The inner property accessor used to aceess the property.</param>
+        /// <returns>
+        /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
+        /// property will be made.
+        /// </returns>
+        IPropertyAccessor Start(
+            WeakReference reference,
+            string propertyName,
+            IPropertyAccessor inner);
+    }
+}

+ 4 - 1
src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs

@@ -10,11 +10,14 @@ namespace Avalonia.Markup.Data.Plugins
     /// Defines an accessor to a property on an object returned by a 
     /// <see cref="IPropertyAccessorPlugin"/>
     /// </summary>
-    public interface IPropertyAccessor : IDisposable
+    public interface IPropertyAccessor : IObservable<object>, IDisposable
     {
         /// <summary>
         /// Gets the type of the property.
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The accessor has not been subscribed to yet.
+        /// </exception>
         Type PropertyType { get; }
 
         /// <summary>

+ 1 - 4
src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.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 System.Collections;
 
 namespace Avalonia.Markup.Data.Plugins
 {
@@ -24,14 +23,12 @@ namespace Avalonia.Markup.Data.Plugins
         /// </summary>
         /// <param name="reference">A weak reference to the object.</param>
         /// <param name="propertyName">The property name.</param>
-        /// <param name="changed">A function to call when the property changes.</param>
         /// <returns>
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.
         /// </returns>
         IPropertyAccessor Start(
             WeakReference reference, 
-            string propertyName, 
-            Action<object> changed);
+            string propertyName);
     }
 }

+ 0 - 35
src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs

@@ -1,35 +0,0 @@
-// 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 System;
-using Avalonia.Data;
-
-namespace Avalonia.Markup.Data.Plugins
-{
-    /// <summary>
-    /// Defines how view model data validation is observed by an <see cref="ExpressionObserver"/>.
-    /// </summary>
-    public interface IValidationPlugin
-    {
-
-        /// <summary>
-        /// Checks whether the data uses a validation scheme supported by this plugin.
-        /// </summary>
-        /// <param name="reference">A weak reference to the data.</param>
-        /// <returns><c>true</c> if this plugin can observe the validation; otherwise, <c>false</c>.</returns>
-        bool Match(WeakReference reference);
-
-        /// <summary>
-        /// Starts monitoring the validation state of an object for the given property.
-        /// </summary>
-        /// <param name="reference">A weak reference to the object.</param>
-        /// <param name="name">The property name.</param>
-        /// <param name="accessor">An underlying <see cref="IPropertyAccessor"/> to access the property.</param>
-        /// <param name="callback">A function to call when the validation state changes.</param>
-        /// <returns>
-        /// A <see cref="ValidatingPropertyAccessorBase"/> subclass through which future interactions with the 
-        /// property will be made.
-        /// </returns>
-        IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback);
-    }
-}

+ 67 - 40
src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs

@@ -2,7 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Collections;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
 using Avalonia.Data;
@@ -13,79 +13,106 @@ namespace Avalonia.Markup.Data.Plugins
     /// <summary>
     /// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
     /// </summary>
-    public class IndeiValidationPlugin : IValidationPlugin
+    public class IndeiValidationPlugin : IDataValidationPlugin
     {
         /// <inheritdoc/>
-        public bool Match(WeakReference reference)
-        {
-            return reference.Target is INotifyDataErrorInfo;
-        }
+        public bool Match(WeakReference reference) => reference.Target is INotifyDataErrorInfo;
 
         /// <inheritdoc/>
-        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
+        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor)
         {
-            return new IndeiValidationChecker(reference, name, accessor, callback);
+            return new Validator(reference, name, accessor);
         }
 
-        private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
+        private class Validator : DataValidatiorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
         {
-            public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
-                : base(reference, name, accessor, callback)
+            WeakReference _reference;
+            string _name;
+
+            public Validator(WeakReference reference, string name, IPropertyAccessor inner)
+                : base(inner)
+            {
+                _reference = reference;
+                _name = name;
+            }
+
+            void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object sender, DataErrorsChangedEventArgs e)
             {
-                var target = reference.Target as INotifyDataErrorInfo;
+                if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
+                {
+                    Observer.OnNext(CreateBindingNotification(Value));
+                }
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+                base.Dispose(disposing);
+
+                var target = _reference.Target as INotifyDataErrorInfo;
+
                 if (target != null)
                 {
-                    if (target.HasErrors)
-                    {
-                        SendValidationCallback(new IndeiValidationStatus(target.GetErrors(name)));
-                    }
-                    WeakSubscriptionManager.Subscribe(
+                    WeakSubscriptionManager.Unsubscribe(
                         target,
                         nameof(target.ErrorsChanged),
                         this);
                 }
             }
 
-            public override void Dispose()
+            protected override void SubscribeCore(IObserver<object> observer)
             {
-                base.Dispose();
                 var target = _reference.Target as INotifyDataErrorInfo;
+
                 if (target != null)
                 {
-                    WeakSubscriptionManager.Unsubscribe(
+                    WeakSubscriptionManager.Subscribe(
                         target,
                         nameof(target.ErrorsChanged),
                         this);
                 }
+
+                base.SubscribeCore(observer);
             }
 
-            public void OnEvent(object sender, DataErrorsChangedEventArgs e)
+            protected override void InnerValueChanged(object value)
             {
-                if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
+                base.InnerValueChanged(CreateBindingNotification(value));
+            }
+
+            private BindingNotification CreateBindingNotification(object value)
+            {
+                var target = (INotifyDataErrorInfo)_reference.Target;
+
+                if (target != null)
                 {
-                    var indei = _reference.Target as INotifyDataErrorInfo;
-                    SendValidationCallback(new IndeiValidationStatus(indei.GetErrors(e.PropertyName)));
+                    var errors = target.GetErrors(_name)?
+                        .Cast<String>()
+                        .Where(x => x != null).ToList();
+
+                    if (errors?.Count > 0)
+                    {
+                        return new BindingNotification(
+                            GenerateException(errors),
+                            BindingErrorType.DataValidationError,
+                            value);
+                    }
                 }
+
+                return new BindingNotification(value);
             }
-        }
 
-        /// <summary>
-        /// Describes the current validation status of a property as reported by an object that implements <see cref="INotifyDataErrorInfo"/>.
-        /// </summary>
-        public class IndeiValidationStatus : IValidationStatus
-        {
-            internal IndeiValidationStatus(IEnumerable errors)
+            private Exception GenerateException(IList<string> errors)
             {
-                Errors = errors;
+                if (errors.Count == 1)
+                {
+                    return new Exception(errors[0]);
+                }
+                else
+                {
+                    return new AggregateException(
+                        errors.Select(x => new Exception(x)));
+                }
             }
-
-            /// <inheritdoc/>
-            public bool IsValid => !Errors?.OfType<object>().Any() ?? true;
-
-            /// <summary>
-            /// The errors on the given property and on the object as a whole.
-            /// </summary>
-            public IEnumerable Errors { get; }
         }
     }
 }

+ 60 - 54
src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs

@@ -9,7 +9,6 @@ using System.Reflection;
 using Avalonia.Data;
 using Avalonia.Logging;
 using Avalonia.Utilities;
-using System.Collections;
 
 namespace Avalonia.Markup.Data.Plugins
 {
@@ -19,43 +18,29 @@ namespace Avalonia.Markup.Data.Plugins
     /// </summary>
     public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
     {
-        /// <summary>
-        /// Checks whether this plugin can handle accessing the properties of the specified object.
-        /// </summary>
-        /// <param name="reference">The object.</param>
-        /// <returns>True if the plugin can handle the object; otherwise false.</returns>
-        public bool Match(WeakReference reference)
-        {
-            Contract.Requires<ArgumentNullException>(reference != null);
-
-            return true;
-        }
+        /// <inheritdoc/>
+        public bool Match(WeakReference reference) => true;
 
         /// <summary>
         /// Starts monitoring the value of a property on an object.
         /// </summary>
         /// <param name="reference">The object.</param>
         /// <param name="propertyName">The property name.</param>
-        /// <param name="changed">A function to call when the property changes.</param>
         /// <returns>
         /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the 
         /// property will be made.
         /// </returns>
-        public IPropertyAccessor Start(
-            WeakReference reference, 
-            string propertyName, 
-            Action<object> changed)
+        public IPropertyAccessor Start(WeakReference reference, string propertyName)
         {
             Contract.Requires<ArgumentNullException>(reference != null);
             Contract.Requires<ArgumentNullException>(propertyName != null);
-            Contract.Requires<ArgumentNullException>(changed != null);
 
             var instance = reference.Target;
             var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName);
 
             if (p != null)
             {
-                return new Accessor(reference, p, changed);
+                return new Accessor(reference, p);
             }
             else
             {
@@ -65,78 +50,99 @@ namespace Avalonia.Markup.Data.Plugins
             }
         }
 
-        private class Accessor : IPropertyAccessor, IWeakSubscriber<PropertyChangedEventArgs>
+        private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
         {
             private readonly WeakReference _reference;
             private readonly PropertyInfo _property;
-            private readonly Action<object> _changed;
 
-            public Accessor(
-                WeakReference reference, 
-                PropertyInfo property, 
-                Action<object> changed)
+            public Accessor(WeakReference reference,  PropertyInfo property)
             {
                 Contract.Requires<ArgumentNullException>(reference != null);
                 Contract.Requires<ArgumentNullException>(property != null);
 
                 _reference = reference;
                 _property = property;
-                _changed = changed;
+            }
 
-                var inpc = reference.Target as INotifyPropertyChanged;
+            public override Type PropertyType => _property.PropertyType;
 
-                if (inpc != null)
+            public override object Value
+            {
+                get
                 {
-                    WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
-                        inpc,
-                        nameof(inpc.PropertyChanged),
-                        this);
+                    var o = _reference.Target;
+                    return (o != null) ? _property.GetValue(o) : null;
                 }
-                else
+            }
+
+            public override bool SetValue(object value, BindingPriority priority)
+            {
+                if (_property.CanWrite)
                 {
-                    Logger.Warning(
-                        LogArea.Binding,
-                        this,
-                        "Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged",
-                        property.Name,
-                        reference.Target,
-                        reference.Target.GetType());
+                    _property.SetValue(_reference.Target, value);
+                    return true;
                 }
-            }
 
-            public Type PropertyType => _property.PropertyType;
+                return false;
+            }
 
-            public object Value => _property.GetValue(_reference.Target);
+            void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
+            {
+                if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
+                {
+                    SendCurrentValue();
+                }
+            }
 
-            public void Dispose()
+            protected override void Dispose(bool disposing)
             {
                 var inpc = _reference.Target as INotifyPropertyChanged;
 
                 if (inpc != null)
                 {
-                    WeakSubscriptionManager.Unsubscribe<PropertyChangedEventArgs>(
+                    WeakSubscriptionManager.Unsubscribe(
                         inpc,
                         nameof(inpc.PropertyChanged),
                         this);
                 }
             }
 
-            public bool SetValue(object value, BindingPriority priority)
+            protected override void SubscribeCore(IObserver<object> observer)
             {
-                if (_property.CanWrite)
+                SendCurrentValue();
+                SubscribeToChanges();
+            }
+
+            private void SendCurrentValue()
+            {
+                try
                 {
-                    _property.SetValue(_reference.Target, value);
-                    return true;
+                    var value = Value;
+                    Observer.OnNext(value);
                 }
-
-                return false;
+                catch { }
             }
 
-            void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
+            private void SubscribeToChanges()
             {
-                if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
+                var inpc = _reference.Target as INotifyPropertyChanged;
+
+                if (inpc != null)
+                {
+                    WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
+                        inpc,
+                        nameof(inpc.PropertyChanged),
+                        this);
+                }
+                else
                 {
-                    _changed(Value);
+                    Logger.Information(
+                        LogArea.Binding,
+                        this,
+                        "Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged",
+                        _property.Name,
+                        _reference.Target,
+                        _reference.Target.GetType());
                 }
             }
         }

+ 68 - 0
src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs

@@ -0,0 +1,68 @@
+// 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 System;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+    /// <summary>
+    /// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
+    /// </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
+    {
+        /// <inheritdoc/>
+        public abstract Type PropertyType { get; }
+
+        /// <inheritdoc/>
+        public abstract object Value { get; }
+
+        /// <summary>
+        /// Stops the subscription.
+        /// </summary>
+        public void Dispose() => Dispose(true);
+
+        /// <inheritdoc/>
+        public abstract bool SetValue(object value, BindingPriority priority);
+
+        /// <summary>
+        /// The currently subscribed observer.
+        /// </summary>
+        protected IObserver<object> Observer { get; private set; }
+
+        /// <inheritdoc/>
+        public IDisposable Subscribe(IObserver<object> observer)
+        {
+            Contract.Requires<ArgumentNullException>(observer != null);
+
+            if (Observer != null)
+            {
+                throw new InvalidOperationException(
+                    "A property accessor can be subscribed to only once.");
+            }
+
+            Observer = observer;
+            SubscribeCore(observer);
+            return this;
+        }
+
+        /// <summary>
+        /// Stops listening to the property.
+        /// </summary>
+        /// <param name="disposing">
+        /// 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>
+        /// When overridden in a derived class, begins listening to the property.
+        /// </summary>
+        protected abstract void SubscribeCore(IObserver<object> observer);
+    }
+}

+ 7 - 0
src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Reactive.Disposables;
 using Avalonia.Data;
 
 namespace Avalonia.Markup.Data.Plugins
@@ -35,5 +36,11 @@ namespace Avalonia.Markup.Data.Plugins
         {
             return false;
         }
+
+        public IDisposable Subscribe(IObserver<object> observer)
+        {
+            observer.OnNext(_error);
+            return Disposable.Empty;
+        }
     }
 }

+ 0 - 46
src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs

@@ -1,46 +0,0 @@
-using System;
-using Avalonia.Data;
-
-namespace Avalonia.Markup.Data.Plugins
-{
-
-    /// <summary>
-    /// A base class for validating <see cref="IPropertyAccessor"/>s that wraps an <see cref="IPropertyAccessor"/> and forwards method calls to it.
-    /// </summary>
-    public abstract class ValidatingPropertyAccessorBase : IPropertyAccessor
-    {
-        protected readonly WeakReference _reference;
-        protected readonly string _name;
-        private readonly IPropertyAccessor _accessor;
-        private readonly Action<IValidationStatus> _callback;
-
-        protected ValidatingPropertyAccessorBase(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
-        {
-            _reference = reference;
-            _name = name;
-            _accessor = accessor;
-            _callback = callback;
-        }
-
-        /// <inheritdoc/>
-        public Type PropertyType => _accessor.PropertyType;
-
-        /// <inheritdoc/>
-        public object Value => _accessor.Value;
-
-        /// <inheritdoc/>
-        public virtual void Dispose() => _accessor.Dispose();
-
-        /// <inheritdoc/>
-        public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority);
-
-        /// <summary>
-        /// Sends the validation status to the callback specified in construction.
-        /// </summary>
-        /// <param name="status">The validation status.</param>
-        protected void SendValidationCallback(IValidationStatus status)
-        {
-            _callback?.Invoke(status);
-        }
-    }
-}

+ 31 - 24
src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs

@@ -9,11 +9,12 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Windows.Input;
 using Avalonia.Data;
+using Avalonia.Logging;
 using Avalonia.Markup.Data.Plugins;
 
 namespace Avalonia.Markup.Data
 {
-    internal class PropertyAccessorNode : ExpressionNode
+    internal class PropertyAccessorNode : ExpressionNode, IObserver<object>
     {
         private IPropertyAccessor _accessor;
         private IDisposable _subscription;
@@ -39,49 +40,55 @@ namespace Avalonia.Markup.Data
             {
                 if (_accessor != null)
                 {
-                    return _accessor.SetValue(value, priority);
+                    try { return _accessor.SetValue(value, priority); } catch { }
                 }
 
                 return false;
             }
         }
 
+        void IObserver<object>.OnCompleted()
+        {
+            // Should not be called by IPropertyAccessor.
+        }
+
+        void IObserver<object>.OnError(Exception error)
+        {
+            // Should not be called by IPropertyAccessor.
+        }
+
+        void IObserver<object>.OnNext(object value)
+        {
+            SetCurrentValue(value);
+        }
+
         protected override void SubscribeAndUpdate(WeakReference reference)
         {
             var instance = reference.Target;
 
             if (instance != null && instance != AvaloniaProperty.UnsetValue)
             {
-                var accessorPlugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
+                var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
+                var accessor = plugin?.Start(reference, PropertyName);
 
-                if (accessorPlugin != null)
+                if (_enableValidation)
                 {
-                    _accessor = ExceptionValidationPlugin.Instance.Start(
-                        reference,
-                        PropertyName,
-                        accessorPlugin.Start(reference, PropertyName, SetCurrentValue),
-                        SendValidationStatus);
-
-                    if (_enableValidation)
+                    foreach (var validator in ExpressionObserver.DataValidators)
                     {
-                        foreach (var validationPlugin in ExpressionObserver.ValidationCheckers)
+                        if (validator.Match(reference))
                         {
-                            if (validationPlugin.Match(reference))
-                            {
-                                _accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
-                            }
+                            accessor = validator.Start(reference, PropertyName, accessor);
                         }
                     }
-
-                    if (_accessor != null)
-                    {
-                        SetCurrentValue(_accessor.Value);
-                        return;
-                    }
                 }
-            }
 
-            CurrentValue = UnsetReference;
+                _accessor = accessor;
+                _accessor.Subscribe(this);
+            }
+            else
+            {
+                CurrentValue = UnsetReference;
+            }
         }
 
         protected override void Unsubscribe(object target)

+ 2 - 2
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests
                 .Select(x => x.Name)
                 .ToArray();
 
-            Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
+            Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names);
         }
 
         [Fact]
@@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests
                 .Select(x => x.Name)
                 .ToArray();
 
-            Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
+            Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names);
         }
 
         [Fact]

+ 18 - 15
tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs

@@ -24,11 +24,12 @@ namespace Avalonia.Controls.UnitTests
                 binding.EnableValidation = true;
                 target.Bind(TextBox.TextProperty, binding);
 
-                Assert.True(target.ValidationStatus.IsValid);
-                target.Text = "20";
-                Assert.False(target.ValidationStatus.IsValid);
-                target.Text = "1";
-                Assert.True(target.ValidationStatus.IsValid);
+                Assert.True(false);
+                //Assert.True(target.ValidationStatus.IsValid);
+                //target.Text = "20";
+                //Assert.False(target.ValidationStatus.IsValid);
+                //target.Text = "1";
+                //Assert.True(target.ValidationStatus.IsValid);
             }
         }
 
@@ -43,11 +44,12 @@ namespace Avalonia.Controls.UnitTests
                 binding.EnableValidation = true;
                 target.Bind(TextBox.TextProperty, binding);
 
-                Assert.True(target.ValidationStatus.IsValid);
-                target.Text = "foo";
-                Assert.False(target.ValidationStatus.IsValid);
-                target.Text = "1";
-                Assert.True(target.ValidationStatus.IsValid);
+                Assert.True(false);
+                //Assert.True(target.ValidationStatus.IsValid);
+                //target.Text = "foo";
+                //Assert.False(target.ValidationStatus.IsValid);
+                //target.Text = "1";
+                //Assert.True(target.ValidationStatus.IsValid);
             }
         }
 
@@ -62,11 +64,12 @@ namespace Avalonia.Controls.UnitTests
                 binding.EnableValidation = true;
                 target.Bind(TextBox.TextProperty, binding);
 
-                Assert.True(target.ValidationStatus.IsValid);
-                target.Text = "20";
-                Assert.False(target.ValidationStatus.IsValid);
-                target.Text = "1";
-                Assert.True(target.ValidationStatus.IsValid);
+                Assert.True(false);
+                //Assert.True(target.ValidationStatus.IsValid);
+                //target.Text = "20";
+                //Assert.False(target.ValidationStatus.IsValid);
+                //target.Text = "1";
+                //Assert.True(target.ValidationStatus.IsValid);
             }
         }
 

+ 3 - 2
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@@ -85,7 +85,8 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="ControlLocatorTests.cs" />
-    <Compile Include="Data\ExceptionValidatorTests.cs" />
+    <Compile Include="Data\Plugins\IndeiValidationPluginTests.cs" />
+    <Compile Include="Data\Plugins\ExceptionValidationPluginTests.cs" />
     <Compile Include="Data\ExpressionNodeBuilderTests.cs" />
     <Compile Include="Data\ExpressionNodeBuilderTests_Errors.cs" />
     <Compile Include="Data\ExpressionObserverTests_Lifetime.cs" />
@@ -97,7 +98,7 @@
     <Compile Include="Data\ExpressionObserverTests_Property.cs" />
     <Compile Include="Data\ExpressionObserverTests_SetValue.cs" />
     <Compile Include="Data\ExpressionObserverTests_Task.cs" />
-    <Compile Include="Data\ExpressionObserverTests_Validation.cs" />
+    <Compile Include="Data\ExpressionObserverTests_DataValidation.cs" />
     <Compile Include="Data\ExpressionSubjectTests.cs" />
     <Compile Include="Data\IndeiValidatorTests.cs" />
     <Compile Include="DefaultValueConverterTests.cs" />

+ 0 - 93
tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs

@@ -1,93 +0,0 @@
-// 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 System;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using Avalonia.Data;
-using Avalonia.Markup.Data.Plugins;
-using Xunit;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
-    public class ExceptionValidatorTests
-    {
-        public class Data : INotifyPropertyChanged
-        {
-            private int nonValidated;
-
-            public int NonValidated
-            {
-                get { return nonValidated; }
-                set { nonValidated = value; NotifyPropertyChanged(); }
-            }
-
-            private int mustBePositive;
-
-            public int MustBePositive
-            {
-                get { return mustBePositive; }
-                set
-                {
-                    if (value <= 0)
-                    {
-                        throw new ArgumentOutOfRangeException(nameof(value));
-                    }
-                    mustBePositive = value;
-                }
-            }
-
-            public event PropertyChangedEventHandler PropertyChanged;
-
-            private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
-            {
-                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
-            }
-        }
-
-        [Fact]
-        public void Setting_Non_Validating_Triggers_Validation()
-        {
-            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new ExceptionValidationPlugin();
-            var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
-            IValidationStatus status = null;
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
-
-            validator.SetValue(5, BindingPriority.LocalValue);
-
-            Assert.NotNull(status);
-        }
-
-        [Fact]
-        public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
-        {
-            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new ExceptionValidationPlugin();
-            var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            IValidationStatus status = null;
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
-            validator.SetValue(5, BindingPriority.LocalValue);
-
-            Assert.True(status.IsValid);
-        }
-        
-        [Fact]
-        public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus()
-        {
-            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new ExceptionValidationPlugin();
-            var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            IValidationStatus status = null;
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
-            validator.SetValue(-5, BindingPriority.LocalValue);
-
-            Assert.False(status.IsValid);
-        }
-    }
-}

+ 65 - 6
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs → tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs

@@ -14,21 +14,40 @@ using Xunit;
 
 namespace Avalonia.Markup.UnitTests.Data
 {
-    public class ExpressionObserverTests_Validation
+    public class ExpressionObserverTests_DataValidation
     {
         [Fact]
-        public void Exception_Validation_Sends_ValidationUpdate()
+        public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
         {
             var data = new ExceptionTest { MustBePositive = 5 };
             var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
             var validationMessageFound = false;
-            observer.Where(o => o is IValidationStatus).Subscribe(_ => validationMessageFound = true);
+
+            observer.OfType<BindingNotification>()
+                .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+                .Subscribe(_ => validationMessageFound = true);
             observer.SetValue(-5);
+
+            Assert.False(validationMessageFound);
+        }
+
+        [Fact]
+        public void Exception_Validation_Sends_DataValidationError()
+        {
+            var data = new ExceptionTest { MustBePositive = 5 };
+            var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+            var validationMessageFound = false;
+
+            observer.OfType<BindingNotification>()
+                .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+                .Subscribe(_ => validationMessageFound = true);
+            observer.SetValue(-5);
+
             Assert.True(validationMessageFound);
         }
 
         [Fact]
-        public void Disabled_Indei_Validation_Does_Not_Subscribe()
+        public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()
         {
             var data = new IndeiTest { MustBePositive = 5 };
             var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
@@ -50,6 +69,42 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal(0, data.SubscriptionCount);
         }
 
+        [Fact]
+        public void Validation_Plugins_Send_Correct_Notifications()
+        {
+            var data = new IndeiTest();
+            var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+            var result = new List<object>();
+
+            observer.Subscribe(x => result.Add(x));
+            observer.SetValue(5);
+            observer.SetValue(-5);
+            observer.SetValue("foo");
+            observer.SetValue(5);
+
+            Assert.Equal(new[]
+            {
+                new BindingNotification(0),
+
+                // Value is notified twice as ErrorsChanged is always called by IndeiTest.
+                new BindingNotification(5),
+                new BindingNotification(5),
+
+                // Value is first signalled without an error as validation hasn't been updated.
+                new BindingNotification(-5),
+                new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5),
+
+                // Exception is thrown by trying to set value to "foo".
+                new BindingNotification(
+                    new ArgumentException("Object of type 'System.String' cannot be converted to type 'System.Int32'."),
+                    BindingErrorType.DataValidationError),
+
+                // Value is set then validation is updated.
+                new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5),
+                new BindingNotification(5),
+            }, result);
+        }
+
         public class ExceptionTest : INotifyPropertyChanged
         {
             private int _mustBePositive;
@@ -75,7 +130,7 @@ namespace Avalonia.Markup.UnitTests.Data
             }
         }
 
-        private class IndeiTest : INotifyDataErrorInfo
+        private class IndeiTest : INotifyDataErrorInfo, INotifyPropertyChanged
         {
             private int _mustBePositive;
             private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
@@ -86,9 +141,11 @@ namespace Avalonia.Markup.UnitTests.Data
                 get { return _mustBePositive; }
                 set
                 {
+                    _mustBePositive = value;
+                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MustBePositive)));
+
                     if (value >= 0)
                     {
-                        _mustBePositive = value;
                         _errors.Remove(nameof(MustBePositive));
                         _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
                     }
@@ -118,6 +175,8 @@ namespace Avalonia.Markup.UnitTests.Data
                 }
             }
 
+            public event PropertyChangedEventHandler PropertyChanged;
+
             public IEnumerable GetErrors(string propertyName)
             {
                 IList<string> result;

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

@@ -75,7 +75,7 @@ namespace Avalonia.Markup.UnitTests.Data
         }
 
         [Fact]
-        public async void Should_Return_BindingError_For_Broken_Chain()
+        public async void Should_Return_BindingNotification_Error_For_Broken_Chain()
         {
             var data = new { Foo = new { Bar = 1 } };
             var target = new ExpressionObserver(data, "Foo.Bar.Baz");

+ 64 - 45
tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.Runtime.CompilerServices;
 using Avalonia.Data;
@@ -13,6 +14,69 @@ namespace Avalonia.Markup.UnitTests.Data
 {
     public class IndeiValidatorTests
     {
+        [Fact]
+        public void Setting_Non_Validating_Does_Not_Trigger_Validation()
+        {
+            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
+            var data = new Data();
+            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated));
+            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor);
+            var results = new List<object>();
+
+            validator.Subscribe(x => results.Add(x));
+            validator.SetValue(5, BindingPriority.LocalValue);
+
+            Assert.Equal(
+                new[]
+                {
+                   new BindingNotification(0),
+                   new BindingNotification(5),
+                }, results);
+        }
+
+        [Fact]
+        public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_BindingNotification()
+        {
+            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
+            var data = new Data { MustBePositive = 1 };
+            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
+            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
+            var results = new List<object>();
+
+            validator.Subscribe(x => results.Add(x));
+            validator.SetValue(5, BindingPriority.LocalValue);
+
+            Assert.Equal(
+                new[]
+                {
+                   new BindingNotification(1),
+                   new BindingNotification(5),
+                }, results);
+        }
+
+        [Fact]
+        public void Setting_Validating_Property_To_Invalid_Value_Returns_DataValidationError()
+        {
+            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
+            var data = new Data { MustBePositive = 1 };
+            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
+            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
+            var results = new List<object>();
+
+            validator.Subscribe(x => results.Add(x));
+            validator.SetValue(-5, BindingPriority.LocalValue);
+
+            Assert.Equal(
+                new[]
+                {
+                   new BindingNotification(1),
+                   new BindingNotification(new Exception("MustBePositive must be positive"), BindingErrorType.DataValidationError, -5),
+                }, results);
+        }
+
         public class Data : INotifyPropertyChanged, INotifyDataErrorInfo
         {
             private int nonValidated;
@@ -64,50 +128,5 @@ namespace Avalonia.Markup.UnitTests.Data
                 }
             }
         }
-
-        [Fact]
-        public void Setting_Non_Validating_Does_Not_Trigger_Validation()
-        {
-            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new IndeiValidationPlugin();
-            var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
-            IValidationStatus status = null;
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
-
-            validator.SetValue(5, BindingPriority.LocalValue);
-
-            Assert.Null(status);
-        }
-
-        [Fact]
-        public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
-        {
-            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new IndeiValidationPlugin();
-            var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            IValidationStatus status = null;
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
-            validator.SetValue(5, BindingPriority.LocalValue);
-
-            Assert.True(status.IsValid);
-        }
-        
-        [Fact]
-        public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus()
-        {
-            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new IndeiValidationPlugin();
-            var data = new Data();
-            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            IValidationStatus status = null;
-            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
-            validator.SetValue(-5, BindingPriority.LocalValue);
-
-            Assert.False(status.IsValid);
-        }
     }
 }

+ 71 - 0
tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs

@@ -0,0 +1,71 @@
+// 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 System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+    public class ExceptionValidationPluginTests
+    {
+        [Fact]
+        public void Produces_BindingNotifications()
+        {
+            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+            var validatorPlugin = new ExceptionValidationPlugin();
+            var data = new Data();
+            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
+            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
+            var result = new List<object>();
+
+            validator.Subscribe(x => result.Add(x));
+            validator.SetValue(5, BindingPriority.LocalValue);
+            validator.SetValue(-2, BindingPriority.LocalValue);
+            validator.SetValue(6, BindingPriority.LocalValue);
+
+            Assert.Equal(new[]
+            {
+                new BindingNotification(0),
+                new BindingNotification(5),
+                new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError),
+                new BindingNotification(6),
+            }, result);
+        }
+
+        public class Data : INotifyPropertyChanged
+        {
+            private int _mustBePositive;
+
+            public int MustBePositive
+            {
+                get { return _mustBePositive; }
+                set
+                {
+                    if (value <= 0)
+                    {
+                        throw new ArgumentOutOfRangeException(nameof(value));
+                    }
+
+                    if (value != _mustBePositive)
+                    {
+                        _mustBePositive = value;
+                        NotifyPropertyChanged();
+                    }
+                }
+            }
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
+            {
+                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+            }
+        }
+    }
+}

+ 138 - 0
tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs

@@ -0,0 +1,138 @@
+// 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 System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+    public class IndeiValidationPluginTests
+    {
+        [Fact]
+        public void Produces_BindingNotifications()
+        {
+            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
+            var data = new Data { Maximum = 5 };
+            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
+            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+            var result = new List<object>();
+
+            validator.Subscribe(x => result.Add(x));
+            validator.SetValue(5, BindingPriority.LocalValue);
+            validator.SetValue(6, BindingPriority.LocalValue);
+            data.Maximum = 10;
+            data.Maximum = 5;
+
+            Assert.Equal(new[]
+            {
+                new BindingNotification(0),
+                new BindingNotification(5),
+
+                // Value is first signalled without an error as validation hasn't been updated.
+                new BindingNotification(6),
+                
+                // Then the ErrorsChanged event is fired.
+                new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
+
+                // Maximum is changed to 10 so value is now valid.
+                new BindingNotification(6),
+
+                // And Maximum is changed back to 5.
+                new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
+            }, result);
+        }
+
+        [Fact]
+        public void Subscribes_And_Unsubscribes()
+        {
+            var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
+            var data = new Data { Maximum = 5 };
+            var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
+            var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+
+            Assert.Equal(0, data.SubscriptionCount);
+            var sub = validator.Subscribe(_ => { });
+            Assert.Equal(1, data.SubscriptionCount);
+            sub.Dispose();
+            Assert.Equal(0, data.SubscriptionCount);
+        }
+
+        public class Data : INotifyDataErrorInfo, INotifyPropertyChanged
+        {
+            private int _value;
+            private int _maximum;
+            private string _error;
+            private EventHandler<DataErrorsChangedEventArgs> _errorsChanged;
+
+            public bool HasErrors => _error != null;
+            public int SubscriptionCount { get; private set; }
+
+            public int Value
+            {
+                get { return _value; }
+                set
+                {
+                    _value = value;
+                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
+                    UpdateError();
+                }
+            }
+
+            public int Maximum
+            {
+                get { return _maximum; }
+                set
+                {
+                    _maximum = value;
+                    UpdateError();
+                }
+            }
+
+            public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
+            {
+                add { _errorsChanged += value; ++SubscriptionCount; }
+                remove { _errorsChanged -= value; --SubscriptionCount; }
+            }
+
+            public event PropertyChangedEventHandler PropertyChanged;
+
+            public IEnumerable GetErrors(string propertyName)
+            {
+                if (propertyName == nameof(Value) && _error != null)
+                {
+                    return new[] { _error };
+                }
+
+                return null;
+            }
+
+            private void UpdateError()
+            {
+                if (_value <= _maximum)
+                {
+                    if (_error != null)
+                    {
+                        _error = null;
+                        _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+                    }
+                }
+                else
+                {
+                    if (_error == null)
+                    {
+                        _error = "Must be less than Maximum";
+                        _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+                    }
+                }
+            }
+        }
+    }
+}

+ 5 - 3
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

@@ -26,7 +26,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
 
             target.ValidationTest = -5;
 
-            Assert.False(target.ValidationStatus.IsValid);
+            Assert.True(false);
+            //Assert.False(target.ValidationStatus.IsValid);
         }
 
         [Fact]
@@ -44,7 +45,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
             target.Bind(TestControl.ValidationTestProperty, binding);
 
             target.ValidationTest = -5;
-            Assert.False(target.ValidationStatus.IsValid);
+            Assert.True(false);
+            //Assert.False(target.ValidationStatus.IsValid);
         }
 
 
@@ -123,7 +125,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
                 }
             }
 
-            protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+            protected override void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
             {
                 if (property == ValidationTestProperty)
                 {