Triggering changes

In WPF, we have a number of Trigger classes that enable us to modify controls, albeit most commonly, just temporarily. All of them extend the TriggerBase base class and therefore inherit its EnterActions and ExitActions properties. These two properties enable us to specify one or more TriggerAction objects to apply when the trigger becomes active and/or inactive respectively.

While most trigger types also contain a Setters property that we can use to define one or more property setters that should occur when a certain condition is met, the EventTrigger class does not. Instead, it provides an Actions property that enables us to set one or more TriggerAction objects to be applied when the trigger becomes active.

Furthermore, unlike the other triggers, the EventTrigger class has no concept of state termination. This means that the action applied by the EventTrigger will not be undone when the triggering condition is no longer true. If you hadn't already guessed this, the conditions that trigger the EventTrigger instances are events, or RoutedEvent objects more specifically. Let's investigate this type of trigger first with a simple example that we saw in the Chapter 4, Becoming Proficient with Data Binding:

<Rectangle Width="300" Height="300" Fill="Orange"> 
  <Rectangle.Triggers> 
    <EventTrigger RoutedEvent="Loaded"> 
      <BeginStoryboard> 
        <Storyboard Storyboard.TargetProperty="Width"> 
          <DoubleAnimation Duration="0:0:1" To="50" AutoReverse="True"  
            RepeatBehavior="Forever" /> 
        </Storyboard> 
      </BeginStoryboard> 
    </EventTrigger> 
  </Rectangle.Triggers> 
</Rectangle> 

In this example, the trigger condition is met when the FrameworkElement.Loaded event is raised. The action that is applied is the start of the declared animation. Note that the BeginStoryboard class actually extends the TriggerAction class and this explains how we are able to declare it within the trigger. This action will be implicitly added into the TriggerActionCollection of the EventTrigger object, although we could have explicitly set it as follows:

<EventTrigger RoutedEvent="Loaded"> 
  <EventTrigger.Actions> 
    <BeginStoryboard> 
      <Storyboard Storyboard.TargetProperty="Width"> 
        <DoubleAnimation Duration="0:0:1" To="50" AutoReverse="True"  
          RepeatBehavior="Forever" /> 
      </Storyboard> 
    </BeginStoryboard> 
  </EventTrigger.Actions> 
</EventTrigger> 

In addition to the EventTrigger class, there are also Trigger, DataTrigger, MultiTrigger and MultiDataTrigger classes that enable us to set properties or control animations when a certain condition, or multiple conditions in the case of the multi triggers, are met. Each has its own merits, but apart from the EventTrigger class, which can be used in any trigger collection, there are some restrictions on where we can use them.

Each control that extends the FrameworkElement class has a Triggers property of type TriggerCollection, that enable us to specify our triggers. However, if you've ever tried to declare a trigger there, then you're probably aware that we are only allowed to define triggers of type EventTrigger there.

However, there are further trigger collections that we can use to declare our other types of triggers. When defining a ControlTemplate, we have access to the ControlTemplate.Triggers collection. For all other requirements, we can declare our other triggers in the Style.Triggers collection. Remember that triggers defined in styles have a higher priority than those declared in templates.

Let's now take a look at the remaining types of triggers and what they can do for us. We start with the most simple, the Trigger class. Note that anything that the property trigger can do, the DataTrigger class can also do. However, the property trigger syntax is simpler and does not involve data binding and so it is more efficient.

 

There are, however, a few requirements to using a property trigger and they are as follows. The relevant property must be a Dependency Property. Unlike the EventTrigger class, the other triggers do not specify actions to be applied when the trigger condition is met, but property setters instead.

We are able to specify one or more Setter objects within each Trigger object and they will also be implicitly added to the trigger's Setters property collection if we do not explicitly specify it. Note that also unlike the EventTrigger class, all other triggers will return the original property value when the trigger condition is no longer satisfied. Let's look at a simple example:

<Button Content="Go"> 
  <Button.Style> 
    <Style TargetType="{x:Type Button}"> 
      <Setter Property="Foreground" Value="Black" /> 
      <Style.Triggers> 
        <Trigger Property="IsMouseOver" Value="True"> 
          <Setter Property="Foreground" Value="Red" /> 
        </Trigger> 
      </Style.Triggers> 
    </Style> 
  </Button.Style> 
