Browse Source

Merge branch 'master' into safe_insets

Max Katz 2 years ago
parent
commit
49b10d61cb
43 changed files with 1182 additions and 486 deletions
  1. 93 11
      nukebuild/BuildTasksPatcher.cs
  2. 5 0
      src/Avalonia.Base/AvaloniaObject.cs
  3. 1 3
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  4. 9 4
      src/Avalonia.Base/AvaloniaProperty.cs
  5. 17 1
      src/Avalonia.Base/AvaloniaPropertyMetadata.cs
  6. 3 24
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  7. 1 0
      src/Avalonia.Base/Input/MouseDevice.cs
  8. 1 0
      src/Avalonia.Base/Input/PenDevice.cs
  9. 61 15
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  10. 10 1
      src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
  11. 5 0
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  12. 26 25
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  13. 19 7
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  14. 11 0
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  15. 8 0
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  16. 3 3
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  17. 9 105
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  18. 133 0
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs
  19. 0 94
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  20. 2 1
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  21. 4 2
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  22. 2 1
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  23. 21 16
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  24. 1 0
      src/Avalonia.Base/StyledElement.cs
  25. 4 2
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  26. 1 1
      src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
  27. 8 0
      src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
  28. 9 1
      src/Avalonia.Base/Styling/Setter.cs
  29. 21 0
      src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs
  30. 1 1
      src/Avalonia.Controls/Control.cs
  31. 6 0
      src/Avalonia.Controls/Image.cs
  32. 15 0
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  33. 10 13
      src/Avalonia.Themes.Fluent/Controls/AdornerLayer.xaml
  34. 1 1
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  35. 13 0
      src/Avalonia.Themes.Simple/Controls/AdornerLayer.xaml
  36. 0 11
      src/Avalonia.Themes.Simple/Controls/FocusAdorner.xaml
  37. 1 1
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
  38. 3 4
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  39. 207 93
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  40. 1 0
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  41. 53 3
      tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
  42. 13 4
      tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
  43. 370 38
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs

+ 93 - 11
nukebuild/BuildTasksPatcher.cs

@@ -4,9 +4,58 @@ using System.IO.Compression;
 using System.Linq;
 using ILRepacking;
 using Mono.Cecil;
+using Mono.Cecil.Cil;
 
 public class BuildTasksPatcher
 {
+    /// <summary>
+    /// This helper class, avoid argument null exception
+    /// when cecil write AssemblyNameDefinition on MemoryStream.
+    /// </summary>
+    private class Wrapper : ISymbolWriterProvider
+    {
+        readonly ISymbolWriterProvider _provider;
+        readonly string _filename;
+
+        public Wrapper(ISymbolWriterProvider provider, string filename)
+        {
+            _provider = provider;
+            _filename = filename;
+        }
+
+        public ISymbolWriter GetSymbolWriter(ModuleDefinition module, string fileName) =>
+            _provider.GetSymbolWriter(module, string.IsNullOrWhiteSpace(fileName) ? _filename : fileName);
+
+        public ISymbolWriter GetSymbolWriter(ModuleDefinition module, Stream symbolStream) =>
+            _provider.GetSymbolWriter(module, symbolStream);
+    }
+
+    private static string GetSourceLinkInfo(string path)
+    {
+        try
+        {
+            using (var asm = AssemblyDefinition.ReadAssembly(path,
+                new ReaderParameters
+                {
+                    ReadWrite = true,
+                    InMemory = true,
+                    ReadSymbols = true,
+                    SymbolReaderProvider = new DefaultSymbolReaderProvider(false),
+                }))
+            {
+                if (asm.MainModule.CustomDebugInformations?.OfType<SourceLinkDebugInformation>()?.FirstOrDefault() is { } sli)
+                {
+                    return sli.Content;
+                }
+            }
+        }
+        catch
+        {
+
+        }
+        return null;
+    }
+
     public static void PatchBuildTasksInPackage(string packagePath)
     {
         using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite),
@@ -19,7 +68,7 @@ public class BuildTasksPatcher
                 {
                     var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
                     Directory.CreateDirectory(tempDir);
-                    var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll");
+                    var temp = Path.Combine(tempDir, entry.Name);
                     var output = temp + ".output";
                     File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName,
                         Path.Combine(tempDir, "Microsoft.Build.Framework.dll"));
@@ -27,41 +76,74 @@ public class BuildTasksPatcher
                     try
                     {
                         entry.ExtractToFile(temp, true);
+                        // Get Original SourceLinkInfo Content
+                        var sourceLinkInfoContent = GetSourceLinkInfo(temp);
                         var repack = new ILRepacking.ILRepack(new RepackOptions()
                         {
                             Internalize = true,
                             InputAssemblies = new[]
                             {
-                                temp, typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0]
-                                    .FullyQualifiedName,
+                                temp,
+                                typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0].FullyQualifiedName,
                                 typeof(Mono.Cecil.Rocks.MethodBodyRocks).Assembly.GetModules()[0].FullyQualifiedName,
                                 typeof(Mono.Cecil.Pdb.PdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName,
-                                typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName
-                                
+                                typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName,
                             },
-                            SearchDirectories = new string[0],
+                            SearchDirectories = Array.Empty<string>(),
+                            DebugInfo = true, // Allowed read debug info
                             OutputFile = output
                         });
                         repack.Repack();
 
-
                         // 'hurr-durr assembly with the same name is already loaded' prevention
                         using (var asm = AssemblyDefinition.ReadAssembly(output,
-                            new ReaderParameters { ReadWrite = true, InMemory = true, }))
+                            new ReaderParameters
+                            {
+                                ReadWrite = true,
+                                InMemory = true,
+                                ReadSymbols = true,
+                                SymbolReaderProvider = new DefaultSymbolReaderProvider(false),
+                            }))
                         {
                             asm.Name = new AssemblyNameDefinition(
                                 "Avalonia.Build.Tasks."
                                 + Guid.NewGuid().ToString().Replace("-", ""),
                                 new Version(0, 0, 0));
-                            asm.Write(patched);
+
+                            var mainModule = asm.MainModule;
+
+                            // If we have SourceLink info copy to patched assembly.
+                            if (!string.IsNullOrEmpty(sourceLinkInfoContent))
+                            {
+                                mainModule.CustomDebugInformations.Add(new SourceLinkDebugInformation(sourceLinkInfoContent));
+                            }
+
+                            // Try to get SymbolWriter if it has it
+                            var reader = mainModule.SymbolReader;
+                            var hasDebugInfo = reader is not null;
+                            var proivder = reader?.GetWriterProvider() is ISymbolWriterProvider p
+                                ? new Wrapper(p, "Avalonia.Build.Tasks.dll")
+                                : default(ISymbolWriterProvider);
+
+                            var parameters = new WriterParameters
+                            {
+#if ISNETFULLFRAMEWORK
+                                StrongNameKeyPair = signingStep.KeyPair,
+#endif
+                                WriteSymbols = hasDebugInfo,
+                                SymbolWriterProvider = proivder,
+                                DeterministicMvid = hasDebugInfo,
+                            };
+                            asm.Write(patched, parameters);
                             patched.Position = 0;
                         }
+
                     }
                     finally
                     {
                         try
                         {
-                            if(Directory.Exists(tempDir))
+                            if (Directory.Exists(tempDir))
                                 Directory.Delete(tempDir, true);
                         }
                         catch
@@ -79,4 +161,4 @@ public class BuildTasksPatcher
             }
         }
     }
-}
+}

+ 5 - 0
src/Avalonia.Base/AvaloniaObject.cs

@@ -784,6 +784,11 @@ namespace Avalonia
             }
         }
 
+        internal void OnUpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
+        {
+            UpdateDataValidation(property, state, error);
+        }
+
         /// <summary>
         /// Gets a description of an observable that van be used in logs.
         /// </summary>

+ 1 - 3
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@@ -199,13 +199,11 @@ namespace Avalonia
             property = property ?? throw new ArgumentNullException(nameof(property));
             binding = binding ?? throw new ArgumentNullException(nameof(binding));
 
-            var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
-
             var result = binding.Initiate(
                 target,
                 property,
                 anchor,
-                metadata?.EnableDataValidation ?? false);
+                property.GetMetadata(target.GetType()).EnableDataValidation ?? false);
 
             if (result != null)
             {

+ 9 - 4
src/Avalonia.Base/AvaloniaProperty.cs

@@ -227,6 +227,7 @@ namespace Avalonia
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
         /// <param name="validate">A value validation callback.</param>
         /// <param name="coerce">A value coercion callback.</param>
+        /// <param name="enableDataValidation">Whether the property is interested in data validation.</param>
         /// <returns>A <see cref="StyledProperty{TValue}"/></returns>
         public static StyledProperty<TValue> Register<TOwner, TValue>(
             string name,
@@ -234,7 +235,8 @@ namespace Avalonia
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
             Func<TValue, bool>? validate = null,
-            Func<AvaloniaObject, TValue, TValue>? coerce = null)
+            Func<AvaloniaObject, TValue, TValue>? coerce = null,
+            bool enableDataValidation = false)
                 where TOwner : AvaloniaObject
         {
             _ = name ?? throw new ArgumentNullException(nameof(name));
@@ -242,7 +244,8 @@ namespace Avalonia
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
                 defaultBindingMode: defaultBindingMode,
-                coerce: coerce);
+                coerce: coerce,
+                enableDataValidation: enableDataValidation);
 
             var result = new StyledProperty<TValue>(
                 name,
@@ -253,7 +256,7 @@ namespace Avalonia
             AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
             return result;
         }
-        
+
         /// <inheritdoc cref="Register{TOwner, TValue}" />
         /// <param name="notifying">
         /// A method that gets called before and after the property starts being notified on an
