diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index be7a14dcf9..9c6038e79c 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -262,42 +262,11 @@ namespace Avalonia if (property.IsDirect) { - var accessor = (IDirectPropertyAccessor)GetRegistered(property); - LogPropertySet(property, value, priority); - accessor.SetValue(this, DirectUnsetToDefault(value, property)); + SetDirectValue(property, value); } else { - PriorityValue v; - var originalValue = value; - - if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) - { - ThrowNotRegistered(property); - } - - if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) - { - throw new ArgumentException(string.Format( - "Invalid value for Property '{0}': '{1}' ({2})", - property.Name, - originalValue, - originalValue?.GetType().FullName ?? "(null)")); - } - - if (!_values.TryGetValue(property, out v)) - { - if (value == AvaloniaProperty.UnsetValue) - { - return; - } - - v = CreatePriorityValue(property); - _values.Add(property, v); - } - - LogPropertySet(property, value, priority); - v.SetValue(value, (int)priority); + SetStyledValue(property, value, priority); } } @@ -361,7 +330,7 @@ namespace Avalonia subscription = source .Select(x => CastOrDefault(x, property.PropertyType)) .Do(_ => { }, () => _directBindings.Remove(subscription)) - .Subscribe(x => DirectBindingSet(property, x)); + .Subscribe(x => SetDirectValue(property, x)); _directBindings.Add(subscription); @@ -642,20 +611,60 @@ namespace Avalonia } /// - /// Sets a property value for a direct property binding. + /// Gets the default value for a property. + /// + /// The property. + /// The default value. + private object GetDefaultValue(AvaloniaProperty property) + { + if (property.Inherits && _inheritanceParent != null) + { + return (_inheritanceParent as AvaloniaObject).GetValueInternal(property); + } + else + { + return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + } + + /// + /// Gets a value + /// without check for registered as this can slow getting the value + /// this method is intended for internal usage in AvaloniaObject only + /// it's called only after check the property is registered + /// + /// The property. + /// The value. + private object GetValueInternal(AvaloniaProperty property) + { + object result = AvaloniaProperty.UnsetValue; + PriorityValue value; + + if (_values.TryGetValue(property, out value)) + { + result = value.Value; + } + + if (result == AvaloniaProperty.UnsetValue) + { + result = GetDefaultValue(property); + } + + return result; + } + + /// + /// Sets the value of a direct property. /// /// The property. /// The value. - /// - private void DirectBindingSet(AvaloniaProperty property, object value) + private void SetDirectValue(AvaloniaProperty property, object value) { - var validated = property.GetMetadata(GetType()).EnableDataValidation; + var metadata = property.GetMetadata(GetType()); var notification = value as BindingNotification; if (notification != null) { - value = notification.Value; - if (notification.ErrorType == BindingErrorType.Error) { Logger.Error( @@ -666,73 +675,85 @@ namespace Avalonia property, ExceptionUtilities.GetMessage(notification.Error)); } + + if (notification.HasValue) + { + value = notification.Value; + } } - if (notification?.HasValue != false) + if (notification == null || notification.HasValue) { - SetValue(property, value); + var accessor = (IDirectPropertyAccessor)GetRegistered(property); + var finalValue = value == AvaloniaProperty.UnsetValue ? + ((IDirectPropertyMetadata)metadata).UnsetValue : value; + + LogPropertySet(property, value, BindingPriority.LocalValue); + + accessor.SetValue(this, finalValue); } - if (validated) + if (metadata.EnableDataValidation) { UpdateDataValidation(property, notification); } } /// - /// Converts an unset value to the default value for a direct property. + /// Sets the value of a styled property. /// - /// The value. /// The property. - /// The value. - private object DirectUnsetToDefault(object value, AvaloniaProperty property) + /// The value. + /// The priority of the value. + private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority) { - return value == AvaloniaProperty.UnsetValue ? - ((IDirectPropertyMetadata)property.GetMetadata(GetType())).UnsetValue : - value; - } + var notification = value as BindingNotification; - /// - /// Gets the default value for a property. - /// - /// The property. - /// The default value. - private object GetDefaultValue(AvaloniaProperty property) - { - if (property.Inherits && _inheritanceParent != null) + // We currently accept BindingNotifications for non-direct properties but we just + // strip them to their underlying value. + if (notification != null) { - return (_inheritanceParent as AvaloniaObject).GetValueInternal(property); + if (!notification.HasValue) + { + return; + } + else + { + value = notification.Value; + } } - else + + var originalValue = value; + + if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) { - return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + ThrowNotRegistered(property); } - } - /// - /// Gets a value - /// without check for registered as this can slow getting the value - /// this method is intended for internal usage in AvaloniaObject only - /// it's called only after check the property is registered - /// - /// The property. - /// The value. - private object GetValueInternal(AvaloniaProperty property) - { - object result = AvaloniaProperty.UnsetValue; - PriorityValue value; - - if (_values.TryGetValue(property, out value)) + if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) { - result = value.Value; + throw new ArgumentException(string.Format( + "Invalid value for Property '{0}': '{1}' ({2})", + property.Name, + originalValue, + originalValue?.GetType().FullName ?? "(null)")); } - if (result == AvaloniaProperty.UnsetValue) + PriorityValue v; + + if (!_values.TryGetValue(property, out v)) { - result = GetDefaultValue(property); + if (value == AvaloniaProperty.UnsetValue) + { + return; + } + + v = CreatePriorityValue(property); + _values.Add(property, v); } - return result; + LogPropertySet(property, value, priority); + v.SetValue(value, (int)priority); } /// diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index 9351443e03..1c8a8067f8 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -66,6 +66,7 @@ namespace Avalonia.Data throw new ArgumentException($"'errorType' may not be None"); } + Value = AvaloniaProperty.UnsetValue; Error = error; ErrorType = errorType; } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index ef87147c9f..99645ace76 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -37,6 +37,20 @@ namespace Avalonia.Base.UnitTests Assert.Empty(target.Notifications); } + [Fact] + public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() + { + var target = new Class1(); + + target.SetValue(Class1.NonValidatedDirectProperty, 6); + target.SetValue(Class1.NonValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); + target.SetValue(Class1.NonValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); + target.SetValue(Class1.NonValidatedDirectProperty, new BindingNotification(7)); + target.SetValue(Class1.NonValidatedDirectProperty, 8); + + Assert.Empty(target.Notifications); + } + [Fact] public void Setting_Validated_Direct_Property_Calls_UpdateDataValidation() { @@ -48,7 +62,16 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7)); target.SetValue(Class1.ValidatedDirectProperty, 8); - Assert.Empty(target.Notifications); + Assert.Equal( + new[] + { + null, // 6 + new BindingNotification(new Exception(), BindingErrorType.Error), + new BindingNotification(new Exception(), BindingErrorType.DataValidationError), + new BindingNotification(7), // 7 + null, // 8 + }, + target.Notifications.AsEnumerable()); } [Fact] @@ -135,6 +158,12 @@ namespace Avalonia.Base.UnitTests nameof(Validated), enableDataValidation: true); + public static readonly DirectProperty NonValidatedDirectProperty = + AvaloniaProperty.RegisterDirect( + nameof(NonValidatedDirect), + o => o.NonValidatedDirect, + (o, v) => o.NonValidatedDirect = v); + public static readonly DirectProperty ValidatedDirectProperty = AvaloniaProperty.RegisterDirect( nameof(Validated), @@ -142,6 +171,7 @@ namespace Avalonia.Base.UnitTests (o, v) => o.ValidatedDirect = v, enableDataValidation: true); + private int _nonValidatedDirect; private int _direct; public int NonValidated @@ -156,6 +186,12 @@ namespace Avalonia.Base.UnitTests set { SetValue(ValidatedProperty, value); } } + public int NonValidatedDirect + { + get { return _direct; } + set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); } + } + public int ValidatedDirect { get { return _direct; }