</Button> 

Here we have a button that will change the color of its text when the user mouse s over it. Unlike the EventTrigger however, its text color will return to its previously set color when the mouse is no longer over the button. Note also that property triggers use the properties of the controls that they are declared in for their conditions, as they have no way of specifying any other target.

As previously mentioned, the DataTrigger class can also perform this same binding. Let's see what that might look like:

<Button Content="Go"> 
  <Button.Style> 
    <Style TargetType="{x:Type Button}"> 
      <Setter Property="Foreground" Value="Black" /> 
      <Style.Triggers> 
        <DataTrigger Binding="{Binding IsMouseOver,  
          RelativeSource={RelativeSource Self}}" Value="True"> 
          <Setter Property="Foreground" Value="Red" /> 
        </DataTrigger> 
      </Style.Triggers> 
    </Style> 
  </Button.Style> 
</Button> 

As you can see, when using a DataTrigger, instead of setting the Property property of the Trigger class, we need to set the Binding property instead. In order to achieve the same functionality as the property trigger, we also need to specify the RelativeSource.Self enumeration member to set the binding source to the control that is declaring the trigger.

The general rule of thumb is that when we are able to use a simple property trigger that uses a property of the host control in its condition, we should use the Trigger class. When we need to use a property of another control, or a data object in our trigger condition, we should use a DataTrigger. Let's look at an interesting practical example now:

<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}"> 
  <Style.Triggers> 
    <DataTrigger Binding="{Binding DataContext.IsEditable,  
      RelativeSource={RelativeSource AncestorType={x:Type UserControl}},
      FallbackValue=True}" Value="False"> 
      <Setter Property="IsReadOnly" Value="True" /> 
    </DataTrigger> 
  </Style.Triggers> 
</Style> 

In this style, we added a DataTrigger element that data binds to an IsEditable property that we could declare in a View Model class, that would determine whether the users could edit the data in the controls on screen or not. This would assume that an instance of the View Model was correctly set as the UserControl.DataContext property.

If the value of the IsEditable property was false, then the TextBox.IsReadOnly property would be set to true and the control would become un-editable. Using this technique, we could make all of the controls in a form editable or un-editable by setting this property from the View Model.

The triggers that we have looked at so far have all used a single condition to trigger their actions or property changes. However, there are occasionally situations when we might need more than a single condition to trigger our property changes. For example, in one situation, we might want one particular style, and in another situation, we might want a different look. Let's see an example:

<Style x:Key="ButtonStyle" TargetType="{x:Type Button}"> 
  <Setter Property="Foreground" Value="Black" /> 
  <Style.Triggers> 
    <Trigger Property="IsMouseOver" Value="True"> 
      <Setter Property="Foreground" Value="Red" /> 
    </Trigger> 
    <MultiTrigger> 
      <MultiTrigger.Conditions> 
        <Condition Property="IsFocused" Value="True" /> 
        <Condition Property="IsMouseOver" Value="True" /> 
      </MultiTrigger.Conditions> 
      <Setter Property="Foreground" Value="Green" /> 
    </MultiTrigger> 
  </Style.Triggers> 
</Style> 

In this example, we have two triggers. The first will change the button text to red when the mouse is over it. The second will change the button text to green if the mouse is over it and the button is focused.

Note that we had to declare the two triggers in this order, as triggers are applied from top to bottom. Had we swapped their order, then the text would never change to green because the single trigger would always override the value set by the first one.

We can specify as many Condition elements as we need within the Conditions collection and as many setters as we need within the MultiTrigger element itself. However, every condition must return true in order for the setters or other trigger actions to be applied.

The same can be said for the last trigger type to be introduced here, the MultiDataTrigger. The difference between this trigger and the previous one is the same as that between the property trigger and the data trigger. That is, the data and multi-data triggers have a much wider range of target sources, while triggers and multi triggers only work with properties of the local control:

