Why be wary of Value Coercion in Dependency Properties

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 – PropertyChangedCallbackCoerceValueCallback 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

  1. ValidateValueCallback
  2. CoerceValueCallback
  3. PropertyChangedCallback

Solution

One solution that could be applied in this scenario is

  • Allow MinimumValue to be set to any value
  • In PropertyChangedCallback of MinimumValue, force coercion of MaximumValue.
  • In CoerceValueCallback of MaximumValue, check if it is less than MinimumValue and set it to MinimumValue.

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 PropertyChangedCallbackCoerceValueCallback 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.