Introduction
This is the second part of the three-part series on how to implement dependency property with validation, coercion and eventing on WPF and Silverlight. This post focuses on Silverlight. Because Silverlight property system supports only PropertyChangedCallback, developers have to implement WPF like CoerceValueCallback and ValidateValueCallback using PropertyChangedCallback and local variables. This can get very tricky, as it will be shown by bugs in Silverlight RangeBase controls in Part III of the post series. It is recommended that you read DependencyProeprty: Validation, Coercion & Change Handling (Part I: WPF) to get some context: how WPF property system works, the pattern for implementing dependency property on WPF, and overview of the simple example we will re-implement with Silverlight in this post.
Silverlight Dependency Property Overview
MSDN has a good Dependency Properties Overview for Silverlight, and it is a must read for anyone who wants to implement dependency properties on Silverlight. Here I just call out the three core classes and two important methods of the programming interface of Silverlight property system, as I did in Part I for WPF:
- DependencyProperty: Silverlight's DependencyProperty is much smaller than WPF's. Its public interface expose no properties , only Register and RegisterAttached static methods, no overloads.
- PropertyMetadata: its public interface exposes no properties, has only constructors that take default value, PropertyChangedCallback, but not CoerceValueCallback.
- DependencyObject: its public interface has GetValue, SetValue, ReadLocalValue, ClearValue methods, but not CoerceValue method.
- DependencyProperty.Register method: please note that it doesn't take ValidateValueCallback parameter.
- PropertyMetadata constructor method: please note that it doesn't take CoerceValueCallback parameter.
Example
Overview
This is the same example as in Part I, but re-implemented on Silverlight. It has a simple class MyButton that inherits from Button and adds a dependency property MyValue. The value of MyValue property must be between 0 and 10, and its effective value is affected by its IsEnabled property inherited from base class: if IsEnabled is true, MyValue must be less than or equal to 5, o/w it must be greater than or equal to 6.
Since MyValue is a dependency property, it should have all the benefits of a dependency property:
- it has the simple CLR property interface;
- it has a default value;
- its value can be set via xaml, code, databinding, style, animation etc;
- its value is dynamically decided by its dependencies and can be be automatically re-calculated whenever its dependencies change;
- it usually has a value changed event for client to plug in and on value changed protected virtual method for subclass to override;
- setting MyValue to an illegal value should throw an ArgumentException, without changing the value of MyValue property or firing its value changed event;
Implementation Pattern
The implementation of MyValue in the source code below follows the common pattern of dependency property on Silverlight and implements/provides all the benefits of dependency property mentioned above. Because Silverlight property system only supports a subset of WPF property system functions, in particular it only has changed callback but doesn't support validation and coercion directly, the implementation pattern for dependency property on Silverlight is actually very tricky and it is very easy to have subtle bugs if not careful, as shown by the debugging/experimenting section later in this post.
- CLR wrapper: public int MyValue { get; set; }
- Dependency property identifier: public static readonly DependencyProperty MyValueProperty
- Registration with property system: DependencyProperty.Register()
- The PropertyChangedCallback function that is the key to all DP functions (validation, coercion, eventing):
- private static void OnMyValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e): PropertyChangedCallback function
- private int _initialMyValue: local state to remember previous value of MyValue property at the root level of PropertyChangedCallback to avoid unnecessary/bogus change notfication
- private int _requestedMyValue: local state to remember the original user requested set value as the base for coercion
- private int _nestLevel: level of recursive calls of PropertyChangedCallback, for implementing validation & coercion, and preventing internal transitions from visible to client by not firing bogus changed notifications
- Validation logic:
- private static bool IsValidMyValue(DependencyPropertyChangedEventArgs e): match WPF's ValicateValueCallback
- private static void ValidationHelper: a common utility to revert invalid change and throw exception in case of validation failure
- Coercion logic:
- private static object CoerceMyValue(DependencyObject d, object value): match WPF's CoerceValueCallback
- private static void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e): to trigger automatic re-calculation of the effective value of MyValue property when its dependency IsEnabled changes.
- Eventing
- public event RoutedPropertyChangedEventHandler<int> MyValueChanged;
- protected virtual void OnMyValueChanged(int oldValue, int newValue)
- private static void OnMyValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) , CoerceMyValue static method
You may need to read through the source code and the debug and experiment section below to appreciate the intricacy and delicacy of how to implement dependency property correctly on Silverlight, since it is really easy to make a mistake here, as evidenced by the bugs in Silverlight.
Source Code
- page.xaml:
1: <UserControl x:Class="SLApp1.Page"2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"4: xmlns:my="clr-namespace:SLApp1"5: Width="400" Height="300">6: <Grid x:Name="LayoutRoot" Background="White">7: <my:MyButton x:Name="mybtn" Content="MyButton" Click="mybtn_Click"8: MyValueChanged="mybtn_MyValueChanged"/>9: </Grid>10: </UserControl>
- page.xaml.cs:
1: using System;2: using System.Windows;3: using System.Windows.Controls;4:5: namespace SLApp16: {7: // exampl to demonstrate how to implement dependency property on Silverlight8: public class MyButton : Button9: {10: // DP: CLR wrapper11: public int MyValue12: {13: get { return (int)GetValue(MyValueProperty); }14: set { SetValue(MyValueProperty, value); }15: }16:17: // DP: dependency property identifier & registration18: public static readonly DependencyProperty MyValueProperty =19: DependencyProperty.Register(20: "MyValue", typeof(int), typeof(MyButton),21: new PropertyMetadata(0, new PropertyChangedCallback(OnMyValuePropertyChanged)));22:23: // DP: validation callback24: private static bool IsValidMyValue(DependencyPropertyChangedEventArgs e)25: {26: int newValue = (int)e.NewValue;27: return newValue >= 0 && newValue <= 10;28: }29:30: // DP: coercion callback31: private static object CoerceMyValue(DependencyObject d, object value)32: {33: MyButton ctrl = (MyButton)d;34: int newValue = (int)value;35: return ctrl.IsEnabled ? Math.Min(5, newValue) : Math.Max(6, newValue);36: }37:38: // DP: common validation helper39: // revert value and throw exception in case of validation failure40: private static void ValidationHelper(41: DependencyObject d,42: DependencyPropertyChangedEventArgs e,43: Predicate<DependencyPropertyChangedEventArgs> p)44: {45: if (!p(e))46: {47: // Isolate SetValue call with _netLevel to prevent state change48: ((MyButton)d)._nestLevel++;49: d.SetValue(e.Property, e.OldValue);50: ((MyButton)d)._nestLevel--;51:52: throw new ArgumentException("Invalid DP value", "e");53: }54: }55:56: // DP: changed callback57: private static void OnMyValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)58: {59: // validation first: in case of failure, revert change,60: // throw ArgumentException, no state change or changed notification61: ValidationHelper(d, e, IsValidMyValue);62:63: MyButton ctrl = (MyButton)d;64: int oldValue = (int)e.OldValue;65: int newValue = (int)e.NewValue;66:67: if (ctrl._nestLevel == 0)68: {69: // remember initial state70: ctrl._initialMyValue = oldValue;71: ctrl._requestedMyValue = newValue;72: }73: ctrl._nestLevel++;74:75: // coercion, DP stores effective value on SL76: int coercedValue = (int)CoerceMyValue(d, e.NewValue);77: if (newValue != coercedValue)78: ctrl.MyValue = coercedValue;79:80: ctrl._nestLevel--;81: if (ctrl._nestLevel == 0 && ctrl.MyValue != ctrl._initialMyValue)82: {83: // changed notification happens only at root level (_nestLevel == 1)84: // and when there is a really a change (MyValue != _initialMyValue)85: ctrl.OnMyValueChanged(oldValue, ctrl.MyValue);86: }87: }88:89: // RE: use direct event to simulate routed event90: public event RoutedPropertyChangedEventHandler<int> MyValueChanged;91:92: // RE: provide a protected virtual for subclass to override93: protected virtual void OnMyValueChanged(int oldValue, int newValue)94: {95: RoutedPropertyChangedEventArgs<int> e =96: new RoutedPropertyChangedEventArgs<int>(oldValue, newValue);97: if (MyValueChanged != null)98: MyValueChanged(this, e);99: }100:101: private int _initialMyValue;102: private int _requestedMyValue;103: private int _nestLevel;104:105: // DP: event handler to trigger re-calculation of DP because of dependency change106: private static void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)107: {108: MyButton ctrl = (MyButton)sender;109:110: // coerce local value to re-calculate effective value111: // set DP only when effective value changes to avoid blow away binding unnecessarily112: ctrl._nestLevel++;113: int oldValue = ctrl.MyValue;114: int newValue = (int)CoerceMyValue(ctrl, ctrl._requestedMyValue);115: if (newValue != ctrl.MyValue)116: {117: ctrl.MyValue = newValue;118: ctrl.OnMyValueChanged(oldValue, ctrl.MyValue);119: }120: ctrl._nestLevel--;121: }122:123: public MyButton()124: {125: _initialMyValue = _requestedMyValue = MyValue; // init state126: IsEnabledChanged += OnIsEnabledChanged; // hook up dependency127: }128: }129:130: public partial class Page : UserControl131: {132: public Page()133: {134: InitializeComponent();135: }136:137: private void mybtn_Click(object sender, RoutedEventArgs e)138: {139: }140:141: private void mybtn_MyValueChanged(object sender, RoutedPropertyChangedEventArgs<int> e)142: {143: int oldValue = e.OldValue;144: int newValue = e.NewValue;145: }146: }147: }148:
Debug & Experiment: How Dependency Property Works on Silverlight
As in Part I, I will demonstrate how dependency property works on Silverlight debug and experiment. I set breakpoint at most of the functions in above source code, launch the application from inside Visual Studio, clicking on the button to break into mybtn_Click function, then check states and do experiments from inside Visual Studio's immediate window.
Default Value as Effective Value
First, let's check the state of mybtn after SLApp1 breaks into mybtn_Click function:
- mybtn.MyValue and mybtn.GetValue both returns 0, the default value of the MyValue DP (DP = DependencyProperty).
- mybtn.ReadLocalValue(MyButton.MyValueProperty) returns DependencyObject.UnsetValue, indicating there is no local value in mybtn for this DP. So it is the Silverlight property system that returns the DP's default value as its effective value.
- mybtn.ClearValue(MyButton.MyValueProperty) has no effect, since there is no local value to clear.
- state variables (_initialMyValue, _requestedMyValue) have been initialized to the effective value of MyValue by MyButton constructor.
Validation
Now let's experiment with validation. Type mybtn.MyValue = -1 in immediate window:
- VS breaks into OnMyValuePropertyChanged PropertyChangedCallback static method, since changed callback is all that Silverlight property system supports.
- The output of mybtn, mybtn.MyValue, mybtn.GetValue(MyButton.MyValueProperty) shows that MyValue has already taken the illegal value -1 as its local and effective value, so it is in an invalid state.
- The Local window shows e.Property has type CustomDependencyProperty. There are many subtle differences between CoreDependencyProperty and CustomDependencyProperty on Silverlight, so many assumptions about WPF DependencyProperty may not be true on Silverlight. Be prepared for try and error.
- To simulate WPF behavior, OnMyValuePropertyChanged calls ValidationHelper first to validate e.NewValue, and to revert the change in case of validation failure, without triggering unnecessary change notification for the illegal new value -1.
- Look at the call stack window and source code window above: ValidationHelper calls IsValidMyValue, which will return false for e.NewValue of -1.
- Because IsValidMyValue (passed in as parameter p) returns false as shown above, ValidationHelper reverts the change by setting the DP value back to e.OldValue. Unfortunately this SetValue call will trigger another round, or even rounds, of changed handling (validation/coercion/changed notification) recursively, even though it is still in the middle of current DP value changed processing and is merely reverting the change that should not have happened to begin with. This recursion is where all the complexity comes from in implementing dependency property correctly on Silverlight.
- The SetValue call is bracketed/protected by a pair of _nestLevell++/_nestLevell-- statements, so when OnMyValuePropertyChanged is called again recursively, _nestLevel will be more than 1, so it knows it is in recursion, and will not trigger changed notifications: in this case, from 0 to -1 and then -1 to 0, since none of those changes would have happened on WPF, and it should be internal only and NOT visible to client listening to the MyValueChanged event.
- Below screen shot shows OnMyValidationPropertyChanged is called recursively while stepping through the SetValue call on line 49 in above screen shot:
- In screen shot above, OnMyValuePropertyChanged shows up twice in call stack window, and we are still inside MyValue.set(-1) call (and "Evaluation of: mybtn.MyValue = -1" from immediate window).
- The output in immediate window shows that the value of MyValue property is now reverted from -1 back to 0.
- The output of ((MyButton)d).GetValue(MyButton.MyValueProperty) is 0 instead of DependencyProperty.UnsetValue, indicating mybtn now has a local value for this DP, instead of taking the default value from DP registration.
- _nestLevel is 1 instead of 0, because OnMyValidationPropertyChanged is being called recursively.
- In locals window, we can see that e's NewValue and OldValue swapped. e.NewValue is now 0 and valid, so ValidationHelper will not trigger another round of change handling.
- Because _nestLevel came in with value 1, so the if clause from line 68 to 72 will not be executed, ie, the recursion will not change local state (_initialMyValue and _requestedMyValue).
- As shown above, execution continues to coercion on line 76. Because mybtn.IsEnabled is true and newValue is 0, so no coercion happens, line 78 is skipped, instead of causing another round of recursion because of coercion.
- _netstLevel will be 1 at line 81, and ctrl.MyValue equals ctrl._initialMyValue, so the if clause from line 82 to 86 for changed notification will be skipped too. OnMyValuePropertyChanged will return, call stack will backtrace to ValidationHelper call, as shown next.
- Back to ValidationHelper, after having restored the old value, ValidationHelper throws ArgumentExeption for the invalid new value, and all this is done without changing state (_initialMyValue and _requestedMyValue) or triggering changed notification (in this case, from 0 to -1 then -1 back to 0).
Coercion
Now let's experiment with coercion: setting mybtn.MyValue to 8 should trigger coercion, mybtn.MyValue should end up as 5 instead of 8 because IsEnabled is true, and MyValueChanged event should be fired with new value 5. Since 8 is a valid value, it should be remembered; when the dependency IsEnabled changes, re-calculation of MyValue's effective value should happen and should change MyValue's effective value to the originally requeste and now valid value 8, and MyValueChanged event should fired with the new value 8.
As shown below, mybtn.MyValue = 8 in immediate window triggered OnMyValuePropertyChanged static method, as in the case of mybtn.MyValue = -1 previously, but with some differences:
- In screen shot above, e.NewValue of 8 is validated by IsValidMyValue, so no SetValue call and recursive changed handling inside ValidationHelper.
- Because this is the root call of changed handling (_nestLevel came into OnMyValuePropertyChanged with value 0), the if clause from line 68 to 72 is executed, to remember the state, as shown by ctrl output in immediate window.
- the execution continues to line 76 for coercion.
- In screen shot above, CoerceMyValue returns 5, the effective value, because IsEnabled is true and the requested new value is 8.
- OnMyValuePropertyChanged sets MyValue to the effective value 5, which will trigger OnMyValuePropertyChanged, IsValidMyValue, CoerceMyValue etc being called recursively again for value 5. Since 5 is valid and already coerced, it will stick.
- As shown above, execution comes back from coercion, mybtn (ctrl) takes 5 as its effective and local value for MyValue DP, but also remembers the requested value 8 with _requestedMyValue field.
- Changed notification happens only at the root level of the call (_nestLevel == 0) and when there is indeed a change of effective value of the DP (MyValue != _initialValue).
- MyValueChanged event always fires for the new effective value (ctrl.MyValue, coercedValue, ctrl.ReadLocalValue() should all be the new effective value).
- Below screen shot shows event handling call stack:
Dynamic Re-calculation of Effective Value as Dependency Changes
Now let's experiment with dynamic/automatic re-calculation of effective value of a dependency property when its dependency changes.
Type mybtn.IsEnabled = false in immediate window:
- IsEnabled change is processed asynchronously, ie, changing IsEnabled doesn't cause its handler OnIsEnabledChanged called immediately. You have to hit F5 immediately to let SLApp1 run so it can pick up the IsEnabledChanged event and break into OnIsEnabledChanged. If you wait for a few seconds and then hit F5, the IsEnabled changed event will be lost forever, and OnIsEnabledChanged will not be called. Even though the event handling is asynchronous and may be lost, the effect of the change is synchronous and stick: the button is now disabled. So if you are not quick enough in hitting F5 after typing in mybtn.IsEnabled = false, you will be able to continue the experiment, since the button is now disabled and won't respond to click, so you will have to re-start the program to continue the experiment. The short call stack also shows the asynchronousness, since "mybtn.IsEnabled = false" returns immediately and print out false in the Immediate window, and the call stack doesn't contain a line like "Evaluation of: mybtn.IsEnabled = false", as in the case of "Evaluation of: mybtn.MyValue = 8" in previous experiment. Asynchronous and transient event handling may be a good performance and reliability improvement, but it also makes things very tricky, so do keep the asynchronous nature in mind!
- The property system doesn't know the dependencies a DP depends on, so developer has to provide the logic, like handling IsEnabledChanged event to trigger re-calculation of MyValue here in the example.
- Please note that we coerce the cached _requestedMyValue, not ctrl.MyValue, the current effective value. Effective value should not be the base of coercion, and may no longer be valid with the dependency change.
- The code inside OnIsEnabledChanged is bracketed/protected with a pair of _nestLevel++/_nestLevel-- statements, to prevent the code and its recursion from changing state (_initialMyValue & _requestedMyValue). The side effect is that no MyValueChanged event will be fired either, so OnIsEnabledChanged has to fire the event explicitly, when there is indeed a change of the effective value.
The last experiment to do is to call mybtn.ClearValue(MyButton.MyValidProperty) in immediate window, which will trigger OnMyValuePropertyChanged called with new value of 0, the default value as new effective value, since the local value of 8 is cleared now:
The new effective value 0 will be coerced to 6 via a recursive call of OnMyValuePropertyChanged (OnMyValuePropertyChanged shows up twice in call stack window below), because IsEnabled is still false. Ultimately mybtn.MyValue will settle down with an effective and local value of 6, and MyValueChanged event is fired with new value 6.
Conclusion
So because Silverlight only supports PropertyValueChangedCallback, implementing validation and coercion on Silverlight can be very tricky, because validation and coercion logic can only be implemented via PropertyValueChangedCallback, and if validation or coercion changes the effective value of the DP, PropertyValueChangedCallback can be called recursively, which complicates the logic greatly. Plus Silverlight doesn't remember the originally requested DP value for later coercion, and does allow DP to be in an invalid state, so there are plenty of places for bugs in implementing dependency property on Silverlight.
This is another long post. If there is enough interest, I can look into providing a code snippet and some helper class to implement the basic plumbing for implementing dependency property on Silverlight, to minimize the differences between WPF and Silverlight property system, and make implementing DP on SL as simple as on WPF.