Browse Source

Reimplemented property validation.

As far as it was in `property-validation-grokys` branch. Things were
messed up on master so had to merge this manually so may be problems.
Steven Kirk 9 years ago
parent
commit
5c33fbc6ee
41 changed files with 720 additions and 352 deletions
  1. 2 1
      samples/BindingTest/BindingTest.csproj
  2. 8 0
      samples/BindingTest/MainWindow.xaml
  3. 29 0
      samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs
  4. 3 0
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  5. 3 3
      src/Avalonia.Base/Avalonia.Base.csproj
  6. 37 6
      src/Avalonia.Base/AvaloniaObject.cs
  7. 17 0
      src/Avalonia.Base/Data/IValidationStatus.cs
  8. 44 0
      src/Avalonia.Base/Data/ObjectValidationStatus.cs
  9. 0 13
      src/Avalonia.Base/Data/ValidationMethods.cs
  10. 0 30
      src/Avalonia.Base/Data/ValidationStatus.cs
  11. 1 1
      src/Avalonia.Base/IPriorityValueOwner.cs
  12. 1 1
      src/Avalonia.Base/PriorityBindingEntry.cs
  13. 1 2
      src/Avalonia.Base/PriorityLevel.cs
  14. 1 1
      src/Avalonia.Base/PriorityValue.cs
  15. 1 2
      src/Avalonia.Controls/Avalonia.Controls.csproj
  16. 4 26
      src/Avalonia.Controls/Control.cs
  17. 0 27
      src/Avalonia.Controls/ControlValidationStatus.cs
  18. 8 0
      src/Avalonia.Controls/TextBox.cs
  19. 3 0
      src/Avalonia.Themes.Default/TextBox.xaml
  20. 12 10
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  21. 2 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  22. 5 5
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  23. 2 2
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  24. 3 2
      src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs
  25. 28 24
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  26. 1 1
      src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
  27. 15 8
      src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs
  28. 17 19
      src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
  29. 8 9
      src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs
  30. 11 16
      src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
  31. 46 0
      src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs
  32. 0 34
      src/Markup/Avalonia.Markup/Data/Plugins/ValidationCheckerBase.cs
  33. 16 6
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  34. 2 2
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  35. 2 1
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  36. 130 0
      tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs
  37. 2 1
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  38. 9 15
      tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
  39. 129 0
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs
  40. 10 17
      tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
  41. 107 65
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

+ 2 - 1
samples/BindingTest/BindingTest.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
@@ -88,6 +88,7 @@
     <Compile Include="TestItemView.xaml.cs">
       <DependentUpon>TestItemView.xaml</DependentUpon>
     </Compile>
+    <Compile Include="ViewModels\ExceptionPropertyErrorViewModel.cs" />
     <Compile Include="ViewModels\MainWindowViewModel.cs" />
     <Compile Include="ViewModels\TestItem.cs" />
   </ItemGroup>

+ 8 - 0
samples/BindingTest/MainWindow.xaml

@@ -68,6 +68,14 @@
         </ContentControl>
       </StackPanel>
     </TabItem>
+    <TabItem Header="Property Validation">
+      <StackPanel Orientation="Horizontal">
+        <StackPanel Margin="18" Gap="4" Width="200" DataContext="{Binding ExceptionPropertyValidation}">
+          <TextBlock FontSize="16" Text="Exception Validation"/>
+          <TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10, EnableValidation=True}"/>
+        </StackPanel>
+      </StackPanel>
+    </TabItem>
     <TabItem Header="Commands">
       <StackPanel Margin="18" Gap="4" Width="200">
         <Button Content="Button" Command="{Binding StringValueCommand}" CommandParameter="Button"/>

+ 29 - 0
samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs

@@ -0,0 +1,29 @@
+// 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 ReactiveUI;
+using System;
+
+namespace BindingTest.ViewModels
+{
+    public class ExceptionPropertyErrorViewModel : ReactiveObject
+    {
+        private int _lessThan10;
+
+        public int LessThan10
+        {
+            get { return _lessThan10; }
+            set
+            {
+                if (value < 10)
+                {
+                    this.RaiseAndSetIfChanged(ref _lessThan10, value);
+                }
+                else
+                {
+                    throw new InvalidOperationException("Value must be less than 10.");
+                }
+            }
+        }
+    }
+}

+ 3 - 0
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@@ -68,5 +68,8 @@ namespace BindingTest.ViewModels
         }
 
         public ReactiveCommand<object> StringValueCommand { get; }
+
+        public ExceptionPropertyErrorViewModel ExceptionPropertyValidation { get; }
+            = new ExceptionPropertyErrorViewModel();
     }
 }

+ 3 - 3
src/Avalonia.Base/Avalonia.Base.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
@@ -44,8 +44,8 @@
       <Link>Properties\SharedAssemblyInfo.cs</Link>
     </Compile>
     <Compile Include="Data\BindingError.cs" />
-    <Compile Include="Data\ValidationMethods.cs" />
-    <Compile Include="Data\ValidationStatus.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" />

+ 37 - 6
src/Avalonia.Base/AvaloniaObject.cs

@@ -50,6 +50,29 @@ 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>
@@ -404,13 +427,13 @@ namespace Avalonia
                 }
 
                 subscription = source
-                    .Where(x =>  !(x is ValidationStatus))
+                    .Where(x =>  !(x is IValidationStatus))
                     .Select(x => CastOrDefault(x, property.PropertyType))
                     .Do(_ => { }, () => _directBindings.Remove(subscription))
                     .Subscribe(x => DirectBindingSet(property, x));
                 validationSubcription = source
-                    .OfType<ValidationStatus>()
-                    .Subscribe(x => DataValidation(property, x));
+                    .OfType<IValidationStatus>()
+                    .Subscribe(x => DataValidationChanged(property, x));
 
                 _directBindings.Add(subscription);
 
@@ -512,10 +535,10 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, ValidationStatus status)
+        void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, IValidationStatus status)
         {
             var property = sender.Property;
-            DataValidation(property, status);
+            DataValidationChanged(property, status);
         }
 
         /// <summary>
@@ -523,9 +546,17 @@ namespace Avalonia
         /// </summary>
         /// <param name="property">The property whose validation state changed.</param>
         /// <param name="status">The new validation state.</param>
