Arguably the most celebrated feature in WPF is the ability to give any user interface element a radically different look without having to give up all of the built-in functionality that it provides. Even with Cascading Style Sheets (CSS), HTML lacks this much power, which is the reason most websites use images to represent buttons rather than “real buttons.” Of course, it’s pretty easy to simulate a button’s behavior with an image in HTML, but what if you want to give a completely different look to a SELECT
element (HTML’s version of ComboBox
)? It’s a lot of work if you want to do more than change simple properties (such as its foreground and background colors).
This chapter explains the four main components of WPF’s restyling support:
Styles—A simple mechanism for separating property values from user interface elements (similar to the relationship between CSS and HTML). Styles are also the foundation for applying the other mechanisms in this chapter.
Templates—Powerful objects that most people are really referring to when they talk about “restyling” in WPF.
Skins—Application-specific collections of styles and/or templates, typically with the ability to be replaced dynamically.
Themes—Visual characteristics of the host operating system, with potential customizations by the end user.
As you’ll see, an important enabler of WPF’s restyling support is the semantics of resources.
A style, represented by the System.Windows.Style
class, is a pretty simple entity. Its main function is to group together property values that could otherwise be set individually. The intent is to then share this group of values among multiple elements.
Take, for example, the three customized Button
s in Figure 14.1. This look is achieved by setting seven properties. Without a Style
, you would need to duplicate these identical assignments on all three Button
s, as shown in Listing 14.1.
<StackPanel Orientation="Horizontal">
<Button FontSize="22" Background="Purple" Foreground="White"
Height="50" Width="50" RenderTransformOrigin=".5,.5">
<Button.RenderTransform>
<RotateTransform Angle="10"/>
</Button.RenderTransform>
1
</Button>
<Button FontSize="22" Background="Purple" Foreground="White"
Height="50" Width="50" RenderTransformOrigin=".5,.5">
<Button.RenderTransform>
<RotateTransform Angle="10"/>
</Button.RenderTransform>
2
</Button>
<Button FontSize="22" Background="Purple" Foreground="White"
Height="50" Width="50" RenderTransformOrigin=".5,.5">
<Button.RenderTransform>
<RotateTransform Angle="10"/>
</Button.RenderTransform>
3
</Button>
</StackPanel>,
But with a Style
, you can add a level of indirection—setting the properties in one place and pointing each Button
to this new element, as shown in Listing 14.2. Style
uses a collection of Setter
s to set the target properties. Creating a Setter
is just a matter of specifying the name of a dependency property (qualified with its class name) and a desired value for it.
Using a Style
is nice for several reasons, such as having only one spot to change if you later have second thoughts about rotating the Button
s or if you want to change their Background
. Defining a Style
as a resource also gives you all the flexibility that the resource mechanism provides. For example, you could define one version of buttonStyle
at the application level but override it with a different Style
(still with a key of buttonStyle
) in an individual Window
’s Resources
collection.
Note that despite its name, there’s nothing inherently visual about a Style
. But it’s typically used for setting properties that affect visuals. Indeed, Style
only enables the setting of dependency properties, which tend to be visual in nature.
Tip
Style
s can even inherit from one another! The following Style
adds bold text to the buttonStyle
defined in Listing 14.2 by using the BasedOn
property:
<Style x:Key="buttonStyleWithBold" BasedOn="{StaticResource buttonStyle}">
<!-- The seven properties set by buttonStyle are inherited -->
<Setter Property="Button.FontWeight" Value="Bold"/>
</Style>
Although you could set an element’s Style
property directly in its XAML definition (using property element syntax), the whole point of using a Style
is to share it among multiple elements, as done in Listing 14.2. Style
supports a few different mechanisms that enable you to control exactly how that sharing occurs.
Although the Style
in Listing 14.2 is shared among three Button
s, with some tweaks it can also be shared among heterogeneous elements. Listing 14.3 accomplishes this by changing each Button.XXX
referenced inside the Style
to Control.XXX
and then applying the new style to many elements. The result is shown in Figure 14.2.
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<Style x:Key="controlStyle">
<Setter Property="Control.FontSize" Value="22"/>
<Setter Property="Control.Background" Value="Purple"/>
<Setter Property="Control.Foreground" Value="White"/>
<Setter Property="Control.Height" Value="50"/>
<Setter Property="Control.Width" Value="50"/>
<Setter Property="Control.RenderTransformOrigin" Value=".5,.5"/>
<Setter Property="Control.RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<Button Style="{StaticResource controlStyle}">1</Button>
<ComboBox Style="{StaticResource controlStyle}">
<ComboBox.Items>2</ComboBox.Items>
</ComboBox>
<Expander Style="{StaticResource controlStyle}" Content="3"/>
<TabControl Style="{StaticResource controlStyle}">
<TabControl.Items>4</TabControl.Items>
</TabControl>
<ToolBar Style="{StaticResource controlStyle}">
<ToolBar.Items>5</ToolBar.Items>
</ToolBar>
<InkCanvas Style="{StaticResource controlStyle}"/>
<TextBox Style="{StaticResource controlStyle}" Text="7"/>
</StackPanel>
You don’t need to worry about a Style
being applied to an element that doesn’t have all the listed dependency properties; the properties that exist are set and the ones that don’t exist are ignored. For example, InkCanvas
doesn’t have Foreground
or FontSize
properties. Yet when the Style
is applied to it in Listing 14.3, all the relevant properties (Background
, Height
, Width
, and so on) are correctly applied. Similarly, adding the following Setter
to the Style
in Listing 14.3 affects the TextBox
but leaves all the other elements looking as they do in Figure 14.2:
<Setter Property="TextBox.TextAlignment" Value="Right"/>
Tip
Any individual element can override aspects of its Style
by directly setting a property to a local value. For example, the Button
in Listing 14.3 could do the following to retain the rotation, size, and so on from controlStyle
yet have a red Background
rather than a purple one:
<Button Style="{StaticResource controlStyle}" Background="Red">1</Button>
This works because of the order of precedence for dependency property values presented in Chapter 3, “WPF Fundamentals.” The local value trumps anything set from a Style
.
Tip
To enable sharing of complex property values even within a Style
, Style
has its own Resources
property. You can leverage this collection to make your Style
self-contained rather than create a potentially brittle dependency to resources defined elsewhere.
If you want to enforce that a Style
can be applied only to a particular type, you can set its TargetType
property accordingly. For example, the following Style
can be applied only to a Button
(or a subclass of Button
):
<Style x:Key="buttonStyle" TargetType="{x:Type Button}">
<Setter Property="Button.FontSize" Value="22"/>
<Setter Property="Button.Background" Value="Purple"/>
<Setter Property="Button.Foreground" Value="White"/>
<Setter Property="Button.Height" Value="50"/>
<Setter Property="Button.Width" Value="50"/>
<Setter Property="Button.RenderTransformOrigin" Value=".5,.5"/>
<Setter Property="Button.RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
</Style>
Any attempt to apply this Style
to a non-Button
generates a compile-time error. Therefore, TargetType="{x:Type Control}"
could be applied to the Style
in Listing 14.3, and it would still work with all the elements except InkCanvas
.
In addition, when you apply a TargetType
to a Style
, you no longer need to prefix the property names inside Setter
s with the type name. So, the previous XAML snippet could be rewritten as follows and have exactly the same meaning:
<Style x:Key="buttonStyle" TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="22"/>
<Setter Property="Background" Value="Purple"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Width" Value="50"/>
<Setter Property="RenderTransformOrigin" Value=".5,.5"/>
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
</Style>
Applying a TargetType
to a Style
gives you another feature as well. If you omit its Key
, the Style
gets implicitly applied to all elements of that target type within the same scope. This is typically called a typed style as opposed to a named style, which is the only kind of Style
you’ve seen so far.
The scope of a typed Style
is determined by the location of the Style
resource. For example, it could implicitly apply to all relevant elements in a Window
if it’s a member of Window.Resources
. Or, it could apply to an entire application if you define it as an application-level resource, as follows:
In such an application, all Button
s get this style by default. But each Button
can still override its appearance by explicitly setting a different Style
or explicitly setting individual properties. Any Button
can restore its default Style
by setting its Style
property to null
.
Warning: TargetType must match exactly for a typed style to be applied!
With a named style, it’s okay for the target element to be a subclass of the TargetType
. But a typed style typically gets applied only to elements whose type matches exactly. This is done to prevent surprises. For example, maybe you’ve created a Style
for all ToggleButton
s in your application but you don’t want it applied to any CheckBox
es. (CheckBox
is a subclass of ToggleButton
.) This behavior is controlled by each element (by its selection of a default style key, covered in the “Themes” section at the end of this chapter). Therefore, it’s possible to write a custom element that inherits the typed style from its base class.
Tip
Style
s can be applied in multiple places. For example, all FrameworkElement
s and FrameworkContentElement
s have a FocusVisualStyle
property in addition to their Style
property. A Style
assigned to FocusVisualStyle
is active only when the element has keyboard focus, and it is very handy for overriding the look of the standard dotted rectangle that indicates keyboard focus (which can look weird when a control has been drastically altered).
Some controls have their own additional places to plug in a Style
. For example, ItemsControl
has an ItemContainerStyle
property that applies to each item’s container (such as ListBoxItem
or ComboBoxItem
). Other controls, such as ToolBar
, expose ResourceKey
properties that represent the keys for several Style
s used internally, such as ButtonStyleKey
and TextBoxStyleKey
. Although these XXXStyleKey
properties are read-only, you can leverage these keys to define your own Style
s that override the default ones. Here’s an example:
<Application ...>
<Application.Resources>
<Style x:Key="{x:Static ToolBar.ButtonStyleKey}" TargetType="{x:Type Button}">
...
</Style>
</Application.Resources>
</Application>
One reason ToolBar
uses ResourceKey
properties instead of Style
properties is that dependency properties do not support dynamic resource references as their default value. ItemsControl
can get away with giving ItemContainerStyle
a default value of null
because the default style for its item container is always the same. ToolBar
, however, requires different default styles, depending on the theme.
Triggers, first introduced in Chapter 3, have a collection of Setter
s just like Style
(and/or collections of TriggerAction
s). But whereas a Style
applies its values unconditionally, a trigger performs its work based on one or more conditions.
Recall that there are three types of triggers:
Property triggers—Invoked when the value of a dependency property changes
Data triggers—Invoked when the value of a plain .NET property changes
Event triggers—Invoked when a routed event is raised
FrameworkElement
, Style
, DataTemplate
, and ControlTemplate
(covered in the next section) all have a Triggers
collection, but whereas Style
and the template classes accept all three types, FrameworkElement
accepts only event triggers. Fortunately, Style
happens to be the logical place to put triggers even if you had a choice because of the ease in sharing them and their direct tie to the visual aspects of elements.
So, let’s look at a few examples of property triggers and data triggers inside styles. We’ll save event triggers for Chapter 17, “Animation.”
A property trigger (represented by the Trigger
class) executes a collection of Setter
s when a specified property has a specified value. And when the property no longer has this value, the property trigger “undoes” the Setter
s.
For example, the following update to buttonStyle
makes the rotation happen only when the mouse pointer is hovering over the Button
and sets the Foreground
to Black
rather than White
:
<Style x:Key="buttonStyle" TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</Style.Triggers>
<Setter Property="FontSize" Value="22"/>
<Setter Property="Background" Value="Purple"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Width" Value="50"/>
<Setter Property="RenderTransformOrigin" Value=".5,.5"/>
</Style>
Figure 14.3 shows the result of applying this Style
to a Button
. The trigger sets the Foreground
to Black
so the content is more readable against the light blue background that Button
s get by default while the mouse is hovering. This “hover background” is not baked into Button
but rather comes from a trigger on Button
’s theme style (discussed in the “Themes” section at the end of the chapter). It can be overridden by explicitly setting the Background
property inside the trigger we just created.
A more complicated example of a property trigger is one that works with data-binding validation rules. In the preceding chapter, we created a JpgValidationRule
class attached to a data-bound TextBox
and ensured that the user only typed in a valid .jpg
filename. To declaratively and visually show the results of a failed validation, you can create a property trigger based on the Validation.HasError
attached property, as follows:
<Style x:Key="textBoxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
In this property trigger, data binding is used to provide an appropriate message inside the ToolTip
. Notice the handy use of RelativeSource
to grab the Validation.Errors
attached property from whatever element this Style
ends up being applied to.
Applying this Style
to a TextBox
such as the following produces the result shown in Figure 14.4 when a validation error is raised:
<TextBox Style="{StaticResource textBoxStyle}">
<TextBox.Text>
<Binding ...>
<Binding.ValidationRules>
<local:JpgValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Listing 14.4 demonstrates another use of property triggers in a style to take advantage of ItemsControl
’s AlternationIndex
property, introduced in Chapter 10, “Items Controls.” It also demonstrates the use of ItemsControl
’s ItemContainerStyle
property to style the sometimes-implicit item containers. (Recall that if you add arbitrary objects or elements to a ListBox
, for example, they get wrapped in ListBoxItem
containers.) Figure 14.5 shows the result.
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Horizontal">
<StackPanel.Resources>
<Style x:Key="AlternatingRowStyle" TargetType="{x:Type Control}">
<Setter Property="Background" Value="Green"/>
<Setter Property="Foreground" Value="White"/>
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<ListBox AlternationCount="2" Margin="10" Width="200"
ItemContainerStyle="{StaticResource AlternatingRowStyle}">
<ListBoxItem>Item 1</ListBoxItem>
<ListBoxItem>Item 2</ListBoxItem>
<ListBoxItem>Item 3</ListBoxItem>
<ListBoxItem>Item 4</ListBoxItem>
<ListBoxItem>Item 5</ListBoxItem>
</ListBox>
<TreeView AlternationCount="2" Margin="10" Width="200"
ItemContainerStyle="{StaticResource AlternatingRowStyle}">
<TreeViewItem Header="Root 1" AlternationCount="2"
ItemContainerStyle="{StaticResource AlternatingRowStyle}">
<TreeViewItem Header="Subitem 1"/>
<TreeViewItem Header="Subitem 2"/>
<TreeViewItem Header="Subitem 3"/>
</TreeViewItem>
<TreeViewItem Header="Root 2" AlternationCount="2"
ItemContainerStyle="{StaticResource AlternatingRowStyle}">
<TreeViewItem Header="Subitem 1"/>
<TreeViewItem Header="Subitem 2"/>
<TreeViewItem Header="Subitem 3"/>
</TreeViewItem>
</TreeView>
</StackPanel>
This style gives items a white foreground on a green background by default, but when its AlternationIndex
attached property is 1
, the triggers change the foreground to black and the background to white. Therefore, this style is meant to be applied as an item container style to an items control with AlternationCount
set to 2
(giving the sequence 0, 1, 0, 1, ...).
Notice that in order to be used for both ListBoxItem
s and TreeViewItem
s, the style generically applies to Control
(the most derived base class common to both) and the attached property is referenced as ItemsControl.AlternationIndex
instead of something more specific (such as ListBox.AlternationIndex
). For this to work as shown in Figure 14.5, every TreeViewItem
with children must also have AlternationCount
set to 2
and the ItemContainerStyle
set to the appropriate style. That’s because TreeViewItem
(an items control itself) doesn’t inherit those settings from its parent.
Data triggers are just like property triggers, except that they can be triggered by any .NET property rather than just dependency properties. (The Setter
s inside a data trigger are still restricted to setting dependency properties, however.)
To use a data trigger, you add a DataTrigger
object to the Triggers
collection and specify the property/value pair. To support plain .NET properties, you specify the relevant property with a Binding
rather than a simple property name.
The following TextBox
has a Style
that triggers the setting of IsEnabled
, based on the value of its Text
property, which is not a dependency property. When Text
is the string "disabled"
, IsEnabled
is set to false
(which is admittedly an unusual application of a data trigger):
<StackPanel Width="200">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Self}, Path=Text}"
Value="disabled">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Self}, Path=Text}"/>
</Style>
</StackPanel.Resources>
<TextBox Margin="3"/>
</StackPanel>
The same Binding
to the Text
property happens to be used outside the trigger, which sets the TextBox
’s Background
to whatever the Text
value is (thanks to the string-to-Brush
type converter). If Text
isn’t set to a valid color name, Background
reverts to its default color because of the way errors in data binding are handled. (The addition of data binding to a normal Setter
such as this can make it seem like it’s part of a trigger when it’s really not.) Figure 14.6 shows this TextBox
with a few different Text
values.
The logic expressed with the previous triggers has been in the form “when property=value, set the following properties.” But more powerful options exist:
Multiple triggers can be applied to the same element (to get a logical OR).
Multiple properties can be evaluated for the same trigger (to get a logical AND).
Because Style.Triggers
can contain multiple triggers, you can create more than one with exactly the same Setter
s to express a logical OR relationship:
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black"/>
</Trigger>
</Style.Triggers>
This means, “if IsMouseOver
is true
or if IsFocused
is true
, apply the rotation and black foreground.”
To express a logical AND relationship, you can use a variation of Trigger
called MultiTrigger
or a variation of DataTrigger
called MultiDataTrigger
. MultiTrigger
and MultiDataTrigger
have a collection of Condition
s that contain the information you would normally put directly inside a Trigger
or DataTrigger
. Therefore, you can use MultiTrigger
as follows:
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsFocused" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black"/>
</MultiTrigger>
</Style.Triggers>
This means, “if IsMouseOver
is true
and if IsFocused
is true
, apply the rotation and black foreground.” MultiDataTrigger
works the same way as MultiTrigger
but with support for plain .NET properties.
Tip
If you want to add even more complex event-driven behavior to a Style
, you can make use of an EventSetter
(which shares a common base class with Setter
) to attach an event handler to any element that makes use of the Style
. EventSetter
s can be added to a Style
just like Setter
s:
<Style x:Key="buttonStyle" TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="22"/>
<EventSetter Event="MouseEnter" Handler="Button_MouseEnter"/>
</Style>
Although this requires procedural code to handle the event, it is, nevertheless, a handy way to share a common handler among many elements without resorting to copying and pasting.
Control
s have many properties you can use to customize their look: Button
has configurable Background
and Foreground Brush
es (which can even be fancy gradients), TabControl
’s tabs can be relocated by setting the TabStripPlacement
property, and so on. But you can do only so much with such properties.
A template, on the other hand, allows you to completely replace an element’s visual tree with anything you can dream up, while keeping all of its functionality intact. And templates (like many other things in WPF) aren’t just some add-on mechanism for third parties; the default visuals for every Control
in WPF are defined in templates (and customized for each Windows theme). The source code for every control is completely separated from its default visual tree representations (or “visual source code”).
Templates and the desire to separate visuals from logic are also the reasons that WPF’s controls don’t expose more simple properties for tweaking their look. For example, it would be nice to change the color of the Expander
’s arrow back in Figure 14.2, as the gray color doesn’t show up nicely against the purple background. This relatively simple change can be accomplished only by defining a new template for Expander
, however. Expander
has no ArrowBrush
or ArrowColor
property because an Expander
with a custom template might not even have an arrow!
There are a few different kinds of templates. What has been described so far is the focus of this section: control templates. Control templates are represented by the ControlTemplate
class that derives from the abstract FrameworkTemplate
class. The other FrameworkTemplate
-derived classes are covered in previous chapters: DataTemplate
(described in the preceding chapter) and ItemsPanelTemplate
(described in Chapter 10). Data templates customize the look of any .NET object, which is especially important for non-UIElement
s, whose default template is simply a TextBlock
containing a string returned by its ToString
method. ItemsPanelTemplate
s can be assigned to an ItemsControl
’s ItemsPanel
as an easy way to alter its layout.
Slick custom visuals undoubtedly involve using 2D (or 3D!) graphics, animation, or other rich media, covered in the next part of the book. This chapter sticks to some simple 2D drawings.
The important piece of the ControlTemplate
class is its VisualTree
content property, which contains the tree of elements that define the desired look. After you define a ControlTemplate
(undoubtedly in XAML), you can attach it to any Control
or Page
by setting its Template
property. Listing 14.5 defines a simple yet slick control template as a resource and then applies it to a single Button
. Figure 14.7 shows the result.
<Grid>
<Grid.Resources>
<ControlTemplate x:Key="buttonTemplate">
<Grid>
<Ellipse Width="100" Height="100">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Blue"/>
<GradientStop Offset="1" Color="Red"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="80" Height="80">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="White"/>
<GradientStop Offset="1" Color="Transparent"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
</ControlTemplate>
</Grid.Resources>
<Button Template="{StaticResource buttonTemplate}">OK</Button>
</Grid>
To get this look, the template’s visual tree uses two circles (created with Ellipse
elements) placed inside a single-cell Grid
. Despite the custom look, the resultant Button
still has a Click
event, an IsDefault
property, and all the other functionality you’d expect. After all, it is still an instance of the Button
class!
Tip
In Listing 14.5, the Button
is considered the templated parent of the elements in the control template’s visual tree. FrameworkElement
and FrameworkContentElement
both have a TemplatedParent
property that represents this relationship.
As with Style
s, Template
s can contain all types of triggers in a Triggers
collection. Listing 14.6 adds triggers to the preceding ControlTemplate
to visually respond to a mouse hover and click. A trigger on Button.IsMouseOver
makes the Button
orange, and a trigger on Button.IsPressed
shrinks the button with a ScaleTransform
to give it a “pushed in” look. Figure 14.8 shows the result.
<Grid>
<Grid.Resources>
<ControlTemplate x:Key="buttonTemplate">
<Grid>
<Ellipse x:Name="outerCircle" Width="100" Height="100">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Blue"/>
<GradientStop Offset="1" Color="Red"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="80" Height="80">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="White"/>
<GradientStop Offset="1" Color="Transparent"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsMouseOver" Value="True">
<Setter TargetName="outerCircle" Property="Fill" Value="Orange"/>
</Trigger>
<Trigger Property="Button.IsPressed" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX=".9" ScaleY=".9"/>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value=".5,.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Grid.Resources>
<Button Template="{StaticResource buttonTemplate}">OK</Button>
</Grid>
Notice that the larger circle in the template’s visual tree is given the name outerCircle
. This is done so it can be referenced by a trigger. The first trigger uses Setter
’s TargetName
property (which makes sense only inside a template) to make its setting of Fill
to Orange
apply to only the outerCircle
element. Omitting the TargetName
would cause an error in this case because the trigger would apply to the entire Button
, which doesn’t have a Fill
property. The capability to target subelements of a template with triggers is essential for sophisticated templates.
Tip
Analogous to Setter
’s TargetName
property, Trigger
(as well as EventTrigger
and Condition
) has a SourceName
property that enables you to react to a change on a specific subelement of a template rather than the entire template. For example, you could have triggers for IsMouseOver
on individual subelements to get a richly customized hover effect.
The second trigger doesn’t need to target a subelement, however. The ScaleTransform
(applied as a RenderTransform
) applies to the entire Button
, as does the setting of RenderTransformOrigin
to center the scaling. It’s hard to convey in Figure 14.8, but a slight centered shrinkage (10% in this case) is a very effective visual effect for a Button
press.
As with Style
, ControlTemplate
has a TargetType
property that can restrict where the template can be applied. It also enables you to remove the class name qualifications on any property references inside a template (such as the values of Trigger.Property
and Setter.Property
). Therefore, the template from Listing 14.6 could be rewritten as follows:
<ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}">
<Grid>
...
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="outerCircle" Property="Fill" Value="Orange"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX=".9" ScaleY=".9"/>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value=".5,.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Note that the Setter
s in this example already had unqualified Property
values in previous listings. That’s because the properties are either qualified by the use of TargetName
or are common to all Control
s. (Without an explicit TargetType
, the target type is implicitly Control
.)
Unlike with a Style
, the use of TargetType
does not enable you to remove the template’s x:Key
(when used in a dictionary). There is no such thing as a default control template; you have to set the template inside a typed Style
to get such behavior.
There’s a bit of a problem with the templates we’ve created so far. Any Button
s they’re applied to look exactly the same, no matter what the values of its properties are. For example, in the last two listings, the Button
has "OK"
as content, but it never gets displayed. If you’re creating a control template that’s meant to be broadly reusable, you need to do some work to respect various properties of the target Control
.
The key to inserting property values from the target element inside a control template is data binding. Fortunately, a class called TemplateBindingExtension
makes this easy.
TemplateBindingExtension
is a markup extension that is similar to Binding
, but simpler, more lightweight, and customized for templates. It’s often referred to as simply TemplateBinding
because of the tendency to omit the Extension
suffix when used in XAML.
The data source for TemplateBinding
is always the target element, and the path is any of its dependency properties, selected by setting TemplateBinding
’s Property
property. Therefore, you could add to the control template in Listing 14.6 a TextBlock
that contains the target Button
’s Content
, as follows:
<TextBlock Text="{TemplateBinding Property=Button.Content}"/>
Or, because TemplateBinding
has a constructor that accepts a dependency property, you could simply write this:
<TextBlock Text="{TemplateBinding Button.Content}"/>
If TargetType
is used to restrict the template’s use for Button
s (or other ContentControl
s), you could simplify this even further, like so:
<TextBlock Text="{TemplateBinding Content}"/>
Of course, a Button
can contain nontext Content
, so using a TextBlock
to display it creates an artificial limitation. To ensure that all types of Content
get displayed properly in the template, you can use a generic ContentControl
instead of a TextBlock
. Listing 14.7 does just that. The ContentControl
is given a Margin
and wrapped in a Viewbox
so it’s displayed at a reasonable size relative to the rest of the Button
.
<ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}">
<Grid>
<Ellipse x:Name="outerCircle" Width="100" Height="100">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Blue"/>
<GradientStop Offset="1" Color="Red"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="80" Height="80">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="White"/>
<GradientStop Offset="1" Color="Transparent"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Viewbox>
<ContentControl Margin="20" Content="{TemplateBinding Content}"/>
</Viewbox>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="outerCircle" Property="Fill" Value="Orange"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX=".9" ScaleY=".9"/>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value=".5,.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Figure 14.9 shows what two Button
s look like with this new control template applied. One Button
has simple "OK"
text content, and the other has an Image
. In both cases, the content is reflected in the new visuals as expected.
Tip
Rather than use a ContentControl
inside a control template, you should use the lighter-weight ContentPresenter
element. ContentPresenter
displays content just like ContentControl
, but it was designed specifically for use in control templates. ContentPresenter
is a primitive building block, whereas ContentControl
is a full-blown control with its own control template (that contains a ContentPresenter
)!
In Listing 14.7, you can replace this:
<ContentControl Margin="20" Content="{TemplateBinding Content}"/>
with this:
<ContentPresenter Margin="20" Content="{TemplateBinding Content}"/>
ContentPresenter
even has a built-in shortcut; if you omit setting its Content
to {TemplateBinding Content}
, it implicitly assumes that’s what you want. So, you can replace the preceding line of code with the following:
<ContentPresenter Margin="20"/>
This works only when the control template is given an explicit TargetType
of ContentControl
or a ContentControl
-derived class (such as Button
).
The remaining templates in this chapter use ContentPresenter
instead of ContentControl
, as that’s what real-world templates use.
Warning: TemplateBinding works only inside a template’s visual tree and doesn’t work with properties on Freezables!
TemplateBinding
doesn’t work outside a template or outside its VisualTree
property, so you can’t even use TemplateBinding
inside a template’s trigger. Furthermore, TemplateBinding
doesn’t work when applied to a Freezable
(for mostly artificial reasons). For example, attempting to bind the Color
property of any explicit Brush
fails.
However, TemplateBinding
is just a less-powerful but convenient shortcut for using a regular Binding
. You can get the same effect by using a regular Binding
with a RelativeSource
equal to {RelativeSource TemplatedParent}
and a Path
equal to the dependency property whose value you want to retrieve. Such a Binding
works in the cases mentioned where TemplateBinding
does not.
No matter what type of control you’re creating a control template for, there are undoubtedly other properties on the target control that should be honored if you want the template to be reusable: Height
and Width
, perhaps Background
, Padding
, and so on. Some properties (such as Foreground
, FontSize
, FontWeight
, and so on) might automatically inherit their desired values thanks to property value inheritance in the visual tree, but other properties need explicit attention.
Listing 14.8 is an update to Listing 14.7 that respects the Background
, Padding
, and Content
properties of the target Button
. It also implicitly respects the size of the target element by removing the explicit Height
and Width
settings and letting the layout system do its job. Listing 14.8 uses a ContentPresenter
rather than a ContentControl
, although both produce the same result.
<ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}">
<Grid>
<Ellipse x:Name="outerCircle">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0"
Color="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=Background.Color}"/>
<GradientStop Offset="1" Color="Red"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse RenderTransformOrigin=".5,.5">
<Ellipse.RenderTransform>
<ScaleTransform ScaleX=".8" ScaleY=".8"/>
</Ellipse.RenderTransform>
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="White"/>
<GradientStop Offset="1" Color="Transparent"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Viewbox>
<ContentPresenter Margin="{TemplateBinding Padding}"/>
</Viewbox>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="outerCircle" Property="Fill" Value="Orange"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX=".9" ScaleY=".9"/>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value=".5,.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
The target Button
’s Padding
is now used as the ContentPresenter
’s Margin
. It’s common to use the element’s Padding
in a template as the Margin
of an inner element. After all, that’s basically the definition of Padding
!
In addition, a few nonintuitive changes have been made to the template’s visual tree to accommodate an externally specified size and Background
. We could have simply used {TemplateBinding Background}
as the Fill
for outerCircle
, giving each Button
the flexibility to specify a solid color, a gradient, and so on. But perhaps the “red glow” at the bottom is a characteristic that we’d like to keep consistent wherever the template is used. In other words, we want to replace only the blue part of the gradient with the externally specified Background
. However, GradientStop.Color
can’t be directly set to {TemplateBinding Background}
because Color
is of type Color
, whereas Background
is of type Brush
(and because GradientStop
derives from Freezable
)! Therefore, the listing uses a normal Binding
instead, which supports referencing the Color
subproperty. (Note that this Binding
works only when Background
is set to a SolidColorBrush
because other Brush
es don’t have a Color
property.)
Both Ellipse
s (or the parent Grid
) could have been given an explicit Height
and Width
matching those of the target Button
by binding to its ActualHeight
and ActualWidth
properties. Instead, these values are omitted altogether because the root element is implicitly given the templated parent’s size anyway! This means that an individual target Button
has the power to make itself look like an ellipse by specifying different values for Width
and Height
. If we wanted to preserve the perfect circular look, we could wrap the entire visual tree in a Viewbox
.
The final trick used by Listing 14.8 is the ScaleTransform
on the inner circle to make it 80% of the size of the outer circle. In previous listings, this transform is unnecessary because both the outer and inner circles have a hard-coded size. But with a dynamic size, ScaleTransform
enables us to effectively perform a little math on the size. (If we wanted a fixed-size difference between the circles, a simple Margin
would do the trick.)
Figure 14.10 shows the rendered result for the following Button
s that make use of this new control template:
<StackPanel Orientation="Horizontal">
<Button Template="{StaticResource buttonTemplate}"
Height="100" Width="100" FontSize="80" Background="Black"
Padding="20" Margin="5">1</Button>
<Button Template="{StaticResource buttonTemplate}"
Height="150" Width="250" FontSize="80" Background="Yellow"
Padding="20" Margin="5">2</Button>
<Button Template="{StaticResource buttonTemplate}"
Height="200" Width="200" FontSize="80" Background="White"
Padding="20" Margin="5">3</Button>
</StackPanel>
Each Button
in Figure 14.10 has values for Background
, Padding
, and Content
that are explicitly used by the control template. Their values for Height
and Width
are implicitly respected by the template, and their FontSize
setting is implicitly picked up by the template’s ContentPresenter
. The size of the font isn’t directly reflected in the rendered output because the template wraps the ContentPresenter
inside a Viewbox
to keep it within the bounds of the outer circle. The Margin
specified on each Button
is not used by the template, but it still affects the StackPanel
layout as usual, giving a little bit of space between each Button
.
Sometimes, you might want to parameterize some aspect of a control template, despite there being no corresponding property on the target control. For example, the template in Listing 14.8 has a hard-coded orange Brush
representing the hover state. What can you do to allow individual Button
s to customize this Brush
? There’s no corresponding property already on Button
to be set!
One option is to define a custom control, using the techniques described in Chapter 20, “User Controls and Custom Controls.” It wouldn’t be too much work to write a new class that derives from Button
and adds a single HoverBrush
property. But that’s a bit heavyweight for such a simple task. Another option would be to define several control templates that each uses a different hover Brush
. But that would be reasonable only if the set of desired Brush
es were small and known. Yet another option would be to define an appropriate attached property somewhere, perhaps on a utility class that already exists.
Instead, what most people resort to is a devious little hack known as hijacking a dependency property. This involves looking at the target control for any dependency properties of the desired type to see if you can leverage them in an unintended way. For example, all Control
s have three properties of type Brush
: Background
, Foreground
, and BorderBrush
. Because Background
and Foreground
already play important roles in Listing 14.8, neither one would look very good as a hover Brush
. (There would also be no way to set the hover Brush
independently of the other two.) But BorderBrush
is a different story. It’s completely unused by the template in Listing 14.8, so why not use that?
There really is no reason not to use it, other than the fact that it makes the usage of the template confusing and less readable. Nevertheless, here’s how you could update the IsMouseOver
trigger from Listing 14.8 to hijack BorderBrush
:
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="outerCircle" Property="Fill"
Value="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=BorderBrush}"/>
</Trigger>
A Binding
must be used in this case rather than a TemplateBinding
because the Trigger
is outside the visual tree.
If the target control doesn’t have an appropriate property, you might even be able to hijack an attached property from a totally unrelated element! When choosing a property, be sure to pay attention to its metadata, such as its default value and what gets triggered when its value changes (such as invalidating layout).
If this hack leaves a bad taste in your mouth, then by all means use an alternative approach. This hack is definitely not recommended by the WPF team! But it’s a useful trick to know about if you’re looking for a quick fix.
When creating a control template for Button
s, visually reacting to hover and pressed states with corresponding triggers is a nice touch, but it’s purely optional. Imagine using the template from Listing 14.8 on a CheckBox
or ToggleButton
, however. (This can be done simply by changing the TargetType
.) Because the template doesn’t show different visuals for the Checked
versus Unchecked
versus Indeterminate
states, it’s a pretty lousy template for these controls!
In fact, the template in Listing 14.8 is still incomplete, even for a Button
! The fact that it doesn’t show any different visuals when IsEnabled
is false
or IsDefaulted
is true
makes it a pretty lousy template!
Therefore, you should consider all the visual states a control should expose when designing a control template for it. This might take the form of triggers on the appropriate properties or events, or it could just be a matter of binding them appropriately.
For example, to be useful, a control template for ProgressBar
must show the current value. Listing 14.9 contains a control template (defined as an application-level resource) for ProgressBar
that makes it look like a pie chart. The most important aspect of the template—filling up the pie according to the current Value
—is accomplished by binding to the templated parent and using value converters to do the necessary trigonometry. In addition to this, triggers on IsEnabled
and IsIndeterminate
alter the visuals for these states. Figures 14.11 and 14.12 show the results for ProgressBar
s such as the following:
<ProgressBar Foreground="{StaticResource foregroundBrush}" Width="100"
Height="100" Value="10" Template="{StaticResource progressPie}"/>
The foregroundBrush
resource is defined as a simple green gradient:
<LinearGradientBrush x:Key="foregroundBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="LightGreen"/>
<GradientStop Offset="1" Color="DarkGreen"/>
</LinearGradientBrush>
<Application x:Class="WindowsApplication1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WindowsApplication1"
StartupUri="Window1.xaml">
<Application.Resources>
<ControlTemplate x:Key="progressPie" TargetType="{x:Type ProgressBar}">
<!-- Resources -->
<ControlTemplate.Resources>
<local:ValueMinMaxToPointConverter x:Key="converter1"/>
<local:ValueMinMaxToIsLargeArcConverter x:Key="converter2"/>
</ControlTemplate.Resources>
<!-- Visual Tree -->
<Viewbox>
<Grid Width="20" Height="20">
<Ellipse x:Name="background" Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}"
Width="20" Height="20" Fill="{TemplateBinding Background}"/>
<Path x:Name="pie" Fill="{TemplateBinding Foreground}">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="10,10" IsClosed="True">
<LineSegment Point="10,0"/>
<ArcSegment Size="10,10" SweepDirection="Clockwise">
<ArcSegment.Point>
<MultiBinding Converter="{StaticResource converter1}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Value"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Minimum"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Maximum"/>
</MultiBinding>
</ArcSegment.Point>
<ArcSegment.IsLargeArc>
<MultiBinding Converter="{StaticResource converter2}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Value"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Minimum"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Maximum"/>
</MultiBinding>
</ArcSegment.IsLargeArc>
</ArcSegment>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</Grid>
</Viewbox>
<!-- Triggers -->
<ControlTemplate.Triggers>
<Trigger Property="IsIndeterminate" Value="True">
<Setter TargetName="pie" Property="Visibility" Value="Hidden"/>
<Setter TargetName="background" Property="Fill">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="Yellow"/>
<GradientStop Offset="1" Color="Brown"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="pie" Property="Fill">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="Gray"/>
<GradientStop Offset="1" Color="White"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Application.Resources>
</Application>
The root of the visual tree is a Viewbox
, so the 20x20 single-cell Grid
can scale appropriately. The background circle (which has a radius of 10 prior to scaling) is given the templated parent’s Background
, BorderBrush
, and BorderThickness
. The “pie” is a Path
(an element covered in the next chapter) that is given the templated parent’s Foreground
and relies on two MultiBinding
s with value converters defined in Listing 14.10 to get the right shape. MultiBinding
is used rather than a simple TemplateBinding
or Binding
so the pie gets updated when any of ProgressBar
’s three relevant properties change: Value
, Minimum
, and Maximum
. The two triggers create the results in Figure 14.12 by filling an element with a hard-coded Brush
(and in the case of IsIndeterminate
, hiding the pie). A more appropriate effect for IsIndeterminate
is probably an animation that spins the pie around, but at least there’s some visual distinction as is. Note that not all of ProgressBar
’s properties are honored by this template. For example, Orientation
is unused, but there’s not a great way to honor it, considering the visual representation.
Tip
Notice that Listing 14.9 defines value converters in ControlTemplate
’s Resources
collection. Like Style
, all FrameworkTemplate
s have their own Resources
collection. This collection can be used to keep templates self-contained.
public class ValueMinMaxToIsLargeArcConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
CultureInfo culture)
{
double value = (double)values[0];
double minimum = (double)values[1];
double maximum = (double)values[2];
// Only return true if the value is 50% of the range or greater
return ((value * 2) >= (maximum - minimum));
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
public class ValueMinMaxToPointConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
CultureInfo culture)
{
double value = (double)values[0];
double minimum = (double)values[1];
double maximum = (double)values[2];
// Convert the value to one between 0 and 360
double current = (value / (maximum - minimum)) * 360;
// Adjust the finished state so the ArcSegment gets drawn as a whole circle
if (current == 360)
current = 359.999;
// Shift by 90 degrees so 0 starts at the top of the circle
current = current - 90;
// Convert the angle to radians
current = current * 0.017453292519943295;
// Calculate the circle's point
double x = 10 + 10 * Math.Cos(current);
double y = 10 + 10 * Math.Sin(current);
return new Point(x, y);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
The first value converter is pretty simple. ArcSegment
’s IsLargeArc
property (from Listing 14.9) must be true
when the pie is more than half full and false
otherwise. Therefore, ValueMinMaxToIsLargeArcConverter
does this simple calculation based on the three values from the target ProgressBar
and returns the appropriate Boolean value.
The second value converter is much more complicated. Its job is to return the proper Point
along the circle’s circumference, according to the current values. To do this, it converts the ProgressBar
’s Value
to an angle (in degrees), makes some adjustments, and then converts it to radians. With this angle, a little trigonometry is used to get the point, based on the fixed radius of 10 and the center point of (10,10).
For a control template designer, knowing all the visual states that need to be respected can be difficult. Each control has a large number of properties, and it might not always be clear which ones are visually important or how to manage all the possible states with triggers. Fortunately, a feature known as the Visual State Manager (VSM) makes this task easier.
The VSM support includes a collection of types and members that make it easy for control authors to formally specify parts and states for their controls, taking the guesswork out of writing control templates that support them all. Importantly, it enables design tools to provide a decent experience for creating complex templates. Blend takes great advantage of such parts and states.
The “parts” portion of the parts and states model has been in WPF from its first release. The idea is that controls can look for specially named elements in the visual tree of the template being applied to them, so that they can apply some logic to those visual pieces. Consider these examples:
If a ProgressBar
control template has elements named PART_Indicator
and PART_Track
, the control ensures that the Width
(or Height
, based on ProgressBar
’s Orientation
) of PART_Indicator
remains the correct percentage of the Width
(or Height
) of PART_Track
, based on ProgressBar
’s Value
, Minimum
, and Maximum
properties. For the pie chart template from Listing 14.9, this behavior is clearly undesirable. But for a template that more closely matches the standard ProgressBar
look, taking advantage of this support greatly simplifies it (and removes the need for procedural code to do the math).
If a ComboBox
control template has a Popup
named PART_Popup
, ComboBox
’s DropDownClosed
event is automatically raised when the Popup
is closed. If it has a TextBox
named PART_EditableTextBox
, it integrates automatically with ComboBox
’s ability to update the selection as the user types.
Controls such as TextBox
and PasswordBox
have most of their functionality tied to an element in the control template called PART_ContentHost
. If you don’t have an element with this name in your control template, you’ll have to reimplement the entire editable surface!
In some cases, the named part can be any FrameworkElement
, but in other cases the type of the named part must be something more specific in order to be respected. Table 14.1 reveals the named parts leveraged by many of WPF’s built-in controls. Derived classes that automatically inherit the named part logic are not listed, such as TextBox
and PasswordBox
, which get their PART_ContentHost
logic from TextBoxBase
.
Therefore, claims of WPF’s controls being “lookless” and having an implementation that’s completely independent from their visuals (such as my own claim earlier in this chapter) aren’t entirely true! However, these “secret handshakes” with magically named parts are optional. This is important, as it means you still have the flexibility to radically change a control’s visuals, such as with the pie chart template for ProgressBar
.
To give design tools the ability to discover every named part available for use, controls document them with a TemplatePartAttribute
on their class—one for each named part—that reveals the name and expected type of the part. WPF also has a convention of using parts named PART_XXX
(a convention broken by one of CalendarItem
’s parts, seen in Table 14.1).
On the one hand, named parts are an implementation detail that you don’t need to know about. On the other hand, you can sometimes create control templates with much less effort by taking advantage of this built-in logic!
The “states” portion of the parts and states model was introduced to WPF in version 4.0. As with control parts, controls can have internal logic to transition to named states that they define (by calling a static VisualStateManager.GoToState
method). Control templates can then use a few elements to organize visual settings specific to each state rather than use triggers. Writing control templates that take advantage of states is optional, but this is the recommended approach. Such templates are not only better supported by tools such as Blend, they are more likely to work for Silverlight controls as well.
The states defined by each control are grouped into mutually exclusive state groups. For example, Button
has four states in a group called CommonStates
—Normal
, MouseOver
, Pressed
, and Disabled
—and two states in a group called FocusStates
—Unfocused
and Focused
. At any time, Button
is in one state from every group, so it is Normal
and Unfocused
by default. This grouping mechanism exists to avoid a long list of states meant to cover every combination of independent properties (such as NormalUnfocused
, NormalFocused
, MouseOverUnfocused
, MouseOverFocused
, and so on).
Table 14.2 lists the groups and states supported by several controls. Notice the explosion of states for DataGridRow
and DataGridRowHeader
; these states really should have been organized into three separate groups. (Someone didn’t get the memo.) States inherited from base classes are not listed; you can find Button
’s states under ButtonBase
. Similarly, DataGridColumnHeader
lists only its SortStates
group, even though it also inherits the two groups from ButtonBase
. Some controls choose not to respect states defined by its base classes. For example, ProgressBar
supports two CommonStates
—Determinate
and Indeterminate
—but overrides functionality in the RangeBase
base class such that its three CommonStates
and two FocusStates
never get invoked.
To write a control template that takes advantage of states, you set the VisualStateManager.VisualStateGroups
attached property on the root element in the template’s visual tree to a collection of VisualStateGroup
objects, each of which has a collection of VisualState
children.
Listing 14.11 updates the pie chart control template from Listing 14.9 to take advantage of ProgressBar
’s visual states. Because ProgressBar
only supports states for Determinate
versus Indeterminate
and does not have states for Normal
versus Disabled
, this template still needs one trigger for the case of IsEnabled
being false
. The previous trigger that acted on IsIndeterminate
being true
has been replaced by acting on the Indeterminate
visual state, however.
<Application x:Class="WindowsApplication1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WindowsApplication1"
StartupUri="Window1.xaml">
<Application.Resources>
<ControlTemplate x:Key="progressPie" TargetType="{x:Type ProgressBar}">
<!-- Resources -->
<ControlTemplate.Resources>
<local:ValueMinMaxToPointConverter x:Key="converter1"/>
<local:ValueMinMaxToIsLargeArcConverter x:Key="converter2"/>
</ControlTemplate.Resources>
<!-- Visual Tree -->
<Viewbox>
<!-- Visual State Groups -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Determinate"/> <!-- Nothing to do for this state -->
<VisualState Name="Indeterminate">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="pie"
Storyboard.TargetProperty="Opacity" To="0" Duration="0"/>
<DoubleAnimation Storyboard.TargetName="backgroundNormal"
Storyboard.TargetProperty="Opacity" To="0" Duration="0"/>
<DoubleAnimation Storyboard.TargetName="backgroundIndeterminate"
Storyboard.TargetProperty="Opacity" To="1" Duration="0"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid Width="20" Height="20">
<Ellipse x:Name="backgroundIndeterminate" Opacity="0"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}" Width="20"
Height="20">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="Yellow"/>
<GradientStop Offset="1" Color="Brown"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse x:Name="backgroundNormal" Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}"
Width="20" Height="20" Fill="{TemplateBinding Background}"/>
<Path x:Name="pie" Fill="{TemplateBinding Foreground}">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="10,10" IsClosed="True">
<LineSegment Point="10,0"/>
<ArcSegment Size="10,10" SweepDirection="Clockwise">
<ArcSegment.Point>
<MultiBinding Converter="{StaticResource converter1}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Value"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Minimum"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Maximum"/>
</MultiBinding>
</ArcSegment.Point>
<ArcSegment.IsLargeArc>
<MultiBinding Converter="{StaticResource converter2}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Value"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Minimum"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Maximum"/>
</MultiBinding>
</ArcSegment.IsLargeArc>
</ArcSegment>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</Grid>
</Viewbox>
<!-- Only one Trigger -->
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="pie" Property="Fill">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="Gray"/>
<GradientStop Offset="1" Color="White"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Application.Resources>
</Application>
The content of each VisualState
is a Storyboard
, a type covered in depth in Chapter 17. It enables you to change certain property values either instantly (as done in Listing 14.11) or with a smooth transition. Changing the arbitrary background Ellipse Fill
to a specific LinearGradientBrush
isn’t feasible with a Storyboard
, so this listing changes the visual tree to contain two Ellipse
s—backgroundNormal
that is visible by default and backgroundIndeterminate
that is not (due to its Opacity
being set to 0
). The transition to the Indeterminate
visual state therefore instantly “animates” the Opacity
of backgroundNormal
to 0
and the Opacity
of backgroundIndeterminate
to 1
. To make this happen more gradually, you can increase the Duration
value on the two DoubleAnimation
s. Chapter 17 reveals all the flexibility that the use of these animation objects can give you. It also revisits the Button
control template created in this chapter (Listing 14.8), to show how it could be rewritten to leverage the VSM.
Tip
VisualStateGroup
has a Transitions
property that can be set to one or more VisualTransition
s that can do automatic animated transitions between any combinations of states. See Chapter 17 for more information.
As with their parts, controls should document their state groups and states by using the TemplateVisualStateAttribute
. However, the built-in WPF controls do not currently do this.
Although all the control templates thus far are applied directly to elements for simplicity, it’s more common to set a Control
’s Template
property inside a Style
and then apply that style to the desired elements:
<Style TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
...
</ControlTemplate>
</Setter.Value>
</Setter>
...
</Style>
Besides the convenience of combining a template with arbitrary property settings, there are important advantages to doing this:
It gives you the effect of default templates. For example, when a typed Style
gets applied to elements by default and that Style
contains a custom control template, the control template gets applied without any explicit markings on those elements!
It enables you to provide default yet overridable property values that control the look of the template. In other words, it enables you to respect the templated parent’s properties but still provide your own default values.
The final point is very relevant for the templates examined so far. For the ProgressBar
pie chart template, I wanted the pie to be filled with a green gradient by default. If such a Brush
is hard-coded inside the template, consumers would have no way to customize the fill. On the other hand, by binding to the templated parent’s Foreground
(which is what Listing 14.9 does), the onus is on every ProgressBar
to set its Foreground
appropriately. ProgressBar
’s default Foreground
is a solid green color, not the desired gradient!
By placing the green gradient in a Style
’s Setter
, however, you get the desired default look while still allowing individual ProgressBar
s to override the fill by explicitly setting their Foreground
property locally. And the {TemplateBinding Foreground}
expression inside the template doesn’t need to change. The Style
could look as follows:
<Style x:Key="pieStyle" TargetType="{x:Type ProgressBar}">
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="LightGreen"/>
<GradientStop Offset="1" Color="DarkGreen"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ProgressBar}">
...
<Path x:Name="pie" Fill="{TemplateBinding Foreground}">
...
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Consumers of the Style
could do the following:
<!-- Use the default gradient fill -->
<ProgressBar Style="{StaticResource pieStyle}"
Width="100" Height="100" Value="10"/>
<!-- Use a solid red fill instead -->
<ProgressBar Style="{StaticResource pieStyle}" Foreground="Red"
Width="100" Height="100" Value="10"/>
Of course, the same approach can be used for other properties, such as Width
and Height
.