HomeBrainDump Property Value Inheritance and More WPF Co...
Property Value Inheritance and More WPF Concepts
In this second part of a four-part series on important concepts in the Windows Presentation Foundation, you'll learn about property value inheritance, attached properties, and more. This article is excerpted from chapter three of the book Windows Presentation Foundation Unleashed, written by Adam Nathan (Sam's Publishing; ISBN: 0672328917).
The term property value inheritance (or property inheritance for short) doesn’t refer to traditional object oriented class-based inheritance, but rather the flowing of property values down the element tree. A simple example of this can be seen in Listing 3.4, which updates the Window from Listing 3.1 by explicitly setting its FontSize and FontStyle dependency properties. Figure 3.4 shows the result of this change. (Notice that the Window automatically resizes to fit all the content thanks to its slick SizeToContent setting!.)
Figure 3.4. The About dialog with FontSize and FontStyle set on the root Window.
LISTING 3.4 The About Dialog with Font Proper ties Set on the Root Window
For the most part, these two settings flow all the way down the tree and are inherited by children. This affects even the Buttons and ListBoxItems, which are three levels down the logical tree. The first Label’s FontSize does not change because it is explicitly marked with a FontSize of 20, overriding the inherited value of 30. The inherited FontStyle setting of Italic affects all Labels, ListBoxItems, and Buttons, however, because none of them have this set explicitly.
Notice that the text in the StatusBar is unaffected by either of these values, despite the fact that it supports these two properties just like the other controls. The behavior of property value inheritance can be subtle in cases like this for two reasons:
Not every dependency property participates in property value inheritance. (Internally, dependency properties can opt in to inheritance by passing FrameworkPropertyMetadataOptions.Inherits to DependencyProperty.Register.)
There may be other higher-priority sources setting the property value, as explained in the next section.
In this case, the latter reason is to blame. A few controls such as StatusBar, Menu, and ToolTip internally set their font properties to match current system settings. This way, users get the familiar experience of controlling their font via Control Panel. The result can be confusing, however, because such controls end up “swallowing” any inheritance from proceeding further down the element tree. For example, if you add a Button as a logical child of the StatusBar in Listing 3.4, its FontSize and FontStyle would be the default values of 12 and Normal, respectively, unlike the other Buttons outside of the StatusBar.
DIGGING DEEPER
Property Value Inheritance in Additional Places
Property value inheritance was originally designed to operate on the element tree, but it has been extended to work in a few other contexts as well. For example, values can be passed down to certain elements that look like children in the XML sense (because of XAML’s property element syntax) but are not children in terms of the logical or visual trees. These pseudochildren can be an element’s triggers or the value of any property (not just Content or Children) as long as it is an object deriving from Freezable. This may sound arbitrary and isn’t well documented, but the intention is that several XAML-based scenarios "just work" as you would expect without having to think about it.
WPF contains many powerful mechanisms that independently attempt to set the value of dependency properties. Without a well-defined mechanism for handling these disparate property value providers, the system would be a bit chaotic and property values could be unstable. Of course, as their name indicates, dependency properties were designed to depend on these providers in a consistent and orderly manner.
Figure 3.5 illustrates the five-step process that WPF runs each dependency property through in order to calculate its final value. This process happens automatically thanks to the built-in change notification in dependency properties.
Figure 3.5. The pipeline for calculating the value of a dependency property.
Step 1: Determine Base Value
Most of the property value providers factor into the base value calculation. The following list reveals the eight providers that can set the value of most dependency properties, in order from highest to lowest precedence:
1. Local value
5. Theme style triggers
2. Style triggers
6. Theme style setters
3. Template triggers
7. Property value inheritance
4. Style setters
8. Default value
You’ve already seen some of the property value providers, such as property value inheritance. Local value technically means any call to DependencyObject.SetValue, but this is typically seen with a simple property assignment in XAML or procedural code (because of the way dependency properties are implemented, as shown previously with Button.IsDefault). Default value refers to the initial value registered with the dependency property, which naturally has the lowest precedence. The other providers, which all involve styles and templates, are explained further in Chapter 10.
This order of precedence explains why StatusBar’s FontSize and FontStyle were not impacted by property value inheritance in Listing 3.4. The setting of StatusBar’s font properties to match system settings is done via theme style setters (#6 in the list). Although this has precedence over property value inheritance (#7 in the list), you can still override these font settings using any mechanism with a higher precedence, such as simply setting local values on the StatusBar.
Step 2: Evaluate
If the value from step one is an expression (an object deriving from System.Windows.Expression), then WPF performs a special evaluation step to convert the expression into a concrete result. In version 3.0 of WPF, expressions only come into play when using dynamic resources (covered in Chapter 8, “Resources”) or data binding (the topic of Chapter 9, “Data Binding”). Future versions of WPF may enable additional kinds of expressions.
Step 3: Apply Animations
If one or more animations are running, they have the power to alter the current property value (using the value after step 2 as input) or completely replace it. Therefore, animations (the topic of Chapter 13) can trump all other property value providers—even local values! This is often a stumbling block for people who are new to WPF.
Step 4: Coerce
After all the property value providers have had their say, WPF takes the almost-final property value and passes it to a CoerceValueCallback delegate, if one was registered with the dependency property. The callback is responsible for returning a new value, based on custom logic. For example, built-in WPF controls such as ProgressBar use this callback to constrain its Value dependency property to a value between its Minimum and Maximum values, returning Minimum if the input value is less than Minimum or Maximum if the input value is greater than Maximum.
Step 5: Validate
Finally, the potentially-coerced value is passed to a ValidateValueCallback delegate, if one was registered with the dependency property. This callback must return true if the input value is valid or false otherwise. Returning false causes an exception to be thrown, cancelling the entire process.
TIP
If you can’t figure out where a given dependency property is getting its current value from, you can use the static DependencyPropertyHelper.GetValueSource method as a debugging aid. This returns a ValueSource structure that contains a few pieces of data: a BaseValueSource enumeration that reveals where the base value came from (step 1 in the process) and Boolean IsExpression, IsAnimated, and IsCoerced properties that reveal information about steps 2-4.
When calling this method on the StatusBar instance from Listing 3.1 or 3.4 with the FontSize or FontStyle property, the returned BaseValueSource is DefaultStyle, revealing that the value comes from a theme style setter. (Theme styles are sometimes referred to as default styles. The enumeration value for a theme style trigger is DefaultStyleTrigger.)
Do not use this method in production code! Future versions of WPF could break assumptions you’ve made about the value calculation, plus treating a property value differently depending on its source goes against the way things are supposed to work in WPF applications.
DIGGING DEEPER
Clearing a Local Value
The earlier “Change Notification” section demonstrated the use of procedural code to change a Button’s Foreground to blue in response to the MouseEnter event, and then changing it back to black in response to the MouseLeave event. The problem with this approach is that black is set as a local value inside MouseLeave, which is much different from the Button’s initial state in which its black Foreground comes from a setter in its theme style. If the theme is changed and the new theme tries to change the default Foreground color (or if other providers with higher precedence try to do the same), it gets trumped by the local setting of black.
What you likely want to do instead is clear the local value and let WPF set the value from the relevant provider with the next-highest precedence. Fortunately, DependencyObject provides exactly this kind of mechanism with its ClearValue method. This can be called on a Button b as follows in C#:
b.ClearValue(Button.ForegroundProperty);
(Button.ForegroundProperty is the static DependencyProperty field.) After calling ClearValue, the local value is simply removed from the equation when WPF recalculates the base value.
Note that the trigger on the IsMouseOver property from the “Change Notification” section does not have the same problem as the implementation with event handlers. A trigger is either active or inactive, and when inactive it is simply ignored in the property value calculation.
An attached property is a special form of dependency property that can effectively be attached to arbitrary objects. This may sound strange at first, but this mechanism has several applications in WPF.
For the About dialog example, imagine that rather than setting FontSize and FontStyle for the entire Window (as done in Listing 3.4), you would rather set them on the inner StackPanel so they are inherited only by the two Buttons. Moving the property attributes to the inner StackPanel element doesn’t work, however, because StackPanel doesn’t have any font-related properties of its own! Instead, you must use the FontSize and FontStyle attached properties that happen to be defined on a class called TextElement. Listing 3.5 demonstrates this, introducing new XAML syntax designed especially for attached properties. This enables the desired property value inheritance, as shown in Figure 3.6
Figure 3.6. The About dialog with FontSize and FontStyle set on both Buttons via inheristance from the inner StackPanel.
LISTING 3.5 The About Dialog with Font Properties Moved to the Inner StackPanel
TextElement.FontSize and TextElement.FontStyle (rather than simply FontSize and FontStyle) must be used in the StackPanel element because StackPanel does not have these properties. When a XAML parser or compiler encounters this syntax, it requires that TextElement (sometimes called the attached property provider) has static methods called SetFontSize and SetFontStyle that can set the value accordingly. Therefore, the StackPanel declaration in Listing 3.5 is equivalent to the following C# code:
StackPanel panel = new StackPanel(); TextElement.SetFontSize(panel, 30); TextElement.SetFontStyle(panel, FontStyles.Italic); panel.Orientation = Orientation.Horizontal; panel.HorizontalAlignment = HorizontalAlignment.Center; Button helpButton = new Button(); helpButton.MinWidth = 75; helpButton.Margin = new Thickness(10); helpButton.Content = "Help"; Button okButton = new Button(); okButton.MinWidth = 75; okButton.Margin = new Thickness(10); okButton.Content = "OK"; panel.Children.Add(helpButton); panel.Children.Add(okButton);
Notice that the enumeration values such as FontStyles.Italic, Orientation.Horizontal, and HorizontalAlignment.Center were previously specified in XAML simply as Italic, Horizontal, and Center, respectively. This is possible thanks to the EnumConverter type converter in the .NET Framework, which can convert any case-insensitive string.
Although the XAML in Listing 3.5 nicely represents the logical attachment of FontSize and FontStyle to StackPanel, the C# code reveals that there’s no real magic here; just a method call that associates an element with an otherwise-unrelated property. One of the interesting things about the attached property abstraction is that no .NET property is a part of it!
Internally, methods like SetFontSize simply call the same DependencyObject.SetValue method that a normal dependency property accessor calls, but on the passed-in DependencyObject rather than the current instance:
Similarly, attached properties also define a static GetXXX method (where XXX is the name of the property) that calls the familiar DependencyObject.GetValue method:
public static double GetFontSize(DependencyObject element) { return(double)element.GetValue(TextElement.FontSizeProperty); }
As with property wrappers for normal dependency properties, these GetXXX and SetXXX methods must not do anything other than making a call to GetValue and SetValue.
DIGGING DEEPER
Understanding the Attached Property Provider
The most confusing part about the FontSize and FontStyle attached properties used in Listing 3.5 is that they aren’t defined by Button or even Control, the base class that defines the normal FontSize and FontStyle dependency properties! Instead, they are defined by the seemingly unrelated TextElement class (and also by the TextBlock class, which could also be used in the preceding examples).
How can this possibly work when TextElement.FontSizeProperty is a separate DependencyProperty field from Control.FontSizeProperty (and TextElement.FontStyleProperty is separate from Control.FontStyleProperty)? The key is the way these dependency properties are internally registered. If you were to look at the source code for TextElement, you would see something like the following:
TextElement.FontSizeProperty = DependencyProperty.RegisterAttached( "FontSize", typeof(double), typeof(TextElement), new FrameworkPropertyMetadata( SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions. AffectsRender | FrameworkPropertyMetadataOptions. AffectsMeasure), new ValidateValueCallback(TextElement.IsValidFontSize));
This is similar to the earlier example of registering Button’s IsDefault dependency property, except that the RegisterAttached method optimizes the handling of property metadata for attached property scenarios.
Control, on the other hand, doesn’t register its FontSize dependency property! Instead, it calls AddOwner on TextElement’s already-registered property, getting a reference to the exact same instance:
Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner( typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits));
Therefore, the FontSize, FontStyle, and other font-related dependency properties inherited by all controls are the same properties exposed by TextElement!
Fortunately, in most cases, the class that exposes an attached property (e.g. GetXXX and SetXXX methods) is the same class that defines the normal dependency property, avoiding this confusion.
Although the About dialog example uses attached properties for advanced property value inheritance, attached properties are most commonly used for layout of user interface elements. (In fact, attached properties were originally designed for WPF’s layout system.) Various Panel-derived classes define attached properties designed to be attached to their children for controlling how they are arranged. This way, each Panel can apply its own custom behavior to arbitrary children without requiring all possible child elements to be burdened with their own set of relevant properties. It also enables systems like layout to be easily extensible, because anyone can write a new Panel with custom attached properties. Chapter 6, “Layout with Panels,” and Chapter 17, “Layout with Custom Panels,” have all the details.
DIGGING DEEPER
Attached Properties as an Extensibility Mechanism
Just like previous technologies such as Windows Forms, many classes in WPF define a Tag property (of type System.Object) intended for storing arbitrary custom data with each instance. But attached properties are a more powerful and flexible mechanism for attaching custom data to any object deriving from DependencyObject. It’s often overlooked that attached properties enable you to effectively add custom data to instances of sealed classes (something that WPF has plenty of)!
A further twist to the story of attached properties is that although setting them in XAML relies on the presence of the static SetXXX method, you can bypass this method in procedural code and call DependencyObject.SetValue directly. This means that you can use any dependency property as an attached property in procedural code. For example, the following line of code attaches ListBox’s IsTextSearchEnabled property to a Button and assigns it a value:
// Attach an unrelated property to a Button and set its value to true: okButton.SetValue(ListBox.IsTextSearchEnabledProperty, true);
Although this seems nonsensical, and it certainly doesn’t magically enable new functionality on this Button, you have the freedom to consume this property value in a way that makes sense to your application or component.
There are more interesting ways to extend elements in this manner. For example, FrameworkElement’s Tag property is a dependency property, so you can attach it to an instance of a GeometryModel3D (a class you’ll see again in Chapter 12, "3D Graphics," that is sealed and does not have a Tag property) as follows:
GeometryModel3D model = new GeometryModel3D(); model.SetValue(FrameworkElement.TagProperty, "my custom data");
This is just one of the ways in which WPF provides extensibility without the need for traditional inheritance.
Just as WPF adds more infrastructure on top of the simple notion of .NET properties, it also adds more infrastructure on top of the simple notion of .NET events. Routed events are events that are designed to work well with a tree of elements. When a routed event is raised, it can travel up or down the visual and logical tree, getting raised on each element in a simple and consistent fashion, without the need for any custom code.
Event routing helps most applications remain oblivious to details of the visual tree (which is good for restyling) and is crucial to the success of WPF’s element composition. For example, Button exposes a Click event based on handling lower-level MouseLeftButtonDown and KeyDown events. When a user presses the left mouse button with the mouse pointer over a standard Button, however, they are really interacting with its ButtonChrome or TextBlock visual child. Because the event travels up the visual tree, the Button eventually sees the event and can handle it. Similarly, for the VCR-style Stop Button in the preceding chapter, a user might press the left mouse button directly over the Rectangle logical child. Because the event travels up the logical tree, the Button still sees the event and can handle it as well. (Yet if you really wish to distinguish between an event on the Rectangle versus the outer Button, you have the freedom to do so.)
Therefore, you can embed arbitrarily complex content inside an element like Button or give it an arbitrarily complex visual tree (using the techniques in Chapter 10), and a mouse left-click on any of the internal elements still results in a Click event raised by the parent Button. Without routed events, producers of the inner content or consumers of the Button would have to write code to patch everything together.
The implementation and behavior of routed events have many parallels to dependency properties. As with the dependency property discussion, we’ll first look at how a simple routed event is implemented to make things more concrete. Then we’ll examine some of the features of routed events and apply them to the About dialog.
Please check back for the next part of the series.