-        protected virtual void DataValidation(AvaloniaProperty property, ValidationStatus status)
+        protected virtual void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
         {
+        }
 
+        /// <summary>
+        /// Updates the validation status of the current object.
+        /// </summary>
+        /// <param name="status">The new validation status.</param>
+        protected void UpdateValidationState(IValidationStatus status)
+        {
+            ValidationStatus = ValidationStatus.UpdateValidationStatus(status);
         }
 
         /// <inheritdoc/>

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

@@ -0,0 +1,17 @@
+// 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; }
+    }
+}

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

@@ -0,0 +1,44 @@
+// 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;
+    }
+}

+ 0 - 13
src/Avalonia.Base/Data/ValidationMethods.cs

@@ -1,13 +0,0 @@
-using System;
-
-namespace Avalonia.Data
-{
-    [Flags]
-    public enum ValidationMethods
-    {
-        None = 0,
-        Exceptions = 1,
-        INotifyDataErrorInfo = 2,
-        All = -1
-    }
-}

+ 0 - 30
src/Avalonia.Base/Data/ValidationStatus.cs

@@ -1,30 +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;
-using System.Text;
-using System.Threading.Tasks;
-
-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 abstract class ValidationStatus
-    {
-        /// <summary>
-        /// True when the data passes validation; otherwise, false.
-        /// </summary>
-        public abstract bool IsValid { get; }
-
-        /// <summary>
-        /// Checks if this validation status came from a currently enabled method of validation checking.
-        /// </summary>
-        /// <param name="enabledMethods">The enabled methods of validation checking.</param>
-        /// <returns>True if enabled; otherwise, false.</returns>
-        public abstract bool Match(ValidationMethods enabledMethods);
-    }
-}

+ 1 - 1
src/Avalonia.Base/IPriorityValueOwner.cs

@@ -23,6 +23,6 @@ namespace Avalonia
         /// </summary>
         /// <param name="sender">The source of the change.</param>
         /// <param name="status">The validation status.</param>
-        void DataValidationChanged(PriorityValue sender, ValidationStatus status);
+        void DataValidationChanged(PriorityValue sender, IValidationStatus status);
     }
 }

+ 1 - 1
src/Avalonia.Base/PriorityBindingEntry.cs

@@ -100,7 +100,7 @@ namespace Avalonia
                 _owner.Error(this, bindingError);
             }
 
-            var validationStatus = value as ValidationStatus;
+            var validationStatus = value as IValidationStatus;
 
             if (validationStatus != null)
             {

+ 1 - 2
src/Avalonia.Base/PriorityLevel.cs

@@ -97,7 +97,6 @@ namespace Avalonia
         /// Adds a binding.
         /// </summary>
         /// <param name="binding">The binding to add.</param>
-        /// <param name="validation">Validation settings for the binding.</param>
         /// <returns>A disposable used to remove the binding.</returns>
         public IDisposable Add(IObservable<object> binding)
         {
@@ -170,7 +169,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="entry">The entry that completed.</param>
         /// <param name="validationStatus">The validation status.</param>
-        public void Validation(PriorityBindingEntry entry, ValidationStatus validationStatus)
+        public void Validation(PriorityBindingEntry entry, IValidationStatus validationStatus)
         {
             _owner.LevelValidation(this, validationStatus);
         }

+ 1 - 1
src/Avalonia.Base/PriorityValue.cs

@@ -184,7 +184,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="priorityLevel">The priority level of the changed entry.</param>
         /// <param name="validationStatus">The validation status.</param>
-        public void LevelValidation(PriorityLevel priorityLevel, ValidationStatus validationStatus)
+        public void LevelValidation(PriorityLevel priorityLevel, IValidationStatus validationStatus)
         {
             _owner.DataValidationChanged(this, validationStatus);
         }

+ 1 - 2
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
@@ -46,7 +46,6 @@
     <Compile Include="Application.cs" />
     <Compile Include="Classes.cs" />
     <Compile Include="ContextMenu.cs" />
-    <Compile Include="ControlValidationStatus.cs" />
     <Compile Include="Design.cs" />
     <Compile Include="DockPanel.cs" />
     <Compile Include="Expander.cs" />

+ 4 - 26
src/Avalonia.Controls/Control.cs

@@ -86,12 +86,6 @@ namespace Avalonia.Controls
         public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
             RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
 
-        /// <summary>
-        /// Defines the <see cref="ValidationStatus"/> property.
-        /// </summary>
-        public static readonly DirectProperty<Control, ControlValidationStatus> ValidationStatusProperty =
-            AvaloniaProperty.RegisterDirect<Control, ControlValidationStatus>(nameof(ValidationStatus), c=> c.ValidationStatus);
-
         private int _initCount;
         private string _name;
         private IControl _parent;
@@ -114,7 +108,7 @@ namespace Avalonia.Controls
             PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
             PseudoClass(IsFocusedProperty, ":focus");
             PseudoClass(IsPointerOverProperty, ":pointerover");
-            PseudoClass(ValidationStatusProperty, status => status != null && !status.IsValid, ":invalid");
+            PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid");
         }
 
         /// <summary>
@@ -406,27 +400,10 @@ namespace Avalonia.Controls
         /// </summary>
         protected IPseudoClasses PseudoClasses => Classes;
 
-        private ControlValidationStatus validationStatus = new ControlValidationStatus();
-
-        /// <summary>
-        /// The current validation status of the control.
-        /// </summary>
-        public ControlValidationStatus ValidationStatus
-        {
-            get
-            {
-                return validationStatus;
-            }
-            private set
-            {
-                SetAndRaise(ValidationStatusProperty, ref validationStatus, value);
-            }
-        }
-
         /// <inheritdoc/>
-        protected override void DataValidation(AvaloniaProperty property, ValidationStatus status)
+        protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
         {
-            base.DataValidation(property, status);
+            base.DataValidationChanged(property, status);
             ValidationStatus.UpdateValidationStatus(status);
         }
 
@@ -511,6 +488,7 @@ namespace Avalonia.Controls
             }
 
             property.Changed.Merge(property.Initialized)
