devxlogo

WPF Wonders: Building Control Templates

WPF Wonders: Building Control Templates

PF’s XAML language defines the controls that make up a user interface. Typically, some kind of container control such as a Grid or StackPanel fills the window. In turn, that container holds other controls, such as Labels, TextBoxes, Buttons, Sliders, and so forth.

Your XAML code defines the user interface’s structure and?to an extent?its behavior. For example, you can define animations in XAML that change control properties such as size and position when certain events occur.

WPF also lets you use templates to define the structure of the controls that make up the interface. By controlling the structure of a Slider, for example, you can change the way it looks and acts. For example, you could use a template to make a Slider display round buttons on its ends, a thin track in the middle, and a fat diamond-shaped thumb for dragging.

This article describes templates, explains how to build and use them, and provides a few useful examples.

What Is a Template?

Figure 1. Slider Dissected: If you look closely at a WPF Slider control, you’ll see that it’s a composite, made up of lots of parts that are actually other controls.

If you look closely at most controls, you’ll find that they’re made up of a number of parts. Figure 1 shows a Slider control with its constituent parts marked.

You might imagine that a single chunk of code makes up the Slider and creates all of these pieces, but that’s not the case. The Slider is made up of an assortment of other controls, including Border, Canvas, Grid, Path, Rectangle, RepeatButton, Thumb, TickBar, and Track controls. For example, the TickBar control draws the tick marks and the Thumb allows the user to click and drag the thumb. Together, those individual controls determine the Slider control’s appearance and behavior.

The control’s template determines the Slider’s pieces and how they behave. It defines the sub-controls that make up the Slider and determines how they interact with the user through the sub-controls’ properties.

This would all be only mildly interesting if it weren’t for the fact that you can change a control’s template. If you want to build an elliptical Slider that lets the user drag a circle around the ellipse’s rim, you can! You’ll have to define how the new template provides a Slider’s typical behavior?but you can do it if you really want to.

Presenting the ContentPresenter

A template contains an arrangement of controls that make up some other control. Defining the controls themselves isn’t difficult, but there are a couple pieces of information that you cannot hardwire directly into the template’s code.

First, the template cannot directly include whatever it is that the control is supposed to display. For example, suppose you’re building a Label template. You can use a TextBlock or some other control (even a regular Label) to define the control, but you cannot hard-code the value the Label will display. (If you were to set the TextBlock’s Text property to “Hello,” the control wouldn’t be able to display anything other than “Hello.”)

WPF provides a ContentPresenter object to tell the template what the control is supposed to display. You place this object inside the template where you want the content displayed and WPF does its best to do the rest.

With that little bit about templates and the ContentPresenter, you can build a simple template. The following code shows a basic template that adds an outline to a Label control. You should define the template in some object’s Resources section. This example creates the template in the Window’s Resources section so you can use it on any Label in that Window.

                                    
?
Figure 2. Outstanding Outline: The OutlinedLabel example program uses a template to draw an outline around its content.

The x:Key attribute gives the template a name so the code can refer to it later. The TargetType attribute controls which type(s) of controls can use the template.

The template contains a Border control that displays a thick red border. That control holds the ContentPresenter. Typically, the label control’s content is simple text, so the ContentPresenter displays the text. However, if you set the Label’s content to something unusual, such as a Button, the ContentPresenter happily drops that into the Border instead.

