Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Guided Tour of WPF – Part 4 (Data templates and triggers)

0.00/5 (No votes)
7 Apr 2007 2  
A guided tour of the Windows Presentation Foundation, one feature at a time.

Table of Contents

  • Part 1 (XAML): Learn about XAML and how it is used in WPF applications.
  • Part 2 (Layout): Learn about layout panels and how they are used to construct user interfaces.
  • Part 3 (Data binding): Learn how WPF data binding works and how it can be used.
  • Part 4 (Data templates and triggers): Learn how data templates and triggers work and how they can be used.
  • Part 5 (Styles): Learn about how UIs can be styled in WPF.

Introduction

This is the fourth article in an introductory series about the Windows Presentation Foundation. In the previous article we examined data binding and how it is used to display information in WPF user interfaces. In this article we examine data templating and triggers, and how they are used in the WPF Horse Race demo application (which is available for download at the top of the first article in this series).

Just like the other articles in this series, this article does not cover its subject matter in exhaustive detail. Instead we will examine just enough of the basics so that we can see how those features are put to use in the demo app. If you want to learn more about how data templating and triggers can be used, refer to the External links section for additional information.

Background

So far in this series of articles, we have seen how XAML, layout panels, and data binding are used to create user interfaces that display simple data. Those fundamental building blocks, however, serve as the foundation upon which more powerful and compelling features of WPF depend.

One of those higher-level features allows you to easily describe how an object of any type can be rendered. In other words, it enables you to create a template, in XAML, of visual elements which can be "expanded" at runtime to render a data object. Of course, the feature I'm referring to is known as "data templating".

Data templates are only one type of template in WPF. This article, in fact this entire series of articles, will not examine the other types of templates because the WPF Horse Race demo application does not use them. In case you are interested, there are also control templates, items panel templates, and hierarchical data templates. Refer to the Other templates sub-section in the External links section toward the bottom of this article for links to information about them.

Another feature of WPF covered in this article is called "triggers". Triggers are another fundamental building block in WPF, upon which many parts of the framework depend. They are, in general, a means of conditionally applying values to properties. Triggers are especially useful when you are writing XAML because they provide a means of evaluating properties at runtime and taking certain actions based on their values. In that sense triggers are a gray area somewhere between the declarative world of XAML markup and the imperative world of the code-behind.

The DataTemplate class

All of WPF's templating functionality is based on the FrameworkTemplate class. DataTemplate derives from FrameworkTemplate, as do all other classes used for templating purposes. This familial relationship between the templating classes is largely irrelevant for typical WPF development scenarios because in practice, you will create templates in XAML, and will not use them in a polymorphic manner.

It is far more straightforward and expedient to create a DataTemplate in XAML than it is to create one programmatically. The code for creating templates is actually very cumbersome, while the XAML for creating the same template is clean and simple. It is technically possible to construct data templates programmatically, but even Microsoft does not recommend it.

Without a DataTemplate

Before we dive into the data template used in the WPF Horse Race application let's see how a simple example works. First take a look at a simple class which represents a river:

namespace WPF_Test
{
 public class River
 {
  string name;
  int milesLong;

  public string Name
  {
   get { return name; }
   set { name = value; }
  }

  public int MilesLong
  {
   get { return milesLong; }
   set { milesLong = value; }
  }
 }
}

If we were to display an instance of this class in a ContentControl, the XAML would be something like this:

<StackPanel>
  <StackPanel.Resources>
    <local:River x:Key="theRiver" Name="Colorado River" MilesLong="1450" />
  </StackPanel.Resources>
  <ContentControl Content="{StaticResource theRiver }" />
</StackPanel>

The UI created by that XAML looks like the following:

Without a data template.

That certainly isn't too impressive. What you see above is what gets displayed as a result of calling ToString() on the River object referenced by the ContentControl. Let's take a moment to examine what is going on here.

The example above creates an instance of the River class, which represents the Colorado River. It also creates a ContentControl which is told to display that River object somehow. The ContentControl examines the River object and tries to figure out how to render it, but since River does not derive from UIElement it has no way of knowing how to do so. Once ContentControl is devoid of options, it ends up calling ToString() on the River object and then displays that text.

With a DataTemplate

Now that we've seen how boring a River object looks in the absence of data templates, it's time to add one into the mix. Here's the same XAML used before, only this time there is a template for the River class:

<StackPanel>
  <StackPanel.Resources>
    <DataTemplate DataType="{x:Type local:River}">
      <Border BorderBrush="Blue" BorderThickness="3" CornerRadius="12">
        <Grid Margin="4">
          <TextBlock>
            <Run Text="The"/>
            <TextBlock Text="{Binding Name}"/>
            <Run Text="is"/>
            <TextBlock Text="{Binding MilesLong}" />
            <Run Text="miles long." />
          </TextBlock>
        </Grid>
      </Border>
    </DataTemplate>

    <local:River x:Key="theRiver" Name="Colorado River" MilesLong="1450" />
  </StackPanel.Resources>

  <ContentControl Content="{StaticResource theRiver}" />
</StackPanel>

This is how that XAML renders:

With a data template.

The River object is displayed much more intelligently when a data template is applied. The information held within it is now displayed as part of a sentence, and that sentence is wrapped in a curved blue border. Keep in mind that the rendition of a River object seen above is completely arbitrary. It can be displayed in whatever way is considered appropriate for the application in which it exists.

Triggers

Another feature of WPF often used in conjunction with templates is known as "triggers". In general a trigger is somewhat like an if block in procedural code; it only executes what it contains when some condition evaluates to true.

For example, here is a modified version of the XAML seen in the previous section. This time the DataTemplate has a Trigger in it which sets the Border's Background property to 'LightGray' and the TextBlock's Foreground property to 'Red' when the mouse cursor is anywhere over the Border.

<StackPanel>
  <StackPanel.Resources>
    <DataTemplate DataType="{x:Type local:River}">
      <Border x:Name="bdr"
        BorderBrush="Blue" BorderThickness="3" CornerRadius="12"
        >
        <Grid Margin="4">
          <TextBlock x:Name="txt">
            <Run Text="The"/>
            <TextBlock Text="{Binding Name}"/>
            <Run Text="is"/>
            <TextBlock Text="{Binding MilesLong}" />
            <Run Text="miles long." />
          </TextBlock>
        </Grid>
      </Border>

      <DataTemplate.Triggers>
        <Trigger SourceName="bdr" Property="IsMouseOver" Value="True">
         <Setter TargetName="bdr" Property="Background" Value="LightGray"/>
         <Setter TargetName="txt" Property="Foreground" Value="Red"/>
        </Trigger>
      </DataTemplate.Triggers>
    </DataTemplate>

    <local:River x:Key="theRiver" Name="Colorado River" MilesLong="1450" />
  </StackPanel.Resources>

  <ContentControl Content="{StaticResource theRiver}" />
</StackPanel>

When that XAML is used, the UI looks like this when the cursor is over the River visuals:

Mouse over river template with trigger.

When the cursor is moved away from the River visuals, the background and foreground colors automatically revert to their previous values. Triggers handle all of that functionality for us, as seen below:

Triggers undo their settings when not applied.

There are several kinds of triggers, each has a different way of determining when it should execute. In the remainder of this article, we will only focus on the two types of triggers used in the WPF Horse Race demo application: DataTrigger and MultiDataTrigger.

Triggers can be used in other places outside of templates, such as within Styles and on any FrameworkElement subclass, but this article only shows them being used within a DataTemplate. For more information about triggers, refer to the Triggers sub-section of the External links section toward the bottom of this article.

How the WPF Horse Race uses Data Templates and Triggers

The WPF Horse Race demo application has one custom DataTemplate, which is used to render instances of the RaceHorse class. The template declaration can be found in the RaceHorseDataTemplate.xaml file. The full template XAML is not shown in this article because it is rather lengthy and would serve no purpose being shown in its entirety. Instead we will examine it piecemeal.

The Visuals for a RaceHorse

The visuals elements in the RaceHorse data template can be distilled down to this basic structure:

<Border>
  <Grid>
    <StackPanel Orientation="Horizontal">
      <!-- This Rectangle is the "progress indicator"
           which follows the horse. -->
      <Rectangle />
      <!--This Image displays the picture of a race horse. -->
      <Image />
    </StackPanel>

    <!-- Displays the horse's name. -->
    <TextBlock />
  </Grid>
</Border>

