Please visit http://www.ningzhang.org!

This is a backup site. Please go to http://www.ningzhang.org for the latest content and best viewing. Thanks!

Nov 6, 2008

DependencyProperty: Validation, Coercion & Change Handling (Part I: WPF)

Introduction

Dependency property is a major innovation of WPF. A dependency property has the same simple and familiar programming interface as a CLR property, but allows its value to be dynamically decided/changed by many factors, like other properties, styles, templates, data-binding, animation, element composition, class inheritance etc, and can be implemented to provide self-contained default value, validation, coercion and eventing logic. Silverlight implements a subset of the WPF property system functions, so there are significant differences between WPF and Silverlight. Straight porting between WPF and Silverlight may not be possible, major source code changes may be required. This three-part post series will use a simple example to demonstrate the common pattern of implementing dependency property on WPF and Silverlight, the difference between the two property systems and how to port from one to another, and then use the NumericUpDown control I wrote for Silverlight Toolkit to demonstrate how complex and tricky dependency property can get on Silverlight. I will only focus on validation, coercion and change handling in this series. When I find time, I may write on other aspects of dependency properties.

 

WPF Dependency Property Overview

MSDN has good Dependency Property Overview, so I will just call out the three core classes and two important methods of the programming interface of WPF property system:

  • DependencyProperty, which provides properties like Name, OwnerType, PropertyType, and methods like Register, RegisterAttached, RegiterReadOnly, RegisterAttachedReadOnly:
    DependencyProperty
  • DependencyObject, which provides methods like GetValue, SetValue, ReadLocalValue, CoerceValue, ClearValue to access/modify the value of a dependency property: 
    DependencyObject
  • PropertyMetadata, which allows the default value, coercion and change handling logic to be specified during dependency property registration: 
    PropertyMetadata
  • DependencyProperty.Register method:
    DependencyProperty.Register
  • PropertyMetadata constructor:
    image

 

Example

The example below has a simple class MyButton, which inherits from Button class and implements a dependency property MyValue. The value of MyValue property must be between 0 to 10, enforced by its validation logic IsValidMyValue(). The effective value of MyValue property depends on its default value(0), user input (set request parameter), and the value of its IsEnabled property inherited from base class. The last dependency is implemented by coercion logic CoerceMyValue and dependency change event handler OnIsEnabledChanged(). Overall the implementation of MyValue demonstrates the common pattern of dependent property on WPF:

  • CLR wrapper: public int MyValue { get; set; }
  • Dependency property identifier: public static readonly DependencyProeprty MyValueProperty
  • Registration with property system: DependencyProperty.Register()
  • Validation logic: IsValidMyValue static method
  • Coercion logic: CoerceMyValue static method

It is common and advised to also implement changed handling logic via:

  • PropertyChangedCallback OnMyValueChanged static method
  • protected virtual void OnMyValueChanged(oldValue, newValue) to raise the changed event, and for subclass to override
  • public static readonly RoutedEvent MyValueChangedEvent routed event identifier and event registration
  • public event RoutedPropertyChangedEventHandler<int> MyValueChanged event for client

 