+                .Where(e => e.Sender is Control)
                 .Subscribe(e =>
                 {
                     if (selector((T)e.NewValue))

+ 0 - 27
src/Avalonia.Controls/ControlValidationStatus.cs

@@ -1,27 +0,0 @@
-using Avalonia.Data;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Avalonia.Controls
-{
-    public class ControlValidationStatus : ValidationStatus, INotifyPropertyChanged
-    {
-        private Dictionary<Type, ValidationStatus> propertyValidation = new Dictionary<Type, ValidationStatus>();
-
-        public override bool IsValid => propertyValidation.Values.All(status => status.IsValid);
-
-        public event PropertyChangedEventHandler PropertyChanged;
-
-        public override bool Match(ValidationMethods enabledMethods) => true;
-
-        public void UpdateValidationStatus(ValidationStatus status)
-        {
-            propertyValidation[status.GetType()] = status;
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(""));
-        }
-    }
-}

+ 8 - 0
src/Avalonia.Controls/TextBox.cs

@@ -235,6 +235,14 @@ namespace Avalonia.Controls
             HandleTextInput(e.Text);
         }
 
+        protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+        {
+            if (property == TextProperty)
+            {
+                UpdateValidationState(status);
+            }
+        }
+
         private void HandleTextInput(string input)
         {
             if (!IsReadOnly)

+ 3 - 0
src/Avalonia.Themes.Default/TextBox.xaml

@@ -57,4 +57,7 @@
   <Style Selector="TextBox:focus /template/ Border#border">
     <Setter Property="BorderBrush" Value="{StyleResource ThemeBorderDarkBrush}"/>
   </Style>
+  <Style Selector="TextBox:invalid /template/ Border#border">
+    <Setter Property="BorderBrush" Value="Red"/>
+  </Style>
 </Styles>

+ 12 - 10
src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs

@@ -4,7 +4,6 @@
 using System;
 using System.Reactive;
 using System.Reactive.Linq;
-using System.Reactive.Subjects;
 using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Markup.Data;
@@ -78,13 +77,13 @@ namespace Avalonia.Markup.Xaml.Data
         public object Source { get; set; }
 
         /// <summary>
-        /// Gets or sets the validation methods for the binding to use.
+        /// Gets or sets a value indicating whether the property should be validated.
         /// </summary>
-        public ValidationMethods ValidationMethods { get; set; }
+        public bool EnableValidation { get; set; }
 
         /// <inheritdoc/>
         public InstancedBinding Initiate(
-            IAvaloniaObject target, 
+            IAvaloniaObject target,
             AvaloniaProperty targetProperty,
             object anchor = null)
         {
@@ -99,7 +98,7 @@ namespace Avalonia.Markup.Xaml.Data
             {
                 observer = CreateElementObserver(
                     (target as IControl) ?? (anchor as IControl),
-                    pathInfo.ElementName ?? ElementName, 
+                    pathInfo.ElementName ?? ElementName,
                     pathInfo.Path);
             }
             else if (Source != null)
@@ -109,7 +108,7 @@ namespace Avalonia.Markup.Xaml.Data
             else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
             {
                 observer = CreateDataContexObserver(
-                    target, 
+                    target,
                     pathInfo.Path,
                     targetProperty == Control.DataContextProperty,
                     anchor);
@@ -207,7 +206,8 @@ namespace Avalonia.Markup.Xaml.Data
                 var result = new ExpressionObserver(
                     () => target.GetValue(Control.DataContextProperty),
                     path,
-                    update, ValidationMethods);
+                    update,
+                    EnableValidation);
 
                 return result;
             }
@@ -218,7 +218,8 @@ namespace Avalonia.Markup.Xaml.Data
                           .OfType<IAvaloniaObject>()
                           .Select(x => x.GetObservable(Control.DataContextProperty))
                           .Switch(),
-                    path, ValidationMethods);
+                    path,
+                    EnableValidation);
             }
         }
 
@@ -228,7 +229,8 @@ namespace Avalonia.Markup.Xaml.Data
 
             var result = new ExpressionObserver(
                 ControlLocator.Track(target, elementName),
-                path, ValidationMethods);
+                path,
+                EnableValidation);
             return result;
         }
 
@@ -236,7 +238,7 @@ namespace Avalonia.Markup.Xaml.Data
         {
             Contract.Requires<ArgumentNullException>(source != null);
 
-            return new ExpressionObserver(source, path, ValidationMethods);
+            return new ExpressionObserver(source, path, EnableValidation);
         }
 
         private ExpressionObserver CreateTemplatedParentObserver(

+ 2 - 2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@@ -29,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                 Mode = Mode,
                 Path = Path,
                 Priority = Priority,
-                ValidationMethods = ValidationMethods
+                EnableValidation = EnableValidation,
             };
         }
 
@@ -41,6 +41,6 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
         public string Path { get; set; }
         public BindingPriority Priority { get; set; } = BindingPriority.LocalValue;
         public object Source { get; set; }
-        public ValidationMethods ValidationMethods { get; set; } = ValidationMethods.None;
+        public bool EnableValidation { get; set; }
     }
 }

+ 5 - 5
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
@@ -46,10 +46,9 @@
     <Compile Include="Data\ExpressionParseException.cs" />
     <Compile Include="Data\ExpressionSubject.cs" />
     <Compile Include="ControlLocator.cs" />
-    <Compile Include="Data\Plugins\ExceptionValidationCheckerPlugin.cs" />
-    <Compile Include="Data\Plugins\IndeiValidationCheckerPlugin.cs" />
-    <Compile Include="Data\Plugins\ValidationCheckerBase.cs" />
-    <Compile Include="Data\Plugins\IValidationCheckerPlugin.cs" />
+    <Compile Include="Data\Plugins\ExceptionValidationPlugin.cs" />
+    <Compile Include="Data\Plugins\IndeiValidationPlugin.cs" />
+    <Compile Include="Data\Plugins\IValidationPlugin.cs" />
     <Compile Include="Data\Plugins\AvaloniaPropertyAccessorPlugin.cs" />
     <Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
     <Compile Include="Data\Plugins\IPropertyAccessor.cs" />
@@ -61,6 +60,7 @@
     <Compile Include="Data\Parsers\ExpressionParser.cs" />
     <Compile Include="Data\Parsers\Reader.cs" />
     <Compile Include="Data\Plugins\PropertyError.cs" />
