Browse Source

Add WPF-style property coercion.

pull/3287/head
Steven Kirk 7 years ago
parent
commit
0f04f4d01a
  1. 10
      src/Avalonia.Base/AvaloniaObject.cs
  2. 7
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 7
      src/Avalonia.Base/IAvaloniaObject.cs
  4. 14
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  5. 21
      src/Avalonia.Base/StyledPropertyBase.cs
  6. 17
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  7. 27
      src/Avalonia.Base/ValueStore.cs
  8. 139
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs

10
src/Avalonia.Base/AvaloniaObject.cs

@ -504,6 +504,16 @@ namespace Avalonia
return new DirectBindingSubscription<T>(this, property, source);
}
/// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
public void CoerceValue<T>(StyledPropertyBase<T> property)
{
_values?.CoerceValue(property);
}
/// <inheritdoc/>
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{

7
src/Avalonia.Base/AvaloniaProperty.cs

@ -257,7 +257,8 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <param name="notifying">
/// 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<TValue, bool> validate = null,
Func<IAvaloniaObject, TValue, TValue> coerce = null,
Action<IAvaloniaObject, bool> notifying = null)
where TOwner : IAvaloniaObject
{
@ -277,7 +279,8 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
defaultBindingMode: defaultBindingMode);
defaultBindingMode: defaultBindingMode,
coerce: coerce);
var result = new StyledProperty<TValue>(
name,

7
src/Avalonia.Base/IAvaloniaObject.cs

@ -106,6 +106,13 @@ namespace Avalonia
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source);
/// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
void CoerceValue<T>(StyledPropertyBase<T> property);
/// <summary>
/// Registers an object as an inheritance child.
/// </summary>

14
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -11,6 +11,7 @@ namespace Avalonia.PropertyStore
private readonly IAvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _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<TValue>(
StyledPropertyBase<TValue> 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;

21
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
/// </summary>
public Func<TValue, bool> ValidateValue { get; }
/// <summary>
/// Gets a value indicating whether this property has any value coercion callbacks defined
/// in its metadata.
/// </summary>
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;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
@ -145,6 +164,8 @@ namespace Avalonia
}
}
HasCoercion |= metadata.CoerceValue != null;
base.OverrideMetadata(type, metadata);
}

17
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -19,18 +19,26 @@ namespace Avalonia
/// </summary>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="defaultBindingMode">The default binding mode.</param>
/// <param name="coerce">A value coercion callback.</param>
public StyledPropertyMetadata(
Optional<TValue> defaultValue = default,
BindingMode defaultBindingMode = BindingMode.Default)
BindingMode defaultBindingMode = BindingMode.Default,
Func<IAvaloniaObject, TValue, TValue> coerce = null)
: base(defaultBindingMode)
{
_defaultValue = defaultValue;
CoerceValue = coerce;
}
/// <summary>
/// Gets the default value for the property.
/// </summary>
internal TValue DefaultValue => _defaultValue.ValueOrDefault();
public TValue DefaultValue => _defaultValue.ValueOrDefault();
/// <summary>
/// Gets the value coercion callback, if any.
/// </summary>
public Func<IAvaloniaObject, TValue, TValue>? CoerceValue { get; private set; }
object IStyledPropertyMetadata.DefaultValue => DefaultValue;
@ -45,6 +53,11 @@ namespace Avalonia
{
_defaultValue = src.DefaultValue;
}
if (CoerceValue == null)
{
CoerceValue = src.CoerceValue;
}
}
}

27
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<T>(_owner, property, this);
_values.AddValue(property, entry);
entry.SetValue(value, priority);
}
else if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry<T>(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<T>(_owner, property, this);
var binding = entry.AddBinding(source, priority);
_values.AddValue(property, entry);
binding.Start();
return binding;
}
else
{
var entry = new BindingEntry<T>(_owner, property, source, priority, this);
@ -134,6 +150,17 @@ namespace Avalonia
}
}
public void CoerceValue<T>(StyledPropertyBase<T> property)
{
if (_values.TryGetValue(property, out var slot))
{
if (slot is PriorityValue<T> p)
{
p.CoerceValue();
}
}
}
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))

139
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<BindingValue<int>>();
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<BindingValue<int>>();
var source2 = new Subject<BindingValue<int>>();
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<int> FooProperty =
AvaloniaProperty.Register<Class1, int>(
"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<int> FooProperty =
Class1.FooProperty.AddOwner<Class2>();
static Class2()
{
FooProperty.OverrideMetadata<Class2>(
new StyledPropertyMetadata<int>(
coerce: CoerceFoo));
}
public int Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public static int CoerceFoo(IAvaloniaObject instance, int value)
{
return -value;
}
}
}
}
Loading…
Cancel
Save