Below is source code of the example:

  • Window1.xaml:
    <Window x:Class="WpfApp1.Window1"
    
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    
        xmlns:my="clr-namespace:WpfApp1"
    
        Title="Window1" Height="300" Width="300">
    
        <StackPanel>
    
            <my:MyButton x:Name="mybtn" Content="MyButton" 
    
                         MyValueChanged="mybtn_MyValueChanged" Click="mybtn_Click"/>
    
        </StackPanel>
    
    </Window>
  • Window1.xaml.cs: 
    using System;
    
    using System.Windows;
    
    using System.Windows.Controls;
    
    namespace WpfApp1
    
    {
    
        // sample class to demonstrate how to implement a DependencyProperty 
    
        public class MyButton : Button
    
        {
    
            // DP: CLR wrapper
    
            public int MyValue
    
            {
    
                get { return (int)GetValue(MyValueProperty); }
    
                set { SetValue(MyValueProperty, value); }
    
            }
    
            // DP: dependency property identifier & registration
    
            public static readonly DependencyProperty MyValueProperty =
    
                DependencyProperty.Register(
    
                    "MyValue", typeof(int), typeof(MyButton),
    
                    new PropertyMetadata(0,
    
                        new PropertyChangedCallback(OnMyValueChanged),
    
                        new CoerceValueCallback(CoerceMyValue)),
    
                    new ValidateValueCallback(IsValidMyValue));
    
            // DP: validation callback
    
            private static bool IsValidMyValue(object value)
    
            {
    
                int newValue = (int)value;
    
                return newValue >= 0 && newValue <= 10;
    
            }
    
            // DP: coercion callback
    
            private static object CoerceMyValue(DependencyObject d, object value)
    
            {
    
                MyButton ctrl = (MyButton)d;
    
                int newValue = (int)value;
    
                return ctrl.IsEnabled ? Math.Min(5, newValue) : Math.Max(6, newValue);
    
            }
    
            // DP: changed callback
    
            private static void OnMyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
            {
    
                MyButton ctrl = (MyButton)d;
    
                int oldValue = (int)e.OldValue;
    
                int newValue = (int)e.NewValue;
    
                ctrl.OnMyValueChanged(oldValue, newValue);
    
            }
    
            // RE: changed event identifier & registration
    
            public static readonly RoutedEvent MyValueChangedEvent =
    
                EventManager.RegisterRoutedEvent("MyValueChanged", RoutingStrategy.Bubble,
    
                    typeof(RoutedPropertyChangedEventHandler<int>), typeof(MyButton));
    
            // RE: changed event
    
            public event RoutedPropertyChangedEventHandler<int> MyValueChanged;
    
            // RE: use a protected virtual to raise event
    
            protected virtual void OnMyValueChanged(int oldValue, int newValue)
    
            {
    
                RoutedPropertyChangedEventArgs<int> e =
    
                    new RoutedPropertyChangedEventArgs<int>(oldValue, newValue);
    
                e.RoutedEvent = MyValueChangedEvent;
    
                RaiseEvent(e);
    
            }
    
            // DP: event handler to trigger re-calculation of MyValue because of dependency change
    
            private static void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
    
            {
    
                MyButton ctrl = (MyButton)sender;
    
                ctrl.CoerceValue(MyButton.MyValueProperty);
    
            }
    
            public MyButton()
    
            {
    
                IsEnabledChanged += OnIsEnabledChanged;
    
            }
    
        }
    
        /// <summary>
    
        /// Interaction logic for Window1.xaml
    
        /// </summary>
    
        public partial class Window1 : Window
    
        {
    
            public Window1()
    
            {
    
                InitializeComponent();
    
            }
    
            private void mybtn_Click(object sender, RoutedEventArgs e)
    
            {
    
            }
    
            private void mybtn_MyValueChanged(object sender, RoutedPropertyChangedEventArgs<int> e)
    
            {
    
                int oldValue = e.OldValue;
    
                int newValue = e.NewValue;
    
            }
    
        }
    
    }

 

How Dependency Property Work on WPF

To demonstrate how dependency property works on WPF, I set a break point on most functions, and used the Immediate debug window to check values and change states.

Registration
  • When the program starts, DependencyProperty.Register is executed, which in turn calls IsValidMyValue to validate the default value 0:

validation called for default value

Effective Value

Click on the MyButton instance to trigger the break point on mybtn_Click event handler, then use the Immediate window to check on MyValue property. As shown below:

  • Even though mybtn.MyValue isn't assigned any value, it has an effective value of 0, the default value of this dependency property, as shown by results of mybtn.MyValue and mybtn.GetValue.
  • It is important that the CLR wrapper and GetValue/SetValue calls should always have the same effect. This is done by the CLR wrappers being just a wrapper for GetValue/SetValue and nothing more.
  • mybtn.ReadLocalValue(MyButton.MyValueProperty) results shows that there is no local storage for MyValue property on mybtn instance.
  • When mybtn.MyValue takes initial value, there is no change notification, as OnMyValueChanged isn't called at program start.

DP has default value, no local storage

Validation

In Immediate window, set mybtn.MyValue to an invalid value -1 by typing in command "mybtn.MyValue = -1":

  • The CLR wrapper is called, which in turn calls SetValue, which triggers validation on the new value -1.

validation called for setting value

  • IsValidMyValue returns false on the new value -1, WPF property system then throws ArgumentException:

validation failure triggers argument exception in setting

  • Check the value of MyValue via three ways: CLR wrapper, GetValue, ReadLocalValue: nothing changed. This is important, since on Silverlight state does change, even for invalid set operation, so we have to write code to restore the state, as I will show in part two of the series.

