I'm writing this article in response to a
newsgroup posting where the reader states:
I am confused by this concept : why it is designed like this? Is
there any document on it's design purpose and intention? The document in Avalon
CTP is not helpful.
I'm sure a lot of people either are, or will be, wondering the
same things, so I figured I would whip together this article for future
reference.
The
DependencyObject/
DependencyProperty design actually exists for several reasons all of
which I'll ultimately touch on within this article, but let's start with one of
the main concepts for which this design exists known as "attached properties".
You'll encounter the concept of attached properties almost immediately as you
start to use Avalon. Attached properties are dependency properties that other
classes declare that can then be applied to any class you will ever throw into
the Avalon mix. Let's look at one of the simplest examples of attached
properties which involves positioning an item on a
Canvas. Canvas has four dependency properties regarding positioning:
TopProperty, BottomProperty, LeftProperty and RightProperty. Now, let's say we
want to just throw a random UI element onto a Canvas. That UI element's class
has no prior knowledge of the existence of the Canvas class or how it chooses
to position the elements it contains. Therefore, when I want to put that UI
element "into" a Canvas, I need to decorate it with Canvas specific dependency
properties to get it to lay out correctly.
Right about now you're probably thinking "Well why not just have
base class for all UI elements with Top, Bottom, Left and Right properties?".
Well the answer to that is simple if you consider a different layout container
such as the
Grid. The Grid doesn't just layout things at a certain static X,Y
coordinate like the Canvas. It uses rows and columns and automagically
positions contained elements based on all the contents of the columns (for X)
and rows (for Y). So, to accomplish this, the Grid offers the following
dependency properties for positioning instead1: ColumnProperty and
RowProperty.
So the next question is: How do these properties get applied to
arbitrary classes? Well first off, the classes obviously need to be
DependencyObject subclasses. From the consumer point of view, DependencyObject
is little more than a glorified Hashtable2. It's job is to store the
current values of any DependencyProperty that is ever applied to it. Setting a
dependency property is simple:
// Create a button
Button myButton = new Button();
// Button ultimately derives from DependencyObject, so let's set
// some properties so that it can be positioned on a Canvas!
myButton.SetValue(Canvas.TopProperty, new Length(100, UnitType.Pixel));
myButton.SetValue(Canvas.LeftProperty, new Length(100, UnitType.Pixel));
// Add the button to a canvas and it will be positioned at 100,100
myCanvas.Children.Add(myButton);
Actually, you know what? That's the long winded version because
Microsoft has defined a well-known pattern for manipulating attched properties
that make it a little easier for us. The pattern basically says that the class
that owns the dependency property should provide static accessor methods to
get/set the value on arbitrary DependencyObject instances. So let's look at
that version:
Button myButton = new Button();
Canvas.SetTop(myButton, new Length(100, UnitType.Pixel));
Canvas.SetLeft(myButton, new Length(100, UnitType.Pixel));
myCanvas.Children.Add(myButton);
Hmmm... wait, that's a long winded version too!!! Why write any
C# at all? Let's see how this pans out in XAML!
<Canvas ID="myCanvas" xmlns="http://schemas.microsoft.com/2003/xaml">
<Button ID="myButton" Canvas.Top="100" Canvas.Left="100"/>
</Canvas>
Remember that pattern I just mentioned? Well XAML relies on it
and translates the Canvas.Top attribute syntax to a Canvas.SetTop call.
Ok, this all seems simple enough now that we've been introduced
to it right? So, what's the big deal? Well, wait... we've only scratched the
surface with the introduction of attached properties. Attached properties
aren't the only place that dependency properties should be used. I
just decided to started with them because they demonstrate one of the key
features that DependencyObject enables, which is the ability to tack
infinite amounts of data onto existing objects without them ever having to know
about it in the first place. So, let's look at a simpler scenario for
dependency properties now, the
Canvas.HeightProperty. This could have easily been designed as a plain
vanilla .NET instance property of type Length
on the Canvas class and nobody else will ever apply this property to
anything but a Canvas, so why go through the trouble of making it a dependency
property? Point of fact, it still does appear as an
instance property on the Cavas class, but the reason it's defined as
a dependency property is simple: to gain generic support from Avalon
in terms of services. The "big deal" once you use dependency
properties is that Avalon can also offer many services to your
DependencyObject subclasses now without having to know about their custom
implementation details (in this case Canvas.Height
). Perhaps the easiest service to demonstrate, which is probably also the most
prominent in Avalon, is
animation. Any dependency property can be animated. Let's take that
sample we've been working with and animate the attached property a little
bit:
<Canvas ID="myCanvas" xmlns="http://schemas.microsoft.com/2003/xaml">
<Button ID="myButton" Canvas.Left="100">
<Canvas.Top>
<LengthAnimation From="0" To="100" Duration="2"
RepeatDuration="Indefinite" AutoReverse="True"/>
</Canvas.Top>
</Button>
</Canvas>
This will animate the button up and down from 0 to 100 (pixels)
over a two second period. Now admittedly, I probably wouldn't want to animate
the position of a Button in the real world, but that's actually the point that
I'm driving at. This could just as easily be a Rectangle or another Canvas or
even my own custom control! The positioning of the element on the Canvas
and animation of it's properties is done exactly the same way for any
DependencyObject subclass that is ever written from now until eternity.
Ok, ok... I can hear the masses screamning: "Bah! Animation, who
needs it!?". Fine, fine... what about the rest of these services that are based
on dependency properties then?
|
Service |
Description |
Default
Values |
Provides abillity to set the default value for anyone who uses a
particular dependency property in one location (see
PropertyMetaData.DefaultValue) |
| Expressions |
Provide the ability to base a dependency property on a calculated
value instead of a constant (those familiar with IE's CSS expression support
already have a head start on this one). Perhaps the coolest part of expressions
is that you can write your own calculation logic quite easily by simply
deriving from
Expression |
| Databinding |
Actually implemented as an Expression, provides the ability
to bind data from various sources to our property values. |
| Inheritance |
For those familiar with CSS, this is the cascading aspect of
something like a font style being applied to the of your HTML document applying
to all descendant elements without explicit font style declarations of their
own |
| Styling |
A
Style in Avalon is basically3 a declaration of a set of
dependency property values |
Property
Invalidation |
Avalon strongly encourages the concept of dependency property
caching for performance reasons4, property invalidation is the act
of notifying the caching class that it's local, cached version is no longer
valid |
So, that's it! Hopefully this article helps you
to understand the necessary level complexity that is introduced by the
DependencyObject/DependencyProperty model. Without
DependencyObject/DependencyProperty, the only way to achevie the same type of
generic behavior in .NET would be through something like reflection
which we all know has serious performance side-effects. The design enables
infinite levels of extensibility and provides powerful services for your
custom classes which allow other developers to use them in ways that you
may never have even imagined.
1 Grid is more
complex than this, but for purposes of this example this is all that is needed
to undersand.
2 In actuality, the design of DependencyObject is more complex with
great consideration for performance.
3 A style can actually get quite a bit more advanced when it defines
a VisualTree
4 For more on cached properties,
check out this section of the SDK