Recall from Chapter 4 that WPF allows you to define a data template, which is a tree of elements to expand in a particular context. Data templates are used to provide an application with the ability to render non-visual objects, as shown in Example 5-22.
Example 5-22. Setting a PlayerMove data template without styles
Using the XAML mapping syntax introduced in Chapter 1, we’ve mapped thePlayerMovertype into the XAML with the mapping directive and thexmlnsattribute, which we’ve used as the data type of the data template. Now, whenever WPF sees aPlayerMove object, such as the content of all of our buttons, the data template will be expanded. In our case, the template consists of a grid to arrange two text blocks, one showing the player name in the middle of the button and one showing the move number in the bottom right, along with some other settings to make things pretty.
Data Templates with Style
However, these property settings are buried inside a data template several layers deep. Just as it’s a good idea to take “magic numbers” out of your code, pulling them out and giving them names for easy maintenance, it’s a good idea to move groups of settings into styles,* as in Example 5-23.
Example 5-23. Setting a PlayerMove data template with styles
It’s common to use styles, which set groups of properties, with data templates, which create groups of elements that have groups of properties. Figure 5-7 shows the result.
Figure 5-7.Showing objects of a custom type using templates and styles
Still, as nice as Figure 5-7 is, the interaction is kind of boring given the capabilities of WPF. Let’s see what we can do with style properties as the application is used.
So far, we’ve se en styles as a collection of Setter elements. When a style is applied, the settings described in the Setter elements are applied unconditionally (unless overridden by per-instance settings). On the other hand, triggers are a way to wrap one or more Setter elements in a condition so that, if the condition is true, the corresponding Setter elements are executed, and when the condition becomes false, the property value reverts to its pre-trigger value.
WPF comes with three kinds of things that you can check in a trigger condition: a dependency property, a .NET property, and an event. The first two directly change values based on a condition, as I described, while the last, an event trigger, is activated when an event happens and then starts (or stops) an animation that causes properties to change.
Property Triggers
The simplest form of a trigger is a property trigger, which watches for a dependency property to have a certain value. For example, if we wanted to light up a button in yellow as the user moves the mouse over it, we can do so by watching for the IsMouseOver property to have a value of True, as in Example 5-24.
Triggers are grouped together under the Style.Triggers element. In this case, we’ve added a Trigger element to the button style. When the IsMouseOverproperty of our button is true, theBackgroundvalue of the button will be set to yellow, as shown in Figure 5-8.
You’ll notice in Figure 5-8 that only the button where the mouse is currently hovering has its background set to yellow, even though other buttons have clearly been under the mouse. There’s no need to worry about setting a property back when the trigger is no longer true—e.g., watching forIsMouseOverto beFalse. The WPF dependency-property system watches for the property trigger to become inactive and reverts to the previous value.
Figure 5-8.A Property trigger in action
Property triggers can be set to watch any of the dependency properties on the control to which your style is targeted and to set any of the dependency properties on the control while the condition is true. In fact, you can use a single trigger to set multiple properties if you like, as in Example 5-25.
Example 5-25. Setting multiple properties with a single trigger
In Example 5-25, we’re setting the background to yellow and the font style to italic when the mouse is over a button.
One property that you’re not allowed to set in a trigger is theStyleproperty itself. If you try, you’ll get the following error:
AStyleobject is not allowed to affect the Style property of the object to which it applies.
This makes sense. Since it’s theStyle that’s setting the property, and that participates in “unsetting” it when the trigger is no longer true, what sense would it make to change the veryStylethat’s providing this orchestration? This would be somewhat like switching out your skis after you’ve launched yourself off of a jump but before you’ve landed.
While you can set as many properties as you like in a property trigger, there can be more than one trigger in a style. When grouped together under the Style.Triggers element, multiple triggers act independently of each other.
For example, we can update our code so that if the mouse is hovering over one of our buttons, it’ll be colored yellow and if the button has focus (the tab and arrow keys move focus around), it’ll be colored green, as in Example 5-26. Figure 5-9 shows the result of one cell having focus and another with the mouse hovering.
If multiple triggers set the same property, the last one wins. For example, in Figure 5-9, if a button has focus and the mouse is over it, the background will be light green because the trigger for theIsFocusedtrigger is last in the list of triggers.
Multi-Condition Property Trigger
If you’d like to check more than one property before a trigger condition is activated—e.g., the mouse is hovering over a button and the button content is empty—you can combine multiple conditions with a multiple-condition property trigger, as in Example 5-27.
Multi-condition property triggers check all of the properties’ values to be set as specified, not just one of them. Here, we’re watching for both a mouse hover and for the content to be null,* reflecting the game logic that only clicking on an empty cell will result in a move.
Figure 5-10 shows the yellow highlight on an empty cell when the mouse hovers, and Figure 5-11 shows the yellow highlight absent when the mouse hovers over a full cell.
Figure 5-10. Multi-condition property trigger with hovering and null content
Figure 5-11. Multi-condition property trigger not triggering as content is not null
Property triggers are great for noticing when the user is interacting with a control displaying your program’s state. However, we’d also like to be able to notice when the program’s state itself changes, such as when a particular player makes a move, and update our style settings accordingly. For that, we have data triggers.
Unlike property triggers, which check only WPF dependency properties, data triggers can check any old .NET object property. While property triggers are generally used to check WPF visual-element properties, data triggers are normally used to check the properties of non-visual objects used as content, such as our PlayerMove objects in Example 5-28.
DataTriggerelements go under theStyle.Triggerselement just like property triggers and, just like property triggers, there can be more than one of them active at any one time. While a property trigger operates on the properties of the visual elements displaying the content, a data trigger operates on the content itself. In our case, the content of each of the cells is aPlayerMove object. In both of our data triggers, we’re binding to thePlayerName property. If the value isX, we’re setting the foreground to red and if it’sO, we’re setting it to green.
Take care where you put the data trigger. In our example, we’ve got theButton-type style and the namedCellTextStylestyle as potential choices. I’ve written this chapter twice now and both times I’ve initially put the data trigger on the button style instead of on the content in the data template. Data triggers are based on content, so make sure you put them into your content styles, not your control styles.
We haven’t had per-player colors since we moved to data templates after setting styles programmatically in Figure 5-5, but data triggers bring us that feature right back, along with all of the other features we’ve been building up, as shown in Figure 5-12.
Unlike property triggers, which rely on the change notification of dependency properties, data triggers rely on an implementation of the standard property-change notification patterns that are built into .NET and are discussed in Chapter 4—e.g.,INotifyPropertyChanged. Since eachPlayerMove object is constant, we don’t need to implement this pattern, but if you’re using data triggers, chances are that you will need to implement it on your custom content classes.
One other especially handy feature of data triggers is that there’s no need for an explicit check for null content. If the content is null, the trigger condition is automatically false, which is why the application isn’t crashing trying to dereference a nullPlayerMoveto get to thePlayerNameproperty.
Figure 5-12. Data triggers in action
Please check back next week for the conclusion to this article.