@@ -267,6 +270,7 @@ namespace Avalonia
             BindingMode defaultBindingMode,
             Func<TValue, bool>? validate,
             Func<AvaloniaObject, TValue, TValue>? coerce,
+            bool enableDataValidation,
             Action<AvaloniaObject, bool>? notifying)
                 where TOwner : AvaloniaObject
         {
@@ -275,7 +279,8 @@ namespace Avalonia
             var metadata = new StyledPropertyMetadata<TValue>(
                 defaultValue,
                 defaultBindingMode: defaultBindingMode,
-                coerce: coerce);
+                coerce: coerce,
+                enableDataValidation: enableDataValidation);
 
             var result = new StyledProperty<TValue>(
                 name,

+ 17 - 1
src/Avalonia.Base/AvaloniaPropertyMetadata.cs

@@ -13,10 +13,13 @@ namespace Avalonia
         /// Initializes a new instance of the <see cref="AvaloniaPropertyMetadata"/> class.
         /// </summary>
         /// <param name="defaultBindingMode">The default binding mode.</param>
+        /// <param name="enableDataValidation">Whether the property is interested in data validation.</param>
         public AvaloniaPropertyMetadata(
-            BindingMode defaultBindingMode = BindingMode.Default)
+            BindingMode defaultBindingMode = BindingMode.Default,
+            bool? enableDataValidation = null)
         {
             _defaultBindingMode = defaultBindingMode;
+            EnableDataValidation = enableDataValidation;
         }
 
         /// <summary>
@@ -31,6 +34,17 @@ namespace Avalonia
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether the property is interested in data validation.
+        /// </summary>
+        /// <remarks>
+        /// Data validation is validation performed at the target of a binding, for example in a
+        /// view model using the INotifyDataErrorInfo interface. Only certain properties on a
+        /// control (such as a TextBox's Text property) will be interested in receiving data
+        /// validation messages so this feature must be explicitly enabled by setting this flag.
+        /// </remarks>
+        public bool? EnableDataValidation { get; private set; }
+
         /// <summary>
         /// Merges the metadata with the base metadata.
         /// </summary>
@@ -44,6 +58,8 @@ namespace Avalonia
             {
                 _defaultBindingMode = baseMetadata.DefaultBindingMode;
             }
+
+            EnableDataValidation ??= baseMetadata.EnableDataValidation;
         }
     }
 }

+ 3 - 24
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@@ -21,10 +21,9 @@ namespace Avalonia
             TValue unsetValue = default!,
             BindingMode defaultBindingMode = BindingMode.Default,
             bool? enableDataValidation = null)
-                : base(defaultBindingMode)
+                : base(defaultBindingMode, enableDataValidation)
         {
             UnsetValue = unsetValue;
-            EnableDataValidation = enableDataValidation;
         }
 
         /// <summary>
@@ -32,16 +31,6 @@ namespace Avalonia
         /// </summary>
         public TValue UnsetValue { get; private set; }
 
-        /// <summary>
-        /// Gets a value indicating whether the property is interested in data validation.
-        /// </summary>
-        /// <remarks>
-        /// Data validation is validation performed at the target of a binding, for example in a
-        /// view model using the INotifyDataErrorInfo interface. Only certain properties on a
-        /// control (such as a TextBox's Text property) will be interested in receiving data
-        /// validation messages so this feature must be explicitly enabled by setting this flag.
-        /// </remarks>
-        public bool? EnableDataValidation { get; private set; }
 
         /// <inheritdoc/>
         object? IDirectPropertyMetadata.UnsetValue => UnsetValue;
@@ -51,19 +40,9 @@ namespace Avalonia
         {
             base.Merge(baseMetadata, property);
 
-            var src = baseMetadata as DirectPropertyMetadata<TValue>;
-
-            if (src != null)
+            if (baseMetadata is DirectPropertyMetadata<TValue> src)
             {
-                if (UnsetValue == null)
-                {
-                    UnsetValue = src.UnsetValue;
-                }
-
-                if (EnableDataValidation == null)
-                {
-                    EnableDataValidation = src.EnableDataValidation;
-                }
+                UnsetValue ??= src.UnsetValue;
             }
         }
     }

+ 1 - 0
src/Avalonia.Base/Input/MouseDevice.cs

@@ -184,6 +184,7 @@ namespace Avalonia.Input
 
                 source?.RaiseEvent(e);
                 _pointer.Capture(null);
+                _lastMouseDownButton = default;
                 return e.Handled;
             }
 

+ 1 - 0
src/Avalonia.Base/Input/PenDevice.cs

@@ -131,6 +131,7 @@ namespace Avalonia.Input
 
                 source?.RaiseEvent(e);
                 pointer.Capture(null);
+                _lastMouseDownButton = default;
                 return e.Handled;
             }
 

+ 61 - 15
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@@ -16,27 +16,37 @@ namespace Avalonia.PropertyStore
         private IDisposable? _subscription;
         private bool _hasValue;
         private TValue? _value;
-        private TValue? _defaultValue;
-        private bool _isDefaultValueInitialized;
+        private UncommonFields? _uncommon;
 
         protected BindingEntryBase(
+            AvaloniaObject target,
             ValueFrame frame,
             AvaloniaProperty property,
             IObservable<BindingValue<TSource>> source)
+            : this(target, frame, property, (object)source)
         {
-            Frame = frame;
-            Source = source;
-            Property = property;
         }
 
         protected BindingEntryBase(
+            AvaloniaObject target,
             ValueFrame frame,
             AvaloniaProperty property,
             IObservable<TSource> source)
+            : this(target, frame, property, (object)source)
+        {
+        }
+
+        private BindingEntryBase(
+            AvaloniaObject target,
+            ValueFrame frame,
+            AvaloniaProperty property,
+            object source)
         {
             Frame = frame;
-            Source = source;
             Property = property;
+            Source = source;
+            if (property.GetMetadata(target.GetType()).EnableDataValidation == true)
+                _uncommon = new() { _hasDataValidation = true };
         }
 
         public bool HasValue
@@ -68,6 +78,20 @@ namespace Avalonia.PropertyStore
             return _value!;
         }
 
+        public bool GetDataValidationState(out BindingValueType state, out Exception? error)
+        {
+            if (_uncommon?._hasDataValidation == true)
+            {
+                state = _uncommon._dataValidationState;
+                error = _uncommon._dataValidationError;
+                return true;
+            }
+
+            state = BindingValueType.Value;
+            error = null;
+            return false;
+        }
+
         public void Start() => Start(true);
 
         public void OnCompleted() => BindingCompleted();
@@ -111,16 +135,28 @@ namespace Avalonia.PropertyStore
         {
             static void Execute(BindingEntryBase<TValue, TSource> instance, BindingValue<TValue> value)
             {
-                if (instance.Frame.Owner is null)
+                if (instance.Frame.Owner is not { } valueStore)
                     return;
 
-                LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value);
+                var owner = valueStore.Owner;
+                var property = instance.Property;
+                var originalType = value.Type;
+
+                LoggingUtils.LogIfNecessary(owner, property, value);
 
-                var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue();
+                if (!value.HasValue && value.Type != BindingValueType.DataValidationError)
+                    value = value.WithValue(instance.GetCachedDefaultValue());
 
-                if (!instance._hasValue || !EqualityComparer<TValue>.Default.Equals(instance._value, effectiveValue))
+                if (instance._uncommon?._hasDataValidation == true)
                 {
-                    instance._value = effectiveValue;
+                    instance._uncommon._dataValidationState = value.Type;
+                    instance._uncommon._dataValidationError = value.Error;
+                }
+
+                if (value.HasValue &&
+                    (!instance._hasValue || !EqualityComparer<TValue>.Default.Equals(instance._value, value.Value)))
+                {
+                    instance._value = value.Value;
                     instance._hasValue = true;
                     if (instance._subscription is not null && instance._subscription != s_creatingQuiet)
                         instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority);
@@ -152,13 +188,23 @@ namespace Avalonia.PropertyStore
 
         private TValue GetCachedDefaultValue()
         {
-            if (!_isDefaultValueInitialized)
+            if (_uncommon?._isDefaultValueInitialized != true)
             {
-                _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
-                _isDefaultValueInitialized = true;
+                _uncommon ??= new();
+                _uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
+                _uncommon._isDefaultValueInitialized = true;
             }
 
-            return _defaultValue!;
+            return _uncommon._defaultValue!;
+        }
+
+        private class UncommonFields
+        {
+            public TValue? _defaultValue;
+            public bool _isDefaultValueInitialized;
+            public bool _hasDataValidation;
+            public BindingValueType _dataValidationState;
+            public Exception? _dataValidationError;
         }
     }
 }

+ 10 - 1
src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs

@@ -9,11 +9,13 @@ namespace Avalonia.PropertyStore
         IDisposable
     {
         private readonly ValueStore _owner;
+        private readonly bool _hasDataValidation;
         private IDisposable? _subscription;
 
         public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
         {
             _owner = owner;
+            _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
             Property = property;
         }
 
@@ -33,10 +35,17 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
+            OnCompleted();
+        }
+
+        public void OnCompleted()
+        {
             _owner.OnLocalValueBindingCompleted(Property, this);
+
+            if (_hasDataValidation)
+                _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
         }
 
-        public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
         public void OnError(Exception error) => OnCompleted();
 
         public void OnNext(T value)

+ 5 - 0
src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs

@@ -10,11 +10,13 @@ namespace Avalonia.PropertyStore
         IDisposable
     {
         private readonly ValueStore _owner;
+        private readonly bool _hasDataValidation;
         private IDisposable? _subscription;
 
         public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
         {
             _owner = owner;
+            _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
             Property = property;
         }
 
@@ -30,6 +32,9 @@ namespace Avalonia.PropertyStore
             _subscription?.Dispose();
             _subscription = null;
             _owner.OnLocalValueBindingCompleted(Property, this);
+
+            if (_hasDataValidation)
+                _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
         }
 
         public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);

