Browse Source

Ensure that the default value can be coerced.

When the coercion function on a property results in the default value being coerced, ensure that state is recorded in the value store.
pull/10822/head
Steven Kirk 3 years ago
parent
commit
e7aaf8a7f7
  1. 6
      src/Avalonia.Base/AvaloniaProperty.cs
  2. 5
      src/Avalonia.Base/DirectPropertyBase.cs
  3. 49
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  4. 42
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  5. 52
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  6. 5
      src/Avalonia.Base/StyledProperty.cs
  7. 125
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs
  8. 5
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

6
src/Avalonia.Base/AvaloniaProperty.cs

@ -499,6 +499,12 @@ namespace Avalonia
/// <param name="o">The object instance.</param>
internal abstract void RouteClearValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped CoerceValue call on a property with its default value to a typed call.
/// </summary>
/// <param name="o">The object instance.</param>
internal abstract void RouteCoerceDefaultValue(AvaloniaObject o);
/// <summary>
/// Routes an untyped GetValue call to a typed call.
/// </summary>

5
src/Avalonia.Base/DirectPropertyBase.cs

@ -117,6 +117,11 @@ namespace Avalonia
o.ClearValue<TValue>(this);
}
internal override void RouteCoerceDefaultValue(AvaloniaObject o)
{
// Do nothing.
}
/// <inheritdoc/>
internal override object? RouteGetValue(AvaloniaObject o)
{

49
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -36,12 +36,23 @@ namespace Avalonia.PropertyStore
/// </summary>
public IValueEntry? BaseValueEntry { get; private set; }
/// <summary>
/// Gets a value indicating whether the property has a coercion function.
/// </summary>
public bool HasCoercion { get; protected set; }
/// <summary>
/// Gets a value indicating whether the <see cref="Value"/> was overridden by a call to
/// <see cref="AvaloniaObject.SetCurrentValue{T}"/>.
/// </summary>
public bool IsOverridenCurrentValue { get; set; }
/// <summary>
/// Gets a value indicating whether the <see cref="Value"/> is the result of the
///
/// </summary>
public bool IsCoercedDefaultValue { get; set; }
/// <summary>
/// Begins a reevaluation pass on the effective value.
/// </summary>
@ -63,10 +74,33 @@ namespace Avalonia.PropertyStore
/// <summary>
/// Ends a reevaluation pass on the effective value.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being reevaluated.</param>
/// <remarks>
/// This method unsubscribes from any unused value entries.
/// Handles coercing the default value if necessary.
/// </remarks>
public void EndReevaluation()
public void EndReevaluation(ValueStore owner, AvaloniaProperty property)
{
if (Priority == BindingPriority.Unset && HasCoercion)
CoerceDefaultValueAndRaise(owner, property);
}
/// <summary>
/// Gets a value indicating whether the effective value represents the default value of the
/// property and can be removed.
/// </summary>
/// <returns>True if the effective value van be removed; otherwise false.</returns>
public bool CanRemove()
{
return Priority == BindingPriority.Unset &&
!IsOverridenCurrentValue &&
!IsCoercedDefaultValue;
}
/// <summary>
/// Unsubscribes from any unused value entries.
/// </summary>
public void UnsubscribeIfNecessary()
{
if (Priority == BindingPriority.Unset)
{
@ -130,6 +164,17 @@ namespace Avalonia.PropertyStore
/// <param name="property">The property being cleared.</param>
public abstract void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property);
/// <summary>
/// Coerces the default value, raising <see cref="AvaloniaObject.PropertyChanged"/>
/// where necessary.
/// </summary>
/// <param name="owner">The associated value store.</param>
/// <param name="property">The property being coerced.</param>
protected abstract void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property);
/// <summary>
/// Gets the current effective value as a boxed value.
/// </summary>
protected abstract object? GetBoxedValue();
protected void UpdateValueEntry(IValueEntry? entry, BindingPriority priority)

42
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
@ -33,19 +32,16 @@ namespace Avalonia.PropertyStore
if (_metadata.CoerceValue is { } coerce)
{
HasCoercion = true;
_uncommon = new()
{
_coerce = coerce,
_uncoercedValue = value,
_uncoercedBaseValue = value,
};
Value = coerce(owner, value);
}
else
{
Value = value;
}
Value = value;
}
/// <summary>
@ -61,7 +57,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(priority != BindingPriority.LocalValue);
UpdateValueEntry(value, priority);
SetAndRaiseCore(owner, (StyledProperty<T>)value.Property, GetValue(value), priority, false);
SetAndRaiseCore(owner, (StyledProperty<T>)value.Property, GetValue(value), priority);
if (priority > BindingPriority.LocalValue &&
value.GetDataValidationState(out var state, out var error))
@ -75,7 +71,7 @@ namespace Avalonia.PropertyStore
StyledProperty<T> property,
T value)
{
SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false);
SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
}
public void SetCurrentValueAndRaise(
@ -83,8 +79,15 @@ namespace Avalonia.PropertyStore
StyledProperty<T> property,
T value)
{
IsOverridenCurrentValue = true;
SetAndRaiseCore(owner, property, value, Priority, true);
SetAndRaiseCore(owner, property, value, Priority, isOverriddenCurrentValue: true);
}
public void SetCoercedDefaultValueAndRaise(
ValueStore owner,
StyledProperty<T> property,
T value)
{
SetAndRaiseCore(owner, property, value, Priority, isCoercedDefaultValue: true);
}
public bool TryGetBaseValue([MaybeNullWhen(false)] out T value)
@ -117,7 +120,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(Priority != BindingPriority.Animation);
Debug.Assert(BasePriority != BindingPriority.Unset);
UpdateValueEntry(null, BindingPriority.Animation);
SetAndRaiseCore(owner, (StyledProperty<T>)property, _baseValue!, BasePriority, false);
SetAndRaiseCore(owner, (StyledProperty<T>)property, _baseValue!, BasePriority);
}
public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
@ -168,6 +171,17 @@ namespace Avalonia.PropertyStore
}
}
protected override void CoerceDefaultValueAndRaise(ValueStore owner, AvaloniaProperty property)
{
Debug.Assert(_uncommon?._coerce is not null);
Debug.Assert(Priority == BindingPriority.Unset);
var coercedDefaultValue = _uncommon!._coerce!(owner.Owner, _metadata.DefaultValue);
if (!EqualityComparer<T>.Default.Equals(_metadata.DefaultValue, coercedDefaultValue))
SetCoercedDefaultValueAndRaise(owner, (StyledProperty<T>)property, coercedDefaultValue);
}
protected override object? GetBoxedValue() => Value;
private static T GetValue(IValueEntry entry)
@ -183,7 +197,8 @@ namespace Avalonia.PropertyStore
StyledProperty<T> property,
T value,
BindingPriority priority,
bool isOverriddenCurrentValue)
bool isOverriddenCurrentValue = false,
bool isCoercedDefaultValue = false)
{
var oldValue = Value;
var valueChanged = false;
@ -191,6 +206,7 @@ namespace Avalonia.PropertyStore
var v = value;
IsOverridenCurrentValue = isOverriddenCurrentValue;
IsCoercedDefaultValue = isCoercedDefaultValue;
if (_uncommon?._coerce is { } coerce)
v = coerce(owner.Owner, value);

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

@ -259,6 +259,27 @@ namespace Avalonia.PropertyStore
{
if (_effectiveValues.TryGetValue(property, out var v))
v.CoerceValue(this, property);
else
property.RouteCoerceDefaultValue(Owner);
}
public void CoerceDefaultValue<T>(StyledProperty<T> property)
{
var metadata = property.GetMetadata(Owner.GetType());
if (metadata.CoerceValue is null)
return;
var coercedDefaultValue = metadata.CoerceValue(Owner, metadata.DefaultValue);
if (EqualityComparer<T>.Default.Equals(metadata.DefaultValue, coercedDefaultValue))
return;
// We have a situation where the default value isn't valid according to the coerce
// function. In this case, we need to create an EffectiveValue entry.
var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetCoercedDefaultValueAndRaise(this, property, coercedDefaultValue);
}
public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
@ -838,20 +859,25 @@ namespace Avalonia.PropertyStore
goto restart;
}
if (current?.Priority == BindingPriority.Unset)
if (current is not null)
{
if (current.BasePriority == BindingPriority.Unset)
{
RemoveEffectiveValue(property);
current.DisposeAndRaiseUnset(this, property);
}
else
current.EndReevaluation(this, property);
if (current.CanRemove())
{
current.RemoveAnimationAndRaise(this, property);
if (current.BasePriority == BindingPriority.Unset)
{
RemoveEffectiveValue(property);
current.DisposeAndRaiseUnset(this, property);
}
else
{
current.RemoveAnimationAndRaise(this, property);
}
}
}
current?.EndReevaluation();
current.UnsubscribeIfNecessary();
}
}
finally
{
@ -923,7 +949,9 @@ namespace Avalonia.PropertyStore
{
_effectiveValues.GetKeyValue(i, out var key, out var e);
if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue)
e.EndReevaluation(this, key);
if (e.CanRemove())
{
RemoveEffectiveValue(key, i);
e.DisposeAndRaiseUnset(this, key);
@ -932,7 +960,7 @@ namespace Avalonia.PropertyStore
break;
}
e.EndReevaluation();
e.UnsubscribeIfNecessary();
}
}
finally

