Chapter 8. Dependency Properties


Here's a program that contains six buttons in a two-row, three-column Grid. Each button lets you change the FontSize property to a value identified by the button text. However, the three buttons in the top row change the FontSize property of the window, while the three buttons in the bottom row change the FontSize property of the clicked button.

SetFontSizeProperty.cs

[View full width]

//---------------------------------------------------- // SetFontSizeProperty.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SetFontSizeProperty { public class SetFontSizeProperty : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SetFontSizeProperty()); } public SetFontSizeProperty() { Title = "Set FontSize Property"; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.CanMinimize; FontSize = 16; double[] fntsizes = { 8, 16, 32 }; // Create Grid panel. Grid grid = new Grid(); Content = grid; // Define row and columns. for (int i = 0; i < 2; i++) { RowDefinition row = new RowDefinition(); row.Height = GridLength.Auto; grid.RowDefinitions.Add(row); } for (int i = 0; i < fntsizes.Length; i++) { ColumnDefinition col = new ColumnDefinition(); col.Width = GridLength.Auto; grid.ColumnDefinitions.Add(col); } // Create six buttons. for (int i = 0; i < fntsizes.Length; i++) { Button btn = new Button(); btn.Content = new TextBlock( new Run("Set window FontSize to " + fntsizes[i])); btn.Tag = fntsizes[i]; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += WindowFontSizeOnClick; grid.Children.Add(btn); Grid.SetRow(btn, 0); Grid.SetColumn(btn, i); btn = new Button(); btn.Content = new TextBlock( new Run("Set button FontSize to " + fntsizes[i])); btn.Tag = fntsizes[i]; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonFontSizeOnClick; grid.Children.Add(btn); Grid.SetRow(btn, 1); Grid.SetColumn(btn, i); } } void WindowFontSizeOnClick(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; FontSize = (double)btn.Tag; } void ButtonFontSizeOnClick(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; btn.FontSize = (double)btn.Tag; } } }



The window constructor initializes its FontSize property to 16 device-independent units. The window creates a Grid and populates it with six buttons. All buttons display content with a TextBlock. (The program works the same if the Content property of each Button is set to a string.) The three buttons on the top row change the FontSize property of the Window object to 8, 16, or 32 units. The three buttons on the bottom row change the FontSize property of the individual button being pressed.

When the program starts running, all the buttons display text in a 16-unit-tall font. This initial FontSize property set for the window seems to filter through the Grid and the buttons to affect the TextBlock element. As you click each of the buttons in the top row, all the buttons change to reflect the new FontSize set for the window.

When you click a button in the bottom row, however, only that button changes because the event handler sets the FontSize property only for the button being clicked. But now click a button on the top row. Any button on which you individually set the FontSize no longer changes in response to changes in the window FontSize. A FontSize set specifically for the Button seems to have priority over a FontSize that the Button inherits from the Window.

The Window, the Grid, the six Button objects, and the six TextBlock objects form an element tree that can be visualized like this:

This element tree consists of all the visual objects explicitly created by the program. By visual object I mean those objects we can actually see on the screen, even if we only have the potential of seeing them. For example, if the TextBlock had no text or the Grid had no children, they would still be considered visual objects. More formally, I mean those objects based on classes that descend from the Visual class, generally by way of UIElement and FrameworkElement.

This little tree is not the tree known as the logical tree in the Windows Presentation Foundation documentation. In this program, the logical tree also includes the RowDefinition and ColumnDefinition objects associated with the Grid, and the Run objects associated with the TextBlock. These objects are not visual objects. They are instead based on classes that derive from ContentElement by way of FrameworkContentElement.

This little tree is also not the tree known in WPF documentation as the visual tree, although it's close. The visual tree may include visual objects that the program does not explicitly create. For example, to display its border and background, the Button object creates an object of type ButtonChrome (defined in the Microsoft.Windows.Themes namespace), and ButtonChrome derives from Visual, UIElement, and FrameworkElement by way of Decorator. Objects of type ButtonChrome are part of the visual tree, and there are probably others that we don't even know about.

