Browse Source

Make untyped bindings produce default value.

This changes the behavior of bindings slightly, in that previously a binding which produced `UnsetValue` (or a binding error) caused the value of any lower priority value of value to take effect. After this change, the binding reverts to the property's default value. This behavior more closely matches WPF and fixes #10110.
pull/10189/head
Steven Kirk 3 years ago
parent
commit
7b00ef6989
  1. 4
      src/Avalonia.Base/AvaloniaObject.cs
  2. 47
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  3. 42
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  4. 25
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  5. 2
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  6. 2
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  7. 5
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  8. 18
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  9. 4
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

4
src/Avalonia.Base/AvaloniaObject.cs

@ -270,8 +270,8 @@ namespace Avalonia
/// <param name="property">The property.</param>
/// <returns>True if the property is set, otherwise false.</returns>
/// <remarks>
/// Checks whether a value is assigned to the property, or that there is a binding to the
/// property that is producing a value other than <see cref="AvaloniaProperty.UnsetValue"/>.
/// Returns true if <paramref name="property"/> is a styled property which has a value
/// assigned to it or a binding targeting it; otherwise false.
/// </remarks>
public bool IsSet(AvaloniaProperty property)
{

47
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -16,6 +16,8 @@ namespace Avalonia.PropertyStore
private IDisposable? _subscription;
private bool _hasValue;
private TValue? _value;
private TValue? _defaultValue;
private bool _isDefaultValueInitialized;
protected BindingEntryBase(
ValueFrame frame,
@ -89,6 +91,7 @@ namespace Avalonia.PropertyStore
protected abstract BindingValue<TValue> ConvertAndValidate(TSource value);
protected abstract BindingValue<TValue> ConvertAndValidate(BindingValue<TSource> value);
protected abstract TValue GetDefaultValue(Type ownerType);
protected virtual void Start(bool produceValue)
{
@ -104,17 +107,6 @@ namespace Avalonia.PropertyStore
};
}
private void ClearValue()
{
if (_hasValue)
{
_hasValue = false;
_value = default;
if (_subscription is not null)
Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority);
}
}
private void SetValue(BindingValue<TValue> value)
{
static void Execute(BindingEntryBase<TValue, TSource> instance, BindingValue<TValue> value)
@ -124,24 +116,20 @@ namespace Avalonia.PropertyStore
LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value);
if (value.HasValue)
{
if (!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);
}
}
else if (value.Type != BindingValueType.DoNothing)
var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue();
if (!instance._hasValue || !EqualityComparer<TValue>.Default.Equals(instance._value, effectiveValue))
{
instance.ClearValue();
instance._value = effectiveValue;
instance._hasValue = true;
if (instance._subscription is not null && instance._subscription != s_creatingQuiet)
instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority);
instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority);
}
}
if (value.Type == BindingValueType.DoNothing)
return;
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
@ -161,5 +149,16 @@ namespace Avalonia.PropertyStore
_subscription = null;
Frame.OnBindingCompleted(this);
}
private TValue GetCachedDefaultValue()
{
if (!_isDefaultValueInitialized)
{
_defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
_isDefaultValueInitialized = true;
}
return _defaultValue!;
}
}
}

42
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@ -10,6 +10,8 @@ namespace Avalonia.PropertyStore
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
private T? _defaultValue;
private bool _isDefaultValueInitialized;
public LocalValueBindingObserver(ValueStore owner, StyledProperty<T> property)
{
@ -41,26 +43,28 @@ namespace Avalonia.PropertyStore
public void OnNext(T value)
{
static void Execute(ValueStore owner, StyledProperty<T> property, T value)
static void Execute(LocalValueBindingObserver<T> instance, T value)
{
if (property.ValidateValue?.Invoke(value) != false)
owner.SetValue(property, value, BindingPriority.LocalValue);
else
owner.ClearLocalValue(property);
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(_owner, Property, value);
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 = _owner;
var property = Property;
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, property, newValue));
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
@ -73,12 +77,13 @@ namespace Avalonia.PropertyStore
LoggingUtils.LogIfNecessary(owner.Owner, property, value);
if (value.HasValue)
owner.SetValue(property, value.Value, BindingPriority.LocalValue);
else if (value.Type != BindingValueType.DataValidationError)
owner.ClearLocalValue(property);
var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue();
owner.SetValue(property, effectiveValue, BindingPriority.LocalValue);
}
if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError)
return;
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
@ -92,5 +97,16 @@ namespace Avalonia.PropertyStore
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
private T GetCachedDefaultValue()
{
if (!_isDefaultValueInitialized)
{
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
_isDefaultValueInitialized = true;
}
return _defaultValue!;
}
}
}