+ 26 - 25
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@@ -11,9 +11,6 @@ namespace Avalonia.PropertyStore
     /// </remarks>
     internal abstract class EffectiveValue
     {
-        private IValueEntry? _valueEntry;
-        private IValueEntry? _baseValueEntry;
-
         /// <summary>
         /// Gets the current effective value as a boxed value.
         /// </summary>
@@ -29,6 +26,16 @@ namespace Avalonia.PropertyStore
         /// </summary>
         public BindingPriority BasePriority { get; protected set; }
 
+        /// <summary>
+        /// Gets the active value entry for the current effective value.
+        /// </summary>
+        public IValueEntry? ValueEntry { get; private set; }
+
+        /// <summary>
+        /// Gets the active value entry for the current base value.
+        /// </summary>
+        public IValueEntry? BaseValueEntry { get; private set; }
+
         /// <summary>
         /// Gets a value indicating whether the <see cref="Value"/> was overridden by a call to 
         /// <see cref="AvaloniaObject.SetCurrentValue{T}"/>.
@@ -63,14 +70,14 @@ namespace Avalonia.PropertyStore
         {
             if (Priority == BindingPriority.Unset)
             {
-                _valueEntry?.Unsubscribe();
-                _valueEntry = null;
+                ValueEntry?.Unsubscribe();
+                ValueEntry = null;
             }
 
             if (BasePriority == BindingPriority.Unset)
             {
-                _baseValueEntry?.Unsubscribe();
-                _baseValueEntry = null;
+                BaseValueEntry?.Unsubscribe();
+                BaseValueEntry = null;
             }
         }
 
@@ -135,40 +142,34 @@ namespace Avalonia.PropertyStore
                 // value, then the current entry becomes our base entry.
                 if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited)
                 {
-                    Debug.Assert(_valueEntry is not null);
-                    _baseValueEntry = _valueEntry;
-                    _valueEntry = null;
+                    Debug.Assert(ValueEntry is not null);
+                    BaseValueEntry = ValueEntry;
+                    ValueEntry = null;
                 }
 
-                if (_valueEntry != entry)
+                if (ValueEntry != entry)
                 {
-                    _valueEntry?.Unsubscribe();
-                    _valueEntry = entry;
+                    ValueEntry?.Unsubscribe();
+                    ValueEntry = entry;
                 }
             }
             else if (Priority <= BindingPriority.Animation)
             {
                 // We've received a non-animation value and have an active animation value, so the
                 // new entry becomes our base entry.
-                if (_baseValueEntry != entry)
+                if (BaseValueEntry != entry)
                 {
-                    _baseValueEntry?.Unsubscribe();
-                    _baseValueEntry = entry;
+                    BaseValueEntry?.Unsubscribe();
+                    BaseValueEntry = entry;
                 }
             }
-            else if (_valueEntry != entry)
+            else if (ValueEntry != entry)
             {
                 // Both the current value and the new value are non-animation values, so the new
                 // entry replaces the existing entry.
-                _valueEntry?.Unsubscribe();
-                _valueEntry = entry;
+                ValueEntry?.Unsubscribe();
+                ValueEntry = entry;
             }
         }
-
-        protected void UnsubscribeValueEntries()
-        {
-            _valueEntry?.Unsubscribe();
-            _baseValueEntry?.Unsubscribe();
-        }
     }
 }

+ 19 - 7
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
+using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
 
 namespace Avalonia.PropertyStore
 {
@@ -61,6 +62,12 @@ namespace Avalonia.PropertyStore
             UpdateValueEntry(value, priority);
 
             SetAndRaiseCore(owner,  (StyledProperty<T>)value.Property, GetValue(value), priority, false);
+
+            if (priority > BindingPriority.LocalValue &&
+                value.GetDataValidationState(out var state, out var error))
+            {
+                owner.Owner.OnUpdateDataValidation(value.Property, state, error);
+            }
         }
 
         public void SetLocalValueAndRaise(
@@ -128,12 +135,10 @@ namespace Avalonia.PropertyStore
 
         public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
         {
-            UnsubscribeValueEntries();
-            DisposeAndRaiseUnset(owner, (StyledProperty<T>)property);
-        }
+            ValueEntry?.Unsubscribe();
+            BaseValueEntry?.Unsubscribe();
 
-        public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty<T> property)
-        {
+            var p = (StyledProperty<T>)property;
             BindingPriority priority;
             T oldValue;
 
@@ -150,9 +155,16 @@ namespace Avalonia.PropertyStore
 
             if (!EqualityComparer<T>.Default.Equals(oldValue, Value))
             {
-                owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true);
+                owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true);
                 if (property.Inherits)
-                    owner.OnInheritedEffectiveValueDisposed(property, Value);
+                    owner.OnInheritedEffectiveValueDisposed(p, Value);
+            }
+
+            if (ValueEntry?.GetDataValidationState(out _, out _) ??
+                BaseValueEntry?.GetDataValidationState(out _, out _) ??
+                false)
+            {
+                owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null);
             }
         }
 

+ 11 - 0
src/Avalonia.Base/PropertyStore/IValueEntry.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Data;
 
 namespace Avalonia.PropertyStore
 {
@@ -22,6 +23,16 @@ namespace Avalonia.PropertyStore
         /// </exception>
         object? GetValue();
 
+        /// <summary>
+        /// Gets the data validation state if supported.
+        /// </summary>
+        /// <param name="state">The binding validation state.</param>
+        /// <param name="error">The current binding error, if any.</param>
+        /// <returns>
+        /// True if the entry supports data validation, otherwise false.
+        /// </returns>
+        bool GetDataValidationState(out BindingValueType state, out Exception? error);
+
         /// <summary>
         /// Called when the value entry is removed from the value store.
         /// </summary>

+ 8 - 0
src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Data;
 
 namespace Avalonia.PropertyStore
 {
@@ -27,5 +28,12 @@ namespace Avalonia.PropertyStore
 
         object? IValueEntry.GetValue() => _value;
         T IValueEntry<T>.GetValue() => _value;
+
+        bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
+        {
+            state = BindingValueType.Value;
+            error = null;
+            return false;
+        }
     }
 }

+ 3 - 3
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
             StyledProperty<T> property,
             IObservable<BindingValue<T>> source)
         {
-            var e = new TypedBindingEntry<T>(this, property, source);
+            var e = new TypedBindingEntry<T>(Owner!.Owner, this, property, source);
             Add(e);
             return e;
         }
@@ -27,7 +27,7 @@ namespace Avalonia.PropertyStore
             StyledProperty<T> property,
             IObservable<T> source)
         {
-            var e = new TypedBindingEntry<T>(this, property, source);
+            var e = new TypedBindingEntry<T>(Owner!.Owner, this, property, source);
             Add(e);
             return e;
         }
@@ -36,7 +36,7 @@ namespace Avalonia.PropertyStore
             StyledProperty<T> property,
             IObservable<object?> source)
         {
-            var e = new SourceUntypedBindingEntry<T>(this, property, source);
+            var e = new SourceUntypedBindingEntry<T>(Owner!.Owner, this, property, source);
             Add(e);
             return e;
         }

+ 9 - 105
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@@ -1,121 +1,25 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
-using Avalonia.Threading;
 
 namespace Avalonia.PropertyStore
 {
-    internal class LocalValueBindingObserver<T> : IObserver<T>,
-        IObserver<BindingValue<T>>,
-        IDisposable
+    internal class LocalValueBindingObserver<T> : LocalValueBindingObserverBase<T>,
+        IObserver<object?>
     {
-        private readonly ValueStore _owner;
-        private IDisposable? _subscription;
-        private T? _defaultValue;
-        private bool _isDefaultValueInitialized;
-
         public LocalValueBindingObserver(ValueStore owner, StyledProperty<T> property)
+            : base(owner, property)
         {
-            _owner = owner;
-            Property = property;
         }
 
-        public StyledProperty<T> Property { get;}
-
-        public void Start(IObservable<T> source)
-        {
-            _subscription = source.Subscribe(this);
-        }
-
-        public void Start(IObservable<BindingValue<T>> source)
-        {
-            _subscription = source.Subscribe(this);
-        }
+        public void Start(IObservable<object?> source) => _subscription = source.Subscribe(this);
 
-        public void Dispose()
+        [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
+        public void OnNext(object? value)
         {
-            _subscription?.Dispose();
-            _subscription = null;
-            _owner.OnLocalValueBindingCompleted(Property, this);
-        }
-
-        public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
-        public void OnError(Exception error) => OnCompleted();
-
-        public void OnNext(T value)
-        {
-            static void Execute(LocalValueBindingObserver<T> instance, T value)
-            {
-                var owner = instance._owner;
-                var property = instance.Property;
-
-                if (property.ValidateValue?.Invoke(value) == false)
-                    value = instance.GetCachedDefaultValue();
-
-                owner.SetValue(property, value, BindingPriority.LocalValue);
-            }
-
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                Execute(this, value);
-            }
-            else
-            {
-                // To avoid allocating closure in the outer scope we need to capture variables
-                // locally. This allows us to skip most of the allocations when on UI thread.
-                var instance = this;
-                var newValue = value;
-                Dispatcher.UIThread.Post(() => Execute(instance, newValue));
-            }
-        }
-
-        public void OnNext(BindingValue<T> value)
-        {
-            static void Execute(LocalValueBindingObserver<T> instance, BindingValue<T> value)
-            {
-                var owner = instance._owner;
-                var property = instance.Property;
-
-                LoggingUtils.LogIfNecessary(owner.Owner, property, value);
-
-                if (value.HasValue)
-                {
-                    var effectiveValue = value.Value;
-                    if (property.ValidateValue?.Invoke(effectiveValue) == false)
-                        effectiveValue = instance.GetCachedDefaultValue();
-                    owner.SetValue(property, effectiveValue, BindingPriority.LocalValue);
-                }
-                else
-                {
-                    owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue);
-                }
-            }
-
-            if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError)
+            if (value == BindingOperations.DoNothing)
                 return;
-
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                Execute(this, value);
-            }
-            else
-            {
-                // To avoid allocating closure in the outer scope we need to capture variables
-                // locally. This allows us to skip most of the allocations when on UI thread.
-                var instance = this;
-                var newValue = value;
-                Dispatcher.UIThread.Post(() => Execute(instance, newValue));
-            }
-        }
-
-        private T GetCachedDefaultValue()
-        {
-            if (!_isDefaultValueInitialized)
-            {
-                _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
-                _isDefaultValueInitialized = true;
-            }
-
-            return _defaultValue!;
+            base.OnNext(BindingValue<T>.FromUntyped(value, Property.PropertyType));
         }
     }
 }