5
src/Avalonia.Base/StyledProperty.cs

@ -176,6 +176,11 @@ namespace Avalonia
o.ClearValue<TValue>(this);
}
internal override void RouteCoerceDefaultValue(AvaloniaObject o)
{
o.GetValueStore().CoerceDefaultValue(this);
}
/// <inheritdoc/>
internal override object? RouteGetValue(AvaloniaObject o)
{

125
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs

@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -182,11 +185,103 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(-150, target.Foo);
}
[Fact]
public void Default_Value_Can_Be_Coerced()
{
var target = new Class1();
var raised = 0;
target.MinFoo = 20;
target.PropertyChanged += (s, e) =>
{
Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal(11, e.OldValue);
Assert.Equal(20, e.NewValue);
Assert.Equal(BindingPriority.Unset, e.Priority);
++raised;
};
target.CoerceValue(Class1.FooProperty);
Assert.Equal(20, target.Foo);
Assert.Equal(1, raised);
}
[Fact]
public void ClearValue_Respects_Coerced_Default_Value()
{
var target = new Class1();
var raised = 0;
target.Foo = 30;
target.MinFoo = 20;
target.PropertyChanged += (s, e) =>
{
Assert.Equal(Class1.FooProperty, e.Property);
Assert.Equal(30, e.OldValue);
Assert.Equal(20, e.NewValue);
Assert.Equal(BindingPriority.Unset, e.Priority);
++raised;
};
target.ClearValue(Class1.FooProperty);
Assert.Equal(20, target.Foo);
Assert.Equal(1, raised);
}
[Fact]
public void Deactivating_Style_Respects_Coerced_Default_Value()
{
var target = new Control1
{
MinFoo = 20,
};
var root = new TestRoot
{
Styles =
{
new Style(x => x.OfType<Control1>().Class("foo"))
{
Setters =
{
new Setter(Control1.FooProperty, 50),
},
},
},
Child = target,
};
var raised = 0;
target.Classes.Add("foo");
root.LayoutManager.ExecuteInitialLayoutPass();
Assert.Equal(50, target.Foo);
target.PropertyChanged += (s, e) =>
{
Assert.Equal(Control1.FooProperty, e.Property);
Assert.Equal(50, e.OldValue);
Assert.Equal(20, e.NewValue);
Assert.Equal(BindingPriority.Unset, e.Priority);
++raised;
};
target.Classes.Remove("foo");
Assert.Equal(20, target.Foo);
Assert.Equal(1, raised);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<int> FooProperty =
AvaloniaProperty.Register<Class1, int>(
"Qux",
"Foo",
defaultValue: 11,
coerce: CoerceFoo);
@ -215,13 +310,15 @@ namespace Avalonia.Base.UnitTests
set => SetValue(InheritedProperty, value);
}
public int MinFoo { get; set; } = 0;
public int MaxFoo { get; set; } = 100;
public List<AvaloniaPropertyChangedEventArgs> CoreChanges { get; } = new();
public static int CoerceFoo(AvaloniaObject instance, int value)
{
return Math.Min(((Class1)instance).MaxFoo, value);
var o = (Class1)instance;
return Math.Clamp(value, o.MinFoo, o.MaxFoo);
}
protected override void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change)
@ -266,5 +363,29 @@ namespace Avalonia.Base.UnitTests
return -value;
}
}
private class Control1 : Control
{
public static readonly StyledProperty<int> FooProperty =
AvaloniaProperty.Register<Control1, int>(
"Foo",
defaultValue: 11,
coerce: CoerceFoo);
public int Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public int MinFoo { get; set; } = 0;
public int MaxFoo { get; set; } = 100;
public static int CoerceFoo(AvaloniaObject instance, int value)
{
var o = (Control1)instance;
return Math.Clamp(value, o.MinFoo, o.MaxFoo);
}
}
}
}

5
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@ -179,6 +179,11 @@ namespace Avalonia.Base.UnitTests
throw new NotImplementedException();
}
internal override void RouteCoerceDefaultValue(AvaloniaObject o)
{
throw new NotImplementedException();
}
internal override object RouteGetValue(AvaloniaObject o)
{
throw new NotImplementedException();

Loading…
Cancel
Save