diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index bf1624eab3..f3a046ef80 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -776,19 +776,19 @@ namespace Avalonia break; } - UpdateDataValidationCore(property, value.Type, value.Error); - } + var metadata = property.GetMetadata(GetType()); - internal void UpdateDataValidationCore(AvaloniaProperty property, - BindingValueType state, - Exception? error) - { - if (property.GetMetadata(GetType()) is { EnableDataValidation: true }) + if (metadata.EnableDataValidation == true) { - UpdateDataValidation(property, state, error); + UpdateDataValidation(property, value.Type, value.Error); } } + internal void OnUpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) + { + UpdateDataValidation(property, state, error); + } + /// /// Gets a description of an observable that van be used in logs. /// diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 5908d9e535..24eb00b2fe 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -9,6 +9,7 @@ namespace Avalonia.PropertyStore IDisposable { private readonly ValueStore _owner; + private readonly bool _hasDataValidation; private IDisposable? _subscription; private T? _defaultValue; private bool _isDefaultValueInitialized; @@ -17,6 +18,7 @@ namespace Avalonia.PropertyStore { _owner = owner; Property = property; + _hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; } public StyledProperty Property { get;} @@ -51,7 +53,10 @@ namespace Avalonia.PropertyStore if (property.ValidateValue?.Invoke(value) == false) value = instance.GetCachedDefaultValue(); - owner.SetValue(property, value, BindingPriority.LocalValue); + owner.SetLocalValue(property, value); + + if (instance._hasDataValidation) + owner.Owner.OnUpdateDataValidation(property, BindingValueType.Value, null); } if (Dispatcher.UIThread.CheckAccess()) @@ -74,23 +79,23 @@ namespace Avalonia.PropertyStore { 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) - { - 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); - } + owner.SetLocalValue(property, value.Value); + if (instance._hasDataValidation) + owner.Owner.OnUpdateDataValidation(property, originalType, value.Error); } - if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) + if (value.Type is BindingValueType.DoNothing) return; if (Dispatcher.UIThread.CheckAccess()) diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 46e6ed810a..cda11faa1a 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Data; using Avalonia.Threading; @@ -8,6 +9,7 @@ namespace Avalonia.PropertyStore IDisposable { private readonly ValueStore _owner; + private readonly bool _hasDataValidation; private IDisposable? _subscription; private T? _defaultValue; private bool _isDefaultValueInitialized; @@ -16,6 +18,7 @@ namespace Avalonia.PropertyStore { _owner = owner; Property = property; + _hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; } public StyledProperty Property { get; } @@ -35,32 +38,28 @@ namespace Avalonia.PropertyStore public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); public void OnError(Exception error) => OnCompleted(); + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] public void OnNext(object? value) { - static void Execute(LocalValueUntypedBindingObserver instance, object? value) + static void Execute(LocalValueUntypedBindingObserver instance, object? untypedValue) { var owner = instance._owner; var property = instance.Property; + var value = BindingValue.FromUntyped(untypedValue, property.PropertyType); + var originalType = value.Type; - if (value is BindingNotification n) - { - value = n.Value; - LoggingUtils.LogIfNecessary(owner.Owner, property, n); - } + LoggingUtils.LogIfNecessary(owner.Owner, property, value); - 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); - } + // 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 == BindingOperations.DoNothing) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index ec6ed392c1..9efc91d44d 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -193,18 +193,7 @@ namespace Avalonia.PropertyStore } else { - if (TryGetEffectiveValue(property, out var existing)) - { - var effective = (EffectiveValue)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(StyledProperty property, T value) + { + if (TryGetEffectiveValue(property, out var existing)) + { + var effective = (EffectiveValue)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)) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index b6036bba8f..9f74d2fc08 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -888,7 +888,8 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var source = new Subject(); 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; }