+ 133 - 0
src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs

@@ -0,0 +1,133 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Threading;
+
+namespace Avalonia.PropertyStore
+{
+    internal class LocalValueBindingObserverBase<T> : IObserver<T>,
+        IObserver<BindingValue<T>>,
+        IDisposable
+    {
+        private readonly ValueStore _owner;
+        private readonly bool _hasDataValidation;
+        protected IDisposable? _subscription;
+        private T? _defaultValue;
+        private bool _isDefaultValueInitialized;
+
+        protected LocalValueBindingObserverBase(ValueStore owner, StyledProperty<T> property)
+        {
+            _owner = owner;
+            Property = property;
+            _hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false;
+        }
+
+        public StyledProperty<T> Property { get;}
+
+        public void Start(IObservable<T> source)
+        {
+            _subscription = source.Subscribe(this);
+        }
+
+        public void Start(IObservable<BindingValue<T>> source)
+        {
+            _subscription = source.Subscribe(this);
+        }
+
+        public void Dispose()
+        {
+            _subscription?.Dispose();
+            _subscription = null;
+            OnCompleted();
+        }
+
+        public void OnCompleted()
+        {
+            if (_hasDataValidation)
+                _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
+
+            _owner.OnLocalValueBindingCompleted(Property, this);
+        }
+
+        public void OnError(Exception error) => OnCompleted();
+
+        public void OnNext(T value)
+        {
+            static void Execute(LocalValueBindingObserverBase<T> instance, T value)
+            {
+                var owner = instance._owner;
+                var property = instance.Property;
+
+                if (property.ValidateValue?.Invoke(value) == false)
+                    value = instance.GetCachedDefaultValue();
+
+                owner.SetLocalValue(property, value);
+
+                if (instance._hasDataValidation)
+                    owner.Owner.OnUpdateDataValidation(property, BindingValueType.Value, null);
+            }
+
+            if (Dispatcher.UIThread.CheckAccess())
+            {
+                Execute(this, value);
+            }
+            else
+            {
+                // To avoid allocating closure in the outer scope we need to capture variables
+                // locally. This allows us to skip most of the allocations when on UI thread.
+                var instance = this;
+                var newValue = value;
+                Dispatcher.UIThread.Post(() => Execute(instance, newValue));
+            }
+        }
+
+        public void OnNext(BindingValue<T> value)
+        {
+            static void Execute(LocalValueBindingObserverBase<T> instance, BindingValue<T> value)
+            {
+                var owner = instance._owner;
+                var property = instance.Property;
+                var originalType = value.Type;
+
+                LoggingUtils.LogIfNecessary(owner.Owner, property, value);
+
+                // Revert to the default value if the binding value fails validation, or if
+                // there was no value (though not if there was a data validation error).
+                if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) ||
+                    (!value.HasValue && value.Type != BindingValueType.DataValidationError))
+                    value = value.WithValue(instance.GetCachedDefaultValue());
+
+                if (value.HasValue)
+                    owner.SetLocalValue(property, value.Value);
+                if (instance._hasDataValidation)
+                    owner.Owner.OnUpdateDataValidation(property, originalType, value.Error);
+            }
+
+            if (value.Type is BindingValueType.DoNothing)
+                return;
+
+            if (Dispatcher.UIThread.CheckAccess())
+            {
+                Execute(this, value);
+            }
+            else
+            {
+                // To avoid allocating closure in the outer scope we need to capture variables
+                // locally. This allows us to skip most of the allocations when on UI thread.
+                var instance = this;
+                var newValue = value;
+                Dispatcher.UIThread.Post(() => Execute(instance, newValue));
+            }
+        }
+
+        private T GetCachedDefaultValue()
+        {
+            if (!_isDefaultValueInitialized)
+            {
+                _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
+                _isDefaultValueInitialized = true;
+            }
+
+            return _defaultValue!;
+        }
+    }
+}

+ 0 - 94
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@@ -1,94 +0,0 @@
-using System;
-using Avalonia.Data;
-using Avalonia.Threading;
-
-namespace Avalonia.PropertyStore
-{
-    internal class LocalValueUntypedBindingObserver<T> : IObserver<object?>,
-        IDisposable
-    {
-        private readonly ValueStore _owner;
-        private IDisposable? _subscription;
-        private T? _defaultValue;
-        private bool _isDefaultValueInitialized;
-
-        public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty<T> property)
-        {
-            _owner = owner;
-            Property = property;
-        }
-
-        public StyledProperty<T> Property { get; }
-
-        public void Start(IObservable<object?> source)
-        {
-            _subscription = source.Subscribe(this);
-        }
-
-        public void Dispose()
-        {
-            _subscription?.Dispose();
-            _subscription = null;
-            _owner.OnLocalValueBindingCompleted(Property, this);
-        }
-
-        public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
-        public void OnError(Exception error) => OnCompleted();
-
-        public void OnNext(object? value)
-        {
-            static void Execute(LocalValueUntypedBindingObserver<T> instance, object? value)
-            {
-                var owner = instance._owner;
-                var property = instance.Property;
-
-                if (value is BindingNotification n)
-                {
-                    value = n.Value;
-                    LoggingUtils.LogIfNecessary(owner.Owner, property, n);
-                }
-
-                if (value == AvaloniaProperty.UnsetValue)
-                {
-                    owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue);
-                }
-                else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue))
-                {
-                    owner.SetValue(property, typedValue, BindingPriority.LocalValue);
-                }
-                else
-                {
-                    owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue);
-                    LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value);
-                }
-            }
-
-            if (value == BindingOperations.DoNothing)
-                return;
-
-            if (Dispatcher.UIThread.CheckAccess())
-            {
-                Execute(this, value);
-            }
-            else if (value != BindingOperations.DoNothing)
-            {
-                // To avoid allocating closure in the outer scope we need to capture variables
-                // locally. This allows us to skip most of the allocations when on UI thread.
-                var instance = this;
-                var newValue = value;
-                Dispatcher.UIThread.Post(() => Execute(instance, newValue));
-            }
-        }
-
-        private T GetCachedDefaultValue()
-        {
-            if (!_isDefaultValueInitialized)
-            {
-                _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
-                _isDefaultValueInitialized = true;
-            }
-
-            return _defaultValue!;
-        }
-    }
-}

+ 2 - 1
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@@ -12,10 +12,11 @@ namespace Avalonia.PropertyStore
         private readonly Func<TTarget, bool>? _validate;
 
         public SourceUntypedBindingEntry(
+            AvaloniaObject target,
             ValueFrame frame, 
             StyledProperty<TTarget> property,
             IObservable<object?> source)
-                : base(frame, property, source)
+                : base(target, frame, property, source)
         {
             _validate = property.ValidateValue;
         }

+ 4 - 2
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@@ -10,18 +10,20 @@ namespace Avalonia.PropertyStore
     internal sealed class TypedBindingEntry<T> : BindingEntryBase<T, T>
     {
         public TypedBindingEntry(
+            AvaloniaObject target,
             ValueFrame frame, 
             StyledProperty<T> property,
             IObservable<T> source)
-                : base(frame, property, source)
+                : base(target, frame, property, source)
         {
         }
 
         public TypedBindingEntry(
+            AvaloniaObject target,
             ValueFrame frame,
             StyledProperty<T> property,
             IObservable<BindingValue<T>> source)
-                : base(frame, property, source)
+                : base(target, frame, property, source)
         {
         }
 

+ 2 - 1
src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs

@@ -12,10 +12,11 @@ namespace Avalonia.PropertyStore
         private readonly Func<object?, bool>? _validate;
 
         public UntypedBindingEntry(
+            AvaloniaObject target,
             ValueFrame frame,
             AvaloniaProperty property,
             IObservable<object?> source)
-            : base(frame, property, source)
+            : base(target, frame, property, source)
         {
             _validate = ((IStyledPropertyAccessor)property).ValidateValue;
         }

+ 21 - 16
src/Avalonia.Base/PropertyStore/ValueStore.cs

@@ -104,7 +104,7 @@ namespace Avalonia.PropertyStore
         {
             if (priority == BindingPriority.LocalValue)
             {
-                var observer = new LocalValueUntypedBindingObserver<T>(this, property);
+                var observer = new LocalValueBindingObserver<T>(this, property);
                 DisposeExistingLocalValueBinding(property);
                 _localValueBindings ??= new();
                 _localValueBindings[property.Id] = observer;
@@ -193,18 +193,7 @@ namespace Avalonia.PropertyStore
             }
             else
             {
-                if (TryGetEffectiveValue(property, out var existing))
-                {
-                    var effective = (EffectiveValue<T>)existing;
-                    effective.SetLocalValueAndRaise(this, property, value);
-                }
-                else
-                {
-                    var effectiveValue = CreateEffectiveValue(property);
-                    AddEffectiveValue(property, effectiveValue);
-                    effectiveValue.SetLocalValueAndRaise(this, property, value);
-                }
-
+                SetLocalValue(property, value);
                 return null;
             }
         }
@@ -223,6 +212,21 @@ namespace Avalonia.PropertyStore
             }
         }
 
+        public void SetLocalValue<T>(StyledProperty<T> property, T value)
+        {
+            if (TryGetEffectiveValue(property, out var existing))
+            {
+                var effective = (EffectiveValue<T>)existing;
+                effective.SetLocalValueAndRaise(this, property, value);
+            }
+            else
+            {
+                var effectiveValue = CreateEffectiveValue(property);
+                AddEffectiveValue(property, effectiveValue);
+                effectiveValue.SetLocalValueAndRaise(this, property, value);
+            }
+        }
+
         public object? GetValue(AvaloniaProperty property)
         {
             if (_effectiveValues.TryGetValue(property, out var v))
@@ -834,8 +838,6 @@ namespace Avalonia.PropertyStore
                         break;
                 }
 
-                current?.EndReevaluation();
-
                 if (current?.Priority == BindingPriority.Unset)
                 {
                     if (current.BasePriority == BindingPriority.Unset)
@@ -848,6 +850,8 @@ namespace Avalonia.PropertyStore
                         current.RemoveAnimationAndRaise(this, property);
                     }
                 }
+
+                current?.EndReevaluation();
             }
             finally
             {
@@ -919,7 +923,6 @@ namespace Avalonia.PropertyStore
                 for (var i = _effectiveValues.Count - 1; i >= 0; --i)
                 {
                     _effectiveValues.GetKeyValue(i, out var key, out var e);
-                    e.EndReevaluation();
 
                     if (e.Priority == BindingPriority.Unset)
                     {
@@ -929,6 +932,8 @@ namespace Avalonia.PropertyStore
                         if (i > _effectiveValues.Count)
                             break;
                     }
+
+                    e.EndReevaluation();
                 }
             }
             finally

+ 1 - 0
src/Avalonia.Base/StyledElement.cs

@@ -46,6 +46,7 @@ namespace Avalonia
                 defaultBindingMode: BindingMode.OneWay,
                 validate: null,
                 coerce: null,
+                enableDataValidation: false,
                 notifying: DataContextNotifying);
 
         /// <summary>

+ 4 - 2
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@@ -16,11 +16,13 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="defaultBindingMode">The default binding mode.</param>
         /// <param name="coerce">A value coercion callback.</param>
+        /// <param name="enableDataValidation">Whether the property is interested in data validation.</param>
         public StyledPropertyMetadata(
             Optional<TValue> defaultValue = default,
             BindingMode defaultBindingMode = BindingMode.Default,
-            Func<AvaloniaObject, TValue, TValue>? coerce = null)
-                : base(defaultBindingMode)
+            Func<AvaloniaObject, TValue, TValue>? coerce = null,
+            bool enableDataValidation = false)
+                : base(defaultBindingMode, enableDataValidation)
         {
             _defaultValue = defaultValue;
             CoerceValue = coerce;

+ 1 - 1
src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Styling
             AvaloniaProperty property,
             BindingMode mode,
             IObservable<object?> source)
-            : base(instance, property, source)
+            : base(target, instance, property, source)
         {
             _target = target;
             _mode = mode;

+ 8 - 0
src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Data;
 using Avalonia.PropertyStore;
 
 namespace Avalonia.Styling
@@ -19,6 +20,13 @@ namespace Avalonia.Styling
 
         public object? GetValue() => _value ??= _template.Build();
 
+        bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
+        {
+            state = BindingValueType.Value;
+            error = null;
+            return false;
+        }
+
         void IValueEntry.Unsubscribe() { }
     }
 }

+ 9 - 1
src/Avalonia.Base/Styling/Setter.cs

@@ -90,6 +90,13 @@ namespace Avalonia.Styling
 
         object? IValueEntry.GetValue() => Value;
 
