Styles, as described in Chapter 8, are great if the changes you’d like to make to a control’s look can be adjusted by the control’s properties (according to your keen aesthetic sense), but what if the control author didn’t leave you enough knobs to get the job done? Rather than diving in to build a custom control, as other presentation libraries would have you do, WPF provides the ability to replace the complete look of the built-in controls while maintaining the existing behavior.
Recall from Chapter 8 that we built a nice little
tic-tac-toe game. However, if we take a closer look at it, we’ll see
that the Button
isn’t quite doing the
job for us. What tic-tac-toe board has rounded inset corners (Figure 9-1)?
What we really want here is to be able to keep the behavior (i.e., holding content and firing click events), but to take over the look of it. WPF allows this kind of thing because the intrinsic controls are built to be lookless (i.e., they provide behavior, but the control’s user can swap out the look completely). The default look comes from the system-provided template, as described in Chapter 12.
Remember from Chapter 6 and Chapter 8 how we used data templates to provide the look of a nonvisual object? We can do the same to a control using a control template—a set of triggers, resources, and most important, elements that provide the look of a control.
To fix our buttons’ looks, we’ll build ourselves a control template resource. Let’s start things off with a simple rectangle (see Example 9-1).
<!-- let's just try one button for now... --> <Button Margin="0,0,2,2" Grid.Row="0" Grid.Column="0" Name="cell00"> <Button.Template> <ControlTemplate> <Grid> <Rectangle /> </Grid> </ControlTemplate> </Button.Template> </Button>
Figure 9-2 shows
the results of setting a single button’s Template
property.
Notice that no vestiges of how the button used to look remain in Figure 9-2. Unfortunately, we can see no vestige of our rectangle, either. The problem is that without a fill explicitly set, the rectangle defaults to no fill, showing the grid’s black background. Let’s set it to our other favorite Halloween color instead:
<ControlTemplate>
<Rectangle Fill="Orange"
/>
</ControlTemplate>
Now we’re getting somewhere, as Figure 9-3 shows.
Notice how square the corners are now? Also, if you click, you won’t get the depression that normally happens with a button (and I don’t mean “a sad feeling”). We have taken complete control over the look of the button or, to paraphrase some ancient pop culture, “all your button are belong to us . . .”
Now that we’re making some progress on the control template,
let’s replicate it to the other buttons. We could do that by setting
each button’s Template
property by
hand, either to a copy of the control template or with a reference to
a ControlTemplate
element that’s
been created in a Resource
element.
However, it’s often most convenient to bundle the control template
with the button’s style, as Example 9-2
illustrates.
<Window.Resources> <Style TargetType="{x:Type Button}"> ... <Setter Property="Template"> <Setter.Value> <ControlTemplate> <Rectangle Fill="Orange" /> </ControlTemplate> </Setter.Value> </Setter> </Style> ... </Window.Resources> ... <!-- No need to set the Template property for each button --> <Button ... Name="cell00" /> ...
As Example 9-2
shows, the Template
property is the
same as any other and can be set with a style. Figure 9-4 shows the
results.
Here we have the classic crosshatch we’ve been aiming for, but
the orange is kind of jarring. What if the Button
object’s Background
property was set to something
more reasonable (maybe white?) and we’re ignoring it, favoring colors
from scary holidays not known for their design sense? We can solve
this problem with template bindings.
If we wanted white buttons, we could hardcode the rectangle’s fill to be white, but what happens when a style wants to change it (maybe somebody really wants an orange tic-tac-toe board)? Instead of hardcoding the fill of the rectangle, we can reach out of the template into the properties of the control by using template binding, as shown in Example 9-3.
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="White" />
...
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Fill="{TemplateBinding Property=Background}"
/>
</ControlTemplate>
</Setter.Value>
</Style>
Template binding is like
data binding, except that the properties to bind come from the control
whose template you’re replacing (called the templated parent). In our case, any
dependency property on the Button
class is fair game as a template binding source. And like data
binding, template binds are smart enough to keep the properties of the
items inside the template up-to-date with changing properties on the
outside as set by styles, animations, and so on.
If you need the expanded options provided by a full binding, you
use a Binding
object inside a
template with a RelativeSource
of
TemplatedParent
to indicate how to
resolve the Path
(see Example 9-4).
<Style TargetType="{x:Type Button}"> <Setter Property="Background" Value="White" /> ... <Setter Property="Template"> <Setter.Value> <ControlTemplate> <Rectangle Fill="{Binding Path=Background, RelativeSource={RelativeSource TemplatedParent}}" /> </ControlTemplate> </Setter.Value> </Style>
You should choose template binding over standard binding inside a template if it meets your needs, as template binding is optimized for just that use.
If you like, you can separate the control template from the style into a separate resource altogether:
<ControlTemplatex:Key="ButtonTemplate"
> <Grid> <Rectangle Fill="{TemplateBindingProperty=Button.Background
}" /> </Grid> </ControlTemplate> <Style TargetType="{x:Type Button}"> ... <Setter Property="Template" Value="{StaticResource ButtonTemplate}" /> </Style>
As with styles, we can avoid prefixing template binding property
names with classes by setting the TargetType
attribute on the ControlTemplate
element:
<ControlTemplate x:Key="ButtonTemplate"TargetType="{x:Type Button}"
> <Grid> <Rectangle Fill="{TemplateBindingProperty=Background
}" /> </Grid> </ControlTemplate>
We’re not quite through with our tic-tac-toe board yet, of course. If we’re going to change the study in pumpkin that Figure 9-4 has become into a playable game, we have to show the moves. To do that, we’ll need a content presenter.
If you’ve ever driven by a billboard or a bus-stop bench that
says “Your advertisement here!” that’s all you need to know to
understand content presenters. A content
presenter is the WPF equivalent of “your content here” that
allows content held by a ContentControl
to be plugged in at
runtime.
In our case, the content is the visualization of our PlayerMove
object. Instead of reproducing
all of that work inside the button’s new control template, we’d just
like to plug it in at the right spot. The job of the content presenter
is to take the content provided by the templated parent and do all of
the things necessary to get it to show up properly, including styles,
triggers, and so on. You can drop the content presenter itself into
your template wherever you’d like to see it. For this application,
we’ll compose a content presenter with the rectangle inside a grid,
using techniques from Chapter 3:
<ControlTemplate TargetType="{x:Type Button}"
>
<Grid>
<Rectangle Fill="{TemplateBinding Property=Background}" />
<ContentPresenter
Content="{TemplateBinding Property=Content}" />
</Grid>
</ControlTemplate>
Further, with the TargetType
property in place, we can drop the explicit template binding on the
Content
property altogether, as it
can be set automatically:
<ControlTemplate TargetType="{x:Type Button}"
>
<Grid>
<Rectangle Fill="{TemplateBinding Property=Background}" />
<!-- with TargetType set, the template binding for the -->
<!-- Content property is no longer required -->
<ContentPresenter />
</Grid>
</ControlTemplate>
I used the Grid
here
because it’s an obvious way to compose the Rectangle
and the ContentPresenter
together into one cell
that takes up the entire available space. However, I also used it to
illustrate a possible performance issue.
When you’re building control templates, you’ve got to keep in mind that they’re likely to be used in multiple places—sometimes hundreds of places. Every element you include will be used each time your control template is expanded, so you want to make sure to use the minimum number of elements.
For example, in our simple control template, there’s no reason
to have a Rectangle
to share the
same cell in the Grid
just to
give the ContentPresenter
a
background color—instead, we can just use a Border
, which has a background color and
can contain our ContentPresenter
.
And because the Border
is only
one element, we don’t need to use the Grid
to arrange it. An optimized version
of this template looks like this:
<ControlTemplate TargetType="{x:Type Button}"> <Border Background="{TemplateBinding Property=Background}"> <ContentPresenter /> </Border> </ControlTemplate>
For the purposes of our example, the control template is expanded only nine times, so there’s no problem, but you should keep element count in mind when you’re composing your content templates.
The content presenter is all we need to get our game back to being functional, as shown in Figure 9-5.
The last little bit of work in our sample is to get the padding
to work. Because the content presenter doesn’t have its own Padding
property, we can’t bind the Padding
property directly (it doesn’t have a
Background
property, either, which
is why we used the Rectangle
and
its Fill
property). For properties
that don’t have a match on the content presenter, you have to find
mappings or compose the elements that provide the functionality you’re
looking for. For example, Padding
is an amount of space inside a control. Margin
, on the other hand, is the amount of
space around the outside of a control. Because they’re both of the
same type, System.Windows.Thickness
, if we could map
the Padding
from the inside of our
button to the outside of the content presenter, [56] our game would look very nice:
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Rectangle Fill="{TemplateBinding Property=Background}" />
<ContentPresenter Margin="{TemplateBinding Property=Padding}"
/>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="White" />
<Setter Property="Padding" Value="8" />
<Setter Property="Template" Value="{StaticResource ButtonTemplate}" />
...
</Style>
Figure 9-6 shows our completed tic-tac-toe variation.
Like the mapping between Padding
and Margin
, building up the elements that give
you the look you want and binding the appropriate properties from the
templated parent is going to be a lot of the work of creating your own
control templates.
Just like styles, control templates support triggers. These let us set up actions in the template itself, regardless of what other triggers the content of the control may or may not also have. For example, if we wanted to add a glow to our buttons as the user hovers, we can do so with a template trigger, as Example 9-5 illustrates.
<Style TargetType="{x:Type Button}"> ... <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Grid> <Rectangle Fill="{TemplateBinding Property=Background}"Name="rect"
/> <ContentPresenter Margin="{TemplateBinding Property=Padding}" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <SetterTargetName="rect"
Property="BitmapEffect"> <Setter.Value> <OuterGlowBitmapEffect GlowColor="Yellow" GlowSize="10" /> </Setter.Value> </Setter> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> ... </Style>
In Example 9-5, we’re setting a
yellow glow whenever the mouse is hovered over the rectangle that
fills the button. We’re using a property trigger, so the value we’re
watching for is a property on the control itself (the IsMouseOver
property, to be precise).
However, we don’t want to set a property on the button; instead, we
want to set the BitmapEffect
property on some inner part of the template (the rectangle, in our
case). This is a very common thing to want to do, and because of that,
a Setter
object inside a control
template allows an extra property to be set that can’t be set in a
style’s Setter
: the TargetName
property. The TargetName
is the name of some element in
the template on which we’d like to set a property (e.g., the element
named rect
in our
example).
Figure 9-7 shows the effect in all its glory.
Take another look at the glow effect in which we swaddled our buttons:
<OuterGlowBitmapEffect GlowColor="Yellow" GlowSize="10" />
Do you notice a problem we’ve run into before? That’s right—in
the same way we were hardcoding the orange fill color a few pages ago,
now we’re hardcoding the glow color and size. “Oh,” you think. “That’s
no problem. I’ll just do what I did before and map the appropriate
properties of the Button
class to
the GlowColor
and GlowSize
properties in the template.” And I
applaud you in the application of your recent learnin', but there
ain’t no properties on the Button
that map to “glow.” In fact, it is often the case in building control
templates that there are more variables you’d like to expose than
there are properties on the control being “templated.”
One popular technique to let us default a custom property for use by the control template is to hijack an existing property for our purposes, as shown in Example 9-6.
<Style TargetType="{x:Type Button}"> ... <Setter Property="Tag"> <Setter.Value> <OuterGlowBitmapEffect GlowColor="Yellow" GlowSize="10" /> </Setter.Value> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Grid> ... </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="rect" Property="BitmapEffect" Value="{Binding Path=Tag, RelativeSource={RelativeSource TemplatedParent}}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> ... <!-- use default bitmap effect set in the style --> <Button Margin="2,0,2,2" Grid.Row="0" Grid.Column="1" Name="cell01" /> ... <!-- use custom bitmap effect, overriding the style's default --> <Button Margin="2,2,2,2" Grid.Row="1" Grid.Column="1" Name="cell11"> <Button.Tag> <BevelBitmapEffect BevelWidth="10" /> </Button.Tag> </Button> ...
In Example 9-6,
our Button
style uses the
Tag
property to pass in a bitmap
effect object to use when the mouse is overhead. The control
template’s trigger uses the value of the Tag
property when the IsMouseOver
property is True
. Notice that we’re using normal
binding (with the TemplatedParent
RelativeSource
) instead of template binding because the
normal binding object has casting support at runtime, whereas
template binding checks the types statically at compile time. The
use of a normal binding enables us to pull a BitmapEffect
out of the Tag
property, which is of type Object
.
When we create a button using this style, the Tag
value acts as a default value, which
we can override with any bitmap effect that tickles our fancy (as
shown on the middle button in Example 9-6).
The problem with repurposing any of the Button
’s properties is that somebody might
actually use the one you pick for something (e.g., the Tag
property is generally a place to store
app-specific data). If this is a worry, the safest thing to do
(short of defining your own custom control type) is to take a page
from Chapter 18 and define your own custom
dependency property:
namespace TicTacToe { public class MouseOverEffectProperties { public static DependencyProperty MouseOverEffectProperty; static MouseOverEffectProperties( ) { OuterGlowBitmapEffect defaultEffect = new OuterGlowBitmapEffect( ); defaultEffect.GlowColor = Colors.Yellow; defaultEffect.GlowSize = 10; MouseOverEffectProperty = DependencyProperty.RegisterAttached( "MouseOverEffect", typeof(BitmapEffect), typeof(MouseOverEffectProperties), new PropertyMetadata(defaultEffect)); } } public static BitmapEffect GetMouseOverEffect(DependencyObject target) { return (BitmapEffect)target.GetValue(MouseOverEffectProperty); } public static void SetMouseOverEffect(DependencyObject target, BitmapEffect value) { target.SetValue(MouseOverEffectProperty, value); } }
Notice that this dependency property is registered as an
attached property that can be attached to any DependencyObject
. Also notice that it has
a built-in default, which simplifies our style, as it needs to list
a value for our new property only if it wants to override the
default. The static GetMouseOverEffect
and SetMouseOverEffect
methods allow us to set
the property value on any dependency object, including our buttons.
With this dependency property in place, we can write our control
template trigger as shown in Example 9-7.
<Window ... xmlns:local="clr-namespace:TicTacToe"
>
...
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter ...
Property="BitmapEffect"
Value="{Binding
Path=(local:MouseOverEffectProperties.MouseOverEffect),
RelativeSource={RelativeSource TemplatedParent}}" />
</Trigger>
</ControlTemplate.Triggers>
...
<Button ...
Tag="howdy"
local:MouseOverEffectProperties.MouseOverEffect="{x:Null}">
...
</window>
Notice that the Path
expression in the Binding
is
surrounded by parentheses, which, as you’ll recall from Chapter 6, means an explicit dependency
property reference. Notice also that we’ve defined an XML namespace
pointing to the CLR namespace where the class lives and that we use
this to specify the path to the dependency property. However,
instead of using the name of the dependency property field (which
has a “Property” suffix), we use the name we registered with the
RegisterAttached
method (which
doesn’t have the “Property” suffix).
Also notice how Example 9-7 overrides the
default value for the property by setting the attached property on
an individual button (to null, in this example), while taking
advantage of the newly available Tag
property for a friendly Western U.S.
greeting.
Instead of creating a new attached dependency property, you can use one of the existing ones, even if it has nothing to do with your control. However, it can be difficult to find an attached property that a) will never be used for anything else on your control; b) is of the correct type; and c) has a name that suggests some kind of semantic relationship with the use of that property in your control template. It does save code, though, if you can use an existing property.
The simple usage of your custom template properties will always be a custom control that has those properties built in, of course. I recommend checking out Chapter 18 for more information about that.
We haven’t been explicit about this yet, but controls expect their templates to provide certain features. The exact set of features varies from one control to the next, but a contract is always in effect between the control and the template. The control’s side of the contract is essentially the set of properties and commands it offers. The template’s side of the contract is less obvious, and is sometimes implicit.
Remember that a control’s job is to provide behavior. The control template provides the visuals. A control may provide a default set of visuals, but it should allow these to be replaced in order to offer the same flexibility as the built-in controls. If you need to provide both custom behavior and custom visuals, build two components: a control, and an element designed to be incorporated into the control’s template. A control that conforms to this approach—where the visuals are separated from the control—is often described as lookless, because the control has no intrinsic appearance or “look.” All of the controls built into WPF are lookless.
Of course, it is not possible for the control to be entirely
independent of its visuals. Any control will impose some requirements
that the template must satisfy if the control is to operate correctly.
The extent of these requirements varies from one control to another.
Button
has fairly simple
requirements: it needs nothing more than a placeholder in which to
inject the content. The slider controls have much more extensive
requirements: the visuals must supply two buttons (increase and
decrease), the “thumb,” and a track for the thumb to run in. Moreover,
they need to be able to respond to clicks or drags on any of these
elements, and to be able to position the thumb.
There is an implied contract between any control type and the style or template. The control allows its appearance to be customized by replacing the visual tree, but the tree must in turn provide certain features on behalf of the control. The nature of the contract will depend on the control—the built-in controls use several different techniques depending on how tightly they depend on the structure of their visuals. The following sections describe the various ways in which a control and its template can be related.
The loosest form of contract between control and template is
where the control simply defines public properties, and allows the
template to decide which of these properties to make visible using
the TemplateBinding
markup
extension. The control does not care what is in the
template.
This is effectively a one-way contract: the control provides properties and demands nothing in return. Despite this, such a control can still respond to user input if necessary—event routing allows events to bubble up from the visuals to the control. The control can handle these events without needing to know anything about the nature of the visuals from which they originated.
Sometimes it is necessary for a control to locate specific
elements in the template. For example, if you write a template for a
ProgressBar
, the control will
look for two parts: the element that it should resize to indicate
progress, and a second so-called “track” element that represents the
full extent of the control. The control modifies the progress
indicator part to be a proportion of the size of the track,
according to the current progress. When the bar’s Value
property is equal to the Maximum
property, the indicator will be
the same size as the track; when the Value
is at Minimum
, the indicator’s size will be
zero; and for values in between, the size is interpolated
appropriately.
The ProgressBar
locates
these two template parts by name. It will expect the template to
contain an element named PART_Indicator
, and another element named
PART_Track
. Example 9-8 shows a very simple
control template with these parts.
<ProgressBar Width="100" Height="25" Value="4" Maximum="10"> <ProgressBar.Template> <ControlTemplate TargetType="{x:Type ProgressBar}"> <Grid> <Rectangle Name="PART_Track" Fill="LightGray" Stroke="Black" /> <Rectangle Name="PART_Indicator" HorizontalAlignment="Left" Margin="2" RadiusX="5" RadiusY="5" Fill="White" Stroke="Blue" /> </Grid> </ControlTemplate> </ProgressBar.Template> </ProgressBar>
Figure 9-8 shows the
results. As you can see, the rectangle with the rounded corners and
the white fill has been sized in proportion to the control’s
Value
—it’s filling about 40
percent of the space provided by the track.
The intrinsic WPF controls mark their part usage with the
TemplatePartAttribute
, which
makes it handy to figure out which controls have which parts
(assuming you’re handy with the metadata API in .NET). Table 9-1 shows the current set of
template parts and their expected type for each WPF control.
Control | Template part name | Expected type |
ComboBox | PART_Popup | Popup |
PART_EditableTextBox | TextBox | |
DocumentViewer | PART_ContentHost | ScrollViewer |
PART_FindToolBarHost | ContentControl | |
FlowDocumentPageViewer | PART_FindToolBarHost | Decorator |
FlowDocumentReader | PART_ContentHost | Decorator |
PART_FindToolBarHost | Decorator | |
FlowDocumentScrollViewer | PART_FindToolBarHost | Decorator |
PART_ToolBarHost | Decorator | |
PART_ContentHost | ScrollViewer | |
Frame | PART_FrameCP | ContentPresenter |
GridViewColumnHeader | PART_HeaderGripper | Thumb |
PART_FloatingHeaderCanvas | Canvas | |
MenuItem | PART_Popup | Popup |
NavigationWindow | PART_NavWinCP | ContentPresenter |
PasswordBox | PART_ContentHost | FrameworkElement |
ProgressBar | PART_Track | FrameworkElement |
PART_Indicator | FrameworkElement | |
ScrollBar | PART_Track | Track |
ScrollViewer | PART_HorizontalScrollBar | ScrollBar |
PART_VerticalScrollBar | ScrollBar | |
PART_ScrollContentPresenter | ScrollContentPresenter | |
Slider | PART_Track | Track |
PART_SelectionRange | FrameworkElement | |
StickyNoteControl | PART_CopyMenuItem | MenuItem |
PART_CloseButton | Button | |
PART_ResizeBottomRightThumb | Thumb | |
PART_IconButton | Button | |
PART_ContentControl | ContentControl | |
PART_TitleThumb | Thumb | |
PART_PasteMenuItem | MenuItem | |
PART_InkMenuItem | MenuItem | |
PART_SelectMenuItem | MenuItem | |
PART_EraseMenuItem | MenuItem | |
TabControl | PART_SelectedContentHost | ContentPresenter |
TextBoxBase | PART_ContentHost | FrameworkElement |
ToolBar | PART_ToolBarPanel | ToolBarPanel |
PART_ToolBarOverflowPanel | ToolBarOverflowPanel | |
TreeViewItem | PART_Header | FrameworkElement |
Some controls expect to find a placeholder element of a
certain type in the template. Controls that support the content
model by deriving from ContentControl
use the element type
approach. They expect to find a ContentPresenter
element in the template,
as you’ve already seen.
In practice, this is a loosely enforced contract. A ContentControl
will not usually complain
if there is no ContentPresenter
in the template. The control doesn’t absolutely depend on the
content being presented in order to function.
In fact, some controls may require more than one placeholder.
For example, controls derived from HeaderedContentControl
require two—one for
the body and one for the header. In this case, we can simply be
explicit about which property the ContentPresenter
presents, as Example 9-9 shows.
<ControlTemplate TargetType="{x:Type local:MyContentControl}"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <ContentPresenter Grid.Row="0" Content="{TemplateBinding Content}" /> <ContentPresenter Grid.Row="1" Content="{TemplateBinding Header}" /> </Grid> </ControlTemplate>
WPF defines two more placeholder types:
ScrollContentPresenter
indicates where the content hosted by a scroll viewer will
go.
You can use ItemsPresenter
in an ItemsControl
to indicate where
generated items should be added.
In addition, if you don’t want to replace the entire control
template, ItemsControl
lets you
replace bits and pieces of itself, which it references in its
default templates (and which you can reference in your own custom
ItemsControl
templates). In fact,
there are two other options for templates on an ItemsControl
. The first we’ve already seen
in Chapter 7: you can supply a
DataTemplate
as the ItemTemplate
property and this will
customize the appearance of each individual item. The second
alternative is that you can set the ItemsPanel
property. This allows you to
customize just the panel used to lay out the list contents. This
uses another template class: ItemsPanelTemplate
. Notice that neither
the ItemTemplate
property nor the
ItemsPanel
property is of the
ControlTemplate
type, but anyone
customizing an ItemsControl
will
want to be familiar with all of the template types that WPF
provides.
At this point, we’ve rounded out the different kinds of
templates available in WPF: data templates, hierarchical data
templates, control templates, and items panel templates.
Fundamentally, they’re all about expanding a template as required
(and they all derive from the FrameworkTemplate
base class), but the
specifics are different and you can’t mix and match
them.
The ItemsPanelTemplate
lets
you change the default panel that lays out items in the list:
<ListBox ItemsSource="{Binding}"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> </ListBox> <ComboBox ItemsSource="{Binding}"> <ComboBox.ItemsPanel> <ItemsPanelTemplate> <UniformGrid /> </ItemsPanelTemplate> </ComboBox.ItemsPanel> </ComboBox>
In this code, we’ve replaced the vertical StackPanel
provided by default in a
ListBox
with a horizontal one.
This code also uses a UniformGrid
to perform a grid layout of the list items in a combo box. These two
changes produce the results you see in Figure 9-9.
You can use any type derived from the Panel
class as the panel template,
including a custom panel if you’ve written such a thing to perform
custom layout. The interesting thing about using a panel in this way
is that although none of the panels supports data binding directly
(e.g., none of them has an ItemsSource
property like an ItemsControl
), the ItemsControl
knows how to manage items in
a panel, so it effectively gives you data binding over the panel of
your choice.
Some controls look for elements marked with a particular
property. For example, controls derived from ItemsControl
, such as ListBox
and MenuItem
, support templates containing an
element with the Panel.IsItemsHost
property set to true.
This identifies the panel that will act as the host for the items in
the control. ItemsControl
uses an
attached property instead of a placeholder to allow you to decide
what type of panel to use to host the items. (ItemsControl
also supports the use of the
ItemsPresenter
typed placeholder
element. This is used when the template does not wish to impose a
particular panel type, and wants to use whatever panel the ItemsPanelTemplate
has specified in the
ItemsPanel
property.) Example 9-10 is a
sample.
<ListBox ItemsSource="{StaticResource items}" Width="120" Height="67">
<ListBox.Template>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border BorderThickness="1" BorderBrush="Black" CornerRadius="10">
<ScrollViewer>
<ScrollViewer.Clip>
<RectangleGeometry Rect="0, 0, 118, 65" />
</ScrollViewer.Clip>
<VirtualizingStackPanel IsItemsHost="True"
/>
</ScrollViewer>
</Border>
</ControlTemplate>
</ListBox.Template>
</ListBox>
Example 9-10
shows the use of a full control template replacing the entire set of
visuals for a ListBox
. Notice
that we have provided a ScrollViewer
; the default ListBox
template supplies one of these, so
we need to provide our own if we want scrolling to work. Notice also
that we’ve provided a panel with the IsItemsHost
property set to True
. We could have used the ItemsPresenter
instead, as we mentioned
earlier, if we wanted the ItemsPanel
property to work. Instead, this
sample ignores the ItemsPanel
and
uses the IsItemsHost
property,
indicating to the ItemsControl
to
which panel it should add the list items. In this case, we’ve used a
VirtualizingStackPanel
, a special
form of StackPanel
optimized for
a large number of items in the data source.[57] This is the same panel type that the default template
for a ListBox
uses. Figure 9-10 shows the
results.
If your goal is to re-create the default look and tweak it,
you will want to use something like the template in Example 9-10. (Here we’re
just tweaking the template by supplying a Clip
geometry in order to make the control
an unusual shape.) However, if you want to radically change the
appearance, the ScrollViewer
is
optional. The only hard requirement is that you supply a panel with
the IsItemsHost
property set to
True
or that you provide an
ItemsPresenter
.
The use of properties to indicate a content placeholder is effectively equivalent to the named parts approach described earlier. However, the named parts approach is far more common—few of the built-in controls use this property-based approach. We describe it here mainly for completeness.
Some controls define custom element types designed for use as a
part of their template, which does more than merely marking the place
where content is to be injected. For example, the Slider
control requires the template to
contain elements to represent the draggable thumb, and the clickable
track in which the thumb runs. The control cannot function unless the
template conforms to the required structure. To enforce this, Slider
requires that the template contain
elements of the special-purpose Thumb
and Track
types.
Neither of these control types is designed for use in isolation.
To emphasize this, both Thumb
and
Track
are defined in the System.Windows.Controls.Primitives
namespace. The only places you would normally use Track
are in the templates for a Slider
or a ScrollBar
. Thumb
is slightly more general-purpose—you
can use it anywhere you require something draggable. But it’s still
designed to be used as part of something else, and is not a control in
its own right.
The Track
control defines a
fixed structure for part of a control template. It has three
properties that contain nested controls. DecreaseRepeatButton
and IncreaseRepeatButton
must contain RepeatButton
controls—these represent the
clickable areas to either side of the thumb. The Thumb
property contains the Thumb
control itself. The Track
manages the sizes and positions of all
three controls, ensuring that they reflect the current properties of
the control at all times.
Example 9-11 shows this technique in action. Notice that the slider uses the named part idiom as well as special-purpose element types.
<Slider Width="100" Height="20" Value="20" Maximum="100"> <Slider.Template> <ControlTemplate TargetType="{x:Type Slider}"> <Track x:Name="PART_Track"> <Track.DecreaseRepeatButton> <RepeatButton Content="<" /> </Track.DecreaseRepeatButton> <Track.Thumb> <Thumb Width="10" /> </Track.Thumb> <Track.IncreaseRepeatButton> <RepeatButton Content=">" /> </Track.IncreaseRepeatButton> </Track> </ControlTemplate> </Slider.Template> </Slider>
Figure 9-11 shows the rather unadventurous results. In a real application, you would also provide templates for the two repeat buttons and the thumb.
The benefit of this approach is that it allows you to enforce
relationships between different parts of the control template. Sliders
and scroll bars use the Track
element to keep the Thumb
correctly
positioned and sized in relation to the two clickable regions that
form the track. In addition, this approach enforces the fact that the
clickable regions are, in turn, represented by RepeatButtons
. The downside is that it is
more complex for the developers using the control because anyone
wishing to define a template for the control must discover and
understand the multiple element types involved.
A lot of the examples in this section talked about how one WPF
template does one thing, while another WPF template does something
else. If you’re curious what the intrinsic WPF templates do, you can
check out the ShowMeTheTemplate
sample provided with this book, as seen in Figure 9-12.
On the lefthand side of the template tool are all of the
framework elements that have template properties of any type. When one
of them is selected, the template properties are shown on the right.
For example, in Figure 9-12, we can
see the Template
property (of type
ControlTemplate
) of the GridSplitter
. The templates shown on the
right are produced by the XAML serializer, so you should be able to
copy and paste them into your own code as a starting place if you’d
prefer to tweak an existing template instead of starting over from
scratch.
The existence of templates leads to an API design dilemma that the
WPF architects had to resolve. If a developer wishes to access the
elements in the UI, should she see the fully expanded tree, containing
all the instantiated templates? Although this would put the developer in
full control, it might be rather cumbersome; often a developer only
really cares that there is a Button
present, not about the structure of its appearance. On the other hand,
to present the simple pre-expansion view would be unnecessarily
limiting.
To solve this problem, WPF lets you work with either the logical
tree or the visual tree. The visual
tree contains most[58] of the elements originally specified (either in markup or
in code) plus all the extra elements added as a result of template
instantiation. The logical tree is
a subset of the visual tree that omits the elements added as a result of
control template instantiation. WPF provides two helper classes for
working with these two trees: VisualTreeHelper
and LogicalTreeHelper
.
For example, consider the following snippet of XAML:
<WrapPanel Name="rootPanel"> <Button>_Click me</Button> </WrapPanel>
Walking this logical tree at runtime using the LogicalTreeHelper
looks like Example 9-12.
public Window1( ) {
InitializeComponent( );
// Can dump the logical tree anytime after InitComp
DumpLogicalTree(rootPanel, 0);
}
void DumpLogicalTree(object parent, int level) {
string typeName = parent.GetType( ).Name;
string name = null;
DependencyObject doParent = parent as DependencyObject;
// Not everything in the logical tree is a dependency object
if( doParent != null ) {
name = (string)(doParent.GetValue(FrameworkElement.NameProperty) ?? "");
}
else {
name = parent.ToString( );
}
Debug.Write(" ".Substring(0, level * 2));
Debug.WriteLine(string.Format("{0}: {1}", typeName, name));
if( doParent == null ) { return; }
foreach( object child in LogicalTreeHelper.GetChildren(doParent)
) {
DumpLogicalTree(child, level + 1);
}
}
Notice that we’re watching for objects that aren’t instances of
the DependencyObject
class (almost
the lowest level in the WPF type hierarchy—only the DispatcherObject
is lower). Not everything in
the logical tree is part of the WPF type hierarchy (e.g., the string we
pass in as the text content of the Button
is going to stay a string when we
examine it):
WrapPanel: rootPanel Button: String: _Click me
The code to walk the instantiated objects with the VisualTreeHelper
is simpler because everything
it encounters is at least a DependencyObject
(see Example 9-13).
protected override void OnContentRendered(EventArgs e) { base.OnContentRendered(e); // Need to wait for layout before visual tree is ready Debug.WriteLine("Visual tree:"); DumpVisualTree(rootPanel, 0); } void DumpVisualTree(DependencyObject parent, int level) { string typeName = parent.GetType( ).Name; string name = (string)(parent.GetValue(FrameworkElement.NameProperty) ?? ""); Debug.Write(" ".Substring(0, level * 2)); Debug.WriteLine(string.Format("{0}: {1}", typeName, name)); for( int i = 0; i !=VisualTreeHelper.GetChildrenCount(parent)
; ++i ) { DependencyObject child =VisualTreeHelper.GetChild(parent, i)
; DumpVisualTree(child, level + 1); } }
In Example 9-13, you’ll notice that
we’re careful to only walk the visual tree in the OnContentRendered
event, which guarantees that
at least a portion of the visual tree has been rendered. This is
important, because the visual tree isn’t expanded until it needs to be.
However, once it’s instantiated, the visual tree is considerably more
verbose than the logical tree:
WrapPanel: rootPanel Button: ButtonChrome: Chrome ContentPresenter: AccessText: TextBlock:
The difference, of course, is that the button control template was instantiated. If you’d like to explore the visual tree produced by a bit of XAML interactively, I suggest the XamlPad tool that comes with the Windows Platform SDK. XamlPad lets you type in XAML and shows you the results as soon as you’ve entered valid XAML. It also has a button that will show you the visual tree of the XAML you’ve typed in. Figure 9-13 shows the visual tree for our sample XAML in a slightly nicer way.
Templates enable a certain kind of UI programming sometimes called data-centric UI and sometimes called data-driven UI. The idea is that the data is the most important thing in our application, and through the use of declarative UI techniques (as enabled by XAML) we shape the data into something suitable for presentation to the user. Ideally, we do this without changing the underlying data at all, but by instead transforming the data on its way to the user as appropriate.
For example, getting back to our tic-tac-toe program, we haven’t been very data-driven at all, instead bundling up the manipulation of the data with the UI code (a.k.a. “putting your logic in the click handler”). A better way to start is to move our game logic into a class of its own that has nothing at all to do with the UI:[59]
class TicTacThree
: INotifyPropertyChanged { public TicTacThree(int dimension) {...} public void NewGame( ) {...} public void Move(int cellNumber) {...} public IEnumerable<Cell> Cells { get {...} } public string CurrentPlayer { get {...} } public bool HasWinner { get {...} } }class Cell
: INotifyPropertyChanged { public Cell(int cellNumber) {...} public int CellNumber { get {...} } public PlayerMove Move { get {...} set {...} } }class PlayerMove
: INotifyPropertyChanged { public string PlayerName { get {...} set {...} } public int MoveNumber { get {...} set {...} } public bool IsPartOfWin { get {...} set {...} } public PlayerMove(string playerName, int moveNumber) {...} }
With this change, the code to hook up our UI to the new game logic is reduced significantly:
class Window1 : Window { TicTacThree game = new TicTacThree(3); // 3x3 grid public Window1( ) { InitializeComponent( ); DataContext = game; } void cell_Click(object sender, RoutedEventArgs e) { Button cell = (Button)sender; int cellNumber = int.Parse(cell.Tag.ToString( )); game.Move(cellNumber); if( game.HasWinner ) { MessageBox.Show("Winner!"); game.NewGame( ); } } }
However, in addition to the styles and templates we’ve built up over the course of our discussion, our XAML is augmented with data binding to show the player moves and the current player:
<Window ...> ... <!— styles and templates as before —> <Button ...Tag="0" Content="{Binding Cells[0].Move}"
Click="cell_Click" /> <Button ...Tag="1" Content="{Binding Cells[1].Move}"
Click="cell_Click" /> ... <TextBlock ...> It's your move, <TextBlock Text="{Binding CurrentPlayer}" /> </TextBlock> </Window>
At this point, we’ve moved further toward data-driven UI, as we’ve separated the data manipulation out of the UI code and are relying on our styles, templates, and data binding to translate the data appropriately for user interaction. However, we have hardcoded knowledge about how much data we are going to get: nine cells. That works great until we add an option to create a tic-tac-toe game of 4 × 4 or 5 × 5 cells, at which point it’s clear we’re not as data-driven as we’d like to be. Luckily, it’s not hard to update our code-behind file:
class Window1 : Window { TicTacThree game = new TicTacThree(3); // 3x3 grid public Window1( ) { ... threeByThreeMenuItem.Click += gameDimensionMenuItem_Click; fourByFourMenuItem.Click += gameDimensionMenuItem_Click; fiveByFiveMenuItem.Click += gameDimensionMenuItem_Click; } void gameDimensionMenuItem_Click(object sender, RoutedEventArgs e) { if( sender == threeByThreeMenuItem ) { game = new TicTacThree(3); } if( sender == fourByFourMenuItem ) { game = new TicTacThree(4); } if( sender == fiveByFiveMenuItem ) { game = new TicTacThree(5); } DataContext = game; } ... }
Updating the XAML is a bit trickier, however, as we need to handle
a variable number of cells. In our case, the UniformGrid
—which arranges things in even rows
and columns—is exactly what we need, but the other panels will be useful
for other kinds of data-driven layouts:
<Window ...> ... <Menu ...> <MenuItem Header="_Game"> <MenuItem Header="_3x3 Game" Name="threeByThreeMenuItem" /> <MenuItem Header="_4x4 Game" Name="fourByFourMenuItem" /> <MenuItem Header="_5x5 Game" Name="fiveByFiveMenuItem" /> </MenuItem> </Menu> ... <ItemsControl ...ItemsSource="{Binding Cells}"
> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <UniformGrid /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Button Content="{Binding Move}" Tag="{Binding CellNumber}" Margin="2" Click="cell_Click"/>
</DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> ... </Window>
Here, we’ve added the menu so that the user can control the
dimensions of the game, and we’ve bound the cells in the game as the
data source to drive our items control. (We’re using an ItemsControl
instead of a ListBox
or a ListView
because we don’t want the selection
behavior those controls provide.) For the panel, we’re using a uniform
grid, which will take however many items we provide and turn them into
an even number of rows and columns. Likewise, for each item, we’re
expanding a data template into a button to show the current cell’s move
(or nothing, if no move has yet been made). We’re also binding the
Tag
property of each Button
to the CellNumber
property of the cell so that we can
use the same button Click
event
handler. However, as we saw in Chapter 7,
we could just as easily use the DataContext
of the sender, as it will point to
the Cell
that was used to instantiate
the data template.
Figure 9-14 shows the results of our data-driven UI work.
The interesting thing to notice is that once we’ve structured our XAML to be free of the hardcoded amount of data to expect, the XAML itself doesn’t get any more complicated, although it does have more functionality as the data changes, as Figure 9-14 shows.
If you decide you need even more control, you are of course free to take over the control templates as well:
<ItemsControl ... ItemsSource="{Binding Cells}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <UniformGrid /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Button Margin="2" Content="{Binding Move}" Tag="{Binding CellNumber}" Click="cell_Click" /> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.Template> <ControlTemplate TargetType="{x:Type ItemsControl}"> <Grid> <Rectangle Fill="Green" RadiusX="20" RadiusY="20" /> <ItemsPresenter Margin="20" /> </Grid> </ControlTemplate> </ItemsControl.Template> </ItemsControl>
Here, we’re replacing the control template with a border
containing an ItemsPresenter
, which,
as you’ll recall from earlier in this chapter, will display the items
according to the templates set by the ItemsPanel
and ItemsTemplate
properties, as Figure 9-15 shows.
This use of the control template isn’t particularly fancy (although it does give our game a nice felt-tabletop look), but it does illustrate just how much control we have over the UI using templates, even when our data is completely UI-agnostic.
Ultimately, driving your UI by the data means making a choice from a range of possibilities. We started by dropping data objects into buttons and using styles and templates to present those objects to the user. Moving to data binding and away from mingling the data manipulation code in with UI code moved us farther along the continuum, as did binding the cells to an items control hosting a uniform grid. However, there are always going to be trade-offs. For example, our menu supports only three dimension choices, no matter how many our game object might support. Further, we haven’t supported the case where the game object might arrange the cells in nonuniform rows and columns. Like all software design choices, how “data-driven” you want your UI to be depends on the goals of your application.
However, whatever trade-offs you make, it is definitely the case that the various templates that WPF provides—data, control, and panel—all give us far more options than we’ve had in desktop presentation frameworks available before now. In fact, the reason I make a point of data-driven UI design is so that you don’t approach WPF programming using the same techniques you used to program user interfaces of old.
As an example of where other UI frameworks don’t quite provide
the kind of data-driven capabilities we’d like, consider another UI
framework that both authors are big fans of: Windows Forms. In Windows
Forms, you absolutely have the ability to bind data to your controls.
In fact, Windows Forms has more controls than WPF has, including the
GridView
, which has no equivalent
in WPF v1.0.
However, although Windows Forms has more controls, each control
has more limited support for what it can display. For example,
although I can hand the Windows Forms ListBox
a list of strings, I can’t hand it a
list of CheckBox
objects or
PlayerMove
objects and have it do
anything more than call each object’s ToString
method. If I want a container to
meaningfully bind to a list of PlayerMove
objects in Windows Forms, I have
to build the custom PlayerMove
container.
With WPF, on the other hand, I can plug custom data types and custom UI frameworks into existing containers by providing data and control templates. This turns out to be considerably less work for the same functionality. WPF is not your father’s UI framework . . .
If the control author didn’t give you the right properties to tailor a control’s look to your liking, you can replace the look completely with a control template (assuming the control author has allowed such a thing). To this end, all of the built-in controls are “lookless,” picking up their default look from the system-wide theme, but leaving you to take it over completely, while keeping the existing behavior intact. If you want to plug into the existing behavior properly, however, some controls have more requirements, which we called the control template “contract.”
If a custom template still doesn’t give you enough control—perhaps you’d like customized behavior—you’ll want to think about building a custom control, which is described in Chapter 18, along with how to support custom control templates on your own custom controls.
[56] * You might be wondering whether
we also need to bind our Margin
property into the control template. It’s a special case: WPF
implements Margin
for all
elements as part of the layout process, so it’s not something our
template needs to worry about.
[57] * The VirtualizingStackPanel
supports
item virtualization, which
is the ability to contain a large number of logical children,
but instantiating UI elements only for the ones currently
visible.
[58] * As one example, no FrameworkContentElement
objects, as
described in Chapter 14, will show
up in the visual tree even though they’re in the logical
tree.
[59] * In fact, this has nothing whatever to do with data-driven UI; as a good coding practice, you should separate your data and your logic from your UI.