Nov 14, 2008

DependencyProperty: Validation, Coercion & Change Handling (Part III: Dependency Property Code Snippet)

Introduction

This is the last part of the three part series on how to implement dependency property with validation, coercion and eventing on WPF and Silverlight. In Part I, I implemented a simple dependency property and debugged through to demonstrate how dependency property works on WPF and the common pattern for implementing dependency property on WPF. In Part , I did the same on Silverlight. Because Silverlight only supports PropertyChangedCallback, but not CoerceValueCallback and ValidateValueCallback, so validation, coercion and change handling all have to be implemented with PropertyChangedCallback. This causes PropertyChangedCallback being called recursively when validation or coercion changes the effective value of the dependency property. Plus other limitations of Silverlight property system, it can get very tricky to implement dependency property correctly. This post I will show some obscure behaviors of Silverlight RangeBase controls and their causes to further demonstrate how tricky this can be. And at last, I will provide a code snippet that implements the complete pattern of dependency property implementation I discussed in Part , as a reward to those who read through the series :-)

Silverlight RangeBase Controls

Overview

ScrollBar, ProgressBar and Slider inherits from RangeBase, which implements Minimum, Maximum and Value dependency properties, with the coercion constraint that Minimum <= Value <= Maximum.

RangeBase

This sounds simple :-), but RangeBase controls' behavior can get very odd. Take a few examples:

  1. <Slider x:Name="sl"/> gives you a slider with Minimum = 0, Value = 0, and Maximum = 10, while <Slider x:Name="sl2" Minimum="-1"/> produces a slider with Minimum = -1, Value = 0, and Maximum = 0.
  2. Click on slider sl2's drag thumb to make sl2 in focus, hit right or up arrow key, then change sl2.Maximum to a positive number 10, sl2.Value magically changes from 0 to 0.1.
  3. Set sl2's property like Value to double.NaN, an invalid value, an exception will throw, but sl2.Value is changed to double.NaN anyway, and no ValueChanged event is fired.
Source Code

Below is the code we will debug and experiment with:

  • page.xaml:
<UserControl x:Class="SLApp2.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:my="clr-namespace:SLApp2"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <StackPanel x:Name="LayoutRoot" Background="White">
        <Slider x:Name="sl"/>
        <my:Slider2 x:Name="sl2" Minimum="-1"/>
        <Button x:Name="btn" Content="Break!"
                Click="btn_Click" HorizontalAlignment="Center"/>
    </StackPanel>
</UserControl>
  • page.xaml.cs:

 

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace SLApp2
{
    // subclass Slider to easily break at property change and key down events.
    public class Slider2 : Slider
    {
        protected override void OnMinimumChanged(double oldMinimum, double newMinimum)
        {
            base.OnMinimumChanged(oldMinimum, newMinimum);
        }
        protected override void OnMaximumChanged(double oldMaximum, double newMaximum)
        {
            base.OnMaximumChanged(oldMaximum, newMaximum);
        }
        protected override void OnValueChanged(double oldValue, double newValue)
        {
            base.OnValueChanged(oldValue, newValue);
        }
        protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
        {
            base.OnKeyDown(e);
        }
    }
    public partial class Page : UserControl
    {
        public Page()
        {
            InitializeComponent();
        }
        // the button click event handler provides a chance to break into debugger 
        // and experiment with RangeBase validation, coercion and change handling.
        private void btn_Click(object sender, RoutedEventArgs e)
        {
        }
    }
}
Debug & Experiment

Build and run above simple Silverlight application, you will see below screen shot. Please notice the thumb location difference between the two sliders:

SilverlightApplication1

Click the Break! button to break into its event handler in Visual Studio, check the value of slider sl and sl2:

  • <Slider x:Name="sl"/>: sl has the correct value of Mininum = Value = 0 and Maximum = 10, while <Slider x:Name="sl2" Minimum="-1"/> sl2 has Minimum = -1 but Maximum = Value = 0;

image

For those who read Part II and are now familiar with the implementation pattern, this is the root cause of sl2's odd behavior:

  • RangeBase leaves _initialMax, _initialVal, _requestedMax, _requestedVal to be initialized by CLR to default value 0. This is OK for Value property, since it defaults to zero anyway; but it is wrong for Maximum property, which defaults to 1 in its DependencyProperty.Register call and uses its default value as its effective value:

RangeBase not initialize its fields

  • Setting sl2.Minimum to -1 triggers OnMinimumPropertyChanged -> CoerceMaximum, which uses the CLR initialized _requestedMax to coerce Maximum. Since _requestedMax is 0 instead of Maximum's current effective value of 1, and 0 happens to be greater than Minimum's current value of -1, so Maximum is change to 0:

RangeBase.CoerceMaximum

  • The reason <Slider x:Name="sl"> has a Maximum value of 10 instead of 1 by default is that its default template has a <Setter Property="Maximum" Value="10"/>. This line also fixes the problem that _requestedMax isn't initialized. This is why sl._requestedMax is 10 in above screen shot.

Slider default template

Next, set a break point at each function, let the application run. Select sl2's thumb to make it in focus, hit right or up arrow key, it breaks into OnKeyDown event handler. Check sl2's value: _requestedVal is 0; step through base.OnKeyDown(e); check sl2 again: _requestedVal becomes 0.1. This is because base.OnKeyDown handles the right/up arrow key stroke and tries to increase Value by SmallIncrement of 0.1; this triggers OnValuePropertyChanged -> CoerceValue, which then coerces Value back to 0, since Maximum is 0; but it remembers the request of increment by setting _requestedVal to 0.1.

image

While still inside Immediate window, set sl2.Maximum to 10. This triggers OnMaxiumPropertyChanged:

image

which in turns triggers CoerceValue, which checks _requestedVal, whose value is now 0.1. Since 0.1 is in the range of [-1, 10], so Value is changed to 0.1:

image

Last, let's set Value to double.NaN. This triggers an ArgumentException, as expected:

image

but Value is changed to double.NaN anyway, and no ValueChanged event is fired because of the ArgumentException:

image

Dependency Property Code Snippet

