From 0f04f4d01afc489d30d7bbbb71a095951e8c7095 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Nov 2019 11:03:49 +0100 Subject: [PATCH] Add WPF-style property coercion. --- src/Avalonia.Base/AvaloniaObject.cs | 10 ++ src/Avalonia.Base/AvaloniaProperty.cs | 7 +- src/Avalonia.Base/IAvaloniaObject.cs | 7 + .../PropertyStore/PriorityValue.cs | 14 ++ src/Avalonia.Base/StyledPropertyBase.cs | 21 +++ src/Avalonia.Base/StyledPropertyMetadata`1.cs | 17 ++- src/Avalonia.Base/ValueStore.cs | 27 ++++ .../AvaloniaObjectTests_Coercion.cs | 139 ++++++++++++++++++ 8 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e6d54727b3..5bddf95616 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -504,6 +504,16 @@ namespace Avalonia return new DirectBindingSubscription(this, property, source); } + /// + /// Coerces the specified . + /// + /// The type of the property. + /// The property. + public void CoerceValue(StyledPropertyBase property) + { + _values?.CoerceValue(property); + } + /// void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child) { diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 31a7f21a1b..8e5716a5bf 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -257,7 +257,8 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A value validation callback. + /// A value validation callback. + /// A value coercion callback. /// /// A method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is @@ -270,6 +271,7 @@ namespace Avalonia bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null, + Func coerce = null, Action notifying = null) where TOwner : IAvaloniaObject { @@ -277,7 +279,8 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + coerce: coerce); var result = new StyledProperty( name, diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index 4fec3d71af..1ccdaa8f0b 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -106,6 +106,13 @@ namespace Avalonia DirectPropertyBase property, IObservable> source); + /// + /// Coerces the specified . + /// + /// The type of the property. + /// The property. + void CoerceValue(StyledPropertyBase property); + /// /// Registers an object as an inheritance child. /// diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 1a4b616bd9..60baf9d405 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -11,6 +11,7 @@ namespace Avalonia.PropertyStore private readonly IAvaloniaObject _owner; private readonly IValueSink _sink; private readonly List> _entries = new List>(); + private readonly Func? _coerceValue; private Optional _localValue; public PriorityValue( @@ -21,6 +22,12 @@ namespace Avalonia.PropertyStore _owner = owner; Property = property; _sink = sink; + + if (property.HasCoercion) + { + var metadata = property.GetMetadata(owner.GetType()); + _coerceValue = metadata.CoerceValue; + } } public PriorityValue( @@ -83,6 +90,8 @@ namespace Avalonia.PropertyStore return binding; } + public void CoerceValue() => UpdateEffectiveValue(); + void IValueSink.ValueChanged( StyledPropertyBase property, BindingPriority priority, @@ -156,6 +165,11 @@ namespace Avalonia.PropertyStore ValuePriority = BindingPriority.LocalValue; } + if (value.HasValue && _coerceValue != null) + { + value = _coerceValue(_owner, value.Value); + } + if (value != Value) { var old = Value; diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index 8e7bfd6467..8c4d683ae0 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -43,6 +43,7 @@ namespace Avalonia _inherits = inherits; ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; if (validate?.Invoke(metadata.DefaultValue) == false) { @@ -75,6 +76,24 @@ namespace Avalonia /// public Func ValidateValue { get; } + /// + /// Gets a value indicating whether this property has any value coercion callbacks defined + /// in its metadata. + /// + internal bool HasCoercion { get; private set; } + + public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue) + { + var metadata = GetMetadata(instance.GetType()); + + if (metadata.CoerceValue != null) + { + return metadata.CoerceValue.Invoke(instance, baseValue); + } + + return baseValue; + } + /// /// Gets the default value for the property on the specified type. /// @@ -145,6 +164,8 @@ namespace Avalonia } } + HasCoercion |= metadata.CoerceValue != null; + base.OverrideMetadata(type, metadata); } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 50236e2c7c..bd6c125776 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -19,18 +19,26 @@ namespace Avalonia /// /// The default value of the property. /// The default binding mode. + /// A value coercion callback. public StyledPropertyMetadata( Optional defaultValue = default, - BindingMode defaultBindingMode = BindingMode.Default) + BindingMode defaultBindingMode = BindingMode.Default, + Func coerce = null) : base(defaultBindingMode) { _defaultValue = defaultValue; + CoerceValue = coerce; } /// /// Gets the default value for the property. /// - internal TValue DefaultValue => _defaultValue.ValueOrDefault(); + public TValue DefaultValue => _defaultValue.ValueOrDefault(); + + /// + /// Gets the value coercion callback, if any. + /// + public Func? CoerceValue { get; private set; } object IStyledPropertyMetadata.DefaultValue => DefaultValue; @@ -45,6 +53,11 @@ namespace Avalonia { _defaultValue = src.DefaultValue; } + + if (CoerceValue == null) + { + CoerceValue = src.CoerceValue; + } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 09961b399b..dec1f62386 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -75,6 +75,13 @@ namespace Avalonia { 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); + } else if (priority == BindingPriority.LocalValue) { _values.AddValue(property, new LocalValueEntry(value)); @@ -97,6 +104,15 @@ namespace Avalonia { return BindExisting(slot, property, source, priority); } + else if (property.HasCoercion) + { + // If the property has any coercion callbacks then always create a PriorityValue. + var entry = new PriorityValue(_owner, property, this); + var binding = entry.AddBinding(source, priority); + _values.AddValue(property, entry); + binding.Start(); + return binding; + } else { var entry = new BindingEntry(_owner, property, source, priority, this); @@ -134,6 +150,17 @@ namespace Avalonia } } + public void CoerceValue(StyledPropertyBase property) + { + if (_values.TryGetValue(property, out var slot)) + { + if (slot is PriorityValue p) + { + p.CoerceValue(); + } + } + } + public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) { if (_values.TryGetValue(property, out var slot)) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs new file mode 100644 index 0000000000..8d8dbb03a2 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs @@ -0,0 +1,139 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Subjects; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_Coercion + { + [Fact] + public void Coerces_Set_Value() + { + var target = new Class1(); + + target.Foo = 150; + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void Coerces_Bound_Value() + { + var target = new Class1(); + var source = new Subject>(); + + target.Bind(Class1.FooProperty, source); + source.OnNext(150); + + Assert.Equal(100, target.Foo); + } + + [Fact] + public void CoerceValue_Updates_Value() + { + var target = new Class1 { Foo = 99 }; + + Assert.Equal(99, target.Foo); + + target.MaxFoo = 50; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(50, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_If_Limit_Changed() + { + var target = new Class1(); + + target.Foo = 150; + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + target.CoerceValue(Class1.FooProperty); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coerced_Value_Can_Be_Restored_From_Previously_Active_Binding() + { + var target = new Class1(); + var source1 = new Subject>(); + var source2 = new Subject>(); + + target.Bind(Class1.FooProperty, source1); + source1.OnNext(150); + + target.Bind(Class1.FooProperty, source2); + source2.OnNext(160); + + Assert.Equal(100, target.Foo); + + target.MaxFoo = 200; + source2.OnCompleted(); + + Assert.Equal(150, target.Foo); + } + + [Fact] + public void Coercion_Can_Be_Overridden() + { + var target = new Class2(); + + target.Foo = 150; + + Assert.Equal(-150, target.Foo); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register( + "Qux", + defaultValue: 11, + coerce: CoerceFoo); + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public int MaxFoo { get; set; } = 100; + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return Math.Min(((Class1)instance).MaxFoo, value); + } + } + + private class Class2 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + Class1.FooProperty.AddOwner(); + + static Class2() + { + FooProperty.OverrideMetadata( + new StyledPropertyMetadata( + coerce: CoerceFoo)); + } + + public int Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public static int CoerceFoo(IAvaloniaObject instance, int value) + { + return -value; + } + } + } +}