From 19078979e38a9facb0be00ed0daeca3bd53c9e3a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 09:40:42 +0100 Subject: [PATCH 1/6] 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); + } + } + } +} From 8741b7e4106fd0ceef9f68ddc5eb3dfae5604e8e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 10:53:03 +0100 Subject: [PATCH 2/6] Fix IsSet with SetCurrentValue. And add unit tests. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 7 +------ .../AvaloniaObjectTests_SetCurrentValue.cs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index fd5cd91a6c..64e3c498e9 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -249,12 +249,7 @@ namespace Avalonia.PropertyStore return false; } - public bool IsSet(AvaloniaProperty property) - { - if (_effectiveValues.TryGetValue(property, out var v)) - return v.Priority < BindingPriority.Inherited; - return false; - } + public bool IsSet(AvaloniaProperty property) => _effectiveValues.TryGetValue(property, out _); public void CoerceValue(AvaloniaProperty property) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 16f924acba..3edf0b105a 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,8 +1,6 @@ using System; -using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; -using Avalonia.Reactive; using Xunit; using Observable = Avalonia.Reactive.Observable; @@ -18,6 +16,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); Assert.True(IsOverridden(target, Class1.FooProperty)); } @@ -34,6 +33,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.FooProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.True(IsOverridden(target, Class1.FooProperty)); } @@ -48,6 +48,7 @@ namespace Avalonia.Base.UnitTests target.SetCurrentValue(Class1.InheritedProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.True(target.IsSet(Class1.InheritedProperty)); Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.InheritedProperty)); Assert.True(IsOverridden(target, Class1.InheritedProperty)); } @@ -61,6 +62,7 @@ namespace Avalonia.Base.UnitTests parent.SetCurrentValue(Class1.InheritedProperty, "newvalue"); Assert.Equal("newvalue", target.GetValue(Class1.InheritedProperty)); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Inherited, GetPriority(target, Class1.InheritedProperty)); Assert.False(IsOverridden(target, Class1.InheritedProperty)); } @@ -74,6 +76,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -88,6 +91,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.InheritedProperty); Assert.Equal("inheritedvalue", target.Inherited); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -101,6 +105,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -114,6 +119,7 @@ namespace Avalonia.Base.UnitTests target.ClearValue(Class1.FooProperty); Assert.Equal("stylevalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -146,6 +152,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", priority); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -160,6 +167,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.Animation); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -174,6 +182,7 @@ namespace Avalonia.Base.UnitTests target.SetValue(Class1.FooProperty, "setvalue", BindingPriority.StyleTrigger); Assert.Equal("setvalue", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); } @@ -191,6 +200,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), priority); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(priority, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); @@ -210,6 +220,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.Animation); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.Animation, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); @@ -229,6 +240,7 @@ namespace Avalonia.Base.UnitTests var s = target.Bind(Class1.FooProperty, Observable.SingleValue("binding"), BindingPriority.StyleTrigger); Assert.Equal("binding", target.Foo); + Assert.True(target.IsSet(Class1.FooProperty)); Assert.Equal(BindingPriority.StyleTrigger, GetPriority(target, Class1.FooProperty)); Assert.False(IsOverridden(target, Class1.FooProperty)); From 0ce9180d7c56b17c7b6a03c44d82161f6d031036 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 11:41:59 +0100 Subject: [PATCH 3/6] Added untyped SetCurrentValue. --- src/Avalonia.Base/AvaloniaObject.cs | 34 +++++++++++++++++++ src/Avalonia.Base/AvaloniaProperty.cs | 7 ++++ src/Avalonia.Base/DirectPropertyBase.cs | 5 +++ src/Avalonia.Base/StyledProperty.cs | 21 ++++++++++++ .../AvaloniaObjectTests_SetCurrentValue.cs | 26 ++++++++++++++ .../AvaloniaPropertyTests.cs | 5 +++ 6 files changed, 98 insertions(+) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 93bbee12b8..5a5827d0aa 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -355,6 +355,40 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// + public void SetCurrentValue(AvaloniaProperty property, object? value) => + property.RouteSetCurrentValue(this, value); + + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The type of the property. + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// public void SetCurrentValue(StyledProperty property, T value) { _ = property ?? throw new ArgumentNullException(nameof(property)); diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 5db4d81f03..1c1d09c3f5 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,6 +496,13 @@ namespace Avalonia object? value, BindingPriority priority); + /// + /// Routes an untyped SetCurrentValue call to a typed call. + /// + /// The object instance. + /// The value. + internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value); + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 9ee1eee0fa..94dfaaab01 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -152,6 +152,11 @@ namespace Avalonia return null; } + internal override void RouteSetCurrentValue(AvaloniaObject o, object? value) + { + RouteSetValue(o, value, BindingPriority.LocalValue); + } + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 79d1b9202d..8e0ecf5544 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -220,6 +220,27 @@ namespace Avalonia } } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + internal override void RouteSetCurrentValue(AvaloniaObject target, object? value) + { + if (value == BindingOperations.DoNothing) + return; + + if (value == UnsetValue) + { + target.ClearValue(this); + } + else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) + { + target.SetCurrentValue(this, (TValue)converted!); + } + else + { + var type = value?.GetType().FullName ?? "(null)"; + throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); + } + } + internal override IDisposable RouteBind( AvaloniaObject target, IObservable source, diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 3edf0b105a..8ad36a583e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -21,6 +21,19 @@ namespace Avalonia.Base.UnitTests Assert.True(IsOverridden(target, Class1.FooProperty)); } + [Fact] + public void SetCurrentValue_Sets_Unset_Value_Untyped() + { + var target = new Class1(); + + target.SetCurrentValue((AvaloniaProperty)Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.GetValue(Class1.FooProperty)); + Assert.True(target.IsSet(Class1.FooProperty)); + Assert.Equal(BindingPriority.Unset, GetPriority(target, Class1.FooProperty)); + Assert.True(IsOverridden(target, Class1.FooProperty)); + } + [Theory] [InlineData(BindingPriority.LocalValue)] [InlineData(BindingPriority.Style)] @@ -140,6 +153,19 @@ namespace Avalonia.Base.UnitTests Assert.Equal(60, target.GetValue(Class1.CoercedProperty)); } + [Fact] + public void SetCurrentValue_Unset_Clears_CurrentValue() + { + var target = new Class1(); + + target.SetCurrentValue(Class1.FooProperty, "newvalue"); + target.SetCurrentValue(Class1.FooProperty, AvaloniaProperty.UnsetValue); + + Assert.Equal("foodefault", target.Foo); + Assert.False(target.IsSet(Class1.FooProperty)); + Assert.False(IsOverridden(target, Class1.FooProperty)); + } + [Theory] [InlineData(BindingPriority.LocalValue)] [InlineData(BindingPriority.Style)] diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 5733159a23..a9b8a5f21b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -179,6 +179,11 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } + internal override void RouteSetCurrentValue(AvaloniaObject o, object value) + { + throw new NotImplementedException(); + } + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) { throw new NotImplementedException(); From 15a0fd6a3f294847dfc26f54690dbf0fb0d86e23 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 16:32:12 +0100 Subject: [PATCH 4/6] ClearLocalValue => ClearValue. It no longer just clears the local value, and the method on `AvaloniaObject` is called simply `ClearValue` so makes sense to use the same naming here. --- src/Avalonia.Base/AvaloniaObject.cs | 8 ++++---- src/Avalonia.Base/PropertyStore/ValueStore.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 5a5827d0aa..2c9efc7767 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -118,7 +118,7 @@ namespace Avalonia { _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - _values.ClearLocalValue(property); + _values.ClearValue(property); } /// @@ -152,7 +152,7 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - _values.ClearLocalValue(property); + _values.ClearValue(property); } /// @@ -329,7 +329,7 @@ namespace Avalonia if (value is UnsetValueType) { if (priority == BindingPriority.LocalValue) - _values.ClearLocalValue(property); + _values.ClearValue(property); } else if (value is not DoNothingType) { @@ -398,7 +398,7 @@ namespace Avalonia if (value is UnsetValueType) { - _values.ClearLocalValue(property); + _values.ClearValue(property); } else if (value is not DoNothingType) { diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 64e3c498e9..03d7efc566 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -155,7 +155,7 @@ namespace Avalonia.PropertyStore return observer; } - public void ClearLocalValue(AvaloniaProperty property) + public void ClearValue(AvaloniaProperty property) { if (TryGetEffectiveValue(property, out var effective) && (effective.Priority == BindingPriority.LocalValue || effective.IsOverridenCurrentValue)) @@ -499,7 +499,7 @@ namespace Avalonia.PropertyStore if (existing == observer) { _localValueBindings?.Remove(property.Id); - ClearLocalValue(property); + ClearValue(property); } } } From fcf1ce74c92174fd77d5b6b09c29e586483029f4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 16:34:46 +0100 Subject: [PATCH 5/6] Use TryGetEffectiveValue. For consistency with the other methods. --- src/Avalonia.Base/PropertyStore/ValueStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 03d7efc566..8b702665f8 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -211,7 +211,7 @@ namespace Avalonia.PropertyStore public void SetCurrentValue(StyledProperty property, T value) { - if (_effectiveValues.TryGetValue(property, out var v)) + if (TryGetEffectiveValue(property, out var v)) { ((EffectiveValue)v).SetCurrentValueAndRaise(this, property, value); } From 5d66bd0c0e7aae7cf7a1f07cd927fbb4bfa1fa28 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 13 Feb 2023 16:55:32 +0100 Subject: [PATCH 6/6] Refactored common code into separate method. --- src/Avalonia.Base/StyledProperty.cs | 59 +++++++++++++---------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 8e0ecf5544..ad1f09066e 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -194,45 +194,48 @@ namespace Avalonia } /// - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] internal override IDisposable? RouteSetValue( AvaloniaObject target, object? value, BindingPriority priority) { - if (value == BindingOperations.DoNothing) - { - return null; - } - else if (value == UnsetValue) - { - target.ClearValue(this); - return null; - } - else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) - { - return target.SetValue(this, (TValue)converted!, priority); - } - else - { - var type = value?.GetType().FullName ?? "(null)"; - throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); - } + if (ShouldSetValue(target, value, out var converted)) + return target.SetValue(this, converted, priority); + return null; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] internal override void RouteSetCurrentValue(AvaloniaObject target, object? value) { - if (value == BindingOperations.DoNothing) - return; + if (ShouldSetValue(target, value, out var converted)) + target.SetCurrentValue(this, converted); + } + + internal override IDisposable RouteBind( + AvaloniaObject target, + IObservable source, + BindingPriority priority) + { + return target.Bind(this, source, priority); + } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + private bool ShouldSetValue(AvaloniaObject target, object? value, [NotNullWhen(true)] out TValue? converted) + { + if (value == BindingOperations.DoNothing) + { + converted = default; + return false; + } if (value == UnsetValue) { target.ClearValue(this); + converted = default; + return false; } - else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) + else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var v)) { - target.SetCurrentValue(this, (TValue)converted!); + converted = (TValue)v!; + return true; } else { @@ -241,14 +244,6 @@ namespace Avalonia } } - internal override IDisposable RouteBind( - AvaloniaObject target, - IObservable source, - BindingPriority priority) - { - return target.Bind(this, source, priority); - } - private object? GetDefaultBoxedValue(Type type) { _ = type ?? throw new ArgumentNullException(nameof(type));