In a word processing document, a “style” is a set of properties to be applied to ranges of content—e.g., text, images, etc. For example, the name of the style I’m using now is called “Normal,Body,b” and for this document in pre-publication, that means a font family of Times, a size of 10, and full justification. Later on in the document, I’ll be using a style called “Code,x,s” that will use a font family of Courier New, a size of 9, and left justification. Styles are applied to content to produce a certain look when the content is rendered.
In WPF, a style is also a set of properties applied to content used for visual rendering. A style can be used to set properties on an existing visual element, such as setting the font weight of a Buttoncontrol, or it can be used to define the way an object looks, such as showing the name and age from aPersonobject. In addition to the features in word processing styles, WPF styles have specific features for building applications, including the ability to associate different visual effects based on user events, provide entirely new looks for existing controls, and even designate rendering behavior for non-visual objects. All of these features come without the need to build a custom control (although that’s still a useful thing to be able to do, as discussed in Chapter 9).
Without Styles
As an example of how styles can make themselves useful in WPF, let’s take a look at a simple implementation of tic-tac-toe in Example 5-1.
This grid layout arranges a set of nine buttons in a 3× 3 grid of tic-tac-toe cells, using the margins on the button for the tic-tac-toe crosshatch. A simple implementation of the game logic in the XAML code-behind file looks like Example 5-2.
Example 5-2. A simple tic-tac-toe implementation
// Window1.xaml.cs ... namespace TicTacToe { public partial class Window1 : Window { // Track the current player (X or O) string currentPlayer;
// Track the list of cells for finding a winner etc. Button[] cells;
public Window1() { InitializeComponent();
// Cache the list of buttons and handle their clicks this.cells = new Button[] { this.cell00, this.cell01, ... }; foreach( Button cell in this.cells ) { cell.Click += cell_Click; }
// Initialize a new game NewGame();
}
// Wrapper around the current player for future expansion, // e.g. updating status text with the current player string CurrentPlayer { get { return this.currentPlayer; } set { this.currentPlayer = value; } }
// Use the buttons to track game state void NewGame() { foreach( Button cell in this.cells ) { cell.Content = null; } CurrentPlayer = "X"; }
// Don't let multiple clicks change the player for a cell if( button.Content != null ) { return; }
// Set button content button.Content = CurrentPlayer;
// Check for winner or a tie if( HasWon(this.currentPlayer) ) { MessageBox.Show("Winner!", "Game Over"); NewGame(); return; } else if( TieGame() ) { MessageBox.Show("No Winner!", "Game Over"); NewGame(); return; }
// Switch player if( CurrentPlayer == "X" ) { CurrentPlayer = "O"; } else { CurrentPlayer = "X"; } }
// Use this.cells to find a winner or a tie bool HasWon(string player) {...} bool TieGame() {...} } }
Our simple tic-tac-toe logic uses strings to represent the players and uses the buttons themselves to keep track of the game state. As each button is clicked, we set the content to the string indicating the current player and switch players. When the game is over, the content for each button is cleared. The middle of a game looks like Figure 5-1.
Figure 5-1. A simple tic-tac-toe game
Notice in Figure 5-1 how the grid background comes through from the margin. These spacers almost make the grid look like a drawn tic-tac-toe board (although we’ll do better later). However, if we’re really looking to simulate a hand-drawn game, we’ve got to do something about the size of the font used on the buttons; it doesn’t match the thickness of the lines.
One way to fix this problem is by setting the size and weight for each of the Button objects, as in Example 5-3.
Example 5-3. Setting control properties individually
While this will make the X’s and O’s look better according to my visual sensibilities today, if I want to change it later, I’ve now committed myself to changing both properties in nine separate places, which is a duplication of effort that offends my coding sensibilities. I’d much prefer to refactor my decisions about the look of my tic-tac-toe cells into a common place for future maintenance. That’s where styles come in handy.
Each “style-able” element in WPF has a Style property, which can be set inline using standard XAML property-element syntax (discussed in Chapter 1), as in Example 5-4.
Because we want to bundle two property values into our style, we have a Style element with twoSetter sub-elements, one for each property we want to set—i.e.,FontSizeandFontWeight—both with theButtonprefix to indicate the class that contains the property. Properties suitable for styling are dependency properties, which are described in Chapter 9.
Due to the extra style syntax and because inline styles can’t be shared across elements, inline styles actually involve more typing than just setting the properties. For this reason, inline styles aren’t used nearly as often as named styles.
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.
In Example 5-5, we’ve used theControlprefix on our properties instead of theButtonprefix to allow the style to be used more broadly, as we’ll soon see.
The TargetType Attribute
As a convenience, if all of the properties can be set on a shared class, like Controlin our example, we can promote the class prefix into theTargetType attribute and remove it from the name of the property, as in Example 5-6.
When providing aTargetTypeattribute, 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.
In this case, theIsCancelproperty is only available onButton, so to set it, we need to switch theTargetTypeattribute for the style.
You may be wondering why I’m setting theFontSizeto"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 forFontSize, while prefixed properties do not. For example, the following works (assuming aTargetTypeis set):
<Setter Property="FontSize" Value="32pt" />
whereas the following does not (regardless of whether aTargetTypeis set or not):
In addition to saving you from typing out the name of the class prefix for every property name, the TargetTypeattribute 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 leaveTargetTypeset toControl, we can apply it to aButtonelement, but not to aTextBlockelement, as the former derives ultimately fromControl but the latter does not.
On the other hand, whileControlandTextBlockboth share the common ancestorFrameworkElement, theFrameworkElementclass doesn’t define aFontSizedependency property, so a style with aTargetTypeofFrameworkElementwon’t let us set theFontSizeproperty because its not there, despite the fact that bothControlandTextBlockhave aFontSizeproperty.
Even with theTargetTypeset toControl, we gain a measure of reuse of our style across classes that derive fromControl—e.g.,Button,Label,Window, etc. However, if we drop theTargetTypeattribute 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 asControl.FontSizeandTextBlock.FontSize, also share an implementation. What that means is that even thoughControlandTextBlockeach define their ownFontSizeproperty, 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 theTargetType attribute from the style definition, using instead the class prefix on each property the style sets. This style can be applied to aButtonelement, as you’d expect, but can also be applied to aTextBlock control, with theFontSizeset as specified by the style. The reason this works is that both theButton, which gets itsFontSizedependency property definition from the Control class, and theTextBlock, which provides it’s ownFontSizedependency property definition, share theFontSizedependency property implementation with theTextElementclass. 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 theTextElementclass, even though it falls into the inheritance tree of neitherControlnorTextBlock, as in Example 5-9.
Figure 5-2. Elements and dependency properties
Example 5-9. Depending on the implementation of dependency properties
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
<!-- does *not* have an IsCancel property --> <TextBlock Style="{StaticResource CellTextStyle}" ... />
In Example 5-10, we’ve added theButton.IsCancelproperty to theCellTextStyle and applied it to theButtonelement, which has this property, and theTextBlockelement, 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 theCellTextStyleon aTextBlockin 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
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 theForegroundproperty to white on theTextBlock. 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.
Figure 5-3. A tic-tac-toe game with style
Please check back next week for the continuation of this article.