5.6. Triggers
So far, we've seen 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.
5.6.1. 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.
Example 5-24. A simple property trigger
<Style TargetType="{x:Type Button}">
...
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True" >
<Setter Property="Background" Value="Yellow" />
</Trigger>
</Style.Triggers>
</Style>
Triggers are grouped together under the Style.Triggers element.
In this case, we've added a TRigger element to the button style. When
the IsMouseOver property of our button is true, the Background
value 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 truee.g., watching for IsMouseOver to be False.
The WPF dependency-property system watches for the property trigger to become
inactive and reverts to the previous value.
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
<Style TargetType="{x:Type Button}">
...
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True" >
<Setter Property="Background" Value="Yellow" />
<Setter Property="FontStyle" Value="Italic" />
</Trigger>
</Style.Triggers>
</Style>
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 the Style property itself. If you try, you'll get the
following error:
A Style object is not allowed to affect the Style
property of the object to which it applies.
This makes sense. Since it's the Style 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 very Style that'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.
|
|
5.6.2. Multiple Triggers
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.
Example 5-26. Multiple property triggers
<Style TargetType="{x:Type Button}">
...
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True" >
<Setter Property="Background" Value="Yellow" />
</Trigger>
<Trigger Property="IsFocused" Value="True" >
<Setter Property="Background" Value="LightGreen" />
</Trigger>
</Style.Triggers>
</Style>
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 the IsFocused trigger is last in the
list of triggers.
5.6.3. Multi-Condition Property Trigger
If you'd like to check more than one property before a trigger
condition is activatede.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.
Example 5-27. A multi-property trigger
<Style TargetType="{x:Type Button}">
...
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="Content" Value="{x:Null}" />
</MultiTrigger.Conditions>
<Setter Property="Background" Value="Yellow" />
</MultiTrigger>
</Style.Triggers>
</Style>
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.
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
.
5.6.4. 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.
Example 5-28. Two data triggers
<Window.Resources>
<Style TargetType="{x:Type Button}">
...
</Style>
<Style x:Key="CellTextStyle" TargetType="{x:Type TextBlock}">
...
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PlayerName}" Value="X">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=PlayerName}" Value="O">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="MoveNumberStyle" TargetType="{x:Type TextBlock}">
...
</Style>
...
<DataTemplate DataType="{x:Type l:PlayerMove}">
<Grid>
<TextBlock
TextContent="{Binding Path=PlayerName}"
Style="{StaticResource CellTextStyle}" />
<TextBlock
TextContent="{Binding Path=MoveNumber}"
Style="{StaticResource MoveNumberStyle}" />
</Grid>
</DataTemplate>
</Window.Resources>
DataTrigger elements go under the Style.Triggers
element 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 a PlayerMove object. In both of our data triggers,
we're binding to the PlayerName property. If the value is X,
we're setting the foreground to red and if it's O, we're setting it to
green.
 |
Take care where you put the data trigger. In our
example, we've got the Button-type style and the named CellTextStyle
style 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 4e.g.,
INotifyPropertyChanged. Since each PlayerMove 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 null PlayerMove to get to the PlayerName
property.
5.6.5. Multi-Condition Data Triggers
Just as property triggers can be combined into "and" conditions
using the MultiTrigger element, data triggers can be combined using
the MultiDataTrigger element. For example, if we wanted to watch for
the 10th move of the game, make sure it's player "O," and do
something special, we can do so as shown in
Example 5-29.
Example 5-29. A multi-data trigger
<?Mapping XmlNamespace="sys" ClrNamespace="System" Assembly="mscorlib" ?>
...
<Window ... xmlns:sys="sys">
<Style x:Key="MoveNumberStyle" TargetType="{x:Type TextBlock}">
...
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=PlayerName}" Value="O" />
<Condition Binding="{Binding Path=MoveNumber}">
<Condition.Value>
<sys:Int32>10</sys:Int32>
</Condition.Value>
</Condition>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="Yellow" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
...
</Window>
The only thing about Example
5-29 that may seem a little strange is the use of the mapping syntax to
bring in the System namespace from the mscorlib .NET assembly. We do
this so that we can pass 10 as an Int32 instead of as a
string; otherwise, the multi-condition data trigger won't match our MoveNumber
property correctly. The multi-condition data trigger in
Example 5-29 sets the background of the move number to yellow to
connote a cause for celebration for this special move that regular tic-tac-toe
doesn't have, but you can use multi-condition data triggers
for celebrations of your own kinds.
5.6.6. Event Triggers
While property triggers check for values on dependency
properties and data triggers check for values on CLR properties, event
triggers watch for events. When an event happens, such as a Click event,
an event trigger responds by raising an animation-related action. While
animation is challenging enough to deserve its own chapter (Chapter
8), Example 5-30 illustrates
a simple animation that will transition a cell from solid yellow to white over
five seconds when an empty cell is clicked.
Example 5-30. An event trigger
<Style TargetType="{x:Type Button}">
...
<Setter Property="Background" Value="White" />
<Style.Storyboards>
<ParallelTimeline Name="CellClickedTimeline" BeginTime="{x:Null}">
<SetterTimeline Path="(Button.Background).(SolidColorBrush.Color)">
<ColorAnimation From="Yellow" To="White" Duration="0:0:5" />
</SetterTimeline>
</ParallelTimeline>
</Style.Storyboards>
<Style.Triggers>
<EventTrigger RoutedEvent="Click">
<EventTrigger.Actions>
<BeginAction TargetName="CellClickedTimeline" />
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
Adding an animation to a style requires two things. The first is
a storyboard with a named timeline that describes what you want to happen. In
our case, we're animating the button's background brush color from yellow to
white over five seconds.
 |
For any property being animated with a nested path,
there needs to be an explicit property setting that creates the top level of
the nesting. In Example 5-30,
this means that we need a Setter element for the Background property.
If the top level of the nesting isn't created, there won't be anything to
animate at runtime.
|
|
The second thing you need is an event trigger to start the
timeline. In our case, when the user clicks on a button with the CellButtonStyle
style applied (all of them, in our case), we begin the action described by the
named timeline in the storyboard.
 |
As of this writing, if you have an event trigger
and a multi-condition property trigger animating the same propertye.g., the Background
of a Button, make sure you put the multi-data trigger in the XAML file
before the event trigger; otherwise, you'll get a nonsensical error at runtime.
|
|
The results of the animation, showing various shades of yellow,
through past clicks can be seen in Figure
5-13.
Property and data triggers let you set properties when
properties change. Event triggers let you trigger events when events happen.
While both are pretty differente.g., you can't set a property with an event
trigger or raise an event with a property or data triggerboth let you add a
degree of interactivity to your applications in a wonderfully declarative way
with little or no code.
For the full scoop on event triggers, you'll want to read
Chapter 8.
|