Skip to content

Commit 0f04f4d

Browse files
committed
Add WPF-style property coercion.
1 parent aa81db7 commit 0f04f4d

8 files changed

Lines changed: 238 additions & 4 deletions

File tree

src/Avalonia.Base/AvaloniaObject.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,16 @@ public IDisposable Bind<T>(
504504
return new DirectBindingSubscription<T>(this, property, source);
505505
}
506506

507+
/// <summary>
508+
/// Coerces the specified <see cref="AvaloniaProperty"/>.
509+
/// </summary>
510+
/// <typeparam name="T">The type of the property.</typeparam>
511+
/// <param name="property">The property.</param>
512+
public void CoerceValue<T>(StyledPropertyBase<T> property)
513+
{
514+
_values?.CoerceValue(property);
515+
}
516+
507517
/// <inheritdoc/>
508518
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
509519
{

src/Avalonia.Base/AvaloniaProperty.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ protected AvaloniaProperty(
257257
/// <param name="defaultValue">The default value of the property.</param>
258258
/// <param name="inherits">Whether the property inherits its value.</param>
259259
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
260-
/// <param name="validate">A value validation callback.</param>
260+
/// <param name="validate">A value validation callback.</param>
261+
/// <param name="coerce">A value coercion callback.</param>
261262
/// <param name="notifying">
262263
/// A method that gets called before and after the property starts being notified on an
263264
/// object; the bool argument will be true before and false afterwards. This callback is
@@ -270,14 +271,16 @@ public static StyledProperty<TValue> Register<TOwner, TValue>(
270271
bool inherits = false,
271272
BindingMode defaultBindingMode = BindingMode.OneWay,
272273
Func<TValue, bool> validate = null,
274+
Func<IAvaloniaObject, TValue, TValue> coerce = null,
273275
Action<IAvaloniaObject, bool> notifying = null)
274276
where TOwner : IAvaloniaObject
275277
{
276278
Contract.Requires<ArgumentNullException>(name != null);
277279

278280
var metadata = new StyledPropertyMetadata<TValue>(
279281
defaultValue,
280-
defaultBindingMode: defaultBindingMode);
282+
defaultBindingMode: defaultBindingMode,
283+
coerce: coerce);
281284

282285
var result = new StyledProperty<TValue>(
283286
name,

src/Avalonia.Base/IAvaloniaObject.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ IDisposable Bind<T>(
106106
DirectPropertyBase<T> property,
107107
IObservable<BindingValue<T>> source);
108108

109+
/// <summary>
110+
/// Coerces the specified <see cref="AvaloniaProperty"/>.
111+
/// </summary>
112+
/// <typeparam name="T">The type of the property.</typeparam>
113+
/// <param name="property">The property.</param>
114+
void CoerceValue<T>(StyledPropertyBase<T> property);
115+
109116
/// <summary>
110117
/// Registers an object as an inheritance child.
111118
/// </summary>

src/Avalonia.Base/PropertyStore/PriorityValue.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal class PriorityValue<T> : IValue<T>, IValueSink
1111
private readonly IAvaloniaObject _owner;
1212
private readonly IValueSink _sink;
1313
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
14+
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
1415
private Optional<T> _localValue;
1516

1617
public PriorityValue(
@@ -21,6 +22,12 @@ public PriorityValue(
2122
_owner = owner;
2223
Property = property;
2324
_sink = sink;
25+
26+
if (property.HasCoercion)
27+
{
28+
var metadata = property.GetMetadata(owner.GetType());
29+
_coerceValue = metadata.CoerceValue;
30+
}
2431
}
2532

2633
public PriorityValue(
@@ -83,6 +90,8 @@ public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPr
8390
return binding;
8491
}
8592

93+
public void CoerceValue() => UpdateEffectiveValue();
94+
8695
void IValueSink.ValueChanged<TValue>(
8796
StyledPropertyBase<TValue> property,
8897
BindingPriority priority,
@@ -156,6 +165,11 @@ private void UpdateEffectiveValue()
156165
ValuePriority = BindingPriority.LocalValue;
157166
}
158167

168+
if (value.HasValue && _coerceValue != null)
169+
{
170+
value = _coerceValue(_owner, value.Value);
171+
}
172+
159173
if (value != Value)
160174
{
161175
var old = Value;

src/Avalonia.Base/StyledPropertyBase.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ protected StyledPropertyBase(
4343

4444
_inherits = inherits;
4545
ValidateValue = validate;
46+
HasCoercion |= metadata.CoerceValue != null;
4647

4748
if (validate?.Invoke(metadata.DefaultValue) == false)
4849
{
@@ -75,6 +76,24 @@ protected StyledPropertyBase(StyledPropertyBase<TValue> source, Type ownerType)
7576
/// </summary>
7677
public Func<TValue, bool> ValidateValue { get; }
7778

79+
/// <summary>
80+
/// Gets a value indicating whether this property has any value coercion callbacks defined
81+
/// in its metadata.
82+
/// </summary>
83+
internal bool HasCoercion { get; private set; }
84+
85+
public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue)
86+
{
87+
var metadata = GetMetadata(instance.GetType());
88+
89+
if (metadata.CoerceValue != null)
90+
{
91+
return metadata.CoerceValue.Invoke(instance, baseValue);
92+
}
93+
94+
return baseValue;
95+
}
96+
7897
/// <summary>
7998
/// Gets the default value for the property on the specified type.
8099
/// </summary>
@@ -145,6 +164,8 @@ public void OverrideMetadata(Type type, StyledPropertyMetadata<TValue> metadata)
145164
}
146165
}
147166

167+
HasCoercion |= metadata.CoerceValue != null;
168+
148169
base.OverrideMetadata(type, metadata);
149170
}
150171

src/Avalonia.Base/StyledPropertyMetadata`1.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,26 @@ public class StyledPropertyMetadata<TValue> : PropertyMetadata, IStyledPropertyM
1919
/// </summary>
2020
/// <param name="defaultValue">The default value of the property.</param>
2121
/// <param name="defaultBindingMode">The default binding mode.</param>
22+
/// <param name="coerce">A value coercion callback.</param>
2223
public StyledPropertyMetadata(
2324
Optional<TValue> defaultValue = default,
24-
BindingMode defaultBindingMode = BindingMode.Default)
25+
BindingMode defaultBindingMode = BindingMode.Default,
26+
Func<IAvaloniaObject, TValue, TValue> coerce = null)
2527
: base(defaultBindingMode)
2628
{
2729
_defaultValue = defaultValue;
30+
CoerceValue = coerce;
2831
}
2932

3033
/// <summary>
3134
/// Gets the default value for the property.
3235
/// </summary>
33-
internal TValue DefaultValue => _defaultValue.ValueOrDefault();
36+
public TValue DefaultValue => _defaultValue.ValueOrDefault();
37+
38+
/// <summary>
39+
/// Gets the value coercion callback, if any.
40+
/// </summary>
41+
public Func<IAvaloniaObject, TValue, TValue>? CoerceValue { get; private set; }
3442

3543
object IStyledPropertyMetadata.DefaultValue => DefaultValue;
3644

@@ -45,6 +53,11 @@ public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty prope
4553
{
4654
_defaultValue = src.DefaultValue;
4755
}
56+
57+
if (CoerceValue == null)
58+
{
59+
CoerceValue = src.CoerceValue;
60+
}
4861
}
4962
}
5063

src/Avalonia.Base/ValueStore.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority
7575
{
7676
SetExisting(slot, property, value, priority);
7777
}
78+
else if (property.HasCoercion)
79+
{
80+
// If the property has any coercion callbacks then always create a PriorityValue.
81+
var entry = new PriorityValue<T>(_owner, property, this);
82+
_values.AddValue(property, entry);
83+
entry.SetValue(value, priority);
84+
}
7885
else if (priority == BindingPriority.LocalValue)
7986
{
8087
_values.AddValue(property, new LocalValueEntry<T>(value));
@@ -97,6 +104,15 @@ public IDisposable AddBinding<T>(
97104
{
98105
return BindExisting(slot, property, source, priority);
99106
}
107+
else if (property.HasCoercion)
108+
{
109+
// If the property has any coercion callbacks then always create a PriorityValue.
110+
var entry = new PriorityValue<T>(_owner, property, this);
111+
var binding = entry.AddBinding(source, priority);
112+
_values.AddValue(property, entry);
113+
binding.Start();
114+
return binding;
115+
}
100116
else
101117
{
102118
var entry = new BindingEntry<T>(_owner, property, source, priority, this);
@@ -134,6 +150,17 @@ public void ClearLocalValue<T>(StyledPropertyBase<T> property)
134150
}
135151
}
136152

153+
public void CoerceValue<T>(StyledPropertyBase<T> property)
154+
{
155+
if (_values.TryGetValue(property, out var slot))
156+
{
157+
if (slot is PriorityValue<T> p)
158+
{
159+
p.CoerceValue();
160+
}
161+
}
162+
}
163+
137164
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
138165
{
139166
if (_values.TryGetValue(property, out var slot))
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) The Avalonia Project. All rights reserved.
2+
// Licensed under the MIT license. See licence.md file in the project root for full license information.
3+
4+
using System;
5+
using System.Reactive.Subjects;
6+
using Avalonia.Data;
7+
using Xunit;
8+
9+
namespace Avalonia.Base.UnitTests
10+
{
11+
public class AvaloniaObjectTests_Coercion
12+
{
13+
[Fact]
14+
public void Coerces_Set_Value()
15+
{
16+
var target = new Class1();
17+
18+
target.Foo = 150;
19+
20+
Assert.Equal(100, target.Foo);
21+
}
22+
23+
[Fact]
24+
public void Coerces_Bound_Value()
25+
{
26+
var target = new Class1();
27+
var source = new Subject<BindingValue<int>>();
28+
29+
target.Bind(Class1.FooProperty, source);
30+
source.OnNext(150);
31+
32+
Assert.Equal(100, target.Foo);
33+
}
34+
35+
[Fact]
36+
public void CoerceValue_Updates_Value()
37+
{
38+
var target = new Class1 { Foo = 99 };
39+
40+
Assert.Equal(99, target.Foo);
41+
42+
target.MaxFoo = 50;
43+
target.CoerceValue(Class1.FooProperty);
44+
45+
Assert.Equal(50, target.Foo);
46+
}
47+
48+
[Fact]
49+
public void Coerced_Value_Can_Be_Restored_If_Limit_Changed()
50+
{
51+
var target = new Class1();
52+
53+
target.Foo = 150;
54+
Assert.Equal(100, target.Foo);
55+
56+
target.MaxFoo = 200;
57+
target.CoerceValue(Class1.FooProperty);
58+
59+
Assert.Equal(150, target.Foo);
60+
}
61+
62+
[Fact]
63+
public void Coerced_Value_Can_Be_Restored_From_Previously_Active_Binding()
64+
{
65+
var target = new Class1();
66+
var source1 = new Subject<BindingValue<int>>();
67+
var source2 = new Subject<BindingValue<int>>();
68+
69+
target.Bind(Class1.FooProperty, source1);
70+
source1.OnNext(150);
71+
72+
target.Bind(Class1.FooProperty, source2);
73+
source2.OnNext(160);
74+
75+
Assert.Equal(100, target.Foo);
76+
77+
target.MaxFoo = 200;
78+
source2.OnCompleted();
79+
80+
Assert.Equal(150, target.Foo);
81+
}
82+
83+
[Fact]
84+
public void Coercion_Can_Be_Overridden()
85+
{
86+
var target = new Class2();
87+
88+
target.Foo = 150;
89+
90+
Assert.Equal(-150, target.Foo);
91+
}
92+
93+
private class Class1 : AvaloniaObject
94+
{
95+
public static readonly StyledProperty<int> FooProperty =
96+
AvaloniaProperty.Register<Class1, int>(
97+
"Qux",
98+
defaultValue: 11,
99+
coerce: CoerceFoo);
100+
101+
public int Foo
102+
{
103+
get => GetValue(FooProperty);
104+
set => SetValue(FooProperty, value);
105+
}
106+
107+
public int MaxFoo { get; set; } = 100;
108+
109+
public static int CoerceFoo(IAvaloniaObject instance, int value)
110+
{
111+
return Math.Min(((Class1)instance).MaxFoo, value);
112+
}
113+
}
114+
115+
private class Class2 : AvaloniaObject
116+
{
117+
public static readonly StyledProperty<int> FooProperty =
118+
Class1.FooProperty.AddOwner<Class2>();
119+
120+
static Class2()
121+
{
122+
FooProperty.OverrideMetadata<Class2>(
123+
new StyledPropertyMetadata<int>(
124+
coerce: CoerceFoo));
125+
}
126+
127+
public int Foo
128+
{
129+
get => GetValue(FooProperty);
130+
set => SetValue(FooProperty, value);
131+
}
132+
133+
public static int CoerceFoo(IAvaloniaObject instance, int value)
134+
{
135+
return -value;
136+
}
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)