+        bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
+        {
+            state = BindingValueType.Value;
+            error = null;
+            return false;
+        }
+
         private AvaloniaProperty EnsureProperty()
         {
             return Property ?? throw new InvalidOperationException("Setter.Property must be set.");
@@ -99,7 +106,8 @@ namespace Avalonia.Styling
         {
             if (!Property!.IsDirect)
             {
-                var i = binding.Initiate(target, Property)!;
+                var hasDataValidation = Property.GetMetadata(target.GetType()).EnableDataValidation ?? false;
+                var i = binding.Initiate(target, Property, enableDataValidation: hasDataValidation)!;
                 var mode = i.Mode;
 
                 if (mode == BindingMode.Default)

+ 21 - 0
src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs

@@ -0,0 +1,21 @@
+using Avalonia.Automation.Peers;
+
+namespace Avalonia.Controls.Automation.Peers
+{
+    public class ImageAutomationPeer : ControlAutomationPeer
+    {
+        public ImageAutomationPeer(Control owner) : base(owner)
+        {
+        }
+
+        override protected string GetClassNameCore()
+        {
+            return "Image";
+        }
+
+        override protected AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Image;
+        }
+    }
+}

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

@@ -403,7 +403,7 @@ namespace Avalonia.Controls
                 {
                     if (_focusAdorner == null)
                     {
-                        var template = GetValue(FocusAdornerProperty);
+                        var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner;
 
                         if (template != null)
                         {

+ 6 - 0
src/Avalonia.Controls/Image.cs

@@ -1,5 +1,6 @@
 using Avalonia.Automation;
 using Avalonia.Automation.Peers;
+using Avalonia.Controls.Automation.Peers;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using Avalonia.Metadata;
@@ -130,5 +131,10 @@ namespace Avalonia.Controls
                 return new Size();
             }
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer()
+        {
+            return new ImageAutomationPeer(this);
+        }
     }
 }

+ 15 - 0
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@@ -34,6 +34,12 @@ namespace Avalonia.Controls.Primitives
         public static readonly AttachedProperty<Control?> AdornerProperty =
             AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, Control?>("Adorner");
 
+        /// <summary>
+        /// Defines the <see cref="DefaultFocusAdorner"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ITemplate<Control>?> DefaultFocusAdornerProperty =
+            AvaloniaProperty.Register<Control, ITemplate<Control>?>(nameof(DefaultFocusAdorner));
+        
         private static readonly AttachedProperty<AdornedElementInfo?> s_adornedElementInfoProperty =
             AvaloniaProperty.RegisterAttached<AdornerLayer, Visual, AdornedElementInfo?>("AdornedElementInfo");
 
@@ -86,6 +92,15 @@ namespace Avalonia.Controls.Primitives
             visual.SetValue(AdornerProperty, adorner);
         }
 
+        /// <summary>
+        /// Gets or sets the default control's focus adorner.
+        /// </summary>
+        public ITemplate<Control>? DefaultFocusAdorner
+        {
+            get => GetValue(DefaultFocusAdornerProperty);
+            set => SetValue(DefaultFocusAdornerProperty, value);
+        }
+        
         private static void AdornerChanged(AvaloniaPropertyChangedEventArgs<Control?> e)
         {
             if (e.Sender is Visual visual)

+ 10 - 13
src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml → src/Avalonia.Themes.Fluent/Controls/AdornerLayer.xaml

@@ -1,14 +1,11 @@
-<Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-  <Styles.Resources>
-    <Thickness x:Key="SystemControlFocusVisualMargin">0</Thickness>
-    <Thickness x:Key="SystemControlFocusVisualPrimaryThickness">2</Thickness>
-    <Thickness x:Key="SystemControlFocusVisualSecondaryThickness">1</Thickness>
-  </Styles.Resources>
-
-  <!--  HighVisibility FocusAdorner  -->
-  <Style Selector=":is(Control)">
-    <Setter Property="FocusAdorner">
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <Thickness x:Key="SystemControlFocusVisualMargin">0</Thickness>
+  <Thickness x:Key="SystemControlFocusVisualPrimaryThickness">2</Thickness>
+  <Thickness x:Key="SystemControlFocusVisualSecondaryThickness">1</Thickness>
+  
+  <ControlTheme x:Key="{x:Type AdornerLayer}" TargetType="AdornerLayer">
+    <Setter Property="DefaultFocusAdorner">
       <FocusAdornerTemplate>
         <Border BorderThickness="{DynamicResource SystemControlFocusVisualPrimaryThickness}"
                 BorderBrush="{DynamicResource SystemControlFocusVisualPrimaryBrush}"
@@ -18,5 +15,5 @@
         </Border>
       </FocusAdornerTemplate>
     </Setter>
-  </Style>
-</Styles>
+  </ControlTheme>
+</ResourceDictionary>

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@@ -4,6 +4,7 @@
     <Styles.Resources>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
+                <MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/AdornerLayer.xaml" />
                 <MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Button.xaml" />
                 <MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/RadioButton.xaml" />
                 <MergeResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/Expander.xaml" />
@@ -74,6 +75,5 @@
         </ResourceDictionary>
     </Styles.Resources>
 
-    <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml" />
     <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/UserControl.xaml" />
 </Styles>

+ 13 - 0
src/Avalonia.Themes.Simple/Controls/AdornerLayer.xaml

@@ -0,0 +1,13 @@
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <ControlTheme x:Key="{x:Type AdornerLayer}" TargetType="AdornerLayer">
+    <Setter Property="DefaultFocusAdorner">
+      <FocusAdornerTemplate>
+        <Rectangle Margin="1"
+                   Stroke="Black"
+                   StrokeDashArray="1,2"
+                   StrokeThickness="1" />
+      </FocusAdornerTemplate>
+    </Setter>
+  </ControlTheme>
+</ResourceDictionary>

+ 0 - 11
src/Avalonia.Themes.Simple/Controls/FocusAdorner.xaml

@@ -1,11 +0,0 @@
-<Style xmlns="https://github.com/avaloniaui"
-       Selector=":is(Control)">
-  <Setter Property="FocusAdorner">
-    <FocusAdornerTemplate>
-      <Rectangle Margin="1"
-                 Stroke="Black"
-                 StrokeDashArray="1,2"
-                 StrokeThickness="1" />
-    </FocusAdornerTemplate>
-  </Setter>
-</Style>

+ 1 - 1
src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

@@ -3,6 +3,7 @@
   <Styles.Resources>
     <ResourceDictionary>
       <ResourceDictionary.MergedDictionaries>
+        <MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/AdornerLayer.xaml" />
         <MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/Button.xaml" />
         <MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/RadioButton.xaml" />
         <MergeResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/Expander.xaml" />
@@ -72,6 +73,5 @@
     </ResourceDictionary>
   </Styles.Resources>
 
-  <StyleInclude Source="avares://Avalonia.Themes.Simple/Controls/FocusAdorner.xaml" />
   <StyleInclude Source="avares://Avalonia.Themes.Simple/Controls/UserControl.xaml" />
 </Styles>

+ 3 - 4
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -888,7 +888,8 @@ namespace Avalonia.Base.UnitTests
             var target = new Class1();
             var source = new Subject<object?>();
             var called = false;
-            var expectedMessageTemplate = "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})";
+            var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
+            var message = "Unable to convert object 'foo' of type 'System.String' to type 'System.Double'.";
 
             LogCallback checkLogMessage = (level, area, src, mt, pv) =>
             {
@@ -898,9 +899,7 @@ namespace Avalonia.Base.UnitTests
                     src == target &&
                     pv[0].GetType() == typeof(Class1) &&
                     (AvaloniaProperty)pv[1] == Class1.QuxProperty &&
-                    (Type)pv[2] == typeof(double) &&
-                    (string)pv[3] == "foo" &&
-                    (Type)pv[4] == typeof(string))
+                    (string)pv[2] == message)
                 {
                     called = true;
                 }

+ 207 - 93
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

@@ -1,115 +1,212 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Reactive.Subjects;
 using Avalonia.Data;
 using Avalonia.UnitTests;
 using Xunit;
 
+#nullable enable
+
 namespace Avalonia.Base.UnitTests
 {
     public class AvaloniaObjectTests_DataValidation
     {
-        [Fact]
-        public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation()
+        public abstract class TestBase<T>
+            where T : AvaloniaProperty<int>
         {
-            var target = new Class1();
-            var source = new Subject<BindingValue<int>>();
+            [Fact]
+            public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
+            {
+                var target = new Class1();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetNonValidatedProperty();
 
-            target.Bind(Class1.NonValidatedProperty, source);
-            source.OnNext(6);
-            source.OnNext(BindingValue<int>.BindingError(new Exception()));
-            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
-            source.OnNext(6);
+                target.Bind(property, source);
+                source.OnNext(6);
+                source.OnNext(BindingValue<int>.BindingError(new Exception()));
+                source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
+                source.OnNext(6);
 
-            Assert.Empty(target.Notifications);
-        }
+                Assert.Empty(target.Notifications);
+            }
 
-        [Fact]
-        public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
-        {
-            var target = new Class1();
-            var source = new Subject<BindingValue<int>>();
+            [Fact]
+            public void Binding_Validated_Property_Calls_UpdateDataValidation()
+            {
+                var target = new Class1();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetProperty();
+                var error1 = new Exception();
+                var error2 = new Exception();
 
-            target.Bind(Class1.NonValidatedDirectProperty, source);
-            source.OnNext(6);
-            source.OnNext(BindingValue<int>.BindingError(new Exception()));
-            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
-            source.OnNext(6);
+                target.Bind(property, source);
+                source.OnNext(6);
+                source.OnNext(BindingValue<int>.DataValidationError(error1));
+                source.OnNext(BindingValue<int>.BindingError(error2));
+                source.OnNext(7);
 
-            Assert.Empty(target.Notifications);
-        }
+                Assert.Equal(new Notification[]
+                {
+                    new(BindingValueType.Value, 6, null),
+                    new(BindingValueType.DataValidationError, 6, error1),
+                    new(BindingValueType.BindingError, 0, error2),
+                    new(BindingValueType.Value, 7, null),
+                }, target.Notifications);
+            }
 
-        [Fact]
-        public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation()
-        {
-            var target = new Class1();
-            var source = new Subject<BindingValue<int>>();
-
-            target.Bind(Class1.ValidatedDirectIntProperty, source);
-            source.OnNext(6);
-            source.OnNext(BindingValue<int>.BindingError(new Exception()));
-            source.OnNext(BindingValue<int>.DataValidationError(new Exception()));
-            source.OnNext(7);
-
-            var result = target.Notifications;
-            Assert.Equal(4, result.Count);
-            Assert.Equal(BindingValueType.Value, result[0].type);
-            Assert.Equal(6, result[0].value);
-            Assert.Equal(BindingValueType.BindingError, result[1].type);
-            Assert.Equal(BindingValueType.DataValidationError, result[2].type);
-            Assert.Equal(BindingValueType.Value, result[3].type);
-            Assert.Equal(7, result[3].value);
+            [Fact]
+            public void Binding_Validated_Property_Calls_UpdateDataValidation_Untyped()
+            {
+                var target = new Class1();
+                var source = new Subject<object>();
+                var property = GetProperty();
+                var error1 = new Exception();
+                var error2 = new Exception();
+
+                target.Bind(property, source);
+                source.OnNext(6);
+                source.OnNext(new BindingNotification(error1, BindingErrorType.DataValidationError));
+                source.OnNext(new BindingNotification(error2, BindingErrorType.Error));
+                source.OnNext(7);
+
+                Assert.Equal(new Notification[]
+                {
+                    new(BindingValueType.Value, 6, null),
+                    new(BindingValueType.DataValidationError, 6, error1),
+                    new(BindingValueType.BindingError, 0, error2),
+                    new(BindingValueType.Value, 7, null),
+                }, target.Notifications);
+            }
+
+            [Fact]
+            public void Binding_Overridden_Validated_Property_Calls_UpdateDataValidation()
+            {
+                var target = new Class2();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetNonValidatedProperty();
+
+                // Class2 overrides the non-validated property metadata to enable data validation.
+                target.Bind(property, source);
+                source.OnNext(1);
+
+                Assert.Equal(1, target.Notifications.Count);
+            }
+
+            [Fact]
+            public void Disposing_Binding_Subscription_Clears_DataValidation()
+            {
+                var target = new Class1();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetProperty();
+                var error = new Exception();
+                var sub = target.Bind(property, source);
+
+                source.OnNext(6);
+                source.OnNext(BindingValue<int>.DataValidationError(error));
+                sub.Dispose();
+
+                Assert.Equal(new Notification[]
+                {
+                    new(BindingValueType.Value, 6, null),
+                    new(BindingValueType.DataValidationError, 6, error),
+                    new(BindingValueType.UnsetValue, 6, null),
+                }, target.Notifications);
+            }
+
+            [Fact]
+            public void Completing_Binding_Clears_DataValidation()
+            {
+                var target = new Class1();
+                var source = new Subject<BindingValue<int>>();
+                var property = GetProperty();
+                var error = new Exception();
+                
+                target.Bind(property, source);
+                source.OnNext(6);
+                source.OnNext(BindingValue<int>.DataValidationError(error));
+                source.OnCompleted();
+
+                Assert.Equal(new Notification[]
+                {
+                    new(BindingValueType.Value, 6, null),
+                    new(BindingValueType.DataValidationError, 6, error),
+                    new(BindingValueType.UnsetValue, 6, null),
+                }, target.Notifications);
+            }
+
+            protected abstract T GetProperty();
+            protected abstract T GetNonValidatedProperty();
         }
 
-        [Fact]
-        public void Binding_Overridden_Validated_Direct_Property_Calls_UpdateDataValidation()
+        public class DirectPropertyTests : TestBase<DirectPropertyBase<int>>
         {
-            var target = new Class2();
-            var source = new Subject<BindingValue<int>>();
+            [Fact]
+            public void Bound_Validated_String_Property_Can_Be_Set_To_Null()
+            {
+                var source = new ViewModel
+                {
+                    StringValue = "foo",
+                };
 
-            // Class2 overrides `NonValidatedDirectProperty`'s metadata to enable data validation.
-            target.Bind(Class1.NonValidatedDirectProperty, source);
-            source.OnNext(1);
+                var target = new Class1
+                {
+                    [!Class1.ValidatedDirectStringProperty] = new Binding
+                    {
+                        Path = nameof(ViewModel.StringValue),
+                        Source = source,
+                    },
+                };
+
+                Assert.Equal("foo", target.ValidatedDirectString);
 
-            Assert.Equal(1, target.Notifications.Count);
+                source.StringValue = null;
+
+                Assert.Null(target.ValidatedDirectString);
+            }
+
+            protected override DirectPropertyBase<int> GetProperty() => Class1.ValidatedDirectIntProperty;
+            protected override DirectPropertyBase<int> GetNonValidatedProperty() => Class1.NonValidatedDirectIntProperty;
         }
 
-        [Fact]
-        public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null()
+        public class StyledPropertyTests : TestBase<StyledProperty<int>>
         {
-            var source = new ViewModel
+            [Fact]
+            public void Bound_Validated_String_Property_Can_Be_Set_To_Null()
             {
-                StringValue = "foo",
-            };
+                var source = new ViewModel
+                {
+                    StringValue = "foo",
+                };
 
-            var target = new Class1
-            {
-                [!Class1.ValidatedDirectStringProperty] = new Binding
+                var target = new Class1
                 {
-                    Path = nameof(ViewModel.StringValue),
-                    Source = source,
-                },
-            };
+                    [!Class1.ValidatedDirectStringProperty] = new Binding
+                    {
+                        Path = nameof(ViewModel.StringValue),
+                        Source = source,
+                    },
+                };
 
-            Assert.Equal("foo", target.ValidatedDirectString);
+                Assert.Equal("foo", target.ValidatedDirectString);
 
-            source.StringValue = null;
+                source.StringValue = null;
 
-            Assert.Null(target.ValidatedDirectString);
+                Assert.Null(target.ValidatedDirectString);
+            }
+
+            protected override StyledProperty<int> GetProperty() => Class1.ValidatedStyledIntProperty;
+            protected override StyledProperty<int> GetNonValidatedProperty() => Class1.NonValidatedStyledIntProperty;
         }
 
+        private record class Notification(BindingValueType type, object? value, Exception? error);
+
         private class Class1 : AvaloniaObject
         {
-            public static readonly StyledProperty<int> NonValidatedProperty =
-                AvaloniaProperty.Register<Class1, int>(
-                    nameof(NonValidated));
-
-            public static readonly DirectProperty<Class1, int> NonValidatedDirectProperty =
+            public static readonly DirectProperty<Class1, int> NonValidatedDirectIntProperty =
                 AvaloniaProperty.RegisterDirect<Class1, int>(
-                    nameof(NonValidatedDirect),
-                    o => o.NonValidatedDirect,
-                    (o, v) => o.NonValidatedDirect = v);
+                    nameof(NonValidatedDirectInt),
+                    o => o.NonValidatedDirectInt,
+                    (o, v) => o.NonValidatedDirectInt = v);
 
             public static readonly DirectProperty<Class1, int> ValidatedDirectIntProperty =
                 AvaloniaProperty.RegisterDirect<Class1, int>(
@@ -118,27 +215,30 @@ namespace Avalonia.Base.UnitTests
                     (o, v) => o.ValidatedDirectInt = v,
                     enableDataValidation: true);
 
-            public static readonly DirectProperty<Class1, string> ValidatedDirectStringProperty =
-                AvaloniaProperty.RegisterDirect<Class1, string>(
+            public static readonly DirectProperty<Class1, string?> ValidatedDirectStringProperty =
+                AvaloniaProperty.RegisterDirect<Class1, string?>(
                     nameof(ValidatedDirectString),
                     o => o.ValidatedDirectString,
                     (o, v) => o.ValidatedDirectString = v,
                     enableDataValidation: true);
 
+            public static readonly StyledProperty<int> NonValidatedStyledIntProperty =
+                AvaloniaProperty.Register<Class1, int>(
+                    nameof(NonValidatedStyledInt));
+
+            public static readonly StyledProperty<int> ValidatedStyledIntProperty =
+                AvaloniaProperty.Register<Class1, int>(
+                    nameof(ValidatedStyledInt),
+                    enableDataValidation: true);
+
             private int _nonValidatedDirect;
             private int _directInt;
-            private string _directString;
+            private string? _directString;
 
-            public int NonValidated
-            {
-                get { return GetValue(NonValidatedProperty); }
-                set { SetValue(NonValidatedProperty, value); }
-            }
-
-            public int NonValidatedDirect
+            public int NonValidatedDirectInt
             {
                 get { return _directInt; }
-                set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); }
+                set { SetAndRaise(NonValidatedDirectIntProperty, ref _nonValidatedDirect, value); }
             }
 
             public int ValidatedDirectInt