+    <Compile Include="Data\Plugins\ValidatingPropertyAccessorBase.cs" />
     <Compile Include="Data\PropertyAccessorNode.cs" />
     <Compile Include="Data\ExpressionNode.cs" />
     <Compile Include="Data\ExpressionObserver.cs" />

+ 2 - 2
src/Markup/Avalonia.Markup/Data/ExpressionNode.cs

@@ -105,12 +105,12 @@ namespace Avalonia.Markup.Data
             CurrentValue = reference;
         }
 
-        protected virtual void SendValidationStatus(ValidationStatus status)
+        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); 
+                _subject.OnNext(status);
             }
             else
             {

+ 3 - 2
src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Markup.Data
 {
     internal static class ExpressionNodeBuilder
     {
-        public static ExpressionNode Build(string expression)
+        public static ExpressionNode Build(string expression, bool enableValidation = false)
         {
             if (string.IsNullOrWhiteSpace(expression))
             {
@@ -16,7 +16,8 @@ namespace Avalonia.Markup.Data
             }
 
             var reader = new Reader(expression);
-            var node = ExpressionParser.Parse(reader);
+            var parser = new ExpressionParser(enableValidation);
+            var node = parser.Parse(reader);
 
             if (!reader.End)
             {

+ 28 - 24
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@@ -6,7 +6,6 @@ using System.Collections.Generic;
 using System.Reactive;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
-using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.Markup.Data.Plugins;
 
@@ -32,11 +31,10 @@ 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<IValidationCheckerPlugin> ValidationCheckers =
-            new List<IValidationCheckerPlugin>
+        public static readonly IList<IValidationPlugin> ValidationCheckers =
+            new List<IValidationPlugin>
             {
-                new IndeiValidationCheckerPlugin(),
-                new ExceptionValidationCheckerPlugin()
+                new IndeiValidationPlugin(),
             };
 
         private readonly WeakReference _root;
@@ -47,23 +45,24 @@ namespace Avalonia.Markup.Data
         private IDisposable _updateSubscription;
         private int _count;
         private readonly ExpressionNode _node;
-        private ValidationMethods _methods;
+        private bool _enableValidation;
 
         /// <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="methods">The validation methods to enable on this observer.</param>
-        public ExpressionObserver(object root, string expression, ValidationMethods methods = ValidationMethods.None)
+        /// <param name="enableValidation">Whether property validation should be enabled.</param>
+        public ExpressionObserver(object root, string expression, bool enableValidation = false)
         {
             Contract.Requires<ArgumentNullException>(expression != null);
 
             _root = new WeakReference(root);
-            _methods = methods;
+            _enableValidation = enableValidation;
+
             if (!string.IsNullOrWhiteSpace(expression))
             {
-                _node = ExpressionNodeBuilder.Build(expression);
+                _node = ExpressionNodeBuilder.Build(expression, enableValidation);
             }
 
             Expression = expression;
@@ -74,17 +73,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="methods">The validation methods to enable on this observer.</param>
-        public ExpressionObserver(IObservable<object> rootObservable, string expression, ValidationMethods methods = ValidationMethods.None)
+        /// <param name="enableValidation">Whether property validation should be enabled.</param>
+        public ExpressionObserver(
+            IObservable<object> rootObservable,
+            string expression,
+            bool enableValidation = false)
         {
             Contract.Requires<ArgumentNullException>(rootObservable != null);
             Contract.Requires<ArgumentNullException>(expression != null);
 
             _rootObservable = rootObservable;
-            _methods = methods;
+            _enableValidation = enableValidation;
+
             if (!string.IsNullOrWhiteSpace(expression))
             {
-                _node = ExpressionNodeBuilder.Build(expression);
+                _node = ExpressionNodeBuilder.Build(expression, enableValidation);
             }
 
             Expression = expression;
@@ -96,12 +99,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="methods">The validation methods to enable on this observer.</param>
+        /// <param name="enableValidation">Whether property validation should be enabled.</param>
         public ExpressionObserver(
-            Func<object> rootGetter, 
+            Func<object> rootGetter,
             string expression,
             IObservable<Unit> update,
-            ValidationMethods methods = ValidationMethods.None)
+            bool enableValidation = false)
         {
             Contract.Requires<ArgumentNullException>(rootGetter != null);
             Contract.Requires<ArgumentNullException>(expression != null);
@@ -109,10 +112,11 @@ namespace Avalonia.Markup.Data
 
             _rootGetter = rootGetter;
             _update = update;
-            _methods = methods;
+            _enableValidation = enableValidation;
+
             if (!string.IsNullOrWhiteSpace(expression))
             {
-                _node = ExpressionNodeBuilder.Build(expression);
+                _node = ExpressionNodeBuilder.Build(expression, enableValidation);
             }
 
             Expression = expression;
@@ -167,7 +171,7 @@ namespace Avalonia.Markup.Data
                     {
                         return (Leaf as PropertyAccessorNode)?.PropertyType;
                     }
-                    else if(_rootGetter != null)
+                    else if (_rootGetter != null)
                     {
                         return _rootGetter()?.GetType();
                     }
@@ -221,8 +225,8 @@ namespace Avalonia.Markup.Data
                 {
                     source = source.TakeUntil(_update.LastOrDefaultAsync());
                 }
-                var validationFiltered = source.Where(o => (o as ValidationStatus)?.Match(_methods) ?? true);
-                var subscription = validationFiltered.Subscribe(observer);
+
+                var subscription = source.Subscribe(observer);
 
                 return Disposable.Create(() =>
                 {
@@ -262,13 +266,13 @@ namespace Avalonia.Markup.Data
 
                     if (_update != null)
                     {
-                        _updateSubscription = _update.Subscribe(x => 
+                        _updateSubscription = _update.Subscribe(x =>
                             _node.Target = new WeakReference(_rootGetter()));
                     }
                 }
                 else if (_rootObservable != null)
                 {
-                    _rootObserverSubscription = _rootObservable.Subscribe(x => 
+                    _rootObserverSubscription = _rootObservable.Subscribe(x =>
                         _node.Target = new WeakReference(x));
                 }
                 else

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

@@ -155,7 +155,7 @@ namespace Avalonia.Markup.Data
         {
             var converted = 
                 value as BindingError ??
-                value as ValidationStatus ??
+                value as IValidationStatus ??
                 Converter.Convert(
                     value,
                     _targetType,

+ 15 - 8
src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs

@@ -7,9 +7,16 @@ using System.Linq;
 
 namespace Avalonia.Markup.Data.Parsers
 {
-    internal static class ExpressionParser
+    internal class ExpressionParser
     {
-        public static ExpressionNode Parse(Reader r)
+        private bool _enableValidation;
+
+        public ExpressionParser(bool enableValidation)
+        {
+            _enableValidation = enableValidation;
+        }
+
+        public ExpressionNode Parse(Reader r)
         {
             var nodes = new List<ExpressionNode>();
             var state = State.Start;
@@ -49,7 +56,7 @@ namespace Avalonia.Markup.Data.Parsers
             return nodes.FirstOrDefault();
         }
 
-        private static State ParseStart(Reader r, IList<ExpressionNode> nodes)
+        private State ParseStart(Reader r, IList<ExpressionNode> nodes)
         {
             if (ParseNot(r))
             {
@@ -66,7 +73,7 @@ namespace Avalonia.Markup.Data.Parsers
 
                 if (identifier != null)
                 {
-                    nodes.Add(new PropertyAccessorNode(identifier));
+                    nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
                     return State.AfterMember;
                 }
             }
@@ -99,7 +106,7 @@ namespace Avalonia.Markup.Data.Parsers
             return State.End;
         }
 
-        private static State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
+        private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
         {
             if (ParseOpenBrace(r))
             {
@@ -111,7 +118,7 @@ namespace Avalonia.Markup.Data.Parsers
 
                 if (identifier != null)
                 {
-                    nodes.Add(new PropertyAccessorNode(identifier));
+                    nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
                     return State.AfterMember;
                 }
 
@@ -119,7 +126,7 @@ namespace Avalonia.Markup.Data.Parsers
             }
         }
 
-        private static State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
+        private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
         {
             var owner = IdentifierParser.Parse(r);
 
@@ -135,7 +142,7 @@ namespace Avalonia.Markup.Data.Parsers
                 throw new ExpressionParseException(r.Position, "Expected ')'.");
             }
 
-            nodes.Add(new PropertyAccessorNode(owner + '.' + name));
+            nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation));
             return State.AfterMember;
         }
 

+ 17 - 19
src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs

@@ -1,31 +1,31 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+// 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;
 
 namespace Avalonia.Markup.Data.Plugins
 {
     /// <summary>
     /// Validates properties that report errors by throwing exceptions.
     /// </summary>
-    public class ExceptionValidationCheckerPlugin : IValidationCheckerPlugin
+    public class ExceptionValidationPlugin : IValidationPlugin
     {
+        public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin();
 
         /// <inheritdoc/>
         public bool Match(WeakReference reference) => true;
 
-
         /// <inheritdoc/>
-        public ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
+        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
         {
             return new ExceptionValidationChecker(reference, name, accessor, callback);
         }
 
-        private class ExceptionValidationChecker : ValidationCheckerBase
+        private class ExceptionValidationChecker : ValidatingPropertyAccessorBase
         {
-            public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
+            public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
                 : base(reference, name, accessor, callback)
             {
             }
@@ -38,6 +38,10 @@ namespace Avalonia.Markup.Data.Plugins
                     SendValidationCallback(new ExceptionValidationStatus(null));
                     return success;
                 }
+                catch (TargetInvocationException ex)
+                {
+                    SendValidationCallback(new ExceptionValidationStatus(ex.InnerException));
+                }
                 catch (Exception ex)
                 {
                     SendValidationCallback(new ExceptionValidationStatus(ex));
@@ -49,7 +53,7 @@ namespace Avalonia.Markup.Data.Plugins
         /// <summary>
         /// Describes the current validation status after setting a property value.
         /// </summary>
-        public class ExceptionValidationStatus : ValidationStatus
+        public class ExceptionValidationStatus : IValidationStatus
         {
             internal ExceptionValidationStatus(Exception exception)
             {
@@ -60,15 +64,9 @@ namespace Avalonia.Markup.Data.Plugins
             /// The thrown exception. If there was no thrown exception, null.
             /// </summary>
             public Exception Exception { get; }
-
-
+            
             /// <inheritdoc/>
-            public override bool IsValid => Exception == null;
-
-            public override bool Match(ValidationMethods enabledMethods)
-            {
-                return (enabledMethods & ValidationMethods.Exceptions) != 0;
-            }
+            public bool IsValid => Exception == null;
         }
     }
 }

+ 8 - 9
src/Markup/Avalonia.Markup/Data/Plugins/IValidationCheckerPlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs

@@ -1,16 +1,15 @@
-using Avalonia.Data;
+// 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;
-using System.Text;
-using System.Threading.Tasks;
+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 IValidationCheckerPlugin
+    public interface IValidationPlugin
     {
 
         /// <summary>
@@ -21,16 +20,16 @@ namespace Avalonia.Markup.Data.Plugins
         bool Match(WeakReference reference);
 
         /// <summary>
-        /// Starts monitering the validation state of an object for the given property.
+        /// 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="ValidationCheckerBase"/> subclass through which future interactions with the 
+        /// A <see cref="ValidatingPropertyAccessorBase"/> subclass through which future interactions with the 
         /// property will be made.
         /// </returns>
-        ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback);
+        IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback);
     }
 }

+ 11 - 16
src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs

@@ -1,11 +1,11 @@
+// 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.Collections;
+using System.ComponentModel;
 using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Avalonia.Data;
-using System.ComponentModel;
-using System.Collections;
 using Avalonia.Utilities;
 
 namespace Avalonia.Markup.Data.Plugins
@@ -13,7 +13,7 @@ namespace Avalonia.Markup.Data.Plugins
     /// <summary>
     /// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
     /// </summary>
-    public class IndeiValidationCheckerPlugin : IValidationCheckerPlugin
+    public class IndeiValidationPlugin : IValidationPlugin
     {
         /// <inheritdoc/>
         public bool Match(WeakReference reference)
@@ -22,14 +22,14 @@ namespace Avalonia.Markup.Data.Plugins
         }
 
         /// <inheritdoc/>
-        public ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
+        public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
         {
             return new IndeiValidationChecker(reference, name, accessor, callback);
         }
 
-        private class IndeiValidationChecker : ValidationCheckerBase, IWeakSubscriber<DataErrorsChangedEventArgs>
+        private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
         {
-            public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
+            public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
                 : base(reference, name, accessor, callback)
             {
                 var target = reference.Target as INotifyDataErrorInfo;
@@ -72,7 +72,7 @@ namespace Avalonia.Markup.Data.Plugins
         /// <summary>
         /// Describes the current validation status of a property as reported by an object that implements <see cref="INotifyDataErrorInfo"/>.
         /// </summary>
-        public class IndeiValidationStatus : ValidationStatus
+        public class IndeiValidationStatus : IValidationStatus
         {
             internal IndeiValidationStatus(IEnumerable errors)
             {
@@ -80,17 +80,12 @@ namespace Avalonia.Markup.Data.Plugins
             }
 
             /// <inheritdoc/>
-            public override bool IsValid => !Errors.OfType<object>().Any();
+            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; }
-
-            public override bool Match(ValidationMethods enabledMethods)
-            {
-                return (enabledMethods & ValidationMethods.INotifyDataErrorInfo) != 0;
-            }
         }
     }
 }

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

@@ -0,0 +1,46 @@
+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);
+        }
+    }
+}

+ 0 - 34
src/Markup/Avalonia.Markup/Data/Plugins/ValidationCheckerBase.cs

@@ -1,34 +0,0 @@
-using System;
-using Avalonia.Data;
-
-namespace Avalonia.Markup.Data.Plugins
-{
-    public abstract class ValidationCheckerBase : IPropertyAccessor
-    {
-        protected readonly WeakReference _reference;
-        protected readonly string _name;
-        private readonly IPropertyAccessor _accessor;
-        private readonly Action<ValidationStatus> _callback;
-
-        protected ValidationCheckerBase(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
-        {
-            _reference = reference;
-            _name = name;
-            _accessor = accessor;
-            _callback = callback;
-        }
-
-        public Type PropertyType => _accessor.PropertyType;
-
-        public object Value => _accessor.Value;
-
-        public virtual void Dispose() => _accessor.Dispose();
-
-        public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority);
-
-        protected void SendValidationCallback(ValidationStatus status)
-        {
-            _callback?.Invoke(status);
-        }
-    }
-}

+ 16 - 6
src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs

@@ -17,10 +17,12 @@ namespace Avalonia.Markup.Data
     {
         private IPropertyAccessor _accessor;
         private IDisposable _subscription;
+        private bool _enableValidation;
 
-        public PropertyAccessorNode(string propertyName)
+        public PropertyAccessorNode(string propertyName, bool enableValidation)
         {
             PropertyName = propertyName;
+            _enableValidation = enableValidation;
         }
 
         public string PropertyName { get; }
@@ -54,13 +56,21 @@ namespace Avalonia.Markup.Data
 
                 if (accessorPlugin != null)
                 {
-                    _accessor = accessorPlugin.Start(reference, PropertyName, SetCurrentValue);
-                    foreach (var validationPlugin in ExpressionObserver.ValidationCheckers.Where(x => x.Match(reference)))
+                    _accessor = ExceptionValidationPlugin.Instance.Start(
+                        reference,
+                        PropertyName,
+                        accessorPlugin.Start(reference, PropertyName, SetCurrentValue),
+                        SendValidationStatus);
+
+                    if (_enableValidation)
                     {
-                        if (validationPlugin != null)
+                        foreach (var validationPlugin in ExpressionObserver.ValidationCheckers)
                         {
-                            _accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
-                        } 
+                            if (validationPlugin.Match(reference))
+                            {
+                                _accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
+                            }
+                        }
                     }
 
                     if (_accessor != null)

+ 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" }, names);
+            Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, 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" }, names);
+            Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
         }
 
         [Fact]

+ 2 - 1
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
   <PropertyGroup>
@@ -91,6 +91,7 @@
   <ItemGroup>
     <Compile Include="ClassesTests.cs" />
     <Compile Include="LayoutTransformControlTests.cs" />
+    <Compile Include="TextBoxTests_ValidationState.cs" />
     <Compile Include="UserControlTests.cs" />
     <Compile Include="DockPanelTests.cs" />
     <Compile Include="EnumerableExtensions.cs" />

+ 130 - 0
tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs

@@ -0,0 +1,130 @@
+// 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 Avalonia.Markup.Xaml.Data;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class TextBoxTests_ValidationState
+    {
+        [Fact]
+        public void Setter_Exceptions_Should_Set_ValidationState()
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new TextBox();
+                var binding = new Binding(nameof(ExceptionTest.LessThan10));
+                binding.Source = new ExceptionTest();
+                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);
+            }
+        }
+
+        [Fact(Skip = "TODO: Not yet passing")]
+        public void Unconvertable_Value_Should_Set_ValidationState()
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new TextBox();
+                var binding = new Binding(nameof(ExceptionTest.LessThan10));
+                binding.Source = new ExceptionTest();
+                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);
+            }
+        }
+
+        [Fact]
+        public void Indei_Should_Set_ValidationState()
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new TextBox();
+                var binding = new Binding(nameof(ExceptionTest.LessThan10));
+                binding.Source = new IndeiTest();
+                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);
+            }
+        }
+
+        private class ExceptionTest
+        {
+            private int _lessThan10;
+
+            public int LessThan10
+            {
+                get { return _lessThan10; }
+                set
+                {
+                    if (value < 10)
+                    {
+                        _lessThan10 = value;
+                    }
+                    else
+                    {
+                        throw new InvalidOperationException("More than 10.");
+                    }
+                }
+            }
+        }
+
+        private class IndeiTest : INotifyDataErrorInfo
+        {
+            private int _lessThan10;
+            private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
+
+            public int LessThan10
+            {
+                get { return _lessThan10; }
+                set
+                {
+                    if (value < 10)
+                    {
+                        _lessThan10 = value;
+                        _errors.Remove(nameof(LessThan10));
+                        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(LessThan10)));
+                    }
+                    else
+                    {
+                        _errors[nameof(LessThan10)] = new[] { "More than 10" };
+                        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(LessThan10)));
+                    }
+                }
+            }
+
+            public bool HasErrors => _lessThan10 >= 10;
+
+            public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
+
+            public IEnumerable GetErrors(string propertyName)
+            {
+                IList<string> result;
+                _errors.TryGetValue(propertyName, out result);
+                return result;
+            }
+        }
+    }
+}

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

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@@ -97,6 +97,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\ExpressionSubjectTests.cs" />
     <Compile Include="Data\IndeiValidatorTests.cs" />
     <Compile Include="DefaultValueConverterTests.cs" />

