5.3. Named Styles
By hoisting the same inline style into a resource (as introduced
in Chapter 1), we
can award it a name and use it by name in our button instances, as shown in
Example 5-5.
Example 5-5. Setting a named style
<!-- Window1.xaml -->
<Window ...>
<Window.Resources>
<Style x:Key="CellTextStyle">
<Setter Property="Control.FontSize" Value="32" />
<Setter Property="Control.FontWeight" Value="Bold" />
</Style>
</Window.Resources>
...
<Button Style="{StaticResource CellTextStyle}" ... x:Name="cell00" />
...
</Window>
In Example 5-5,
we've used the Control prefix on our properties instead of the Button
prefix to allow the style to be used more broadly, as we'll soon see.
5.3.1. The TargetType Attribute
As a convenience, if all of the properties can be set on a
shared class, like Control in our example, we can promote the class
prefix into the TargetType attribute and remove it from the name of
the property, as in Example 5-6.
Example 5-6. A target-typed style
<Style x:Key="CellTextStyle" TargetType="{x:Type Control}">
<Setter Property="FontSize" Value="32" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
When providing a TargetType attribute, you can only set
properties available on that type. If you'd like to expand to a greater set of
properties down the inheritance tree, you can do so by using a more derived
type, as in Example 5-7.
Example 5-7. A more derived target-typed style
<Style x:Key="CellTextStyle" TargetType="{x:Type Button}">
<!-- IsCancel is a Button-specific property -->
<Setter Property="IsCancel" Value="False" />
<Setter Property="FontSize" Value="32" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
In this case, the IsCancel property is only available
on Button, so to set it, we need to switch the TargetType attribute
for the style.
 |
You may be wondering why I'm setting the FontSize
to "32" instead of "32pt" when the latter is more in line
with how font sizes are specified and the two representations are definitely
not equivalent (the former is pixels, while the latter is points). I'm using
pixels because as of this writing, WPF styles using a non-prefixed property
allow "32pt" to be specified for FontSize, while prefixed
properties do not. For example, the following works (assuming a TargetType
is set):
<Setter Property="FontSize" Value="32pt" />
whereas the following does not (regardless of whether a TargetType
is set or not):
<Setter Property="Control.FontSize" Value="32pt" />
Hopefully this problem will have been fixed by the time you read
this (and not replaced with others).
|
|
5.3.2. Reusing Styles
In addition to saving you from typing out the name of the class
prefix for every property name, the TargetType attribute will also
check that all classes that have the style applied are an instance of that type
(or derived type). What that means is that if we leave TargetType set
to Control, we can apply it to a Button element, but not to a
TextBlock element, as the former derives ultimately from Control
but the latter does not.
On the other hand, while Control and TextBlock
both share the common ancestor FrameworkElement, the FrameworkElement
class doesn't define a FontSize dependency property, so a style with a
TargetType of FrameworkElement won't let us set the FontSize
property because its not there, despite the fact that both Control and
TextBlock have a FontSize property.
Even with the TargetType set to Control, we
gain a measure of reuse of our style across classes that derive from Controle.g.,
Button, Label, Window, etc. However, if we drop the TargetType
attribute from the style altogether, we gain a measure of reuse of styles
across controls that don't have a common base but share a dependency-property
implementation. In my experimentation, I've found that dependency properties
that share the same name across classes, such as Control.FontSize and TextBlock.FontSize,
also share an implementation. What that means is that even though Control
and TextBlock each define their own FontSize property, at
runtime they share the implementation of this property, so I can write code
like Example 5-8.
Example 5-8. Reusing a style between different element
types
...
<Style x:Key="CellTextStyle">
<Setter Property="Control.FontSize" Value="32" />
</Style>
...
<!-- derives from Control -->
<Button Style="{StaticResource CellTextStyle}" ... />
<!-- does *not* derive from Control -->
<TextBlock Style="{StaticResource CellTextStyle}" ... />
...
In Example 5-8,
I've dropped the TargetType attribute from the style definition, using
instead the class prefix on each property the style sets. This style can be
applied to a Button element, as you'd expect, but can also be applied
to a TextBlock control, with the FontSize set as specified by
the style. The reason this works is that both the Button, which gets
its FontSize dependency property definition
from the Control class, and the TextBlock, which provides it's own FontSize
dependency property definition, share the FontSize
dependency property implementation with the TextElement
class. Figure 5-2 shows the
relationship of elements to their dependency-property implementations.
As Figure 5-2 shows,
if we wanted to, we could redefine our style in terms of the TextElement
class, even though it falls into the inheritance tree of neither Control
nor TextBlock, as in Example
5-9.
Example 5-9. Depending on the implementation of
dependency properties
<Style x:Key="CellTextStyle">
<Setter Property="TextElement.FontSize" Value="32" />
</Style>
...
<Button Style="{StaticResource CellTextStyle}" ... />
<TextBlock Style="{StaticResource CellTextStyle}" ... />
Taking this further, if we'd like to define a style that
contains properties not shared by every element we apply them to, we can do
that, too, as in Example 5-10.
Example 5-10. Styles can have properties that targets
don't have
<Style x:Key="CellTextStyle">
<Setter Property="TextElement.FontSize" Value="32" />
<Setter Property="Button.IsCancel" Value="False" />
</Style>
...
<!-- has an IsCancel property -->
<Button Style="{StaticResource CellTextStyle}" ... />
<!-- does *not* have an IsCancel property -->
<TextBlock Style="{StaticResource CellTextStyle}" ... />
In Example 5-10,
we've added the Button.IsCancel property to the CellTextStyle
and applied it to the Button element, which has this property, and the
TextBlock element, which doesn't. This is OK. At runtime, WPF will
apply the dependency properties that exist on the elements that have them and
silently swallow the ones that aren't present.
 |
