The only pre-requisite for using Dependency Properties is that your class inherits from DependencyObject. This actually is not a "big ask"; DependencyObject is a very low level class within WPF class hierarchy, just about everything in the WPF framework inherits from it already. Here is a screenshot from Reflector, giving you a rough idea of its descendants:
If you simply want to take advantage of this property system in your own classes there is nothing stopping you from simply inheriting from DependencyObject directly. Once that part of the puzzle is complete you need to define a property, a simple property for storing an integer could be achieved with code similar to the following:
public int Value { get { return (int)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } // Using a DependencyProperty as the backing store for Value. // This enables animation, styling, binding, etc... public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(int), typeof(MyObject), new UIPropertyMetadata(1));
This code was spat out by using the Visual Studio 2005 code snippet called propdp, which has kindly been provided by the WPF team. Notice how the existing .Net property system is being employed, this makes it possible to use these easily in the XAML and adheres to one of core goals the designers of WPF from the beginning: be consistent with the current platform.
There is a “gotcha” here that I want to draw to your attention – suppose you want to validate the incoming value before setting the property, let’s say the Value property can only be a number between 1 and 5, how should we go about that?
The most intuitive thing to do would be the put some code before the call the SetValue(...) in the Value setter. This, however, will likely result in your validation code from rarely being executed, if ever, which might not be what you would expect. The reason for this is to do with performance optimizations that the WPF team have done on the property system; a side effect of this is that the traditional setters are not always called.
However, all is not lost, we can still achieve the required validation we need. The answer is in the fourth parameter in the call to DependencyProperty.Register(...) – here is an object of type UIPropertyMetadata, in our previous example we used this class purely to set the default value for the property, but it is fully defined as:
This is a nice and simple class. The overload that we’re interested in is the one highlighted. The first parameter is a default value for the property, then there are two callbacks. Logically the two callbacks are fired at exactly the right time to achieve the same effect as having written code directly into the setter, before and after the SetValue(...) call. The code shown below outlines where the two callbacks are logically called:
public int Value { get { return (int)GetValue(ValueProperty); } set { // CoerceValueCallback SetValue(ValueProperty, value); // PropertyChangedCallback } }
public class MyObject : DependencyObject { public int Value { get { return (int)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(int), typeof(MyObject), new UIPropertyMetadata(0, OnValueChanged, OnCoerceValue)); public static object OnCoerceScore (DependencyObject source, object value) { if(value < 1) value = 1; if(value > 5) value = 5; return value; } public static void OnScoreChanged(DependencyObject source, DependencyPropertyChangedEventArgs e) { } }
Notice that in the coerce call we're ensuring that the value is always an integer from 1 to 5. What happens now if someone wants to inherit from this class, but their subclass can accept values from 1 to 10?
This question leads me to the crux of this what I wanted to talk about in this post, which is the best practice advice on implementing these two callbacks. The code below outlines how I would recommend solving this exact issue:
[Note: The screenshot is showing the incorrect level of accessibility on the static event handlers, they should be set to private, see the comments for this for more details.]
The solution is to simply call virtual instance methods on the target class – this allows any future inheritors of the class to be notified and take any action they so wish. Also notice that another benefit of this approach is that all the type casting is only ever done once, in the static callback handlers.
You might have noticed that this screen shot is actually showing a code snippet being expanded in Visual Studio – yes, I have written a snippet to help the C# developers out there work with this pattern. It is freely available from here, enjoy.
6 comments:
Shouldn't the static methods be private to help enforce type safety? That'll ensure that all access goes through the dependency property mechanisms too.
Hey onovotny, you're absolutely right - thank you for the feedback. I have changed the code snippet to reflect the correct level of accessibility.
Thanks again.
-PJ
Hi Paul, I wanted to try the same using attached properties. This dont work because the source will not be of the same class. So owner will be null and the non - static method will not be called.
Do you any proposal to solve this?
Greetings, mannabaron
Hey mannabaron, thank you for your question.
You are right in that this approach will not work for attached properties; however, it could be made to work in a couple of ways - but before I go into that I wondered if you would share your use case with me? The main stubling block in my thinking is that there may never be an instance class associated with the owner of the attached property (think scrollbars properties here), but if there is a surrounding element (thing Canvas) then the solutions are different.
Thanks for reading.
-PJ
The server with your snippet has vanished. Do you still have the snippet, and is there somewhere else I can find it?
Hey Gil, my apologies, I've uploaded a new version of the snippet to the following location: https://www.box.com/s/6fg7e71yu5qgzd018piy -- I have also updated the link in the post.
Thanks for reading.
-PJ
Post a Comment