+ 9 - 15
tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs

@@ -1,15 +1,11 @@
 // 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 Avalonia.Markup.Data.Plugins;
 using System;
-using System.Collections.Generic;
 using System.ComponentModel;
-using System.Linq;
 using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading.Tasks;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
 using Xunit;
 
 namespace Avalonia.Markup.UnitTests.Data
@@ -53,10 +49,10 @@ namespace Avalonia.Markup.UnitTests.Data
         public void Setting_Non_Validating_Triggers_Validation()
         {
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new ExceptionValidationCheckerPlugin();
+            var validatorPlugin = new ExceptionValidationPlugin();
             var data = new Data();
             var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
-            ValidationStatus status = null;
+            IValidationStatus status = null;
             var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
 
             validator.SetValue(5, BindingPriority.LocalValue);
@@ -68,27 +64,25 @@ namespace Avalonia.Markup.UnitTests.Data
         public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
         {
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new ExceptionValidationCheckerPlugin();
+            var validatorPlugin = new ExceptionValidationPlugin();
             var data = new Data();
             var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            ValidationStatus status = null;
+            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 ExceptionValidationCheckerPlugin();
+            var validatorPlugin = new ExceptionValidationPlugin();
             var data = new Data();
             var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            ValidationStatus status = null;
+            IValidationStatus status = null;
             var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
 
             validator.SetValue(-5, BindingPriority.LocalValue);

+ 129 - 0
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs

@@ -0,0 +1,129 @@
+// 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.Linq;
+using System.Reactive.Linq;
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using Avalonia.Markup.Data;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+    public class ExpressionObserverTests_Validation
+    {
+        [Fact]
+        public void Exception_Validation_Sends_ValidationUpdate()
+        {
+            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.SetValue(-5);
+            Assert.True(validationMessageFound);
+        }
+
+        [Fact]
+        public void Disabled_Indei_Validation_Does_Not_Subscribe()
+        {
+            var data = new IndeiTest { MustBePositive = 5 };
+            var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
+
+            observer.Subscribe(_ => { });
+
+            Assert.Equal(0, data.SubscriptionCount);
+        }
+
+        [Fact]
+        public void Enabled_Indei_Validation_Subscribes()
+        {
+            var data = new IndeiTest { MustBePositive = 5 };
+            var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+            var sub = observer.Subscribe(_ => { });
+
+            Assert.Equal(1, data.SubscriptionCount);
+            sub.Dispose();
+            Assert.Equal(0, data.SubscriptionCount);
+        }
+
+        public class ExceptionTest : INotifyPropertyChanged
+        {
+            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));
+            }
+        }
+
+        private class IndeiTest : INotifyDataErrorInfo
+        {
+            private int _mustBePositive;
+            private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
+            private EventHandler<DataErrorsChangedEventArgs> _errorsChanged;
+
+            public int MustBePositive
+            {
+                get { return _mustBePositive; }
+                set
+                {
+                    if (value >= 0)
+                    {
+                        _mustBePositive = value;
+                        _errors.Remove(nameof(MustBePositive));
+                        _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
+                    }
+                    else
+                    {
+                        _errors[nameof(MustBePositive)] = new[] { "Must be positive" };
+                        _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
+                    }
+                }
+            }
+
+            public bool HasErrors => _mustBePositive >= 0;
+
+            public int SubscriptionCount { get; private set; }
+
+            public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
+            {
+                add
+                {
+                    _errorsChanged += value;
+                    ++SubscriptionCount;
+                }
+                remove
+                {
+                    _errorsChanged -= value;
+                    --SubscriptionCount;
+                }
+            }
+
+            public IEnumerable GetErrors(string propertyName)
+            {
+                IList<string> result;
+                _errors.TryGetValue(propertyName, out result);
+                return result;
+            }
+        }
+    }
+}