<StackPanel> 
  <CheckBox Name="ShowErrors" Content="Show Errors" Margin="0,0,0,10" /> 
  <TextBlock> 
    <TextBlock.Style> 
      <Style TargetType="{x:Type TextBlock}"> 
        <Setter Property="Text" Value="No Errors" /> 
        <Style.Triggers> 
          <MultiDataTrigger> 
            <MultiDataTrigger.Conditions> 
              <Condition Binding="{Binding IsValid}" Value="False" /> 
              <Condition Binding="{Binding IsChecked,
                ElementName=ShowErrors}" Value="True" /> 
            </MultiDataTrigger.Conditions> 
            <MultiDataTrigger.Setters> 
              <Setter Property="Text" Value="{Binding ErrorList}" /> 
            </MultiDataTrigger.Setters> 
          </MultiDataTrigger> 
        </Style.Triggers> 
      </Style> 
    </TextBlock.Style> 
  </TextBlock> 
  ... 
</StackPanel> 

This example demonstrates the wider reach of the MultiDataTrigger class, due to its access to the wide range of binding sources. We have a Show Errors checkbox, a No Errors textblock, and let's say, some other form fields that are not displayed here. One of the conditions of this trigger uses the ElementName property to set the binding source to the checkbox and requires it to be checked.

The other condition binds to an IsValid property from our View Model that would be set to true if there were no validation errors. The idea is that when the checkbox is checked and there are validation errors, the Text property of the TextBlock element will be data bound to another View Model property named ErrorList, which could output a description of the validation errors.

Also note that in this example, we explicitly declared the Setters collection property and defined our setter within it. However, that is optional and we could have implicitly added the setter to the same collection without declaring the collection, as shown in the previous MultiTrigger example.

Before moving onto the next topic, let's take a moment to investigate the EnterActions and ExitActions properties of the TriggerBase class that enable us to specify one or more TriggerAction objects to apply when the trigger becomes active and/or inactive respectively.

Note that we cannot specify style setters in these collections, as they are not TriggerAction objects; setters can be added to the Setters collection. Instead, we use these properties to start animations when the trigger becomes active and/or inactive. To do that, we need to add a BeginStoryboard element, which extends the TriggerAction class. Let's see an example:

<TextBox Width="200" Height="28"> 
  <TextBox.Style> 
    <Style TargetType="{x:Type TextBox}"> 
      <Setter Property="Opacity" Value="0.25" /> 
      <Style.Triggers> 
        <Trigger Property="IsMouseOver" Value="True"> 
          <Trigger.EnterActions> 
            <BeginStoryboard> 
              <Storyboard Storyboard.TargetProperty="Opacity"> 
                <DoubleAnimation Duration="0:0:0.25" To="1.0" /> 
              </Storyboard> 
            </BeginStoryboard> 
          </Trigger.EnterActions> 
          <Trigger.ExitActions> 
            <BeginStoryboard> 
              <Storyboard Storyboard.TargetProperty="Opacity"> 
                <DoubleAnimation Duration="0:0:0.25" To="0.25" /> 
              </Storyboard> 
            </BeginStoryboard> 
          </Trigger.ExitActions> 
        </Trigger> 
      </Style.Triggers> 
    </Style> 
  </TextBox.Style> 
</TextBox> 

In this example, the Trigger condition relates to the IsMouseOver property of the TextBox control. Note that declaring our animations in the EnterActions and ExitActions properties when using the IsMouseOver property is effectively the same as having two EventTrigger elements, one for the MouseEnter event and one for MouseLeave event.

In this example, the animation in the EnterActions collection will start as the user's mouse cursor enters the control and the animation in the ExitActions collection will start as the user's mouse cursor leaves the control.

We'll thoroughly cover animations later, in Chapter 7, Mastering Practical Animations, but in short, the animation that starts as the user's mouse cursor enters the control will fade in the control from being almost transparent to being opaque.

The other animation will return the TextBox control to an almost transparent state when the user's mouse cursor leaves the control. This creates a nice effect when a mouse is dragged over a number of controls with this style. Now that we have a good understanding of triggers, let's move on to find other ways of customizing the standard .NET controls.