By now you probably have got the idea that it is pretty tricky to implement dependency property with validation, coercion and change handling correctly on Silverlight. To make this easier, below is a code snippet that provides most of the code for above mentioned implementation pattern. This code snippet was originally authored by Ted Glaza, and I modified it to provide the complete pattern. You can remove stuff you don't need for simpler dependency properties, like those that don't need validation, coercion, or change handling. You do still need to fill in your own validation, coercion, and change handling logic by modifying functions like IsValid$property$, Coerce$property$, On$property$PropertyChanged, or On$property$Changed etc, and add a single definition of private int _nestLevel. But at least you don't have to worry about most of differences between WFP and Silverlight property systems, and focus only on validation, coercion and change handling logic themselves.

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>Silverlight Dependency Property</Title>
      <Shortcut>sdp</Shortcut>
      <Description>Code snippet for a dependency property with validation, coercion and changed event.</Description>
      <Author>Ning Zhang</Author>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal>
          <ID>property</ID>
          <ToolTip>Property name</ToolTip>
          <Default>MyProperty</Default>
        </Literal>
        <Literal>
          <ID>type</ID>
          <ToolTip>Property type</ToolTip>
          <Default>object</Default>
        </Literal>
        <Literal>
          <ID>defaultValue</ID>
          <ToolTip>Default value</ToolTip>
          <Default>null</Default>
        </Literal>
        <Literal Editable="false">
          <ID>classname</ID>
          <ToolTip>Class name</ToolTip>
          <Function>ClassName()</Function>
          <Default>MyClass</Default>
        </Literal>
        <Literal Editable="false">
          <ID>SystemWindowsDependencyProperty</ID>
          <Function>SimpleTypeName(global::System.Windows.DependencyProperty)</Function>
        </Literal>
        <Literal Editable="false">
          <ID>SystemWindowsDependencyObject</ID>
          <Function>SimpleTypeName(global::System.Windows.DependencyObject)</Function>
        </Literal>
        <Literal Editable="false">
          <ID>SystemWindowsDependencyPropertyChangedEventArgs</ID>
          <Function>SimpleTypeName(global::System.Windows.DependencyPropertyChangedEventArgs)</Function>
        </Literal>
        <Literal Editable="false">
          <ID>SystemWindowsPropertyMetadata</ID>
          <Function>SimpleTypeName(global::System.Windows.PropertyMetadata)</Function>
        </Literal>
      </Declarations>
      <Code Language="csharp">
        <![CDATA[#region public $type$ $property$
        /// <summary>
        /// Gets or sets the value of $property$ dependency property.
        /// </summary>
        public $type$ $property$
        {
            get { return ($type$)GetValue($property$Property); }
            set { SetValue($property$Property, value); }
        }
        /// <summary>
        /// Identifies the $property$ dependency property.
        /// </summary>
        public static readonly $SystemWindowsDependencyProperty$ $property$Property =
            $SystemWindowsDependencyProperty$.Register(
                "$property$",
                typeof($type$),
                typeof($classname$),
                new $SystemWindowsPropertyMetadata$($defaultValue$, On$property$PropertyChanged));
        /// <summary>
        /// $property$Property property changed handler.
        /// </summary>
        /// <param name="d">$classname$ that changed its $property$.</param>
        /// <param name="e">Event arguments.</param>
        private static void On$property$PropertyChanged($SystemWindowsDependencyObject$ d, $SystemWindowsDependencyPropertyChangedEventArgs$ e)
        {
            $classname$ source = ($classname$)d;
            $type$ newValue = ($type$)e.NewValue;
            $type$ oldValue = ($type$)e.OldValue;
            
            // validate newValue
            if (!IsValid$property$(newValue))
            {
                // revert back to e.OldValue
                source._nestLevel++;
                source.SetValue(e.Property, e.OldValue);
                source._nestLevel--;
                
                // throw ArgumentException
                throw new ArgumentException("Invalid $property$Property value", "e");
            }
            
            if (source._nestLevel == 0)
            {
                // remember initial state
                source._initial$property$ = oldValue;
                source._requested$property$ = newValue;
            }
            
            source._nestLevel++;
            
            // coerce newValue
            $type$ coercedValue = ($type$)Coerce$property$(d, e.NewValue);
            if (newValue != coercedValue)
            {
                // always set $property$Property to coerced value
                source.$property$ = coercedValue;
            }
            
            source._nestLevel--;
            
            if (source._nestLevel == 0 && source.$property$ != source._initial$property$)
            {
                // fire changed event only at root level and when there is indeed a change
                source.On$property$Changed(oldValue, source.$property$);
            }
        }
        /// <summary>
        /// $property$Property validation handler.
        /// </summary>
        /// <param name="value">New value of $property$Property.</param>
        /// <returns>
        /// Returns true if value is valid for $property$Property, false otherwise.
        /// </returns>
        private static bool IsValid$property$($type$ value)
        {
            return true;
        }
        /// <summary>
        /// $property$Property coercion handler.
        /// </summary>
        /// <param name="d">$classname$ that changed its $property$.</param>
        /// <param name="value">Event arguments.</param>
        /// <returns>
        /// Coerced effective value of $property$Property from input parameter value.
        /// </returns>
        private static object Coerce$property$($SystemWindowsDependencyObject$ d, object value)
        {
            $classname$ source = ($classname$)d;
            $type$ newValue = ($type$)value;
            return newValue;
        }
        /// <summary>
        /// $property$Property changed event.
        /// </summary>
        public event RoutedPropertyChangedEventHandler<$type$> $property$Changed;
        
        /// <summary>
        /// Called by On$property$PropertyChanged static method to fire $property$Changed event.
        /// </summary>
        /// <param name="oldValue">The old value of $property$.</param>
        /// <param name="newValue">The new value of $property$.</param>
        protected virtual void On$property$Changed($type$ oldValue, $type$ newValue)
        {
            RoutedPropertyChangedEventArgs<$type$> e = 
                new RoutedPropertyChangedEventArgs<$type$>(oldValue, newValue);
            if ($property$Changed != null)
            {
                $property$Changed(this, e);
            }
        }
        
        /// <summary>
        /// Cached previous value of $property$Property.
        /// </summary>
        private $type$ _initial$property$ = $defaultValue$;
        /// <summary>
        /// Cached originally requested value of $property$Property by user.
        /// </summary>
        private $type$ _requested$property$;
        #endregion public $type$ $property$
]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

 

Conclusion

I hope the series helped you in understanding and implementing dependency properties on WPF and Silverlight. Both WPF and Silverlight are great platforms, and will be fundamental for software development, possibly more than what Win32 did before.

As always, feedback, suggestions, corrections are welcome. Thanks!