The following code shows how the OutlinedLabel example program (available in the downloadable sample code in both VB.NET and C#), shown in Figure 2, uses this template. The Template attribute tells the control to use the temOutlinedLabel template.

Using Template Bindings

The temOutlinedLabel template is useful, albeit just barely. It displays a thick red outline around some text. Obviously, this template is useful only if you want a thick red border, but what if you wanted a thin green outline instead? You could try setting the Label's BorderBrush property to Green and the BorderThickness property to 1 manually?but because the brush and thickness are hard coded in the template's Border control, the template would ignore the new values.

The solution to this dilemma is similar to the one used to bring the text content into the template. Just as the ContentPresenter pulls the value assigned to the control (the Label's content in this case) into the template, template bindings can bring other values into the template.

To use a template binding, set a template property equal to a TemplateBinding object, specifying the name of the property that you want to bring in. The property should be one provided by the control using the template.

For example, to use the Label control's BorderBrush property, you might set the Border control's BorderBrush property to {TemplateBinding BorderBrush}.

Here's an improved version of the template that obeys the control's BorderBrush and BorderThickness properties.

                

The following code shows how the OutlinedLabel2 example program uses this template to produce a result similar to the one shown in Figure 2. Using this template, however, when you change the Label control's BorderBrush and BorderThickness properties, the template changes its Border control to match.

You may be asking yourself, "What's the point?" After all, the Label control itself has BorderBrush and BorderThickness properties, so why not just use it without the template?"

The answer is that if you simply wanted to reproduce the features of a Label, it would be silly to create a template.

Here's a more interesting template. It starts with a Border that uses the Label control's BorderBrush and BorderThickness properties. That control also contains a second Border separated from the first by a margin the width of the Label's BorderThickness value. This inner Border contains the ContentPresenter. Notice that the ContentPresenter's HorizontalAlignment and VerticalAlignment properties center the content inside the Border.

                                     
?
Figure 3. Refined Line: The DoubleOutlinedLabel program uses two Border controls to draw a Label with a double outline.

The DoubleOutlinedLabel example program uses this template in the following code. Figure 3 shows the result.

Note that this template produces a result that you can't produce by simply manipulating the Label control's properties.

The following code shows an even more interesting Label template.

                                                                                                                                                                                                                    

This template uses a Grid to hold a Border and a Canvas that sits on top of the Border.

The first Border contains another Border as in the previous example, but this time the inner Border holds a TextBlock that provides features for wrapping and trimming text (ending text that won't fit with an ellipsis). Without such a template, a normal Label doesn't wrap text, and truncates any text that won't fit.

The Canvas is filled with a LinearGradientBrush whose Opacity is initially set to 0, so it is invisible.

?
Figure 4. Disabled Label: The BetterLabel example program uses the temOutlinedLabel template.

The template's Triggers> section holds a single trigger. When the Label control's IsEnabled property is False, the trigger changes the Canvas's Opacity value to 0.5, so it becomes semi-transparent and partially obscures the Border.

The first step in defining this template is to decide what controls it will use. The following code shows how the ShapedButton program defines the controls used by its template.

                                                                                                                                    

This template starts with a Grid. Grids are often handy because they make centering and aligning other controls easy.

The grid contains a Polygon named pgnBody that draws the control's diamond-shaped background. This control has a thick beveled edge.

Next, the template includes the ContentPresenter. Because it is included after pgnBody, it is drawn on top so the user can see it. The ContentPresenter's VerticalAlignment and HorizontalAlignment properties center it in the Grid. (In the main XAML code, the third Button contains a StackPanel holding two Labels with contents "Button" and "3." Notice in Figure 5 that the ContentPresenter includes the StackPanel and its contents in the Button.)

The only other control that the template uses is a second Polygon named pgnCover that fits over the first Polygon. It is filled with a LinearGradientBrush that shades from light blue to white. This Polygon's Opacity is initially 0.5 so it tones down all the template's other controls.

Those four controls (a Grid, two Polygons, and the ContentPresenter) define the template's structure. Now the template needs to describe how these controls should change for different control states.

To make that a little easier, the template includes a Resources section that defines the following three bitmap effects.

                                                             

The first effect defines an outer glow that takes effect when the mouse is over the button.

The second effect defines a new bevel effect that makes the edges of the button seem bulged up (the third Button in the last image in Figure 5) when the button is pressed.

The final effect defines a larger glow, which the program applies when the button is pressed.

The final piece to the template is its Triggers> section, shown in the following code. Triggers are applied in the order in which they are listed in the code, so later triggers can override the behavior of earlier triggers. For example, if the mouse is both over the Button and the mouse is pressed, the mouseover trigger occurs first, followed by the mouse-pressed trigger.

                                                                                                                                                    

The IsMouseOver trigger sets the pgnCover control's Opacity to 0, which turns it transparent, effectively removing the cover and letting the remaining controls show at full brightness. The trigger also sets the ContentPresenter's bitmap effect to display a glow.

The IsFocused trigger also sets the pgnCover control's Opacity to 0, but it also sets the ContentPresenter's Opacity to 1 so it is fully opaque and very bright. Finally, it gives the ContentPresenter the same glow as the previous trigger.

The IsDefaulted trigger sets the pgnCover control's Opacity to 0.25, making it more transparent than it normally is, but not as transparent as when the mouse is over the Button or when the Button has focus. The trigger also gives the main background Polygon pgnBody a thick dashed border (see the first image in Figure 5).

The IsPressed trigger changes the background Polygon's bitmap effect to the bulging beveled edge and gives the ContentPresenter the larger glow.

The IsEnabled = False trigger comes last, so it takes precedence over all of the other triggers. This trigger sets the pgnCover control's Opacity to 0.75, so it is relatively opaque and the Button seems lighter than ever. The Button control itself automatically disables the controls so they don't respond to the user.

Download the example and experiment with it. First, give the normal behaviors a try. Use the Tab key to move between the Buttons and see how the defaulted control responds. Next, try moving the mouse over the Buttons and clicking on them.

Finally, if you're feeling adventurous, try changing the template's controls and triggers to see what else you can make the example do.

Spectacular ScrollBars

While a Button provides a lot of states and interacts closely with the user, it's still a relatively simple control. It has only a single part, even if you built it out of a bunch of pieces.

In contrast, many controls contain several pieces that have distinct functions. For example, consider the ScrollBar shown in Figure 6. A ScrollBar contains two RepeatButtons on its ends and a Track in the middle. The Track contains two more RepeatButtons and a Thumb.

?
Figure 6. ScrollBar Parts: A ScrollBar consists of two RepeatButtons and a Track. The Track consists of two more RepeatButtons and a Thumb.

All these pieces must work together to make the control function properly. For example, when the user clicks and drags the Thumb, the control must adjust the sizes of the RepeatButtons on either side of it.

To make all of the pieces fit together properly, you need to add two techniques to those demonstrated in the Button example.

First, you need to give certain key controls specific names, so the ScrollBar can identify its parts. In this example, name the Track control PART_Track. You can learn what controls need special names by searching for "PART_" in the online documentation. For example, here's the ScrollBar's documentation.

The second thing required to make the pieces fit together is to provide Command attributes to the component controls to tell the ScrollBar what's happening. For example, when the user clicks the ScrollBar's leftmost RepeatButton, that control should invoke the ScrollBar's LineLeftCommand to tell it that it should adjust its Value by the SmallChangeAmount.

For a ScrollBar, the commands are:

  • ScrollBar.LineLeftCommand: The user clicked the small decrease button.
  • ScrollBar.PageLeftCommand: The user clicked the large decrease button.
  • ScrollBar.LineRightCommand: The user clicked the small increase button.
  • ScrollBar.PageRightCommand: The user clicked the large increase button.
  • ScrollBar.LineUpCommand: The user clicked the small decrease button in vertical orientation.
  • ScrollBar.PageUpCommand: The user clicked the large decrease button in vertical orientation.
  • ScrollBar.LineDownCommand: The user clicked the small increase button in vertical orientation.
  • ScrollBar.PageDownCommand: The user clicked the large increase button in vertical orientation.
?
Figure 7. Simple ScrollBar: The SimpleScrollBar example program demonstrates normal and templated ScrollBars.

The SimpleScrollBar example program is shown in Figure 7. The pale controls are standard ScrollBars and the shaded controls use a customized template. Unfortunately, the code is quite long and contains a lot of boring detail so I'll discuss only the highlights here. You can download the example program to see the template it all of its glory.

The template's structure begins with a Grid control that has three rows and three columns. Depending on the ScrollBar's orientation, the constituent controls are arranged vertically or horizontally so they span the Grid's width or height. For example, if the ScrollBar is oriented vertically, then its small Up button (with the red plus sign in Figure 7) gets Row = 0, Column = 0, ColumnSpan = 3 so the control spans the Grid's upper row.

The Grid contains two RepeatButtons with a Track control in between.

The following code shows the first RepeatButton's definition. The control's content is a Path that draws a thick red horizontal dash.

    

Notice that the control's Command attribute is set to ScrollBar.LineLeftCommand. When the RepeatButton fires, it calls the ScrollBar's LineLeftCommand so the control decreases its Value by the SmallChange amount.

The following code shows the Track control's definition. Notice that it's named PART_Track. That lets the ScrollBar know that this is its track, so it can arrange the Track's component controls appropriately.

                                                

The Track contains two more RepeatButtons (with appropriate Command attributes) and a Thumb.

The RepeatButtons have Background properties set to brushes defined in the template's Resources section. Those brushes give the RepeatButtons the shaded appearance you can see in Figure 7.

The template's triggers change properties depending on whether the control is oriented vertically or horizontally. The triggers position the controls in the proper Grid rows and columns, and set their Command attributes (for example, the small down button's command is either LineLeftCommand or LineUpCommand). They also set the Track's RepeatButtons' Background properties to horizontal or vertical gradient brushes.

The following code shows part of the template's Triggers section.

                                            ... Position the other controls ...                                ... Similar code for vertical orientation ...    

This template produces a distinctive look, but it's still not completely customized. The RepeatButtons it uses are still fairly generic. Although these controls display distinctive backgrounds, they have the normal RepeatButton behaviors. In particular, when the mouse floats over one, it changes its appearances to the mouse-over-button look. To fix that and any other remaining behaviors you don't like, you could make a separate template for the RepeatButtons.

Learning About Templates

Building a template for a complicated control such as a ScrollBar takes a lot of work. To get it right, you need to know what parts make up the control, what commands they should invoke, and what special names they need. So how do you learn all of this?

A good starting point is Microsoft's Control Styles and Templates Web page. That page provides links to other pages that describe different kinds of controls and the states they should handle. Those pages also show the controls' default templates (which are generally quite long).

?
Figure 8. Template Tattletale: The ShowTemplate example program makes a control display its template.

You can also interrogate a control to make it give up its secrets. The ShowTemplate example program shown in Figure 8 makes a control (a Slider in this case) display its template.

The following code shows how the ShowTemplate program works.

XmlWriterSettings writer_settings = new XmlWriterSettings();writer_settings.Indent = true;writer_settings.IndentChars = "    ";writer_settings.NewLineOnAttributes = true;StringBuilder sb = new StringBuilder();XmlWriter xml_writer = XmlWriter.Create(sb, writer_settings);XamlWriter.Save(Target.Template, xml_writer);txtResult.Text = sb.ToString();

When you click its button, the program creates an XmlWriterSettings object and sets its properties to produce a nicely formatted output. It creates a StringBuilder that uses the settings and attaches it to an XmlWriter. The code uses the XamlWriter class to save the control's template into the XmlWriter and displays the result.

To display the templates for other controls, simply replace the Slider with another control, name it Target, and run the program.

Download the examples and give them a try. Then try making some changes to see what you can accomplish. Handling all of the myriad details needed to build a really robust template can be challenging but the results can be spectacular.

devx-admin

Share the Post: