Browse Source

Initial impl of data validation for styled properties.

Currently only local value bindings implemented.
pull/10423/head
Steven Kirk 3 years ago
parent
commit
d787a72f7f
  1. 16
      src/Avalonia.Base/AvaloniaObject.cs
  2. 29
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  3. 37
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  4. 28
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  5. 7
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

16
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);
}
/// <summary>
/// Gets a description of an observable that van be used in logs.
/// </summary>

29
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<T> 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())

37
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<T> 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<T> instance, object? value)
static void Execute(LocalValueUntypedBindingObserver<T> instance, object? untypedValue)
{
var owner = instance._owner;
var property = instance.Property;
var value = BindingValue<T>.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)

28
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -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))

7
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;
}

Loading…
Cancel
Save