The Visual Tree
At its core, Avalon provides an API for defining visual trees for rendering content to various mediums (monitor, printer, etc.). The visual tree is based on the composite design pattern. The visual tree is a conceptual model that is the culmination of several phsyical classes and interfaces.
Visuals
Elements that want to fit into the visual tree implement IVisual at the bare minimum which exposes typical composite pattern members such as Children and HasChildren as well as providing other support methods and properties for living within the tree. There are three fundamental types of visuals in the Avalon system:
- ContainerVisual - Acts as a container (surprise!) for other elements within the tree and does not do any rendering of it's own.
- DrawingVisual - Participates in the rendering process of the tree by actually drawing a representation of itself using a DrawingContext (more on this below).
- RetainedVisual - Like DrawingVisual, this type also participates in the rendering process of the tree, but instead of having to render it's content every single time, it only needs to render when it's contents have been invalidated. For those who work with GDI+ this should sound like a familiar concept. In fact in .NET when you override Control::OnPaint you are passed a PaintEventArgs which identifies the clip rectangle that needs to be repainted1. Well, as I mentioned previously, we know Avalon has a strong relationship with DirectX. I won't go into the specifics of DirectX here, but basically I surmise that using a RetainedVisual allows your rendered data to be shipped to your computer's GPU (Graphics Processing Unit) and left there. You will never need to re-render that data unless it is invalidated for some reason. This results in amazing performance benefits. If you were rendering your control frequently, that data has to travel all the way to the video hardware everytime. This is an amazing bottleneck and eliminating it results in blazingly faster graphics even with thousands of elements in the visual tree. Most visuals in the Avalon system are RetainedVisual subclasses for this reason.
UIContext
No more writing a message pump in WinMain. No more STA threads. Say hello to the new synchronization technique for Windows client applications: The UIContext class and it's child the UIContextObject class. UIContext is, in the simplest terms, a suped-up Monitor. Anyone who has done any work with threads should be familiar with what a monitor is being that it is perhaps the simplest of synchronization primitives in any threading package. Before any UIContextObject can do any work within it's parent UIContext, it must aquire access to the underlying monitor using either a call to UIContext::Enter method followed by a call to UIContext::Exit when work is complete or, the more IDisposable friendly approach, UIContext::Access. In addition to the monitor wrapping, the UIContext class provides an asynchronus approach to invoking methods within its context using the familiar Begin/EndInvoke pattern. The one big difference between UIContext and a basic monitor implementation is the added support for scheduling based on a UIContextPriority enum. UIContext also exposes several events that are fired to notify when certain context operations take place as well to filter exceptions that might occur within the context.
As a final word on this subject, the truth is, I lied in the very first line of this section: message pumps do still exist. The thing is you are so abstracted from them that you don't even need to concern yourselves with it. They exist in the abstract form of something called a UIDispatcher. Specifically for the Avalon windows message pump there is a Win32Dispatcher and for backwards compatibility there is also an HwndDispatcher.
Rendering
Let's talk about rendering briefly. I mentioned DrawingContext in passing when I introduced the DrawingVisual and RetainedVisual controls. So what is a DrawingContext? Well, when you need to actually “push pixels” around, this is how you'll do it in Avalon. In an analogy most of us will understand: DrawingContext is to Avalon as Graphics is to System.Drawing (GDI+). In fact, the classes share many of the same concepts/methods, with the Avalon version offering more support for rich media types such as video. DrawingContext, by the way, derives from UIContextObject which means that all drawing within a context is synchronized by the UIContext the DrawingContext belongs to.
Dependencies
There exists a concept of property extensibility in Avalon, referred to as dependencies. Two core classes are used to implement the concept of dependencies: DependencyObject and DependencyProperty. Every object used in the visual tree is subclass of DependencyObject because Visual subclasses it. 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 hashtable2.
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 access3. 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 instance4. 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.
Summary
So, now that you've been introduced to all this low level visual tree “goo”, you can most likely ignore the majority of it for the rest of your natural lifetime unless you're a big geek like me that likes to know how everything works. Naturally there will be those who are developing custom controls that may require fine grain access to those rendering aspects and therefore will need to work with this lower layer, but they'll certainly be the minority. The majority of developers working with Avalon will never actually need to worry about the finer details of the visual layer because most of what the average application requires in terms of rendering will be provided by higher layers in the Avalon framework.
Back to Main Article
1 Sadly, a lot of control writers ignore this concept for simplicity's sake and simply re-render their entire control, but that can be quite an expensive operation in complex scenarios.
2 For the uninitiated, keys in a hashtable cannot be mutable otherwise their hashes would logically change.
3 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.
4 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.