@@ -147,20 +247,32 @@ namespace Avalonia.Base.UnitTests
                 set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); }
             }
 
-            public string ValidatedDirectString
+            public string? ValidatedDirectString
             {
                 get { return _directString; }
                 set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); }
             }
 
-            public List<(BindingValueType type, object value)> Notifications { get; } = new();
+            public int NonValidatedStyledInt
+            {
+                get { return GetValue(NonValidatedStyledIntProperty); }
+                set { SetValue(NonValidatedStyledIntProperty, value); }
+            }
+
+            public int ValidatedStyledInt
+            {
+                get => GetValue(ValidatedStyledIntProperty);
+                set => SetValue(ValidatedStyledIntProperty, value);
+            }
+
+            public List<Notification> Notifications { get; } = new();
 
             protected override void UpdateDataValidation(
                 AvaloniaProperty property,
                 BindingValueType state,
-                Exception error)
+                Exception? error)
             {
-                Notifications.Add((state, GetValue(property)));
+                Notifications.Add(new(state, GetValue(property), error));
             }
         }
 
@@ -168,16 +280,18 @@ namespace Avalonia.Base.UnitTests
         {
             static Class2()
             {
-                NonValidatedDirectProperty.OverrideMetadata<Class2>(
+                NonValidatedDirectIntProperty.OverrideMetadata<Class2>(
                     new DirectPropertyMetadata<int>(enableDataValidation: true));
+                NonValidatedStyledIntProperty.OverrideMetadata<Class2>(
+                    new StyledPropertyMetadata<int>(enableDataValidation: true));
             }
         }
 
         public class ViewModel : NotifyingBase
         {
-            private string _stringValue;
+            private string? _stringValue;
 
-            public string StringValue
+            public string? StringValue
             {
                 get { return _stringValue; }
                 set { _stringValue = value; RaisePropertyChanged(); }

+ 1 - 0
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@@ -198,6 +198,7 @@ namespace Avalonia.Base.UnitTests
                     defaultBindingMode: BindingMode.OneWay,
                     validate: null,
                     coerce: null,
+                    enableDataValidation: false,
                     notifying: FooNotifying);
 
             public int NotifyCount { get; private set; }

+ 53 - 3
tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs

@@ -1,13 +1,63 @@
 using Avalonia.Controls;
 using Avalonia.Input;
+using Avalonia.Input.Raw;
 using Avalonia.Media;
+using Avalonia.Platform;
 using Avalonia.UnitTests;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests.Input
 {
     public class MouseDeviceTests : PointerTestsBase
     {
+        [Fact]
+        public void Initial_Buttons_Are_Not_Set_Without_Corresponding_Mouse_Down()
+        {
+            using var scope = AvaloniaLocator.EnterScope();
+            var settingsMock = new Mock<IPlatformSettings>();
+            var threadingMock = new Mock<IPlatformThreadingInterface>();
+
+            threadingMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true);
+
+            AvaloniaLocator.CurrentMutable.BindToSelf(this)
+                .Bind<IPlatformSettings>().ToConstant(settingsMock.Object);
+
+            using var app = UnitTestApplication.Start(
+                new TestServices(
+                    inputManager: new InputManager(),
+                    threadingInterface: threadingMock.Object));
+
+            var renderer = RendererMocks.CreateRenderer();
+            var device = new MouseDevice();
+            var impl = CreateTopLevelImplMock(renderer.Object);
+
+            var control = new Control();
+            var root = CreateInputRoot(impl.Object, control);
+           
+            MouseButton button = default;
+
+            root.PointerReleased += (s, e) => button = e.InitialPressMouseButton;
+
+            var down = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown);
+            var up = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp);
+
+            SetHit(renderer, control);
+
+            impl.Object.Input!(up);
+
+            Assert.Equal(MouseButton.None, button);
+
+            impl.Object.Input!(down);
+            impl.Object.Input!(up);
+
+            Assert.Equal(MouseButton.Left, button);
+           
+            impl.Object.Input!(up);
+
+            Assert.Equal(MouseButton.None, button);        
+        }
+
         [Fact]
         public void Capture_Is_Transferred_To_Parent_When_Control_Removed()
         {
@@ -37,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Input
             impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
 
             Assert.NotNull(result);
-            
+           
             result.Capture(control);
             Assert.Same(control, result.Captured);
 
@@ -67,8 +117,8 @@ namespace Avalonia.Base.UnitTests.Input
                     })
                 }
             });
-            
-            
+           
+           
             Point? result = null;
             root.PointerMoved += (_, a) =>
             {

+ 13 - 4
tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs

@@ -55,20 +55,29 @@ public abstract class PointerTestsBase
         return root;
     }
 