No state change after invalid set operation

Coercion

Again in Immediate window, try set mybtn.MyValue to 8, which is valid, but because mybtn.IsEnabled is true, the effective value must be less than or equal to 5:

  • First, validation is called on the new value 8, via CLR wrapper and SetValue:

validation called during setting

  • IsValidMyValue(8) returns true, so coercion logic CoerceMyValue is called, via CLR wrapper, SetValue, UpdateEffectiveValue, and ProcessCoerceValue:

Coercion called during setting

  • CoerceMyValue(8) returns a different value 5, so WPF property system calls IsValidMyvalue on the coerced effective value 5:

Coerced value goes through validation as well

  • IsValidMyValue(5) returns true, so WPF property system calls changed logic OnMyValueChanged, with oldValue as 0 and newValue as 5. Both values are effective values, since there was no old value in mybtn instance, and the new value is actually 8. OnMyValueChanged in turns call its proctected virtual version, which raises MyValueChanged routed event, and executes MyValueChanged event handlers, if there is any. 

Changed callback to raise changed event

  • Check the value of MyValue property: both CLR wrapper and GetValue return the new effective value 5, but GetLocalValue returns 8, remembering the original user request. This is very important, as we will see next.

Coercion causes local value and effective value difference

Dynamic Re-calculation as Dependency Changes

Now let's set mybtn.IsEnabled to false in Immediate window. This changes one of the dependencies for MyValue, which should trigger re-calculation of the effective value of MyValue property, if I have coded it right (and I did, of course :-):

  • First, please note that IsEnabled change processing is synchronous, i.e., OnIsEnabledChanged is called before "mybtn.IsEnabled = false" returns. This is important since IsEnabled change handling is asynchronous on Silverlight.
  • We have to trigger the recalculation since the property system doesn't know MyValue depends on IsEnabled. This is done by handling IsEnabledChanged event (IsEnabledChanged += OnIsEnabledChanged in MyButton()) and call CoerceValue from inside OnIsEnabledChanged event handler.
  • CoerceValue in trun calls UpdateEffectiveValue, ProcessCoerceValue, and then our coercion logic CoerceMyValue. Please note that CoerceMyValue is called with newValue of 8, not the current effective value 5. The original set request parameter 8 is remembered by mybtn instance locally, as shown by ReadLocalValue result before. This is important, since Silverlight doesn't do this, which makes implementing coercion very tricky, as will be shown in part 3 of the blog series with bugs in Silverlight RangeBase and its subclasses like Slider and ScrollBar.

Dependency change triggers coercion

  • Since mybtn.IsEnabled is now false, the originally requested value 8 becomes valid, so CoerceMyValue returns 8.
  • 8 is different from previous effective value of 5, so UpdateEffectieValue calls NotifyPropertyChange, which ultimately calls our changed logic OnMyValueChanged with oldValue of 5 and newValue of 8. This happens even though there is no new set operation for MyValue property. It is the IsEnabled dependency change that caused MyValue effective value change, which in turns triggers change notification and event handling for MyValue property, and this is all happening synchronously.

Coercion triggers change notification

  • After the coercion, change notification, and event handling finishes, check the value of MyValue property: now all three methods (mybtn.MyValue, mybtn.GetValue, mybtn.ReadLocalValue) return the same result.

Effective value and local value the same

  • one last experiment: type mybtn.ClearValue(MyButton.MyValueProperty) in Immediate window to clear the local value (8) of the dependency property, which in turn triggers change handling logic OnMyValueChanged, with newValue of 0, the new effective value derived from MyValue's default property.

ClearValue triggers change handling 

Conclusion

As we can see from above example and experiment, WPF has a powerful property system, which makes both implementing and using dependency properties very easy. It is a lot trickier on Silverlight though, because Silverlight only implements a subset of WPF functionality. In part two of the post series, I will re-implement above example on Silverlight, and demonstrate the difference between WPF and Silverlight. In part three, I will use RangeBase and NumericUpDown to demonstrate the gotcha on Silverlight, which is the original motivation for this post series. Stay tuned!

PS: the first post of the series ended up a lot longer and more time consuming than I expected, so I will stop here. If there is enough interest, I can upload the project for you to experiment, and a code snippet that provides most of the plumbing for implementing dependency property on WPF. Thanks!