Browse Source

Initial implementation of SetCurrentValue.

Steven Kirk 2 years ago
parent
commit
19078979e3

+ 19 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -355,6 +355,23 @@ namespace Avalonia
             SetDirectValueUnchecked(property, value);
         }
 
+        public void SetCurrentValue<T>(StyledProperty<T> 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);
+            }
+        }
+
         /// <summary>
         /// Binds a <see cref="AvaloniaProperty"/> to an observable.
         /// </summary>
@@ -547,7 +564,8 @@ namespace Avalonia
                     property,
                     GetValue(property),
                     BindingPriority.LocalValue,
-                    null);
+                    null,
+                    false);
             }
 
             return _values.GetDiagnostic(property);

+ 12 - 11
src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs

@@ -3,28 +3,23 @@ using Avalonia.Data;
 namespace Avalonia.Diagnostics
 {
     /// <summary>
-    /// Holds diagnostic-related information about the value of a <see cref="AvaloniaProperty"/>
-    /// on a <see cref="AvaloniaObject"/>.
+    /// Holds diagnostic-related information about the value of an <see cref="AvaloniaProperty"/>
+    /// on an <see cref="AvaloniaObject"/>.
     /// </summary>
     public class AvaloniaPropertyValue
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AvaloniaPropertyValue"/> class.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <param name="value">The current property value.</param>
-        /// <param name="priority">The priority of the current value.</param>
-        /// <param name="diagnostic">A diagnostic string.</param>
-        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;
         }
 
         /// <summary>
@@ -46,5 +41,11 @@ namespace Avalonia.Diagnostics
         /// Gets a diagnostic string.
         /// </summary>
         public string? Diagnostic { get; }
+
+        /// <summary>
+        /// Gets a value indicating whether the <see cref="Value"/> was overridden by a call to 
+        /// <see cref="AvaloniaObject.SetCurrentValue{T}"/>.
+        /// </summary>
+        public bool IsOverriddenCurrentValue { get; }
     }
 }

+ 6 - 0
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@@ -29,6 +29,12 @@ namespace Avalonia.PropertyStore
         /// </summary>
         public BindingPriority BasePriority { 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>
         /// Begins a reevaluation pass on the effective value.
         /// </summary>

+ 18 - 11
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<T>)value.Property, GetValue(value), priority);
+            SetAndRaiseCore(owner,  (StyledProperty<T>)value.Property, GetValue(value), priority, false);
         }
 
         public void SetLocalValueAndRaise(
@@ -65,7 +65,16 @@ namespace Avalonia.PropertyStore
             StyledProperty<T> property,
             T value)
         {
-            SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
+            SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false);
+        }
+
+        public void SetCurrentValueAndRaise(
+            ValueStore owner,
+            StyledProperty<T> 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<T>)property, _baseValue!, BasePriority);
+            SetAndRaiseCore(owner, (StyledProperty<T>)property, _baseValue!, BasePriority, false);
         }
 
         public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
@@ -158,15 +167,16 @@ namespace Avalonia.PropertyStore
             ValueStore owner,
             StyledProperty<T> 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<T>.Default.Equals(Value, v))
+            if (!EqualityComparer<T>.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<T>.Default.Equals(_baseValue, bv)))
+            if (!EqualityComparer<T>.Default.Equals(_baseValue, bv))
             {
                 _baseValue = v;
                 baseValueChanged = true;

+ 20 - 3
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<T>(StyledProperty<T> property, T value)
+        {
+            if (_effectiveValues.TryGetValue(property, out var v))
+            {
+                ((EffectiveValue<T>)v).SetCurrentValueAndRaise(this, property, value);
+            }
+            else
+            {
+                var effectiveValue = new EffectiveValue<T>(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)

+ 270 - 0
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<string> FooProperty =
+                AvaloniaProperty.Register<Class1, string>(nameof(Foo), "foodefault");
+            public static readonly StyledProperty<string> InheritedProperty =
+                AvaloniaProperty.Register<Class1, string>(nameof(Inherited), "inheriteddefault", inherits: true);
+            public static readonly StyledProperty<double> CoercedProperty =
+                AvaloniaProperty.Register<Class1, double>(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);
+            }
+        }
+    }
+}