WPF's ability to apply styles to objects that don't
have all of the properties defined in the style is analogous to applying the
Word Normal style, which includes a font size property of its own, to both a
range of text and an image. Even though Word knows that images don't have a
font size, it applies the portions of the Normal style that do make sense (such
as the justification property), ignoring the rest.
|
|
Getting back to our sample, we can use the CellTextStyle
on a TextBlock in a new row to show whose turn it is, as in
Example 5-11.
Example 5-11. Applying a style to Button and TextBlock
elements
<Window.Resources>
<Style x:Key="CellTextStyle">
<Setter Property="TextElement.FontSize" Value="32" />
<Setter Property="TextElement.FontWeight" Value="Bold" />
</Style>
</Window.Resources>
<Grid Background="Black">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Style="{StaticResource CellTextStyle}" ... />
...
<TextBlock
Style="{StaticResource CellTextStyle}"
Foreground="White"
Grid.Row="3"
Grid.ColumnSpan="3"
x:Name="statusTextBlock" />
</Grid>
</Window>
This reuse of the style across controls of different types gives
me a consistent look in my application, as shown in
Figure 5-3.
One thing you'll notice is that the status text in
Figure 5-3 is white, while the text in the buttons is black. Since
black is the default text color, if we want the status text to show up against
a black background, we have to change the color to something else, hence the
need to set the Foreground property to white on the TextBlock.
Setting per-instance properties works just fine in combination with the style,
and you can combine the two techniques of setting property values as you see
fit.
5.3.3. Overriding Style Properties
Further, if we want to override
a style property on a specific instance, we can do so by setting the property
on the instance, as in Example 5-12.
Example 5-12. Overriding the FontWeight property from
the style
<Style x:Key="CellTextStyle">
<Setter Property="TextElement.FontSize" Value="32" />
<Setter Property="TextElement.FontWeight" Value="Bold" />
</Style>
...
<TextBlock
Style="{StaticResource CellTextStyle}"
FontWeight="Thin" ... />
In Example 5-12,
the TextBlock instance property setting of FontWeight take
precedence over the style property settings of FontWeight.
5.3.4. Inheriting Style Properties
To complete the object-oriented triumvirate of reuse, override,
and inheritance, you can inherit a style from
a base style, adding new properties or overriding existing ones, as in
Example 5-13.
Example 5-13. Style inheritance
<Style x:Key="CellTextStyle">
<Setter Property="TextElement.FontSize" Value="32" />
<Setter Property="TextElement.FontWeight" Value="Bold" />
</Style>
<Style x:Key="StatusTextStyle" BasedOn="{StaticResource CellTextStyle}">
<Setter Property="TextElement.FontWeight" Value="Thin" />
<Setter Property="TextElement.Foreground" Value="White" />
<Setter Property="TextBlock.HorizontalAlignment" Value="Center" />
</Style>
The BasedOn style attribute is used to designate the
base style. In Example 5-13,
the StatusTextStyle style inherits all of the CellTextStyle property
setters, overrides the FontWeight, and adds setters for Foreground
and HorizontalAlignment. Notice that the HorizontalAlignment property
uses a TextBlock prefix; this is because TextElement doesn't
have a HorizontalAlignment property.
Our current use of styles causes our tic-tac-toe game to look
like Figure 5-4.
Our application so far is pretty good, especially with the thin
font weight on the status text, but we can do better.
5.3.5. Setting Styles Programmatically
Once a style has a name, it's easily available from our code.
For example, we might decide that we'd like each player to have their own
style. In this case, using named styles in XAML at compile time won't do the
trick, since we want to set the style based on the content, which isn't known
until runtime. However, there's nothing that requires us to set the Style
property of a control statically; we can set it programmatically as well, as we
do in Example 5-14.
Example 5-14. Setting styles programmatically
public partial class Window1 : Window {
void cell_Click(object sender, RoutedEventArgs e) {
Button button = (Button)sender;
...
// Set button content
button.Content = this.CurrentPlayer;
...
if( this.CurrentPlayer == "X" ) {
button.Style = (Style)FindResource("XStyle");
this.CurrentPlayer == "O";
}
else {
button.Style = (Style)FindResource("OStyle");
this.CurrentPlayer == "X";
}
...
}
...
}
In Example 5-14,
whenever the player clicks, in addition to setting the button's content, we
pull a named style out of the window's resources and use that to set the
button's style. This assumes a pair of named styles defined in the window's
scope, as in Example 5-15.
Example 5-15. Styles pulled out via FindResource
<Window.Resources>
<Style x:Key="CellTextStyle">
<Setter Property="TextElement.FontSize" Value="32" />
<Setter Property="TextElement.FontWeight" Value="Bold" />
</Style>
<Style x:Key="XStyle" BasedOn="{StaticResource CellTextStyle}">
<Setter Property="TextElement.Foreground" Value="Red" />
</Style>
<Style x:Key="OStyle" BasedOn="{StaticResource CellTextStyle}">
<Setter Property="TextElement.Foreground" Value="Green" />
</Style>
</Window.Resources>
With these styles in place and the code to set the button style
along with content, we get Figure 5-5.
Notice that the Xs and Os are colored according to the named
player styles. In this particular case (and in many other cases, too), data
triggers (discussed in "Data
Triggers," later in this chapter) should be preferred to setting styles
programmatically, but you never know when you're going to have to jam.
 |
As with all XAML constructs, you are free to create
styles themselves programmatically.
Appendix A is a good introduction on how to think about going back and
forth between XAML and code.
|
|
|