When defining any sort of property, you need
to face the possibility that it may be set incorrectly. With
traditional .NET properties, you might try to catch this sort of problem
in the property setter. With dependency properties, this isn't
appropriate, because the property may be set directly through the WPF
property system using the SetValue() method.
Instead, WPF provides two ways to prevent invalid values:
ValidateValueCallback.
This callback can accept or reject new values. Usually, this callback
is used to catch obvious errors that violate the constraints of the
property. You can supply it as an argument to the
DependencyProperty.Register() method.
CoerceValueCallback.
This callback can change new values into something more acceptable.
Usually, this callback is used to deal with conflicting dependency
property values that are set on the same object. These values might be
independently valid but aren't consistent when applied together. To use
this callback, supply it as a constructor argument when creating the
FrameworkPropertyMetadata object, which you then pass to the
DependencyProperty.Register() method.
Here's how all the pieces come into play when an application attempts to set a dependency property:
First,
the CoerceValueCallback method has the opportunity to modify the
supplied value (usually, to make it consistent with other properties) or
return DependencyProperty.UnsetValue, which rejects the change
altogether.
Next,
the ValidateValueCallback is fired. This method returns true to accept a
value as valid or returns false to reject it. Unlike the
CoerceValueCallback, the ValidateValueCallback does not have access to
the actual object on which the property is being set, which means you
can't examine other property values.
Finally,
if both these previous stages succeed, the PropertyChangedCallback is
triggered. At this point, you can raise a change event if you want to
provide notification to other classes.
1. The Validation Callback
As you saw earlier, the DependencyProperty.Register() method accepts an optional validation callback:
MarginProperty = DependencyProperty.Register("Margin",
typeof(Thickness), typeof(FrameworkElement), metadata,
new ValidateValueCallback(FrameworkElement.IsMarginValid));
You can use this callback to enforce the validation
that you'd normally add in the set portion of a property procedure. The
callback you supply must point to a method that accepts an object
parameter and returns a Boolean value. You return true to accept the
object as valid and false to reject it.
The validation of the FrameworkElement.Margin
property isn't terribly interesting because it relies on an internal
Thickness.IsValid() method. This method makes sure the Thickness object
is valid for its current use (representing a margin). For example, it
may be possible to construct a perfectly acceptable Thickness object
that isn't acceptable for setting the margin. One example is a Thickness
object with negative dimensions. If the supplied Thickness object isn't
valid for a margin, the IsMarginValid property returns false:
private static bool IsMarginValid(object value)
{
Thickness thickness1 = (Thickness) value;
return thickness1.IsValid(true, false, true, false);
}
There's one limitation with validation callbacks:
they are static methods that don't have access to the object that's
being validated. All you get is the newly applied value. Although that
makes them easier to reuse, it also makes it impossible to create a
validation routine that takes other properties into account. The classic
example is an element with Maximum and Minimum properties. Clearly, it
should not be possible to set the Maximum to a value that's less than
the Minimum. However, you can't enforce this logic with a validation
callback because you'll have access only to one property at a time.
NOTE
The preferred approach to solve this problem is to use value coercion.
Coercion is a step that occurs just before validation, and it allows
you to modify a value to make it more acceptable (for example, raising
the Maximum so it's at least equal to the Minimum) or disallow the
change altogether. The coercion step is handled through another
callback, but this one is attached to the FrameworkPropertyMetadata
object, which is described in the next section.
2. The Coercion Callback
You use the CoerceValueCallback through the FrameworkPropertyMetadata object. Here's an example:
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
metadata.CoerceValueCallback = new CoerceValueCallback(CoerceMaximum);
DependencyProperty.Register("Maximum", typeof(double),
typeof(RangeBase), metadata);
The CoerceValueCallback allows you to deal with
interrelated properties. For example, the ScrollBar provides Maximum,
Minimum, and Value properties, all of which are inherited from the
RangeBase class. One way to keep these properties aligned is to use
property coercion.
For example, when the Maximum is set, it must be coerced so that it can't be less than the Minimum:
private static object CoerceMaximum(DependencyObject d, object value)
{
RangeBase base1 = (RangeBase)d;
if (((double) value) < base1.Minimum)
{
return base1.Minimum;
}
return value;
}
In other words, if the value that's applied to the
Maximum property is less than the Minimum, the Minimum value is used
instead to cap the Maximum. Notice that the CoerceValueCallback passes
two parameters—the value that's being applied and the object to which it's being applied.
When the Value is set, a similar coercion takes
place. The Value property is coerced so that it can't fall outside of
the range defined by the Minimum and Maximum, using this code:
internal static object ConstrainToRange(DependencyObject d, object value)
{
double newValue = (double)value;
RangeBase base1 = (RangeBase)d;
double minimum = base1.Minimum;
if (newValue < minimum)
{
return minimum;
}
double maximum = base1.Maximum;
if (newValue > maximum)
{
return maximum;
}
return newValue;
}
The Minimum property doesn't use value coercion at
all. Instead, once it has been changed, it triggers a
PropertyChangedCallback that forces the Maximum and Value properties to
follow along by manually triggering their coercion:
private static void OnMinimumChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
RangeBase base1 = (RangeBase)d;
...
base1.CoerceMaximum(RangeBase.MaximumProperty);
base1.CoerceValue(RangeBase.ValueProperty);
}
Similarly, once the Maximum has been set and coerced, it manually coerces the Value property to fit:
private static void OnMaximumChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
RangeBase base1 = (RangeBase)d;
...
base1.CoerceValue(RangeBase.ValueProperty);
base1.OnMaximumChanged((double) e.OldValue, (double)e.NewValue);
}
The end result is that if you set conflicting values,
the Minimum takes precedence, the Maximum gets its say next (and may
possibly be coerced by the Minimum), and then the Value is applied (and
may be coerced by both the Maximum and the Minimum).
The goal of this somewhat confusing sequence of steps
is to ensure that the ScrollBar properties can be set in various orders
without causing an error. This is an important consideration for
initialization, such as when a window is being created for a XAML
document. All WPF controls guarantee that their properties can be set in
any order, without causing any change in behavior.
A careful review of the previous code calls this goal into question. For example, consider this code:
ScrollBar bar = new ScrollBar();
bar.Value = 100;
bar.Minimum = 1;
bar.Maximum = 200;
When the ScrollBar is first created, Value is 0, Minimum is 0, and Maximum is 1.
After the second line of code, the Value property is
coerced to 1 (because initially the Maximum property is set to the
default value 1). But something remarkable happens when you reach the
fourth line of code. When the Maximum property is changed, it triggers
coercion on both the Minimum and Value properties. This coercion acts on
the values you specified originally.
In other words, the local value of 100 is still stored by the WPF
dependency property system, and now that it's an acceptable value, it
can be applied to the Value property. Thus, after this single line of
code executes, two properties have changed. Here's a closer look at
what's happening:
ScrollBar bar = new ScrollBar();
bar.Value = 100;
// (Right now bar.Value returns 1.)
bar.Minimum = 1;
// (bar.Value still returns 1.)
bar.Maximum = 200;
// (Now now bar.Value returns 100.)
This behavior persists no matter when you set the
Maximum property. For example, if you set a Value of 100 when the window
loads and set the Maximum property later when the user clicks a button,
the Value property is still restored to its rightful value of 100 at
that point. (The only way to prevent this from taking place is to set a
different value or remove the local value that you've applied using the
ClearValue() method that all elements inherit from DependencyObject.)
This behavior is due to WPF's property resolution
system, which you learned about earlier. Although WPF stores the exact
local value you've set internally, it evaluates what the property should be (using coercion and a few other considerations) when you read the property.
NOTE
Long-time Windows Forms programmers may
remember the ISupportInitialize interface, which was used to solve
similar problems in property initialization by wrapping a series of
property changes into a batch process. Although you can use
ISupportInitialize with WPF (and the XAML parser respects it), few of
the WPF elements use this technique. Instead, it's encouraged to resolve
these problems using value coercion. There are a number of reasons that
coercion is preferred. For example, coercion solves other problems that
can occur when an invalid value is applied through a data binding or
animation, unlike the ISupportInitialize interface.