So I was going to dive right in and do a part 2 on the WPF ListView
tutorial recently, but as I was writing the code I realized that a lot of it relies
on some new and very different constructs that WPF provides to
developers. Two of these are deep enough topics on their own that I
thought it would be a good idea to give an introduction to them before I
dove back into the ListView stuff. So today we are going to talk about
Dependency Properties, and in a future tutorial I will talk about how
binding works in WPF.
What are dependency properties? The quick definition from the MSDNdocs says that
a dependency property is a "property that is backed by the WPF property
system." Not really a great one-line explanation, but really, I can't
blame them. I can't come up with a good one line explanation either. But
essentially, it gives you a bunch of infrastructure to do all the things
that you often want to do with a normal property - validate it, coerce
it into a proper range, give out change notifications, and a number of
other aspects. We aren't going to touch on everything that a dependency
property can do today - because there is a lot. But we will be talking
about how to use them, how to create them, and how to set up
validation/coercion/change notification.
So one of the first things I thought was weird about the definition of a
dependency property is that it is a static. This didn't make sense to me
at first - this property needs to store info relevant to a particular
instance of a class, how is it going to do that if it is static? But
then, as I read more about them, I realized that a dependency property
definition was exactly that - a definition. You are essentially saying
that class A will have a property B - and it makes sense that that
definition would be static. The actual storage of a value for a
dependency property is deep inside the WPF property system - you never
have to worry about it.
One thing you do have to note, though, is that for a class to contain
dependency properties, it has to in some way derive from
DependencyObject.
In deriving from this class, you get all the infrastructure needed to
participate in the WPF dependency property system.
So at first glance, all the properties on the new WPF controls seem to
be regular old properties. But don't be fooled - this is often just a
simple wrapper around a dependency property (and the documentation will
usually say this). So what does it mean for a property to be a simple
wrapper around a dependency property? Lets look at the
FrameworkElement.Height
property as an example:
public double Height
{
get
{
return (double)GetValue(HeightProperty);
}
set
{
SetValue(HeightProperty, value);
}
}
Your first two questions are probably "What are these
GetValue
and
SetValue
functions?" and "What is this HeightProperty
?", and those
are very good questions indeed. Well, GetValue
and SetValue
are
functions you get by deriving from DependencyObject
. They allow you,
as you might have guessed, to get and set the values of dependency
properties. The HeightProperty
is the dependency property itself - the
static definition part of the FrameworkElement
class.
So far, this is just added complexity, and you're wondering why there is
yet another level of indirection on things. But here is where things get
interesting. In the "old ways", to set up some sort of change
notification on the
Height
property here, you would have had to
override it in a new class deriving from this FrameworkElement
class
and added whatever you needed in the 'set' part of the property here.
However, you no longer have to do things like that. Instead, you can:FrameworkElement myElement;
//
// myElement gets set to an element
//
DependencyPropertyDescriptor dpd;
dpd = DependencyPropertyDescriptor.FromProperty(
FrameworkElement.HeightProperty, typeof(FrameworkElement))
dpd.AddValueChanged(myElement, myHeightChangedFunction);
And now you have a function that will get called any time the height
changes on
myElement
! I think that is pretty handy. You can detach the
hook just as easily:DependencyPropertyDescriptor dpd;
dpd = DependencyPropertyDescriptor.FromProperty(
FrameworkElement.HeightProperty, typeof(FrameworkElement))
dpd.RemoveValueChanged(myElement, myHeightChangedFunction);
OK, enough about other people's dependency properties - lets go make our
own! Below we have a extremely simple class with a dependency property
for "LastName", as well as a property wrapper around it:
public class Person : DependencyObject
{
public static readonly DependencyProperty LastNameProperty =
DependencyProperty.Register("LastName", typeof(string), typeof(Person));
public string LastName
{
get
{
return (string)GetValue(LastNameProperty);
}
set
{
SetValue(LastNameProperty, value);
}
}
}
The
Register
call is pretty simple - you give the property a name (in
this case "LastName"), you say what type of information the property
will hold (in this case a string), and you say what type of object this
property is attached to (in this case Person). A little verbose,
especially when you add in the property wrapper, but not too bad. And
I'm sure Microsoft will find a way to streamline the syntax in the next
version of C#.
A little bit of a side note here - don't ever put anything but the
GetValue
and SetValue
calls inside the property wrapper. This is
because you never know if someone will set the property through the
wrapper, or straight through a SetValue
call - so you don't want to
put any extra logic in the property wrapper. For example, when you set
the value of a dependency property in XAML, it will not use the property
wrapper - it will hit the SetValue
call directly, bypassing anything
that you happened to put in the property wrapper.
Back to creating dependency properties. The constructor for
Register
has a couple more optional arguments. And we are going to jump right in
and use them all!public class Person : DependencyObject
{
public static readonly DependencyProperty LastNameProperty =
DependencyProperty.Register("LastName", typeof(string), typeof(Person),
new PropertyMetadata("No Name", LastNameChangedCallback, LastNameCoerceCallback),
LastNameValidateCallback);
private static void LastNameChangedCallback(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
Console.WriteLine(e.OldValue + " " + e.NewValue);
}
private static object LastNameCoerceCallback(DependencyObject obj, object o)
{
string s = o as string;
if (s.Length > 8)
s = s.Substring(0, 8);
return s;
}
private static bool LastNameValidateCallback(object value)
{
return value != null;
}
public string LastName
{
get
{
return (string)GetValue(LastNameProperty);
}
set
{
SetValue(LastNameProperty, value);
}
}
}
The two other arguments to
Register
are a PropertyMetadata
instance
and a validateValueCallback
. There are a couple different classes that
derive from PropertyMetadata
, but today we are just using the base
one. The PropertyMetadata
instance allows us to set a default value
(in this case "No Name"), a property changed callback
(LastNameChangedCallback
), and a coerce value callback
(LastNameCoerceCallback
). The default value does exactly what you
might expect. The change callback can do whatever you want it to do, and
you have plenty of information with which to do it. The
DependencyPropertyChangedEventArgs
contain both the old and the new
values of the property, as well as a reference to what property was
changed. And the DependencyObject
passed in is the object on which the
property was changed. This is needed because, as you can see, this
method is static. So every time the "LastName" property is changed on
any object, this function will get called.
The same goes for the coerce callback - we get the object on which the
property is being changed, and the possible new value. Here we get the
opportunity to change the value - in this case, apparently last names
are forced to be 8 characters or less.
And finally, we have the
LastNameValidateCallback
. This just gets the
possible new value, and returns true or false. If it returns false, an
exception is blown - so in this case, if anyone ever tries to set a null
last name, they better watch out.
So there you go, the basics on dependency property usage and creation.
There are a couple areas I haven't covered - inheritance, attached
properties, and overriding metadata to name a few. But hopefully this
gets off on the right foot with respect to dependency properties.
Comments
Post a Comment