+ 10 - 17
tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs

@@ -1,18 +1,13 @@
 // 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 Avalonia.Markup.Data.Plugins;
 using System;
-using System.Collections.Generic;
+using System.Collections;
 using System.ComponentModel;
-using System.Linq;
 using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading.Tasks;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
 using Xunit;
-using System.Collections;
 
 namespace Avalonia.Markup.UnitTests.Data
 {
@@ -74,10 +69,10 @@ namespace Avalonia.Markup.UnitTests.Data
         public void Setting_Non_Validating_Does_Not_Trigger_Validation()
         {
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new IndeiValidationCheckerPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
             var data = new Data();
             var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
-            ValidationStatus status = null;
+            IValidationStatus status = null;
             var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
 
             validator.SetValue(5, BindingPriority.LocalValue);
@@ -89,27 +84,25 @@ namespace Avalonia.Markup.UnitTests.Data
         public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
         {
             var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
-            var validatorPlugin = new IndeiValidationCheckerPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
             var data = new Data();
             var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            ValidationStatus status = null;
+            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 IndeiValidationCheckerPlugin();
+            var validatorPlugin = new IndeiValidationPlugin();
             var data = new Data();
             var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
-            ValidationStatus status = null;
+            IValidationStatus status = null;
             var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
 
             validator.SetValue(-5, BindingPriority.LocalValue);

+ 107 - 65
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

@@ -1,111 +1,153 @@
+using System;
 using Avalonia.Controls;
+using Avalonia.Data;
 using Avalonia.Markup.Xaml.Data;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading.Tasks;
 using Xunit;
 
 namespace Avalonia.Markup.Xaml.UnitTests.Data
 {
     public class BindingTests_Validation
     {
-        public class Data : INotifyPropertyChanged
+        [Fact]
+        public void Disabled_Validation_Should_Trigger_Validation_Change_On_Exception()
         {
-            private string mustbeNonEmpty;
-
-            public string MustBeNonEmpty
+            var source = new ValidationTestModel { MustBePositive = 5 };
+            var target = new TestControl { DataContext = source };
+            var binding = new Binding
             {
-                get { return mustbeNonEmpty; }
-                set
-                {
-                    if (string.IsNullOrEmpty(value))
-                    {
-                        throw new ArgumentException(nameof(value));
-                    }
-                    mustbeNonEmpty = value;
-                }
-            }
+                Path = nameof(source.MustBePositive),
+                Mode = BindingMode.TwoWay,
 
-            public event PropertyChangedEventHandler PropertyChanged;
+                // Even though EnableValidation = false, exception validation is enabled.
+                EnableValidation = false,
+            };
 
-            private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
-            {
-                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
-            }
+            target.Bind(TestControl.ValidationTestProperty, binding);
+
+            target.ValidationTest = -5;
+
+            Assert.False(target.ValidationStatus.IsValid);
         }
 
         [Fact]
-        public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Direct()
+        public void Enabled_Validation_Should_Trigger_Validation_Change_On_Exception()
         {
-            var source = new Data { MustBeNonEmpty = "Test" };
-            var target = new TextBlock { DataContext = source };
+            var source = new ValidationTestModel { MustBePositive = 5 };
+            var target = new TestControl { DataContext = source };
             var binding = new Binding
             {
-                Path = nameof(source.MustBeNonEmpty),
-                Mode = Avalonia.Data.BindingMode.TwoWay,
-                ValidationMethods = Avalonia.Data.ValidationMethods.None
+                Path = nameof(source.MustBePositive),
+                Mode = BindingMode.TwoWay,
+                EnableValidation = true,
             };
-            target.Bind(TextBlock.TextProperty, binding);
-            
-            target.Text = "";
 
-            Assert.True(target.ValidationStatus.IsValid);
+            target.Bind(TestControl.ValidationTestProperty, binding);
+
+            target.ValidationTest = -5;
+            Assert.False(target.ValidationStatus.IsValid);
         }
 
+
         [Fact]
-        public void Enabled_Validation_Should_Trigger_Validation_Change_Direct()
+        public void Passed_Validation_Should_Not_Add_Invalid_Pseudo_Class()
         {
-            var source = new Data { MustBeNonEmpty = "Test" };
-            var target = new TextBlock { DataContext = source };
+            var control = new TestControl();
+            var model = new ValidationTestModel { MustBePositive = 1 };
             var binding = new Binding
             {
-                Path = nameof(source.MustBeNonEmpty),
-                Mode = Avalonia.Data.BindingMode.TwoWay,
-                ValidationMethods = Avalonia.Data.ValidationMethods.All
+                Path = nameof(model.MustBePositive),
+                Mode = BindingMode.TwoWay,
+                EnableValidation = true,
             };
-            target.Bind(TextBlock.TextProperty, binding);
-            
-            target.Text = "";
-            Assert.False(target.ValidationStatus.IsValid);
+
+            control.Bind(TestControl.ValidationTestProperty, binding);
+            control.DataContext = model;
+            Assert.DoesNotContain(control.Classes, x => x == ":invalid");
         }
 
         [Fact]
-        public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Styled()
+        public void Failed_Validation_Should_Add_Invalid_Pseudo_Class()
         {
-            var source = new Data { MustBeNonEmpty = "Test" };
-            var target = new TextBlock { DataContext = source };
+            var control = new TestControl();
+            var model = new ValidationTestModel { MustBePositive = 1 };
             var binding = new Binding
             {
-                Path = nameof(source.MustBeNonEmpty),
-                Mode = Avalonia.Data.BindingMode.TwoWay,
-                ValidationMethods = Avalonia.Data.ValidationMethods.None
+                Path = nameof(model.MustBePositive),
+                Mode = BindingMode.TwoWay,
+                EnableValidation = true,
             };
-            target.Bind(Control.TagProperty, binding);
-
-            target.Tag = "";
 
-            Assert.True(target.ValidationStatus.IsValid);
+            control.Bind(TestControl.ValidationTestProperty, binding);
+            control.DataContext = model;
+            control.ValidationTest = -5;
+            Assert.Contains(control.Classes, x => x == ":invalid");
         }
 
         [Fact]
-        public void Enabled_Validation_Should_Trigger_Validation_Change_Styled()
+        public void Failed_Then_Passed_Validation_Should_Remove_Invalid_Pseudo_Class()
         {
-            var source = new Data { MustBeNonEmpty = "Test" };
-            var target = new TextBlock { DataContext = source };
+            var control = new TestControl();
+            var model = new ValidationTestModel { MustBePositive = 1 };
+
             var binding = new Binding
             {
-                Path = nameof(source.MustBeNonEmpty),
-                Mode = Avalonia.Data.BindingMode.TwoWay,
-                ValidationMethods = Avalonia.Data.ValidationMethods.All
+                Path = nameof(model.MustBePositive),
+                Mode = BindingMode.TwoWay,
+                EnableValidation = true,
             };
-            target.Bind(Control.TagProperty, binding);
 
-            target.Tag = "";
-            Assert.False(target.ValidationStatus.IsValid);
+            control.Bind(TestControl.ValidationTestProperty, binding);
+            control.DataContext = model;
+
+
+            control.ValidationTest = -5;
+            Assert.Contains(control.Classes, x => x == ":invalid");
+            control.ValidationTest = 5;
+            Assert.DoesNotContain(control.Classes, x => x == ":invalid");
+        }
+
+        private class TestControl : Control
+        {
+            public static readonly StyledProperty<int> ValidationTestProperty
+                = AvaloniaProperty.Register<TestControl, int>(nameof(ValidationTest), 1, defaultBindingMode: BindingMode.TwoWay);
+
+            public int ValidationTest
+            {
+                get
+                {
+                    return GetValue(ValidationTestProperty);
+                }
+                set
+                {
+                    SetValue(ValidationTestProperty, value);
+                }
+            }
+
+            protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+            {
+                if (property == ValidationTestProperty)
+                {
+                    UpdateValidationState(status);
+                }
+            }
+        }
+        
+        private class ValidationTestModel
+        {
+            private int mustBePositive;
+
+            public int MustBePositive
+            {
+                get { return mustBePositive; }
+                set
+                {
+                    if (value <= 0)
+                    {
+                        throw new ArgumentOutOfRangeException(nameof(value));
+                    }
+                    mustBePositive = value;
+                }
+            }
         }
     }
 }