25
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@ -1,5 +1,4 @@
using System;
using System.Security.Cryptography;
using Avalonia.Data;
using Avalonia.Threading;
@ -10,6 +9,8 @@ namespace Avalonia.PropertyStore
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
private T? _defaultValue;
private bool _isDefaultValueInitialized;
public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty<T> property)
{
@ -49,11 +50,7 @@ namespace Avalonia.PropertyStore
if (value == AvaloniaProperty.UnsetValue)
{
owner.ClearLocalValue(property);
}
else if (value == BindingOperations.DoNothing)
{
// Do nothing!
owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue);
}
else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue))
{
@ -61,11 +58,14 @@ namespace Avalonia.PropertyStore
}
else
{
owner.ClearLocalValue(property);
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);
@ -79,5 +79,16 @@ namespace Avalonia.PropertyStore
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
private T GetCachedDefaultValue()
{
if (!_isDefaultValueInitialized)
{
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
_isDefaultValueInitialized = true;
}
return _defaultValue!;
}
}
}

2
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@ -31,5 +31,7 @@ namespace Avalonia.PropertyStore
{
throw new NotSupportedException();
}
protected override TTarget GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType);
}
}

2
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@ -48,5 +48,7 @@ namespace Avalonia.PropertyStore
return value;
}
protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType);
}
}

5
src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs

@ -29,5 +29,10 @@ namespace Avalonia.PropertyStore
{
throw new NotSupportedException();
}
protected override object? GetDefaultValue(Type ownerType)
{
return ((IStyledPropertyMetadata)Property.GetMetadata(ownerType)).DefaultValue;
}
}
}

18
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -372,6 +372,20 @@ namespace Avalonia.Base.UnitTests
Assert.Null(target.GetValue(property));
}
[Fact]
public void LocalValue_Bind_Generic_To_ValueType_Accepts_UnsetValue()
{
var target = new Class1();
var source = new Subject<BindingValue<double>>();
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(BindingValue<double>.Unset);
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.True(target.IsSet(Class1.QuxProperty));
}
[Fact]
public void LocalValue_Bind_NonGeneric_To_ValueType_Accepts_UnsetValue()
{
@ -383,7 +397,7 @@ namespace Avalonia.Base.UnitTests
source.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.False(target.IsSet(Class1.QuxProperty));
Assert.True(target.IsSet(Class1.QuxProperty));
}
[Fact]
@ -397,7 +411,7 @@ namespace Avalonia.Base.UnitTests
source.OnNext(AvaloniaProperty.UnsetValue);
Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
Assert.False(target.IsSet(Class1.QuxProperty));
Assert.True(target.IsSet(Class1.QuxProperty));
}
[Fact]

4
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@ -66,7 +66,7 @@ namespace Avalonia.Base.UnitTests
}
[Fact]
public void Reverts_To_Lower_Priority_If_Style_Binding_Fails_Validation()
public void Reverts_To_DefaultValue_If_Style_Binding_Fails_Validation_2()
{
var target = new Class1();
var source = new Subject<int>();
@ -75,7 +75,7 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.FooProperty, source, BindingPriority.StyleTrigger);
source.OnNext(150);
Assert.Equal(10, target.GetValue(Class1.FooProperty));
Assert.Equal(11, target.GetValue(Class1.FooProperty));
}
[Fact]

Loading…
Cancel
Save