Still, the tree shown on the previous page is a useful simplification for understanding how properties such as FontSize are handled. When a program explicitly sets the FontSize property of an object in a visual tree, all of the objects that are lower in the tree also get that same FontSize. These lower objects in the tree are said to "inherit" the property from their parent (but don't confuse this notion of inheritance with class inheritance). However, if a particular object had previously had its FontSize property explicitly set, it is not subject to this inheritance process.

So, when you click one of the buttons in the top row, the event handler in the program sets the FontSize property for the window, and there's obviously some additional work done behind the scenes so that the same FontSize property is set for all the window's descendents, except that any descendent that has previously had its own FontSize property set rejects this inherited value.

One interesting aspect of all this is that Grid doesn't even have a FontSize property. Obviously, the propagation of the FontSize property through the tree is somewhat more sophisticated than a simple parent-to-child handoff. The Grid lets its children get the new FontSize even though the Grid itself has no FontSize property.

When you click one of the buttons in the bottom row, the event handler sets the FontSize property for the Button, which then passes this new value to the TextBlock. If you happened to insert two lines like this:

(btn.Content as TextBlock).FontSize = 12; 


in the for loop (after each Content property has been set), the whole program would stop working. After TextBlock receives an explicit FontSize setting, it rejects any values passed down through inheritance.

If the FontSize property is never explicitly set by the program, objects that have this property get a default value (which happens to be 11 units).

It is possible to formulate some generalizations about the FontSize property. There is a default value of FontSize, but that default is low in priority. A value inherited from an ancestor in an element tree is higher in priority than the default, and a value explicitly set on the object has the highest priority.

FontSize isn't the only property that works like this. UIElement defines four properties (AllowDrop, IsEnabled, IsVisible, and SnapToDevicePixels) that are inherited through the element tree. The CultureInfo, FlowDirection, and InputScope properties defined by FrameworkElement also propagate through the tree, as well as the FontFamily, FontSize, FontStretch, FontStyles, FontWeight, and Foreground properties defined by Control. (The Background property is not inherited through the tree, but the default is null, which is rendered as transparent, so visual children may seem to inherit the Background set on their parents.)

Imagine for a moment you are designing a system like the Windows Presentation Foundation and you wanted to implement a process for properties to be inherited through an element tree. You'd probably seek a way to do it in a consistent manner with a minimum of duplicated code.

As you click buttons in the SetFontSizeProperty program, you witness them dynamically changing size to accommodate the new FontSize values, and this change in button size also affects the Grid and the Window itself. Obviously, changes in the FontSize property cause the Grid to revise its initial layout. As a hypothetical designer of the WPF, you'd probably want changes in the FontFamily and the other font properties to affect the program in the same way. However, you wouldn't want changes to Foreground to force Grid to revise its layout. All that's necessary with a change in the Foreground is for buttons to get redrawn.

In previous chapters, you saw how instances of classes such as Brush and Shapes can be made to dynamically respond to changes in their properties. This concept forms the basis of the animation system of the WPF. In Chapter 4, you saw how a button could be bound to a property of the window, and you'll discover in later chapters of this book that properties of objects can also be subject to templates and styles. If you were designing a system like the WPF, you'd probably want to handle all these features in a consistent manner.

Perhaps as a hypothetical designer of the WPF, you might even stumble upon the concept of dependency propertiesso called because they are properties that depend on a number of other properties and outside influences, sometimes in an extensive and intricate manner.

In conventional .NET programming, the value of an object's font size would be stored by the class in a private field, perhaps initialized with a default value:

double fntsize = 11; 


This private field would be publicly visible as the FontSize property:

public double FontSize {     get     {         return fntsize;     }     set     {         fntsize = value;         ...     } } 


The ellipsis in the set accessor indicates that there's probably more code that needs to be executed here. Perhaps the control needs to change its size or at least be repainted. Perhaps a FontSizeChanged event needs to be fired. Or perhaps descendents in the element tree need to be enumerated for FontSize inheritance.

In the Windows Presentation Foundation, the use of dependency properties allows much of this notification work to occur automatically in common scenarios. The Control class defines a FontSize property of type double (as we'll see) but it also defines an associated field named FontSizeProperty of type DependencyProperty:

public class Control: FrameworkElement {     ...     public static readonly DependencyProperty FontSizeProperty;     ... } 


This field is known as a dependency property. It's public and it's static, which means that the field is referenced with the class name rather than an object name. A static read-only field can only be set in the field definition itself or in a static constructor. Generally, a class creates an object of type DependencyProperty by a call to the static DependencyProperty.Register method, which may look something like this:

FontSizeProperty = DependencyProperty.Register("FontSize", typeof(double),                                                typeof(Control)); 


The arguments are the text name of the property associated with the dependency property, the data type of the property, and the class type registering the property. In actuality, there's usually some more code involved. The DependencyProperty object often includes metadata that describes some crucial aspects of the property. The following code is probably much closer to how the FontSizeProperty is actually registered in the Control class:

public class Control: FrameworkElement {     ...     public static readonly DependencyProperty FontSizeProperty;     ...     static Control()     {         FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();         metadata.DefaultValue = 11;         metadata.AffectsMeasure = true;         metadata.Inherits = true;         metadata.IsDataBindingAllowed = true;         metadata.DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;         FontSizeProperty =             DependencyProperty.Register("FontSize", typeof(double),                    typeof(Control), metadata, ValidateFontSize);     }     static bool ValidateFontSize(object obj)     {         double dFontSize = (double) obj;         return dFontSize > 0 && dFontSize <= 35791;     }     ... } 


This metadata indicates a default value of 11. The FontSize property affects the size of the element, so AffectsMeasure is set to true. The property (as we've seen) is also inherited through the element tree, a characteristic indicated by the Inherits property. Data binding is allowed, and the DefaultUpdateSourceTrigger indicates how data-binding is to work. (I'll get deeper into data-binding in Chapter 23.) In addition, the DependencyProperty.Register call includes the metadata as an argument, and also a method provided by the Control class to validate FontSize values. The method returns true if the value is greater than zero and less than a certain maximum. (I've determined by experimentation that there's an upper limit of 35,791 logical units for the FontSize.)

All this preliminary work pays off when it comes time to define the actual FontSize property. Here's how the Control class probably does it:

public class Control: FrameworkElement {     ...         public double FontSize         {             set             {                 SetValue(FontSizeProperty, value);             }             get             {                 return (double) GetValue(FontSizeProperty);             }         }     ... } 


Where do the SetValue and GetValue methods come from? These methods are defined in the DependencyObject class from which much of the WPF descends:

Object

      DispatcherObject (abstract)

            DependencyObject

                   Visual (abstract)

                           UIElement

                                 FrameworkElement

                                       Control

Watch out for similar names: Both DependencyObject and DependencyProperty are classes. Many classes in the WPF descend from DependencyObject and thus have SetValue and GetValue methods. These methods work with fields defined as static DependencyProperty objects.

Even though the DependencyProperty object passed to the SetValue and GetValue methods is static, SetValue and GetValue are instance methods, and they are setting and obtaining values associated with the particular instance. The DependencyObject is maintaining the current value, and handling all the routine stuff as well. For example, if SetValue hasn't yet been called for the particular Control instance, GetValue returns the value of the DefaultValue property associated with the metadata of FontSizeProperty.

When a program such as SetFontSizeProperty sets the FontSize property, the SetValue method has some work to do. The method must call the validation method to determine if the value is proper. If not, it throws an exception. Because the AffectsMeasure flag is set for FontSizeProperty, the SetValue method must cause a recalculation in the size of the control, which also requires the control to be redrawn. That redrawing logic uses the new FontSize in displaying text. (For a property like Foreground, the AffectsMeasure flag is false but the AffectsRender flag is true. The control isn't resized, but it is redrawn.) The SetValue method must also pass the new value down through the tree as a result of the Inherits flag. Elements in the tree can accept that new value for themselves, or reject it if their own FontSize property has been explicitly set.

Although examining how the FontSize property works is certainly helpful in understanding dependency properties, the lessons are hammered home only when you define a dependency property on your own.

The class that follows inherits from Button but includes a new dependency property. This class is called SpaceButton, and before you get ideas about launching buttons into orbit, you should know that the SpaceButton merely displays text with spaces inserted between each letter. SpaceButton adds two properties to those found in the normal Button class: Text and Space. For contrast, the class implements the Text property as a traditional .NET property, while the Space property is implemented in conjunction with a dependency property. The SpaceButton sets its Content property to the Text string with spaces inserted as indicated by the value of the Space property.

SpaceButton.cs

[View full width]

//-------------------------------------------- // SpaceButton.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SetSpaceProperty { public class SpaceButton : Button { // A traditional .NET private field and public property. string txt; public string Text { set { txt = value; Content = SpaceOutText(txt); } get { return txt; } } // A DependencyProperty and public property. public static readonly DependencyProperty SpaceProperty; public int Space { set { SetValue(SpaceProperty, value); } get { return (int)GetValue(SpaceProperty); } } // Static constructor. static SpaceButton() { // Define the metadata. FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.DefaultValue = 1; metadata.AffectsMeasure = true; metadata.Inherits = true; metadata.PropertyChangedCallback += OnSpacePropertyChanged; // Register the DependencyProperty. SpaceProperty = DependencyProperty.Register ("Space", typeof(int), typeof (SpaceButton), metadata, ValidateSpaceValue); } // Callback method for value validation. static bool ValidateSpaceValue(object obj) { int i = (int)obj; return i >= 0; } // Callback method for property changed. static void OnSpacePropertyChanged (DependencyObject obj, DependencyPropertyChangedEventArgs args) { SpaceButton btn = obj as SpaceButton; btn.Content = btn.SpaceOutText(btn.txt); } // Method to insert spaces in the text. string SpaceOutText(string str) { if (str == null) return null; StringBuilder build = new StringBuilder(); foreach (char ch in str) build.Append(ch + new string(' ', Space)); return build.ToString(); } } }



The program begins with the traditionally coded Text property. The string itself is stored in a private field named txt. Traditional .NET properties often need to include code in the set accessor so that the class actually does something with the new property. This Text property calls the SpaceOutText method to insert spaces in the text and then sets the Content property from that.

The rest of the file is the overhead for the Space property. The class defines a public static read-only field named SpaceProperty and implements the Space property set and get accessors with the SetValue and GetValue calls defined by DependencyObject. The Space property has nothing more to do.

The static constructor of the class sets up the metadata and registers the dependency property. Notice the two callback methods involved with this property. The ValidateSpaceValue property returns true if the value is acceptable. A negative number of spaces wouldn't make sense, so ValidateSpaceValue returns false for negative values.

The OnSpacePropertyChanged method will be called whenever the property changes. Like the set accessor for the Text property, this method sets the button Content property from the return value of SpaceOutText. The SpaceOutText method refers to the Space property to obtain the number of desired spaces.

Both callback methods must be defined as static. That's not a big deal for the ValidateSpaceValue method, but OnSpacePropertyChanged needs to cast the first argument to a SpaceButton and use that to reference everything in the object it needs. But it basically does the same thing that the set accessor of the Text property does.

Notice that the SpaceButton class sets the AffectsMeasure and Inherits properties of the metadata to true, but includes no other code to implement these features. It's all automatic.

The FontSize property that we were examining earlier is defined in Control and inherited by both Button and Window. This new Space property I've added to Button is supposed to be inherited through the element tree, but it's not defined anywhere else. I want to demonstrate property inheritance, so let's make a class named SpaceWindow that implements this same dependency property. SpaceWindow doesn't do anything itself with the property, so the class is rather shorter than SpaceButton.

SpaceWindow.cs

[View full width]

//-------------------------------------------- // SpaceWindow.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SetSpaceProperty { public class SpaceWindow : Window { // DependencyProperty and property. public static readonly DependencyProperty SpaceProperty; public int Space { set { SetValue(SpaceProperty, value); } get { return (int)GetValue(SpaceProperty); } } // Static constructor. static SpaceWindow() { // Define metadata. FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.Inherits = true; // Add owner to SpaceProperty and override metadata. SpaceProperty = SpaceButton.SpaceProperty.AddOwner (typeof(SpaceWindow)); SpaceProperty.OverrideMetadata(typeof (SpaceWindow), metadata); } } }



Just as in SpaceButton, this class defines both the SpaceProperty field and the Space property itself. The static constructor doesn't register a new Space property, however. Instead, it adds another owner to the Space property registered by the SpaceButton class. When a class adds a new owner to a previously registered dependency property, the original metadata isn't applied, and the class must create its own metadata. Because SpaceWindow doesn't do much with this Space property, it only needs to ensure that the Inherits flag is set.

The final class in this project is very similar to SetFontSizeProperty and is called SetSpaceProperty:

SetSpaceProperty.cs

[View full width]

//------------------------------------------------- // SetSpaceProperty.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SetSpaceProperty { public class SetSpaceProperty : SpaceWindow { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SetSpaceProperty()); } public SetSpaceProperty() { Title = "Set Space Property"; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.CanMinimize; int[] iSpaces = { 0, 1, 2 }; Grid grid = new Grid(); Content = grid; for (int i = 0; i < 2; i++) { RowDefinition row = new RowDefinition(); row.Height = GridLength.Auto; grid.RowDefinitions.Add(row); } for (int i = 0; i < iSpaces.Length; i++) { ColumnDefinition col = new ColumnDefinition(); col.Width = GridLength.Auto; grid.ColumnDefinitions.Add(col); } for (int i = 0; i < iSpaces.Length; i++) { SpaceButton btn = new SpaceButton(); btn.Text = "Set window Space to " + iSpaces[i]; btn.Tag = iSpaces[i]; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += WindowPropertyOnClick; grid.Children.Add(btn); Grid.SetRow(btn, 0); Grid.SetColumn(btn, i); btn = new SpaceButton(); btn.Text = "Set button Space to " + iSpaces[i]; btn.Tag = iSpaces[i]; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonPropertyOnClick; grid.Children.Add(btn); Grid.SetRow(btn, 1); Grid.SetColumn(btn, i); } } void WindowPropertyOnClick(object sender, RoutedEventArgs args) { SpaceButton btn = args.Source as SpaceButton; Space = (int)btn.Tag; } void ButtonPropertyOnClick(object sender, RoutedEventArgs args) { SpaceButton btn = args.Source as SpaceButton; btn.Space = (int)btn.Tag; } } }



Notice that this class inherits from SpaceWindow rather than Window. Just as in the earlier program, it creates six buttons but these buttons are of type SpaceButton. These buttons show up with text that contains one space between each character because that's what the default is. Just as in the previous program, when you click a button in the top row, all the buttons change, but when you click a button in the bottom row, only that button changes and remains unchangeable thereafter. By implementing a dependency property, the SpaceButton class is now ready for data binding, styling, animation, and all kinds of craziness.

Another mystery is now about to be solved. Chapter 5 introduced the DockPanel and with it static methods named SetDock and GetDock. Subsequently, similar properties were encountered with Grid and Canvas. The SetDock property certainly has an odd syntax:

DockPanel.SetDock(ctrl, Dock.Right); 


Alternatively, perhaps it would have been possible for UIElement to define a Dock property so that the property could be set like this:

ctrl.Dock = Dock.Right; // Not the way it's done in WPF. 


That's how it's done in Windows Forms. But in the WPF, this Dock property would come into play only when the control is a child of a DockPanel. There might be other panelseven ones that you createthat might require other properties, and it doesn't make sense to burden UIElement with all these properties for all the different types of panels.

Another possibility would be to have an expanded Add method of the Children property:

dock.Children.Add(ctrl, Dock.Right); // Not the way it's done in WPF. 


But that Children property is actually an object of type UIElementCollection, and it's used for all panels, not just the DockPanel. Another possibility would involve an instance property of DockPanel named SetDock:

dock.SetDock(ctrl, Dock.Right); // Not the way it's done in WPF. 


This would work, of course, but the DockPanel would have to maintain a second collection of controls with their associated Dock members. While performing layout, the DockPanel would enumerate all the elements in its Children collection and then search the second collection for a matching element for the Dock member. You might have assumed that the actual syntax:

DockPanel.SetDock(ctrl, Dock.Right); 


involved storing the control and the associated Dock value in some kind of collection. But the SetDock and GetDock properties of DockPanel are actually implemented like this:

public class DockPanel : Panel {     ...     public static readonly DependencyProperty DockProperty;     ...     public static void SetDock(UIElement el, Dock dck)     {         el.SetValue(DockProperty, dck);     }     public static Dock GetDock(UIElement el)     {         return (Dock) el.GetValue(DockProperty);     } } 


DockProperty is defined as a DependencyProperty but it is registered with a DependencyProperty.RegisterAttached method, so it's called an attached property. If it were a normal dependency property, DockProperty would be associated with a property named Dock with calls to SetValue and GetValue. There is no property named Dock. Instead, DockProperty is referred to in two static methods defined by DockPanel named SetDock and GetDock. These methods call SetValue and GetValue for the element (or control) passed as an argument to the methods. These are the same SetValue and GetValue methods defined by DependencyObject and used in connection with dependency properties. (However, notice that the class implementing the attached property doesn't call its own SetValue and GetValue methods. For that reason, a class implementing an attached property doesn't need to derive from DependencyObject.)

Here's the call to SetDock again as it might be encountered in a typical program:

DockPanel.SetDock(ctrl, Dock.Right); 


That call is exactly equivalent to this call:

ctrl.SetValue(DockPanel.DockProperty, Dock.Right); 


You can verify this for yourself by replacing calls to SetDock with equivalent calls to SetValue.

When that ctrl object gets a call to its SetValue method with an initial argument of DockPanel.DockProperty, it probably stores the property and the value in a collection of some sort. But this collection is part of the child element, and not part of the DockPanel object.

When the DockPanel object is laying out the elements, it can obtain the Dock value associated with the control by calling its static GetDock method:

Dock dck = DockPanel.GetDock(ctrl); 


This call is equivalent to:

Dock dck = (Dock) ctrl.GetValue(DockPanel.DockProperty); 


Attached properties are much less common than ordinary dependency properties, and you've already encountered many of the most important ones.

If you are now exploring the Fields section of the class documentation, you've also seen static fields named (for example) KeyDownEvent of type RoutedEvent. The role of those fields will become apparent in the next chapter.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net