Tuesday, August 25, 2009

Dependency Property

In Avalon, the extesibility is modeled through the use of the DependencyObject and DependencyProperty classes. Basically every object in the visual tree is subclass of DependencyObject. This enables any instance in the tree to be extended with additional data (“properties”) that we might want to apply to it.

First, a DependencyProperty is a runtime defined tuple of a well-known name, a specific “owner type”, some optional meta-data and specific value type (e.g. String, Int32, MyCustomType). You create custom dependency properties by calling DependencyProperty::Register. Because you're forced through this factory method, the dependency property system can guarentee the properties to be created uniquely. Also, DependencyProperty instances are immutable once created. This is important and I'll explain why in a second.

Next, a DependencyObject is basically an object that provides a container for these dependency properties. Support comes in the form of three basic methods for manipulating these properties:

SetValue - Sets the value for a specific DependencyProperty
GetValue - Gets the value of a specific DependencyProperty
ClearValue - Removes the declaration of a specific DependencyProperty altogether
If you go look at the signatures for those mutator methods, you'll see a pattern that resembles protected dictionary access. Now, remember I said DependencyProperty instances are immutable? Well it's because the DependencyProperty instances are used as the keys to the internal dependency property hashtable1.

So, with the lower level stuff out of the way, let's look at real world scenario where dependency properties are used. The fundamental example in Avalon is this simple placing of something like a rectangle on a canvas:




This example, declared in XAML, basically demonstrates a rectangle being absolutely positioned on a canvas in Avalon. Let's take a look at the same example had I coded it in hand in C#:

// Build canvas
Canvas myCanvas = new Canvas();

// Build rectangle
Rectangle myRectangle = new Rectangle();

myRectangle.Width = new Length(50);
myRectangle.Height = new Length(50);
myRectangle.Fill = Brushes.Blue;

// Position rectangle within canvas
Canvas.SetTop(myRectangle, new Length(100));
Canvas.SetLeft(myRectangle, new Length(100));

// Add rectangle as child element of canvas
myCanvas.Children.Add(myRectangle);

So, as we can see, what XAML ends up doing is translating attributes declared with the dotted notation into calls to static method calls that conform to a specific signature. Interesting, no? Well even more interesting is what Canvas.SetTop/Left are actually doing internally. The Canvas needs to remember how to lay out my specific Rectangle with regards to the specific Canvas instance. So now, let's look at an expanded example of what's really happening here had we not used the SetTop/Left helper methods:

// This
Canvas.SetTop(myRectangle, new Length(100));
Canvas.SetLeft(myRectangle, new Length(100));

// Is the same as this
myRectangle.SetValue(Canvas.TopProperty, new Length(100));
myRectangle.SetValue(Canvas.LeftProperty, new Length(100));

Internally, probably during static initialization, the Canvas class has registered dependency properties using the registration process mentioned earlier. It then stored the resulting DependencyProperty instances in static fields for fast/easy access2. During initialization of the visual tree, the property values are applied to the children of the Canvas. Now, when the Canvas goes to perform its layout logic for its child elements, it can simply read these properties from each of the elements it's laying out.

Now, I hear some of you say: “Hey this could have been done differently! The Canvas could have managed a hashtable of it's own for it's positioning properties.” Well, you're right there, it could have been done that way. In fact, if you're at all familiar with the way IExtenderProvider and things like the ToolTip component work in .NET, that's exactly what you have to do. However, consider how many people have to write that same code over and over, so the first benfit of this architecture is that we save time by not having to write any code. The second benefit of this architecture is that instead of everyone maintaining their own hashtables for an object instance to property value(s) map, there is one hashtable per DependencyObject instance3. The only thing anyone wanting to write dependency properties has to do is Register their own custom name/Type pair and storage/retrieval of those values is as simple as a three method dictionary access API.

So now that we've had a crash course in Avalon's dependency property architecture, back to the original question at hand. Could this same approach have been used to solve WinFS' extensibility story? How about Indigo? If so, how? The architecture makes sense to me in terms of Avalon, but certainly not in terms of WinFS. Especially not since in WinFS, if I remember correctly, I can can query the extensibility data independent of the WinFS item it actually extends. In Indigo, I'm actually not sure where the dependency property architecture comes into play. Perhaps Don can provide us with an example.

1 For the uninitiated, keys in a hashtable cannot be mutable otherwise their hashes would logically change.
2 Technically they could be looked up in the cache via DependencyProperty::FromName, but that undoubtedly would have to go through something like two hashtable lookups and performance would crawl to a halt.
3 Most people are probably freaking out at the thought of a hashtable instance per object in the visual tree, but fear not. I don't have the ability to go ILDASM at the moment, but odds are it's deffered instantiated or uses one giant static hashtable managed at the DependencyObject class level.