If you are not quite familiar with Value Coercion, it allows you to change/correct value of a property, when it is assigned an unexpected value. This also allows you to ensure relative properties are also kept in sync or in other words, allows you to enforce relation between properties of an object. For example, Date of Birth should not exceed Date of Demise, or Minimum Value in a slider should not exceed the Maximum Value.
You could achieve such a synchronization mechanism using Value Coercion. Consider the following code.
// Note that code has a 'little problem', we will discuss it shortly.
public int MinimumValue
{
get { return (int)GetValue(MinimumValueProperty); }
set { SetValue(MinimumValueProperty, value); }
}
public static readonly DependencyProperty MinimumValueProperty =
DependencyProperty.Register(nameof(MinimumValue), typeof(int), typeof(Configuration), new PropertyMetadata(0, null, new CoerceValueCallback(OnMinimumValueCoerce)));
private static object OnMinimumValueCoerce(DependencyObject d, object baseValue)
{
if(d is Configuration config && baseValue is int newValue)
{
var maxValue = config.MaximumValue;
var oldValue = config.MinimumValue;
return maxValue > newValue ? newValue : oldValue;
}
return baseValue;
}
public int MaximumValue
{
get { return (int)GetValue(MaximumValueProperty); }
set { SetValue(MaximumValueProperty, value); }
}
public static readonly DependencyProperty MaximumValueProperty =
DependencyProperty.Register(nameof(MaximumValue), typeof(int), typeof(Configuration), new PropertyMetadata(0,null,new CoerceValueCallback(OnMaximumCoerce)));
private static object OnMaximumCoerce(DependencyObject d, object baseValue)
{
if(d is Configuration config && baseValue is int newValue)
{
var minValue = config.MinimumValue;
var oldValue = config.MaximumValue;
return newValue > minValue ? newValue : oldValue;
}
return baseValue;
}
In the above code, you are using the CoerceValueCallback
within the PropertyMetada
to ensure MinimumValue and MaximumValue are kept in sync with each other. Every time a new value is assigned for MinimumValue
, it checks if the value is exceeds the MaximumValue
. If yes, it reverts the changes and retains the old value.
Little Problem
While the above code looks seemingly harmless, there is a small problem associated with. If you examine the code, we have coercing values for both MinimumValue
and Maximum
value. Now imagine a scenario when are using the Dependency properties in your code.
<controls:Configuration MinimumValue="{Binding MinValue}" MaximumValue="{Binding MaxValue}" />
Consider the initial values of MinValue
and MaxValue
are 1 and 100. At the first glance, this looks valid value, however if you execute the code, you would realize the MinimumValue nevers gets set. Instead, the OnMinimumValueCoerce
comes into play and reverts the changes. This is becase, at first the MinimumValue
is set (by the order in which is defined in our Xaml – Change the order and behavior would be different).
When a value 1 is assigned to MinimumValue
, the OnMinimumValueCoerce
notices that it exceeds the current value of MaximumValue
(which hasn’t changed yet and is still default of 0). This causes the values to reverts as per our logic in OnMinimumValueCoerce
.
PropertyChangedCallback Vs CoerceValueCallback Vs ValidateValueCallback
At this point, it is worth noticing the difference between the 3 seemingly similiar callbacks associated with DependencyProperty – PropertyChangedCallback
, CoerceValueCallback
and ValidateValueCallback
.
The difference could be summarized as
- PropertyChangedCallback – Reacts to a value change
- ValidateValueCallback – Determine if the value is valid
- CoerceValueCallback – Coerce a value.
Order of execution
- ValidateValueCallback
- CoerceValueCallback
- PropertyChangedCallback
Solution
One solution that could be applied in this scenario is
- Allow
MinimumValue
to be set to any value - In
PropertyChangedCallback
ofMinimumValue
, force coercion ofMaximumValue
. - In
CoerceValueCallback
ofMaximumValue
, check if it is less thanMinimumValue
and set it toMinimumValue
.
For example,
public int MinimumValue
{
get { return (int)GetValue(MinimumValueProperty); }
set { SetValue(MinimumValueProperty, value); }
}
public static readonly DependencyProperty MinimumValueProperty =
DependencyProperty.Register(nameof(MinimumValue), typeof(int), typeof(Configuration), new PropertyMetadata(0, new PropertyChangedCallback(OnMinimumValueChanged)));
private static void OnMinimumValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(d is Configuration config)
{
config.CoerceValue(MaximumValueProperty);
}
}
public int MaximumValue
{
get { return (int)GetValue(MaximumValueProperty); }
set { SetValue(MaximumValueProperty, value); }
}
public static readonly DependencyProperty MaximumValueProperty =
DependencyProperty.Register(nameof(MaximumValue), typeof(int), typeof(Configuration), new PropertyMetadata(0,null,new CoerceValueCallback(OnMaximumCoerce)));
private static object OnMaximumCoerce(DependencyObject d, object baseValue)
{
if(d is Configuration config && baseValue is int newValue)
{
var minValue = config.MinimumValue;
return newValue > minValue ? newValue : minValue;
}
return baseValue;
}
As you can see, while 3 muskeeters of PropertyChangedCallback
, CoerceValueCallback
and ValidateValueCallback
are extremly useful, one needs to be aware of similiar issues which could come along and hence one needs to be aware of what is the difference between them and the sequence of execution.