From 19078979e38a9facb0be00ed0daeca3bd53c9e3a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 09:40:42 +0100 Subject: [PATCH] Initial implementation of SetCurrentValue. --- src/Avalonia.Base/AvaloniaObject.cs | 20 +- .../Diagnostics/AvaloniaPropertyValue.cs | 23 +- .../PropertyStore/EffectiveValue.cs | 6 + .../PropertyStore/EffectiveValue`1.cs | 29 +- src/Avalonia.Base/PropertyStore/ValueStore.cs | 23 +- .../AvaloniaObjectTests_SetCurrentValue.cs | 270 ++++++++++++++++++ 6 files changed, 345 insertions(+), 26 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 74dc55355b..93bbee12b8 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -355,6 +355,23 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + public void SetCurrentValue(StyledProperty property, T value) + { + _ = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + LogPropertySet(property, value, BindingPriority.LocalValue); + + if (value is UnsetValueType) + { + _values.ClearLocalValue(property); + } + else if (value is not DoNothingType) + { + _values.SetCurrentValue(property, value); + } + } + /// /// Binds a to an observable. /// @@ -547,7 +564,8 @@ namespace Avalonia property, GetValue(property), BindingPriority.LocalValue, - null); + null, + false); } return _values.GetDiagnostic(property); diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs index 4189fd5234..0b3e62f1cc 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs @@ -3,28 +3,23 @@ using Avalonia.Data; namespace Avalonia.Diagnostics { /// - /// Holds diagnostic-related information about the value of a - /// on a . + /// Holds diagnostic-related information about the value of an + /// on an . /// public class AvaloniaPropertyValue { - /// - /// Initializes a new instance of the class. - /// - /// The property. - /// The current property value. - /// The priority of the current value. - /// A diagnostic string. - public AvaloniaPropertyValue( + internal AvaloniaPropertyValue( AvaloniaProperty property, object? value, BindingPriority priority, - string? diagnostic) + string? diagnostic, + bool isOverriddenCurrentValue) { Property = property; Value = value; Priority = priority; Diagnostic = diagnostic; + IsOverriddenCurrentValue = isOverriddenCurrentValue; } /// @@ -46,5 +41,11 @@ namespace Avalonia.Diagnostics /// Gets a diagnostic string. /// public string? Diagnostic { get; } + + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverriddenCurrentValue { get; } } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 04d3c805c2..78f0ad46b7 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -29,6 +29,12 @@ namespace Avalonia.PropertyStore /// public BindingPriority BasePriority { get; protected set; } + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverridenCurrentValue { get; set; } + /// /// Begins a reevaluation pass on the effective value. /// diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 3e20dcce56..0d93e9d8ed 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -57,7 +57,7 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); } public void SetLocalValueAndRaise( @@ -65,7 +65,16 @@ namespace Avalonia.PropertyStore StyledProperty property, T value) { - SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false); + } + + public void SetCurrentValueAndRaise( + ValueStore owner, + StyledProperty property, + T value) + { + IsOverridenCurrentValue = true; + SetAndRaiseCore(owner, property, value, Priority, true); } public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) @@ -98,7 +107,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -158,15 +167,16 @@ namespace Avalonia.PropertyStore ValueStore owner, StyledProperty property, T value, - BindingPriority priority) + BindingPriority priority, + bool isOverriddenCurrentValue) { - Debug.Assert(priority < BindingPriority.Inherited); - var oldValue = Value; var valueChanged = false; var baseValueChanged = false; var v = value; + IsOverridenCurrentValue = isOverriddenCurrentValue; + if (_uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); @@ -209,7 +219,6 @@ namespace Avalonia.PropertyStore T baseValue, BindingPriority basePriority) { - Debug.Assert(priority < BindingPriority.Inherited); Debug.Assert(basePriority > BindingPriority.Animation); Debug.Assert(priority <= basePriority); @@ -225,7 +234,7 @@ namespace Avalonia.PropertyStore bv = coerce(owner.Owner, baseValue); } - if (priority != BindingPriority.Unset && !EqualityComparer.Default.Equals(Value, v)) + if (!EqualityComparer.Default.Equals(Value, v)) { Value = v; valueChanged = true; @@ -233,9 +242,7 @@ namespace Avalonia.PropertyStore _uncommon._uncoercedValue = value; } - if (priority != BindingPriority.Unset && - (BasePriority == BindingPriority.Unset || - !EqualityComparer.Default.Equals(_baseValue, bv))) + if (!EqualityComparer.Default.Equals(_baseValue, bv)) { _baseValue = v; baseValueChanged = true; diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index a758360545..fd5cd91a6c 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -7,7 +7,6 @@ using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Styling; using Avalonia.Utilities; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -159,8 +158,9 @@ namespace Avalonia.PropertyStore public void ClearLocalValue(AvaloniaProperty property) { if (TryGetEffectiveValue(property, out var effective) && - effective.Priority == BindingPriority.LocalValue) + (effective.Priority == BindingPriority.LocalValue || effective.IsOverridenCurrentValue)) { + effective.IsOverridenCurrentValue = false; ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true); } } @@ -209,6 +209,20 @@ namespace Avalonia.PropertyStore } } + public void SetCurrentValue(StyledProperty property, T value) + { + if (_effectiveValues.TryGetValue(property, out var v)) + { + ((EffectiveValue)v).SetCurrentValueAndRaise(this, property, value); + } + else + { + var effectiveValue = new EffectiveValue(Owner, property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetCurrentValueAndRaise(this, property, value); + } + } + public object? GetValue(AvaloniaProperty property) { if (_effectiveValues.TryGetValue(property, out var v)) @@ -616,11 +630,13 @@ namespace Avalonia.PropertyStore { object? value; BindingPriority priority; + bool overridden = false; if (_effectiveValues.TryGetValue(property, out var v)) { value = v.Value; priority = v.Priority; + overridden = v.IsOverridenCurrentValue; } else if (property.Inherits && TryGetInheritedValue(property, out v)) { @@ -637,7 +653,8 @@ namespace Avalonia.PropertyStore property, value, priority, - null); + null, + overridden); } private int InsertFrame(ValueFrame frame) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs new file mode 100644 index 0000000000..16f924acba --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -0,0 +1,270 @@ +using System; +using System.Reactive.Linq; +using Avalonia.Data; +using Avalonia.Diagnostics; +using Avalonia.Reactive; +using Xunit; +using Observable = Avalonia.Reactive.Observable; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_SetCurrentValue + { + [Fact] + public void SetCurrentValue_Sets_Unset_Value() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void SetCurrentValue_Overrides_Existing_Value(BindingPriority priority) + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "oldvalue", priority); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void SetCurrentValue_Overrides_Inherited_Value() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetValue(Class1.InheritedProperty, "inheritedvalue"); + target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.InheritedProperty)); + Assert.True(IsOverridden(target, Class1.InheritedProperty)); + } + + [Fact] + public void SetCurrentValue_Is_Inherited() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.Equal(BindingPriority.Inherited, GetPriority(target, Class1.InheritedProperty)); + Assert.False(IsOverridden(target, Class1.InheritedProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Unset_Priority() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("foodefault", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Inherited_Priority() + { + var parent = new Class1(); + var target = new Class1 { InheritanceParent = parent }; + + parent.SetValue(Class1.InheritedProperty, "inheritedvalue"); + target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); + target.ClearValue(Class1.InheritedProperty); + + Assert.Equal("inheritedvalue", target.Inherited); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("foodefault", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void ClearValue_Clears_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "stylevalue", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.ClearValue(Class1.FooProperty); + + Assert.Equal("stylevalue", target.Foo); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void SetCurrentValue_Can_Be_Coerced() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.CoercedProperty, 60); + Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); + + target.CoerceMax = 50; + target.CoerceValue(Class1.CoercedProperty); + Assert.Equal(50, target.GetValue(Class1.CoercedProperty)); + + target.CoerceMax = 100; + target.CoerceValue(Class1.CoercedProperty); + Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void SetValue_Overrides_CurrentValue_With_Unset_Priority(BindingPriority priority) + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", priority); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void Animation_Value_Overrides_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.Animation); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Fact] + public void StyleTrigger_Value_Overrides_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "current"); + target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.StyleTrigger); + + Assert.Equal("setvalue", target.Foo); + Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + + [Theory] + [InlineData(BindingPriority.LocalValue)] + [InlineData(BindingPriority.Style)] + [InlineData(BindingPriority.Animation)] + public void Binding_Overrides_CurrentValue_With_Unset_Priority(BindingPriority priority) + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), priority); + + Assert.Equal("binding", target.Foo); + Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("foodefault", target.Foo); + } + + [Fact] + public void Animation_Binding_Overrides_CurrentValue_With_LocalValue_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "localvalue"); + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.Animation); + + Assert.Equal("binding", target.Foo); + Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("current", target.Foo); + } + + [Fact] + public void StyleTrigger_Binding_Overrides_CurrentValue_With_Style_Priority() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetCurrentValue(Class1.FooProperty, "current"); + + var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.StyleTrigger); + + Assert.Equal("binding", target.Foo); + Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + + s.Dispose(); + + Assert.Equal("style", target.Foo); + } + + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetDiagnostic(property).Priority; + } + + private bool IsOverridden(AvaloniaObject target, AvaloniaProperty property) + { + return target.GetDiagnostic(property).IsOverriddenCurrentValue; + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty InheritedProperty = + AvaloniaProperty.Register(nameof(Inherited), "inheriteddefault", inherits: true); + public static readonly StyledProperty CoercedProperty = + AvaloniaProperty.Register(nameof(Coerced), coerce: Coerce); + + public string Foo => GetValue(FooProperty); + public string Inherited => GetValue(InheritedProperty); + public double Coerced => GetValue(CoercedProperty); + public double CoerceMax { get; set; } = 100; + + private static double Coerce(AvaloniaObject sender, double value) + { + return Math.Min(value, ((Class1)sender).CoerceMax); + } + } + } +}