+    protected static RawPointerEventArgs CreateRawPointerArgs(
+        IPointerDevice pointerDevice,
+        IInputRoot root,
+        RawPointerEventType type,
+        Point? position = default)
+    {
+        return new RawPointerEventArgs(pointerDevice, 0, root, type, position ?? default, default);
+    }
+
     protected static RawPointerEventArgs CreateRawPointerMovedArgs(
         IPointerDevice pointerDevice,
         IInputRoot root,
-        Point? positition = null)
+        Point? position = null)
     {
         return new RawPointerEventArgs(pointerDevice, 0, root, RawPointerEventType.Move,
-            positition ?? default, default);
+            position ?? default, default);
     }
 
     protected static PointerEventArgs CreatePointerMovedArgs(
-        IInputRoot root, IInputElement? source, Point? positition = null)
+        IInputRoot root, IInputElement? source, Point? position = null)
     {
         return new PointerEventArgs(InputElement.PointerMovedEvent, source, new Mock<IPointer>().Object, (Visual)root,
-            positition ?? default, default, PointerPointProperties.None, KeyModifiers.None);
+            position ?? default, default, PointerPointProperties.None, KeyModifiers.None);
     }
 
     protected static Mock<IPointerDevice> CreatePointerDeviceMock(

+ 370 - 38
tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs

@@ -1,72 +1,404 @@
 using System;
-using System.Reactive.Linq;
+using System.Collections;
+using System.ComponentModel;
 using Avalonia.Controls;
 using Avalonia.Data;
-using Avalonia.Data.Core;
-using Avalonia.Markup.Data;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
 using Xunit;
 
+#nullable enable
+
 namespace Avalonia.Markup.UnitTests.Data
 {
     public class BindingTests_DataValidation
     {
-        [Fact]
-        public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_LocalValue()
+        public abstract class TestBase<T>
+            where T : AvaloniaProperty<int>
         {
-            var textBlock = new TextBlock
+            [Fact]
+            public void Setter_Exception_Causes_DataValidation_Error()
+            {
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(ExceptionValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                target.DataContext = new ExceptionValidatingModel();
+                target.Bind(property, binding);
+
+                Assert.Equal(20, target.GetValue(property));
+
+                target.SetValue(property, 200);
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<ArgumentOutOfRangeException>(target.DataValidationError);
+
+                target.SetValue(property, 10);
+
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
+
+            [Fact]
+            public void Indei_Error_Causes_DataValidation_Error()
+            {
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(IndeiValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                target.DataContext = new IndeiValidatingModel();
+                target.Bind(property, binding);
+
+                Assert.Equal(20, target.GetValue(property));
+
+                target.SetValue(property, 200);
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+
+                target.SetValue(property, 10);
+
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
+
+            [Fact]
+            public void Disposing_Binding_Subscription_Clears_DataValidation()
             {
-                DataContext = new Class1(),
-            };
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(ExceptionValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                target.DataContext = new IndeiValidatingModel
+                {
+                    Value = 200,
+                };
+                
+                var sub = target.Bind(property, binding);
 
-            var target = new Binding(nameof(Class1.Foo));
-            var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false);
-            var subject = (BindingExpression)instanced.Source;
-            object result = null;
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
 
-            subject.Subscribe(x => result = x);
+                sub.Dispose();
 
-            Assert.IsType<string>(result);
+                Assert.Null(target.DataValidationError);
+            }
+
+            private protected abstract (DataValidationTestControl, T) CreateTarget();
         }
 
-        [Fact]
-        public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue()
+        public class DirectPropertyTests : TestBase<DirectPropertyBase<int>>
         {
-            var textBlock = new TextBlock
+            private protected override (DataValidationTestControl, DirectPropertyBase<int>) CreateTarget()
             {
-                DataContext = new Class1(),
-            };
+                return (new ValidatedDirectPropertyClass(), ValidatedDirectPropertyClass.ValueProperty);
+            }
+        }
 
-            var target = new Binding(nameof(Class1.Foo));
-            var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
-            var subject = (BindingExpression)instanced.Source;
-            object result = null;
+        public class StyledPropertyTests : TestBase<StyledProperty<int>>
+        {
+            [Fact]
+            public void Style_Binding_Supports_Data_Validation()
+            {
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(IndeiValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
+
+                var model = new IndeiValidatingModel();
+                var root = new TestRoot
+                {
+                    DataContext = model,
+                    Styles =
+                    {
+                        new Style(x => x.Is<DataValidationTestControl>())
+                        {
+                            Setters =
+                            {
+                                new Setter(property, binding)
+                            }
+                        }
+                    },
+                    Child = target,
+                };
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                Assert.Equal(20, target.GetValue(property));
 
-            subject.Subscribe(x => result = x);
+                model.Value = 200;
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+
+                model.Value = 10;
+
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
+
+            [Fact]
+            public void Style_With_Activator_Binding_Supports_Data_Validation()
+            {
+                var (target, property) = CreateTarget();
+                var binding = new Binding(nameof(IndeiValidatingModel.Value))
+                {
+                    Mode = BindingMode.TwoWay
+                };
 
-            Assert.Equal(new BindingNotification("foo"), result);
+                var model = new IndeiValidatingModel
+                {
+                    Value = 200,
+                };
+
+                var root = new TestRoot
+                {
+                    DataContext = model,
+                    Styles =
+                    {
+                        new Style(x => x.Is<DataValidationTestControl>().Class("foo"))
+                        {
+                            Setters =
+                            {
+                                new Setter(property, binding)
+                            }
+                        }
+                    },
+                    Child = target,
+                };
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+                target.Classes.Add("foo");
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+
+                target.Classes.Remove("foo");
+                Assert.Equal(0, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+
+                target.Classes.Add("foo");
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+
+                model.Value = 10;
+
+                Assert.Equal(10, target.GetValue(property));
+                Assert.Null(target.DataValidationError);
+            }
+
+            [Fact]
+            public void Data_Validation_Can_Switch_Between_Style_And_LocalValue_Binding()
+            {
+                var (target, property) = CreateTarget();
+                var model1 = new IndeiValidatingModel { Value = 200 };
+                var model2 = new IndeiValidatingModel { Value = 300 };
+                var binding1 = new Binding(nameof(IndeiValidatingModel.Value));
+                var binding2 = new Binding(nameof(IndeiValidatingModel.Value)) { Source = model2 };
+
+                var root = new TestRoot
+                {
+                    DataContext = model1,
+                    Styles =
+                    {
+                        new Style(x => x.Is<DataValidationTestControl>())
+                        {
+                            Setters =
+                            {
+                                new Setter(property, binding1)
+                            }
+                        }
+                    },
+                    Child = target,
+                };
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+
+                var sub = target.Bind(property, binding2);
+                Assert.Equal(300, target.GetValue(property));
+                Assert.Equal("Invalid value: 300.", target.DataValidationError?.Message);
+
+                sub.Dispose();
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+            }
+
+
+            [Fact]
+            public void Data_Validation_Can_Switch_Between_Style_And_StyleTrigger_Binding()
+            {
+                var (target, property) = CreateTarget();
+                var model1 = new IndeiValidatingModel { Value = 200 };
+                var model2 = new IndeiValidatingModel { Value = 300 };
+                var binding1 = new Binding(nameof(IndeiValidatingModel.Value));
+                var binding2 = new Binding(nameof(IndeiValidatingModel.Value)) { Source = model2 };
+
+                var root = new TestRoot
+                {
+                    DataContext = model1,
+                    Styles =
+                    {
+                        new Style(x => x.Is<DataValidationTestControl>())
+                        {
+                            Setters =
+                            {
+                                new Setter(property, binding1)
+                            }
+                        },
+                        new Style(x => x.Is<DataValidationTestControl>().Class("foo"))
+                        {
+                            Setters =
+                            {
+                                new Setter(property, binding2)
+                            }
+                        },
+                    },
+                    Child = target,
+                };
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+
+                target.Classes.Add("foo");
+                Assert.Equal(300, target.GetValue(property));
+                Assert.Equal("Invalid value: 300.", target.DataValidationError?.Message);
+
+                target.Classes.Remove("foo");
+                Assert.Equal(200, target.GetValue(property));
+                Assert.IsType<DataValidationException>(target.DataValidationError);
+                Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message);
+            }
+
+            private protected override (DataValidationTestControl, StyledProperty<int>) CreateTarget()
+            {
+                return (new ValidatedStyledPropertyClass(), ValidatedStyledPropertyClass.ValueProperty);
+            }
+        }
+
+        internal class DataValidationTestControl : Control
+        {
+            public Exception? DataValidationError { get; protected set; }
+        }
+
+        private class ValidatedStyledPropertyClass : DataValidationTestControl
+        {
+            public static readonly StyledProperty<int> ValueProperty =
+                AvaloniaProperty.Register<ValidatedStyledPropertyClass, int>(
+                    "Value",
+                    enableDataValidation: true);
+
+            public int Value
+            {
+                get => GetValue(ValueProperty);
+                set => SetValue(ValueProperty, value);
+            }
+
+            protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
+            {
+                if (property == ValueProperty)
+                {
+                    DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null;
+                }
+            }
         }
 
-        [Fact]
-        public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_TemplatedParent()
+        private class ValidatedDirectPropertyClass : DataValidationTestControl
         {
-            var textBlock = new TextBlock
+            public static readonly DirectProperty<ValidatedDirectPropertyClass, int> ValueProperty =
+                AvaloniaProperty.RegisterDirect<ValidatedDirectPropertyClass, int>(
+                    "Value",
+                    o => o.Value,
+                    (o, v) => o.Value = v,
+                    enableDataValidation: true);
+
+            private int _value;
+
+            public int Value
             {
-                DataContext = new Class1(),
-            };
+                get => _value;
+                set => SetAndRaise(ValueProperty, ref _value, value);
+            }
 
-            var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template };
-            var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
-            var subject = (BindingExpression)instanced.Source;
-            object result = null;
+            protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
+            {
+                if (property == ValueProperty)
+                {
+                    DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null;
+                }
+            }
+        }
 
-            subject.Subscribe(x => result = x);
+        private class ExceptionValidatingModel
+        {
+            public const int MaxValue = 100;
+            private int _value = 20;
 
-            Assert.IsType<string>(result);
+            public int Value
+            {
+                get => _value;
+                set
+                {
+                    if (value > MaxValue)
+                        throw new ArgumentOutOfRangeException(nameof(value));
+                    _value = value;
+                }
+            }
         }
 
-        private class Class1
+        private class IndeiValidatingModel : INotifyDataErrorInfo
         {
-            public string Foo { get; set; } = "foo";
+            public const int MaxValue = 100;
+            private bool _hasErrors;
+            private int _value = 20;
+
+            public int Value
+            {
+                get => _value;
+                set
+                {
+                    _value = value;
+                    HasErrors = value > MaxValue;
+                }
+            }
+
+            public bool HasErrors 
+            {
+                get => _hasErrors;
+                private set
+                {
+                    if (_hasErrors != value)
+                    {
+                        _hasErrors = value;
+                        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+                    }
+                }
+            }
+
+            public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
+
+            public IEnumerable GetErrors(string? propertyName)
+            {
+                if (propertyName == nameof(Value) && _value > MaxValue)
+                    yield return $"Invalid value: {_value}.";
+            }
         }
     }
 }