Here is a visual explanation for how the elements in the template correspond to what you see as a RaceHorse runs a race:

A visual mapping of the data template to its resultant visuals.

(Note: The yellow rectangle in the image above, which represents the StackPanel, was added just for the sake of clarity. It does not actually appear on screen when the application is running.)

The root Border element defines the boundaries of the RaceHorse's "race pit" (the area in which it runs). The Border contains a Grid panel, which, in turn, contains a StackPanel and a TextBlock. The StackPanel holds the RaceHorse's progress indicator and a picture of a horse. A StackPanel was used to hold those two elements so that as the Rectangle (i.e. the progress indicator) becomes wider, the Image will be moved to the right along with it.

The TextBlock displays the RaceHorse's name. Since the TextBlock is declared beneath the StackPanel (lexically), it will be rendered on top of the StackPanel, with respect to the z-order. This ensures that the horse's name is always visible.

Setting properties on the winner of a race

After the template's visuals are declared, there are also some trigger declarations. One of them is a DataTrigger. A DataTrigger executes its contents when the supplied Binding returns a specific value. Here is the XAML for that trigger:

<!-- Set special values for the winner of the race. -->
<DataTrigger Binding="{Binding IsWinner}" Value="True">
    <Setter TargetName="progressIndicator"
            Property="Fill" Value="{StaticResource WinnerBrush}" />

    <Setter TargetName="horseName"
            Property="Foreground" Value="Black" />

    <Setter TargetName="horseName"
            Property="FontWeight" Value="Bold" />

    <Setter TargetName="horseName"
            Property="HorizontalAlignment" Value="Center" />
</DataTrigger>

That DataTrigger waits for the templated RaceHorse's IsWinner property to change from false to true. When that happens (i.e. the RaceHorse wins a race) all Setters contained within it are executed.

The Setter class provides a way to assign a value to a property. It is especially convenient when used in XAML, but can be used in the code-behind as well. When a RaceHorse wins a race the Setters modify the template's visuals, so that the progress indicator is rendered with a golden brush and its name stands out visually.

In the screenshot below, the RaceHorse named 'Fresh Spice' won the race:

Fresh Spice won the race.

When the next race starts and Fresh Spice's IsWinner property is set back to false, the properties affected by that trigger will automatically be reverted back to their previous values.

In case you are curious as to how the DataTrigger knows when the IsWinner property value changes, the answer lies in the fact that RaceHorse implements INotifyPropertyChanged. A RaceHorse object's PropertyChanged event is raised when its IsWinner property value changes. The Binding class is aware of the INotifyPropertyChanged interface and is notified when the PropertyChanged event has been raised for the IsWinner property.

Fading Away Losing RaceHorses

When a RaceHorse completes a race but does not finish first, it turns red and then fades away. By "fades away," I mean that the Opacity property of the RaceHorse's Border element is animated down to 60% over three quarters of a second. This article does not cover WPF's support for animation, but it is important to know that animations can be started in response to a trigger being executed.

In order to change the color of and fade away a RaceHorse which lost a race, a trigger must know two pieces of information: whether the RaceHorse finished the race yet, and if it lost the race. This type of "AND" expression can be implemented with a MultiDataTrigger, as seen below:

<!-- This MultiDataTrigger affects losers of the race. -->
<MultiDataTrigger>
  <MultiDataTrigger.Conditions>
    <Condition Binding="{Binding IsFinished}" Value="True" />
    <Condition Binding="{Binding IsWinner}" Value="False" />
  </MultiDataTrigger.Conditions>

  <!-- Apply the "finished the race" brush to
       the horse's progress indicator. -->
  <Setter TargetName="progressIndicator"
          Property="Fill" Value="{StaticResource FinishedBrush}" />

  <!-- Fade the race pit in and out if the horse lost the race. -->
  <MultiDataTrigger.EnterActions>
    <!-- Fade away the RaceHorse's Border element when it loses a race. -->
  </MultiDataTrigger.EnterActions>

  <MultiDataTrigger.ExitActions>
    <!-- Fade in the RaceHorse's Border element when a new race starts. -->
  </MultiDataTrigger.ExitActions>
</MultiDataTrigger>

External links

DataTemplates

Triggers

Other templates

History

  • April 6, 2007 – Created the article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here