If you're using Windows Presentation Foundation, this article explains some important concepts, such as logical and visual trees, that are unique to WPF (and can make it tricky to learn). This article, the first part of a four-part series, is excerpted from chapter three of the book Windows Presentation Foundation Unleashed, written by Adam Nathan (Sam's Publishing; ISBN: 0672328917).
To finish Part I of this book, and before getting to the really fun topics, it’s helpful to examine some of the main concepts that WPF introduces above and beyond what .NET programmers are already familiar with. The topics in this chapter are some of the main culprits responsible for WPF’s notoriously steep learning curve. By familiarizing yourself with these concepts now, you’ll be able to approach the rest of this book (or any other WPF documentation) with confidence.
Some of this chapter’s concepts are brand new (such as logical and visual trees), but others are just extensions of concepts that should be quite familiar (such as properties and events). As you learn about each one, you’ll also see how to apply it to a very simple piece of user interface that most programs need—an About dialog.
Logical and Visual Trees
XAML is natural for representing a user interface because of its hierarchical nature. In WPF, user interfaces are constructed from a tree of objects known as a logical tree.
Listing 3.1 defines the beginnings of a hypothetical About dialog, using a Window as the root of the logical tree. The Window has a StackPanel child element (described in Chapter 6, “Layout with Panels”) containing a few simple controls plus another StackPanel which contains Buttons.
Figure 3.1 shows the rendered dialog (which you can easily produce by pasting the content of Listing 3.1 into a tool such as XamlPad), and Figure 3.2 illustrates the logical tree for this dialog.
Note that a logical tree exists even for WPF user interfaces that aren’t created in XAML. Listing 3.1 could be implemented entirely in procedural code and the logical tree would be identical.
Figure 3.1. The rendered dialog from Listing 3.1
The logical tree concept is straightforward, but why should you care about it? Because just about every aspect of WPF (properties, events, resources, and so on) has behavior tied to the logical tree. For example, property values are sometimes propagated down the tree to child elements automatically, and raised events can travel up or down the tree. Both of these behaviors are discussed later in this chapter.
A similar concept to the logical tree is the visual tree. A visual tree is basically an expansion of a logical tree, in which nodes are broken down into their core visual components. Rather than leaving each element as a “black box,” a visual tree exposes the visual implementation details. For example, although a ListBox is logically a single control, its default visual representation is composed of more primitive WPF elements: a Border, two ScrollBars, and more.
Figure 3.2.The logical tree for Listing 3.1.
Not all logical tree nodes appear in the visual tree; only the elements that derive from System.Windows.Media.Visual or System.Windows.Media.Visual3D are included. Other elements (and simple string content, as in Listing 3.1) are not included because they don’t have inherent rendering behavior of their own.
TIP
XamlPad contains a button in its toolbar that reveals the visual tree (and property values) for any XAML that it renders. It doesn’t work when hosting a Window (as in Figure 3.1), but you can change the Window element to a page (and remove the SizeToContent property) to take advantage of this functionality.
Figure 3.3 illustrates the default visual tree for Listing 3.1 when running on Windows Vista with the Aero theme. This diagram exposes some inner components of the UI that are currently invisible, such as the ListBox’s two ScrollBars and each Label’s Border. It also reveals that Button, Label, and ListBoxItem are all comprised of the same elements, except Button uses an obscure ButtonChrome element rather than a Border. (These controls have other visual differences as the result of different default property values. For example, Button has a default Margin of 10 on all sides whereas Label has a default Margin of 0.)
Because they enable you to peer inside the deep composition of WPF elements, visual trees can be surprisingly complex. Fortunately, although visual trees are an essential part of the WPF infrastructure, you often don’t need to worry about them unless you’re radically restyling controls (covered in Chapter 10, “Styles, Templates, Skins, and Themes”) or doing low-level drawing (covered in Chapter 11, “2D Graphics”). Writing code that depends on a specific visual tree for a Button, for example, breaks one of WPF’s core tenets—the separation of look and logic. When someone restyles a control like Button using the techniques described in Chapter 10, its entire visual tree is replaced with something that could be completely different.
Figure 3.3. The visual tree for Listing 3.1, with logical tree nodes emphasized.
That said, you can easily traverse both the logical and visual trees using the somewhat symmetrical System.Windows.LogicalTreeHelper and System.Windows.Media.
WARNING
Avoid writing code that depends on a specific visual tree!
Whereas a logical tree is static without programmer intervention (such as dynamically adding/removing elements), a visual tree can change simply by a user switching to a different Windows theme!
VisualTreeHelper classes. Listing 3.2 contains a code-behind file for Listing 3.1 that, when run under a debugger, outputs a simple depth-first representation of both the logical and visual trees for the About dialog. (This requires adding x:Class="AboutDialog" and the corresponding xmlns:x directive to Listing 3.1 in order to hook it up to this procedural code.)
LISTING 3.2 Walking and Printing the Logical and Visual Trees
using System; using System.Diagnostics; using System.Windows; using System.Windows.Media;
public partial class AboutDialog : Window { public AboutDialog() { InitializeComponent(); PrintLogicalTree(0, this); }
void PrintLogicalTree(int depth, object obj) { // Print the object with preceding spaces that represent its depth Debug.WriteLine(new string(‘ ‘, depth) + obj);
// Sometimes leaf nodes aren’t DependencyObjects (e.g. strings) if (!(obj is DependencyObject)) return;
// Recursive call for each logical child foreach (object child in LogicalTreeHelper.GetChildren( obj as DependencyObject))
PrintLogicalTree(depth + 1, child); }
void PrintVisualTree(int depth, DependencyObject obj) { // Print the object with preceding spaces that represent its depth Debug.WriteLine(new string(‘ ‘, depth) + obj);
// Recursive call for each visual child for(int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) PrintVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i)); } }
When calling these methods with a depth of 0 and the current Window instance, the result is a text-based tree with the exact same nodes shown in Figures 3.2 and 3.3. Although the logical tree can be traversed within the Window’s constructor, the visual tree is empty until the Window undergoes layout at least once. That is why PrintVisualTree is called within OnContentRendered, which doesn’t get called until after layout occurs.
Navigating either tree can sometimes be done with instance methods on the elements themselves. For example, the Visual class contains three protected members (VisualParent, VisualChildrenCount, and GetVisualChild) for examining its visual parent and children. FrameworkElement, a common base class for controls such as Button and Label, defines a public Parent property representing the logical parent. Specific subclasses of FrameworkElement expose their logical children in different ways. For example, some classes expose a Children collection, and other classes (such as Button and Label) expose a Content property, enforcing that the element can only have one logical child.
TIP
Visual trees like the one in represented in Figure 3.3 are often referred to simply as element trees, because they encompass both elements in the logical tree and elements specific to the visual tree. The term visual tree is then used to describe any subtree that contains visual only (illogical?) elements. For example, most people would say that Window’s default visual tree consists of a Border, AdornerDecorator, two AdornerLayers, a ContentPresenter, and nothing more. In Figure 3.3, the top-most StackPanel is generally not considered to be the visual child of the ContentPresenter, despite the fact that VisualTreeHelper presents it as one.
WPF introduces a new type of property called a dependency property, used throughout the platform to enable styling, automatic data binding, animation, and more. You might first meet this concept with skepticism, as it complicates the picture of .NET types having simple fields, properties, methods, and events. But after you understand the problems that dependency properties solve, you will likely accept them as a welcome addition.
A dependency property depends on multiple providers for determining its value at any point in time. These providers could be an animation continuously changing its value, a parent element whose property value trickles down to its children, and so on. Arguably the biggest feature of a dependency property is its built-in ability to provide change notification.
The motivation for adding such intelligence to properties is to enable rich functionality directly from declarative markup. The key to WPF’s declarative-friendly design is its heavy use of properties. Button, for example, has 96 public properties! Properties can be easily set in XAML (directly or by a design tool) without any procedural code. But without the extra plumbing in dependency properties, it would be hard for the simple action of setting properties to get the desired results without writing additional code.
In this section, we’ll briefly look at the implementation of a dependency property to make this discussion more concrete, and then we’ll dig deeper into some of the ways that dependency properties add value on top of plain .NET properties:
Change notification
Property value inheritance
Support for multiple providers
Understanding most of the nuances of dependency properties is usually only important for custom control authors. However, even casual users of WPF end up needing to be aware of what they are and how they work. For example, you can only style and animate dependency properties. After working with WPF for a while you might find yourself wishing that all properties would be dependency properties!
In practice, dependency properties are just normal .NET properties hooked into some extra WPF infrastructure. This is all accomplished via WPF APIs; no .NET languages (other than XAML) have an intrinsic understanding of a dependency property.
Listing 3.3 demonstrates how Button effectively implements one of its dependency properties called IsDefault.
LISTING 3.3 A Standard Dependency Property Implementation
public class Button : ButtonBase { // The dependency property public static readonly DependencyProperty IsDefaultProperty;
static Button() { // Register the property Button.IsDefaultProperty = DependencyProperty.Register("IsDefault", typeof(bool), typeof(Button), new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsDefaultChanged))); ... }
// A .NET property wrapper (optional) public bool IsDefault { get { return (bool)GetValue(Button.IsDefaultProperty); } set { SetValue(Button.IsDefaultProperty, value); } }
// A property changed callback (optional) private static void OnIsDefaultChanged( DependencyObject o, DependencyPropertyChangedEventArgs e) { ... } ... }
The static IsDefaultProperty field is the actual dependency property, represented by the System.Windows.DependencyProperty class. By convention all DependencyProperty fields are public, static, and have a Property suffix. Dependency properties are usually created by calling the static DependencyProperty.Register method, which requires a name (IsDefault), a property type (bool), and the type of the class claiming to own the property (Button). Optionally (via different overloads of Register), you can pass metadata that customizes how the property is treated by WPF, as well as callbacks for handling property value changes, coercing values, and validating values. Button calls an overload of Register in its static constructor to give the dependency property a default value of false and to attach a delegate for change notifications.
Finally, the traditional .NET property called IsDefault implements its accessors by calling GetValue and SetValue methods inherited from System.Windows.DependencyObject, a low-level base class from which all classes with dependency properties must derive. GetValue returns the last value passed to SetValue or, if SetValue has never been called, the default value registered with the property. The IsDefault .NET property (sometimes called a property wrapper in this context) is not strictly necessary; consumers of Button could always directly call the GetValue/SetValue methods because they are exposed publicly. But the .NET property makes programmatic reading and writing of the property much more natural for consumers, and it enables the property to be set via XAML.
WARNING
.NET property wrappers are bypassed at run-time when setting dependency properties in XAML!
Although the XAML compiler depends on the property wrapper at compile-time, at run-time WPF calls the underlying GetValue and SetValue methods directly! Therefore, to maintain parity between setting a property in XAML and procedural code, it’s crucial that property wrappers do not contain any logic in addition to the GetValue/SetValue calls. If you want to add custom logic, that’s what the registered callbacks are for. All of WPF’s built-in property wrappers abide by this rule, so this warning is for anyone writing a custom class with its own dependency properties.
On the surface, Listing 3.3 looks like an overly verbose way of representing a simple Boolean property. However, because GetValue and SetValue internally use an efficient sparse storage system and because IsDefaultProperty is a static field (rather than an instance field), the dependency property implementation saves per-instance memory compared to a typical .NET property. If all the properties on WPF controls were wrappers around instance fields (as most .NET properties are), they would consume a significant amount of memory because of all the local data attached to each instance. Having 96 fields for each Button, 89 fields for each Label, and so forth would add up quickly! Instead, 78 out of Button’s 96 properties are dependency properties, and 71 out of Label’s 89 properties are dependency properties.
The benefits of the dependency property implementation extend to more than just memory usage, however. It centralizes and standardizes a fair amount of code that property implementers would have to write to check thread access, prompt the containing element to be re-rendered, and so on. For example, if a property requires its element to be re-rendered when its value changes (such as Button’s Background property), it can simply pass the FrameworkPropertyMetadataOptions.AffectsRender flag to an overload of DependencyProperty.Register. In addition, this implementation enables the three features listed earlier that we’ll now examine one-by-one, starting with change notification.
Whenever the value of a dependency property changes, WPF can automatically trigger a number of actions depending on the property’s metadata. These actions can be re-rendering the appropriate elements, updating the current layout, refreshing data bindings, and much more. One of the most interesting features enabled by this built-in change notification is property triggers, which enable you to perform your own custom actions when a property value changes without writing any procedural code.
For example, imagine that you want the text in each Button from the About dialog in Listing 3.1 to turn blue when the mouse pointer hovers over it. Without property triggers, you can attach two event handlers to each Button, one for its MouseEnter event and one for its MouseLeave event:
These two handlers could be implemented in a C# code-behind file as follows:
// Change the foreground to blue when the mouse enters the button void Button_MouseEnter(object sender, MouseEventArgs e) { Button b = sender as Button; if (b != null) b.Foreground = Brushes.Blue; }
// Restore the foreground to black when the mouse exits the button void Button_MouseLeave(object sender, MouseEventArgs e) { Button b = sender as Button; if (b != null) b.Foreground = Brushes.Black; }
With a property trigger, however, you can accomplish this same behavior purely in XAML. The following concise Trigger object is (just about) all you need:
This trigger can act upon Button’s IsMouseOver property, which becomes true at the same time the MouseEnter event is raised and false at the same time the MouseLeave event is raised. Note that you don’t have to worry about reverting Foreground to black when IsMouseOver changes to false. This is automatically done by WPF!
The only trick is assigning this Trigger to each Button. Unfortunately, because of an artificial limitation in WPF version 3.0, you can’t apply property triggers directly to elements such as Button. They can only be applied inside a Style object, so an in-depth examination of property triggers is saved for Chapter 10. In the meantime, if you want to experiment with property triggers, you could apply the preceding Trigger to a Button by wrapping it in a few intermediate XML elements as follows:
Property triggers are just one of three types of triggers supported by WPF. A data trigger is a form of property trigger that works for all .NET properties (not just dependency properties), also covered in Chapter 10. An event trigger enables you to declaratively specify actions to take when a routed event (covered later in the chapter) is raised. Event triggers always involve working with animations or sounds, so they aren’t covered until Chapter 13, "Animation."
WARNING
Don’t be fooled by an element’s Triggers collection!
FrameworkElement’s Triggers property is a read/write collection of TriggerBase items (the common base class for all three types of triggers), so it looks like an easy way to attach property triggers to controls such as Button. Unfortunately, this collection can only contain event triggers in version 3.0 of WPF simply because the WPF team didn’t have time to implement this support. Attempting to add a property trigger (or data trigger) to the collection causes an exception to be thrown at run-time.
Please check back for the second part of this series.