diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index b0ff591682..88b99cd99a 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -311,7 +311,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public IDisposable SetValue( StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue) @@ -335,8 +338,10 @@ namespace Avalonia } else if (!(value is DoNothingType)) { - Values.SetValue(property, value, priority); + return Values.SetValue(property, value, priority); } + + return null; } /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index a4c7fa95a5..0f82042dcd 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -458,7 +458,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public static void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( this IAvaloniaObject target, AvaloniaProperty property, object value, @@ -467,7 +470,7 @@ namespace Avalonia target = target ?? throw new ArgumentNullException(nameof(target)); property = property ?? throw new ArgumentNullException(nameof(property)); - property.RouteSetValue(target, value, priority); + return property.RouteSetValue(target, value, priority); } /// @@ -478,7 +481,10 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - public static void SetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + public static IDisposable SetValue( this IAvaloniaObject target, AvaloniaProperty property, T value, @@ -490,11 +496,10 @@ namespace Avalonia switch (property) { case StyledPropertyBase styled: - target.SetValue(styled, value, priority); - break; + return target.SetValue(styled, value, priority); case DirectPropertyBase direct: target.SetValue(direct, value); - break; + return null; default: throw new NotSupportedException("Unsupported AvaloniaProperty type."); } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index aa7a675764..b0858f8bc7 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -496,7 +496,10 @@ namespace Avalonia /// The object instance. /// The value. /// The priority. - internal abstract void RouteSetValue( + /// + /// An if setting the property can be undone, otherwise null. + /// + internal abstract IDisposable? RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority); diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 39ed3b084f..d0dd841a70 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -114,7 +114,7 @@ namespace Avalonia } /// - internal override void RouteSetValue( + internal override IDisposable? RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) @@ -133,6 +133,8 @@ namespace Avalonia { throw v.Error!; } + + return null; } /// diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index fb85ae222c..81a212b087 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -65,7 +65,7 @@ namespace Avalonia /// The property. /// The value. /// The priority of the value. - void SetValue( + IDisposable SetValue( StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue); diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs index f15f56e32b..aa054c46ff 100644 --- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs @@ -10,16 +10,20 @@ namespace Avalonia.PropertyStore /// . /// /// The property type. - internal class ConstantValueEntry : IPriorityValueEntry + internal class ConstantValueEntry : IPriorityValueEntry, IDisposable { + private IValueSink _sink; + public ConstantValueEntry( StyledPropertyBase property, T value, - BindingPriority priority) + BindingPriority priority, + IValueSink sink) { Property = property; Value = value; Priority = priority; + _sink = sink; } public StyledPropertyBase Property { get; } @@ -28,6 +32,7 @@ namespace Avalonia.PropertyStore Optional IValue.Value => Value.ToObject(); BindingPriority IValue.ValuePriority => Priority; - public void Reparent(IValueSink sink) { } + public void Dispose() => _sink.Completed(Property, this, Value); + public void Reparent(IValueSink sink) => _sink = sink; } } diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 4ef8f650fa..affb20f334 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -78,8 +78,10 @@ namespace Avalonia.PropertyStore public void ClearLocalValue() => UpdateEffectiveValue(); - public void SetValue(T value, BindingPriority priority) + public IDisposable? SetValue(T value, BindingPriority priority) { + IDisposable? result = null; + if (priority == BindingPriority.LocalValue) { _localValue = value; @@ -87,10 +89,13 @@ namespace Avalonia.PropertyStore else { var insert = FindInsertPoint(priority); - _entries.Insert(insert, new ConstantValueEntry(Property, value, priority)); + var entry = new ConstantValueEntry(Property, value, priority, this); + _entries.Insert(insert, entry); + result = entry; } UpdateEffectiveValue(); + return result; } public BindingEntry AddBinding(IObservable> source, BindingPriority priority) diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index d1f961a567..53fcb51c5b 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -194,7 +194,7 @@ namespace Avalonia } /// - internal override void RouteSetValue( + internal override IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) @@ -203,7 +203,7 @@ namespace Avalonia if (v.HasValue) { - o.SetValue(this, (TValue)v.Value, priority); + return o.SetValue(this, (TValue)v.Value, priority); } else if (v.Type == BindingValueType.UnsetValue) { @@ -213,6 +213,8 @@ namespace Avalonia { throw v.Error; } + + return null; } /// diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index e310be0f0a..104c06de0f 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -70,23 +70,25 @@ namespace Avalonia return false; } - public void SetValue(StyledPropertyBase property, T value, BindingPriority priority) + public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) { if (property.ValidateValue?.Invoke(value) == false) { throw new ArgumentException($"{value} is not a valid value for '{property.Name}."); } + IDisposable? result = null; + if (_values.TryGetValue(property, out var slot)) { - SetExisting(slot, property, value, priority); + result = SetExisting(slot, property, value, priority); } else if (property.HasCoercion) { // If the property has any coercion callbacks then always create a PriorityValue. var entry = new PriorityValue(_owner, property, this); _values.AddValue(property, entry); - entry.SetValue(value, priority); + result = entry.SetValue(value, priority); } else if (priority == BindingPriority.LocalValue) { @@ -95,10 +97,13 @@ namespace Avalonia } else { - var entry = new ConstantValueEntry(property, value, priority); + var entry = new ConstantValueEntry(property, value, priority, this); _values.AddValue(property, entry); _sink.ValueChanged(property, priority, default, value); + result = entry; } + + return result; } public IDisposable AddBinding( @@ -205,21 +210,23 @@ namespace Avalonia } } - private void SetExisting( + private IDisposable? SetExisting( object slot, StyledPropertyBase property, T value, BindingPriority priority) { + IDisposable? result = null; + if (slot is IPriorityValueEntry e) { var priorityValue = new PriorityValue(_owner, property, this, e); _values.SetValue(property, priorityValue); - priorityValue.SetValue(value, priority); + result = priorityValue.SetValue(value, priority); } else if (slot is PriorityValue p) { - p.SetValue(value, priority); + result = p.SetValue(value, priority); } else if (slot is LocalValueEntry l) { @@ -232,7 +239,7 @@ namespace Avalonia else { var priorityValue = new PriorityValue(_owner, property, this, l); - priorityValue.SetValue(value, priority); + result = priorityValue.SetValue(value, priority); _values.SetValue(property, priorityValue); } } @@ -240,6 +247,8 @@ namespace Avalonia { throw new NotSupportedException("Unrecognised value store slot type."); } + + return result; } private IDisposable BindExisting( diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 4b477287e8..1b8cd787f2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -284,6 +284,41 @@ namespace Avalonia.Base.UnitTests Assert.Equal("newvalue", target.GetValue(Class1.FrankProperty)); } + [Fact] + public void Disposing_Style_SetValue_Reverts_To_DefaultValue() + { + Class1 target = new Class1(); + + var d = target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style); + d.Dispose(); + + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Disposing_Style_SetValue_Reverts_To_Previous_Style_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.Style); + var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Style); + d.Dispose(); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void Disposing_Animation_SetValue_Reverts_To_Previous_Local_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.LocalValue); + var d = target.SetValue(Class1.FooProperty, "bar", BindingPriority.Animation); + d.Dispose(); + + Assert.Equal("foo", target.GetValue(Class1.FooProperty)); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 6bb8dfe1f5..788376b2fd 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -146,7 +146,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - internal override void RouteSetValue( + internal override IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority) diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 8c76445645..5e69b8490d 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -24,7 +24,11 @@ namespace Avalonia.Base.UnitTests Owner, TestProperty, NullSink, - new ConstantValueEntry(TestProperty, "1", BindingPriority.StyleTrigger)); + new ConstantValueEntry( + TestProperty, + "1", + BindingPriority.StyleTrigger, + NullSink)); Assert.Equal("1", target.Value.Value); Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority);