C H A P T E R  30

WPF Resources, Animations, and Styles

This chapter introduces you to three important (and interrelated) topics that will deepen your understanding of the Windows Presentation Foundation (WPF) API. The first order of business is to learn the role of logical resources. As you will see, the logical resource (also known as an object resource) system is a way to name and refer to commonly used objects within a WPF application. While logical resources are often authored in XAML, they can also be defined in procedural code.

Next, you will learn how to define, execute, and control an animation sequence. Despite what you might think, WPF animations are not limited to video game or multimedia applications. Under the WPF API, animations can be as subtle as making a button appear to glow when it receives focus, or expanding the size of a selected row in a DataGrid. Understanding animations is a key aspect of building custom control templates (as you’ll see in Chapter 31).

We’ll wrap up by exploring the role of WPF styles. Much like a web page that uses CSS or the ASP.NET theme engine, a WPF application can define a common look and feel for a set of controls. You can define these styles in markup and store them as object resources for later use, and you can also apply them dynamically at runtime.

Understanding the WPF Resource System

Our first task is to examine the topic of embedding and accessing application resources. WPF supports two flavors of resources. The first is a binary resource, and this category typically includes items most programmers consider a resource in the traditional sense (embedded image files or sound clips, icons used by the application, and so on).

The second flavor, termed object resources or logical resources, represents a named .NET object that can be packaged and reused throughout the application. While any .NET object can be packaged as an object resource, logical resources are particularly helpful when working with graphical data of any sort, given that you can define commonly used graphic primitives (brushes, pens, animations, etc.) and refer to them when required.

Working with Binary Resources

Before we get to the topic of object resources, let’s quickly examine how to package up binary resources such as icons or image files (e.g., company logos or images for an animation) into your applications. If you’d like to follow along, create a new WPF application named BinaryResourcesApp using Visual Studio. Update the markup for your initial window to use a DockPanel as the layout root, like so:

<Window x:Class="BinaryResourcesApp.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Fun with Binary Resources" Height="500" Width="649">

  <DockPanel LastChildFill="True">
  </DockPanel>

</Window>

Now, let’s say your application needs to display one of three image files inside part of the window, based on user input. The WPF Image control can be used to not only display a typical image file (*.bmp, *.gif, *.ico, *.jpg, *.png, *.wdp, or *.tiff) but also data in a DrawingImage (as you saw in Chapter 29). You might build a UI for your window that supports a DockPanel containing a simple toolbar with Next and Previous buttons. Below this toolbar you can place an Image control, which currently does not have a value set to the Source property, as we will do this in code:

<Window x:Class="BinaryResourcesApp.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Fun with Binary Resources" Height="500" Width="649">

  <DockPanel LastChildFill="True">
    <ToolBar Height="60" Name="picturePickerToolbar" DockPanel.Dock="Top">
      <Button x:Name="btnPreviousImage" Height="40" Width="100" BorderBrush="Black"
              Margin="5" Content="Previous" Click="btnPreviousImage_Click"/>
      <Button x:Name="btnNextImage" Height="40" Width="100" BorderBrush="Black"
              Margin="5" Content="Next" Click="btnNextImage_Click"/>
    </ToolBar>

    <!-- We will fill this Image in code. -->
    <Border BorderThickness="2" BorderBrush="Green">
      <Image x:Name="imageHolder" Stretch="Fill" />
    </Border>
  </DockPanel>

</Window>

Please note that the Click event has been handled for each Button object. Assuming you have used the IDE to handle these events, you will have two empty methods in your C# code file. So, how can we code the Click event handlers to cycle through the image data? More importantly, do we want to have the image data located on the user’s hard drive or embedded in our compiled assembly? Let’s examine our options.

Including Loose Resource Files in a Project

Let’s assume you want to ship your image files as a set of loose files in a subdirectory of the application install path. Using the Solution Explorer window of Visual Studio, you can right-click on your project node and select the Add images New Folder menu option to create such a subdirectory, which I have called Images.

Now, when you right-click on this folder, you can select the Add images Existing Item menu option, to copy the image files into the new subdirectory. In the downloadable source code for this project, you will find three image files named Welcome.jpg, Dogs.jpg, and Deer.jpg that you can include in this project, or simply add three image files of your choice. Figure 30-1 shows the current setup.

images

Figure 30-1. A new subdirectory that contains image data in our WPF project

Configuring the Loose Resources

When you want Visual Studio to copy project content to your output directory, you need to adjust a few settings using the Properties window. To ensure that the content of your Images folder is copied to the inDebug folder, begin by selecting each image in the Solution Explorer. Now, with these images still selected, use the Properties window to set the Build Action property to Resource, and the Copy to Output Directory property to Copy always (see Figure 30-2).

images

Figure 30-2. Configuring the image data to be copied to our output directory

If you recompile your program, you can now click on the Show all Files button of the Solution Explorer and view the copied Image folder under your inDebug directory (you might need to click the refresh button). See Figure 30-3.

images

Figure 30-3. The copied data

Programmatically Loading an Image

WPF provides a class named BitmapImage, which is part of the System.Windows.Media.Imaging namespace. This class allows you to load data from an image file whose location is represented by a System.Uri object. If you handle the Loaded event of your window, you might fill a List<T> of BitmapImages like so:

public partial class MainWindow : Window
{
  // A List of BitmapImage files.
  List<BitmapImage> images = new List<BitmapImage>();

  // Current position in the list.
  private int currImage = 0;
  private const int MAX_IMAGES = 2;

  private void Window_Loaded(object sender, RoutedEventArgs e)
  {
    try
    {
      string path = Environment.CurrentDirectory;

      // Load these images when the window loads.
      images.Add(new BitmapImage(new Uri(string.Format(@"{0}ImagesDeer.jpg", path))));
      images.Add(new BitmapImage(new Uri(string.Format(@"{0}ImagesDogs.jpg", path))));
      images.Add(new BitmapImage(new Uri(string.Format(@"{0}ImagesWelcome.jpg", path))));

      // Show first image in the List<>.
      imageHolder.Source = images[currImage];                
    }
    catch (Exception ex)
    {
      MessageBox.Show(ex.Message);
    }
  }
...
}

Notice that this class also defines an int member variable (currImage) that will allow the Click event handlers to loop through each item in the List<T> and display it in the Image control by setting the Source property. (Here, our Loaded event handler sets the Source property to the first image in the List<T>.) In addition, our MAX_IMAGES constant will let us test for upper and lower limits as we iterate over the list. Here are the Click handlers that do exactly this:

private void btnPreviousImage_Click(object sender, RoutedEventArgs e)
{                  
  if (--currImage < 0)
    currImage = MAX_IMAGES;          
  imageHolder.Source = images[currImage];
}

private void btnNextImage_Click(object sender, RoutedEventArgs e)
{
  if (++currImage > MAX_IMAGES)
    currImage = 0;        
  imageHolder.Source = images[currImage];
}

At this point, you can run your program and flip through each picture.

Embedding Application Resources

If you’d rather configure your image files to be compiled directly into your .NET assembly as binary resources, select the image files in Solution Explorer (in the Images folder, not in the inDebugImages folder). Then change the Build Action property to Resource, and the Copy to Output Directory property to Do not copy (see Figure 30-4).

images

Figure 30-4. Configuring the images to be embedded resources

Now, using Visual Studio’s Build menu, select the Clean Solution option to wipe out the current contents of inDebugImages, then rebuild your project. Refresh the Solution Explorer, and observe the absence of data in your inDebugImages directory. With the current build options, your graphical data is no longer copied to the output folder and is now embedded within the assembly itself.

With this adjustment, we will now need to modify our code to load these images by extracting them from the compiled assembly:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
  try
  {
    images.Add(new BitmapImage(new Uri(@"/Images/Deer.jpg", UriKind.Relative)));
    images.Add(new BitmapImage(new Uri(@"/Images/Dogs.jpg", UriKind.Relative)));
    images.Add(new BitmapImage(new Uri(@"/Images/Welcome.jpg", UriKind.Relative)));
    imageHolder.Source = images[currImage];                
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
}

Notice in this case, we no longer need to determine the installation path and can simply list the resources by name, which takes into account the name of the original subdirectory. Also notice, when we create our Uri objects, we specify a UriKind value of Relative. In any case, at this point our executable is a stand-alone entity that can be run from any location on the machine, as all compiled data is within the binary. Figure 30-5 shows the completed application.

images

Figure 30-5. Our simple picture viewer

Images Source Code The BinaryResourcesApp can be found under the Chapter 30 subdirectory.

Working with Object (Logical) Resources

When you are building a WPF application, it is very common to define a blurb of XAML to use in multiple locations within a window, or perhaps across multiple windows or projects. For example, say you have created the perfect linear gradient brush, which consists of ten lines of markup. Now, you want to use that brush as the background color for every Button control in the project (which consists of eight windows) for a total of 16 Buttons.

The worst thing you could do is to copy and paste the XAML to each and every control. Clearly, this would be a nightmare to maintain, as you would need to make numerous changes anytime you wanted to tweak the look and feel of the brush.

Thankfully, object resources allow us to define a blob of XAML, give it a name, and store it in a fitting dictionary for later use. Like a binary resource, object resources are often compiled into the assembly that requires them. However, you don’t need to tinker with the Build Action property to do so. As long as you place your XAML into the correct location, the compiler will take care of the rest.

Working with object resources is a big part of WPF development. As you will see, object resources can be far more complex than a custom brush. You could define a XAML-based animation, a 3D rendering, a custom control style, data template, control template, and more, and package each one as a reusable resource.

The Role of the Resources Property

As mentioned, object resources must be placed in a fitting dictionary object in order to be used across an application. As it stands, every descendant of FrameworkElement supports a Resources property. This property encapsulates a ResourceDictionary object that contains the defined object resources. The ResourceDictionary can hold any type of item as it operates on System.Object types and may be manipulated via XAML or procedural code.

In WPF, all controls, Windows, Pages (used when building navigation applications or XBAP programs), and UserControls extend FrameworkElement, so just about all widgets provide access to a ResourceDictionary. Furthermore, the Application class, while not extending FrameworkElement, supports an identically named Resources property for the same purpose.

Defining Window-Wide Resources

To begin exploring the role of object resources, create a new WPF application named ObjectResourcesApp using Visual Studio and change the initial Grid to a horizontally aligned StackPanel layout manager. Into this StackPanel, define two Button controls (we really don’t need much to illustrate the role of object resources, so this will do):

<Window x:Class="ObjectResourcesApp.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Fun with Object Resources" Height="350" Width="525">

  <StackPanel Orientation="Horizontal">
    <Button Margin="25" Height="200" Width="200" Content="OK" FontSize="20"/>
    <Button Margin="25" Height="200" Width="200" Content="Cancel" FontSize="20"/>
  </StackPanel>

</Window>

Now, select the OK button and set the Background color property to a custom brush type using the integrated brush editor (discussed in Chapter 29). After you’ve done so, notice how the brush is embedded within the scope of the <Button> and </Button> tags, as shown here:

<Button Margin="25" Height="200" Width="200" Content="OK" FontSize="20">
  <Button.Background>
    <RadialGradientBrush>
      <GradientStop Color="#FFC44EC4" Offset="0" />
      <GradientStop Color="#FF829CEB" Offset="1" />
      <GradientStop Color="#FF793879" Offset="0.669" />
    </RadialGradientBrush>
  </Button.Background>
</Button>

To allow the Cancel button to use this brush as well, we should promote the scope of our <RadialGradientBrush> to a parent element’s resource dictionary. For example, if we move it to the <StackPanel>, both buttons can use the same brush, as they are child elements of the layout manager. Even better, we could package the brush into the resource dictionary of the window itself, so all aspects of the window’s content (nested panels, etc.) can freely make use of it.

When you need to define a resource, you use property element syntax to set the Resources property of the owner. You also give the resource item an x:Key value, which will be used by other parts of the window when they want to refer to the object resource. Be aware that x:Key and x:Name are not the same! The x:Name attribute allows you to gain access to the object as a member variable in your code file, while the x:Key attribute allows you to refer to an item in a resource dictionary.

Visual Studio allows you to promote a resource to a higher scope using their respective Properties windows. To do so, first identify the property that has the complex object you want to package as a resource (the Background property, in our example). Next to the property, you’ll see a small white square that, when clicked, will open a pop-up menu. From here, select the Convert to New Resource... option (see Figure 30-6).

images

Figure 30-6. Moving a complex object into a resource container

You are now asked to name your resource (myBrush), and specify where to place it. For this example, leave the default selection of the current document (see Figure 30-7).

images

Figure 30-7. Naming the object resource

When you’re done, the markup will be restructured like this:

<Window x:Class="ObjectResourcesApp.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Fun with Object Resources" Height="350" Width="525">

  <Window.Resources>
    <RadialGradientBrush x:Key="myBrush">
      <GradientStop Color="#FFC44EC4" Offset="0" />
      <GradientStop Color="#FF829CEB" Offset="1" />
      <GradientStop Color="#FF793879" Offset="0.669" />
    </RadialGradientBrush>
  </Window.Resources>

  <StackPanel Orientation="Horizontal">
    <Button Margin="25" Height="200" Width="200" Content="OK"
            FontSize="20" Background="{StaticResource myBrush}"></Button>
    <Button Margin="25" Height="200" Width="200" Content="Cancel" FontSize="20"/>
  </StackPanel>

</Window>

Notice the new <Window.Resources> scope, which now contains the RadialGradientBrush object, which has a key value of myBrush.

The {StaticResource} Markup Extension

The other change that took place when we extracted our object resource was that the property that was the target of the extraction (again, Background) now makes use of the {StaticResource} markup extension. As you can see, the key name is specified as an argument. Now, if the Cancel button opts to use the same brush to paint its background, it is free to do so. Or, if the Cancel button had some complex content, any subelement of that Button could also use the window-level resource—for example, the Fill property of an Ellipse:

<StackPanel Orientation="Horizontal">
  <Button Margin="25" Height="200" Width="200" Content="OK" FontSize="20"  
          Background="{StaticResource myBrush}">
  </Button>

  <Button Margin="25" Height="200" Width="200" FontSize="20">
    <StackPanel>
      <Label HorizontalAlignment="Center" Content= "No Way!"/>
      <Ellipse Height="100" Width="100" Fill="{StaticResource myBrush}"/>
    </StackPanel>
  </Button>
</StackPanel>

The {DynamicResource} Markup Extension

It is also possible for a property to use the {DynamicResource} markup extension when connecting to a keyed resource. To understand the difference, name your OK button btnOK and handle the Click event. In this event handler, use the Resources property to obtain the custom brush, and then change some aspect of it, like so:

private void btnOK_Click(object sender, RoutedEventArgs e)
{
  // Get the brush and make a change.
  RadialGradientBrush b = (RadialGradientBrush)Resources["myBrush"];
  b.GradientStops[1] = new GradientStop(Colors.Black, 0.0);
}

Images Note We are using the Resources indexer to locate a resource by name here. Be aware, however, that this will throw a runtime exception if the resource can’t be found. You could also use the TryFindResource() method, which will not throw a runtime error but simply return null if the specified resource can’t be located.

If you run this application and click the OK button, you will see the brush’s change is accounted for and each button updates to render the modified brush. However, what if you completely changed the type of brush specified by the myBrush key? For example:

private void btnOK_Click(object sender, RoutedEventArgs e)
{
  // Put a totally new brush into the myBrush slot.
  Resources["myBrush"] = new SolidColorBrush(Colors.Red);
}

This time, when you click the button, neither updates as expected. This is because the {StaticResource} markup extension applies the resource only once and stays “connected” to the original object during the life of the application. However, if we change each occurrence of {StaticResource} to {DynamicResource} in our markup, we find our custom brush has been replaced with the expected solid red brush.

Essentially, the {DynamicResource} markup extension is able to detect whether the underlying keyed object has been replaced with a new object. As you might guess, this requires some extra runtime infrastructure, so you should typically stick to using {StaticResource} unless you know you have an object resource that will be swapped with a different object at runtime, and you want all items using that resource to be informed.

Application-Level Resources

When you have object resources in a window’s resource dictionary, all items in the window are free to make use of it, but other windows in the application cannot. Give your Cancel button a name of btnCancel and handle the Click event. Insert a new window into your current project (named TestWindow.xaml) that contains a single Button, which, when clicked, will close the window.

public partial class TestWindow : Window
{
  public TestWindow()
  {
    InitializeComponent();
  }

  private void btnClose_Click(object sender, RoutedEventArgs e)
  {
    this.Close();
  }
}

Now, in the Click handler of the Cancel button on your first window, just load and display this new window, like so:

private void btnCancel_Click(object sender, RoutedEventArgs e)
{
  TestWindow w = new TestWindow();
  w.Owner = this;
  w.WindowStartupLocation = WindowStartupLocation.CenterOwner;
  w.ShowDialog();
}

So, if the new window wants to use myBrush, it currently won’t be able to as it is not within the correct “scope.” The solution is to define the object resource at the application level, rather than at the level of a specific window. There is no way to automate this within Visual Studio, so simply cut the current brush object out of the <Windows.Resources> scope, and place it in the <Application.Resources> scope in your App.xaml file.

<Application x:Class="ObjectResourcesApp.App"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  StartupUri="MainWindow.xaml">

  <Application.Resources>
    <RadialGradientBrush x:Key="myBrush">
      <GradientStop Color="#FFC44EC4" Offset="0" />
      <GradientStop Color="#FF829CEB" Offset="1" />
      <GradientStop Color="#FF793879" Offset="0.669" />
    </RadialGradientBrush>
  </Application.Resources>

</Application>

Now your TestWindow is free to use this same brush to paint its background. If you find the Background property for this new Window, click the “Brush resources” tab to view your your application-level resources (see Figure 30-8)

images

Figure 30-8. Applying application-level resources

Defining Merged Resource Dictionaries

Application-level resources are a good starting point, but what if you need to define a set of complex (or not so complex) resources that need to be reused across multiple WPF projects? In this case, you want to define what is known as a merged resource dictionary. This is nothing more than a .xaml file that contains a collection of object resources. A single project can have as many of these files as required (one for brushes, one for animations, and so forth), each of which can be inserted using the Add New Item dialog box activated via the Project menu (see Figure 30-9).

images

Figure 30-9. Inserting a new merged resource dictionary

In the new MyBrushes.xaml file, we will want to cut the current resources in the Application.Resources scope and move them into our dictionary, like so:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <RadialGradientBrush x:Key="myBrush">
    <GradientStop Color="#FFC44EC4" Offset="0" />
    <GradientStop Color="#FF829CEB" Offset="1" />
    <GradientStop Color="#FF793879" Offset="0.669" />
  </RadialGradientBrush>

</ResourceDictionary>

Now, even though this resource dictionary is part of our project, we will get runtime errors. The reason is that all resource dictionaries must be merged (typically at the application level) into an existing resource dictionary. To do this, use the following format (note that multiple resource dictionaries can be merged by adding multiple <ResourceDictionary> elements within the

<ResourceDictionary.MergedDictionaries> scope): <Application x:Class="ObjectResourcesApp.App"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  StartupUri="MainWindow.xaml">
    
  <!-- Bring in the logical resources
       from the MyBrushes.xaml file. -->
  <Application.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source = "MyBrushes.xaml"/>
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </Application.Resources>

</Application>

Defining a Resource-Only Assembly

Last but not least, it is possible to create .NET class libraries that contain nothing but dictionaries of object resources. This can be useful if you have defined a set of themes that need to be used on a machine-wide level. You could package up the object resource into a dedicated assembly, and then applications that need to make use of them could load them into memory.

The easiest way to build a resource-only assembly is to actually begin with a WPF User Control Library project. Add such a project (named MyBrushesLibrary) to your current solution, using the Add images New Project menu option of Visual Studio (see Figure 30-10).

images

Figure 30-10. Adding a User Control Library as a starting point for a resource-only library

Now, completely delete the UserControl1.xaml file from the project (the only items we really want are the referenced WPF assemblies). Next, drag and drop the MyBrushes.xaml file into your MyBrushesLibrary project and delete it from the ObjectResourcesApp project. Your Solution Explorer should now look like Figure 30-11.

images

Figure 30-11. Moving the MyBrushes.xaml file into our new library project

Compile your User Control Library project. Next, reference this library from the ObjectResourcesApp project using the Add Reference dialog box. Now, you will want to merge these binary resources into the application-level resource dictionary of the ObjectResourcesApp project. Doing so, however, requires some rather funky syntax, shown here:

<Application.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
      <!--The syntax is /NameOfAssembly;Component/NameOfXamlFileInAssembly.xaml -->
      <ResourceDictionary Source = "/MyBrushesLibrary;Component/MyBrushes.xaml"/>
    </ResourceDictionary.MergedDictionaries>
  </ResourceDictionary>
</Application.Resources>

First, be aware that this string is space-sensitive. If you have extra white space around your semicolon or forward slashes, you will generate runtime errors. The first part of the string is the friendly name of the external library (no file extension). After the semicolon, you type in the word Component followed by the name of the compiled binary resource, which will be identical to the original XAML resource dictionary.

That wraps up our examination of WPF’s resource management system. You will make good use of these techniques for most of your applications, as well as during the remainder of the WPF chapters of this text. Next up, let’s investigate the integrated animation API of Windows Presentation Foundation.

Images Source Code The ObjectResourcesApp project can be found under the Chapter 30 subdirectory.

Understanding WPF’s Animation Services

In addition to the graphical rendering services you examined in Chapter 29, WPF supplies a programming interface to support animation services. The term animation may bring to mind visions of spinning company logos, a sequence of rotating image resources (to provide the illusion of movement), text bouncing across the screen, or specific types of programs such as video games or multimedia applications.

While WPF’s animation APIs could certainly be used for such purposes, animation can be used any time you want to give an application additional flair. For example, you could build an animation for a button on a screen that magnifies slightly when the mouse cursor hovers within its boundaries (and shrinks back once the mouse cursor moves beyond the boundaries). Or you could animate a window so that it closes using a particular visual appearance, such as slowly fading into transparency. In fact, WPF’s animation support can be used within any sort of application (a business application, multimedia programs, video games, etc.) whenever you want to provide a more engaging user experience.

As with many other aspects of WPF, the notion of building animations is nothing new. What is new is that, unlike other APIs you might have used in the past (including Windows Forms), developers are not required to author the necessary infrastructure by hand. Under WPF, there’s no need to create the background threads or timers used to advance the animation sequence, define custom types to represent the animation, erase and redraw images, or bother with tedious mathematical calculations. Like other aspects of WPF, we can build an animation entirely using XAML, entirely using C# code, or using a combination of the two.

Images Note Visual Studio has no support for authoring animations using GUI animation tools. If you author an animation with Visual Studio, you will do so by typing in the XAML directly. However, Expression Blend does indeed have a built-in animation editor that can simplify your life a good deal.

The Role of the Animation Class Types

To understand WPF’s animation support, we must begin by examining the animation classes within the System.Windows.Media.Animation namespace of PresentationCore.dll. Here you will find over 100 different class types that are named using the Animation token.

All of these classes can be placed into one of three broad categories. First, any class that follows the name convention DataTypeAnimation (ByteAnimation, ColorAnimation, DoubleAnimation, In32Animation, etc.) allows you to work with linear interpolation animations. This enables you to change a value smoothly over time from a start value to a final value.

Next, the classes that follow the naming convention DataTypeAnimationUsingKeyFrames (StringAnimationUsingKeyFrames, DoubleAnimationUsingKeyFrames, PointAnimationUsingKeyFrames, etc.) represent “key frame animations,” which allow you to cycle through a set of defined values over a period of time. For example, you could use key frames to change the caption of a button by cycling through a series of individual characters.

Finally, classes that follow the DataTypeAnimationUsingPath naming convention (DoubleAnimationUsingPath, PointAnimationUsingPath, among others) are path-based animations that allow you to animate objects to move along a path you define. By way of an example, if you were building a GPS application, you could use a path-based animation to move an item along the quickest travel route to the user’s destination.

Now, obviously, these classes are not used to somehow provide an animation sequence directly to a variable of a particular data type (after all, how exactly could we animate the value “9” using an Int32Animation?).

For example, consider the Label type’s Height and Width properties, both of which are dependency properties wrapping a double. If you wanted to define an animation that would increase the height of a label over a time span, you could connect a DoubleAnimation object to the Height property and allow WPF to take care of the details of performing the actual animation itself. By way of another example, if you wanted to transition the color of a brush type from green to yellow over a period of five seconds, you could do so using the ColorAnimation type.

To be very clear, these Animation classes can be connected to any dependency property of a given object that matches the underlying types. As explained in Chapter 31, dependency properties are a specialized form of property required by many WPF services including animation, data binding, and styles.

By convention, a dependency property is defined as a static, read-only field of the class, and is named by suffixing the word Property to the normal property name. For example, the dependency property for the Height property of a Button would be accessed in code using Button.HeightProperty.

The To, From, and By Properties

All Animation classes define the following handful of key properties that control the starting and ending values used to perform the animation:

  • To: This property represents the animation’s ending value.
  • From: This property represents the animation’s starting value.
  • By: This property represents the total amount by which the animation changes its starting value.

Despite the fact that all Animation classes support the To, From, and By properties, they do not receive them via virtual members of a base class. The reason for this is that the underlying types wrapped by these properties vary greatly (integers, colors, Thickness objects, etc.), and representing all possibilities using a single base class would result in very complex coding constructs.

On a related note, you might also wonder why .NET generics were not used to define a single generic animation class with a single type parameter (e.g., Animate<T>). Again, given that there are so many underlying data types (colors, vectors, ints, strings, etc.) used to animated dependency properties, it would not be as clean a solution as you might expect (not to mention XAML has only limited support for generic types).

The Role of the Timeline Base Class

Although a single base class was not used to define virtual To, From, and By properties, the Animation classes do share a common base class: System.Windows.Media.Animation.Timeline. This type provides a number of additional properties that control the pacing of the animation, as described in Table 30-1.

Images

Authoring an Animation in C# Code

Specifically, we will build a Window that contains a Button, which has the odd behavior of spinning in a circle (based on the upper-left corner) whenever the mouse enters its surface area. Begin by creating a new WPF application named SpinningButtonAnimationApp, using Visual Studio. Update the initial markup like the following (note we are handling the button’s MouseEnter event):

<Window x:Class="SpinningButtonAnimationApp.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Animations in C# code" Height="350"
  Width="525" WindowStartupLocation="CenterScreen">
  <Grid>
    <Button x:Name="btnSpinner" Height="50" Width="100" Content="I Spin!"
            MouseEnter="btnSpinner_MouseEnter"/>
  </Grid>
</Window>

Now, import the System.Windows.Media.Animation namespace and add the following code in the window’s C# code file:

public partial class MainWindow : Window
{
  private bool isSpinning = false;

  private void btnSpinner_MouseEnter(object sender, MouseEventArgs e)
  {
    if (!isSpinning)
    {
      isSpinning = true;

      // Make a double animation object, and register
      // with the Completed event.
      DoubleAnimation dblAnim = new DoubleAnimation();
      dblAnim.Completed += (o, s) => { isSpinning = false; };

      // Set the start value and end value.
      dblAnim.From = 0;
      dblAnim.To = 360;

      // Now, create a RotateTransform object, and set
      // it to the RenderTransform property of our
      // button.
      RotateTransform rt = new RotateTransform();
      btnSpinner.RenderTransform = rt;

      // Now, animation the RotateTransform object.
      rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim);
    }
  }
}

The first major task of this method is to configure a DoubleAnimation object, which will start at the value 0 and end at the value 360. Notice that we are handling the Completed event on this object as well, to toggle a class-level bool variable that is used to ensure that if an animation is currently being performed, we don’t “reset” it to start again.

Next, we create a RotateTransform object that is connected to the RenderTransform property of our Button control (btnSpinner). Last but not least, we inform the RenderTransform object to begin animating its Angle property using our DoubleAnimation object. When you are authoring animations in code, you typically do so by calling BeginAnimation(), and pass in the underlying dependency property you would like to animate (remember, by convention, this is a static field on the class), followed by a related animation object.

Let’s add another animation to the program, which will cause the button to fade into invisibility when clicked. First, handle the Click event of the btnSpinner object, then add the following code in the resulting event handler:

private void btnSpinner_Click(object sender, RoutedEventArgs e)
{
  DoubleAnimation dblAnim = new DoubleAnimation();
  dblAnim.From = 1.0;
  dblAnim.To = 0.0;
  btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}

Here, we are changing the Opacity property value to fade the button out of view. Currently, however, this is hard to do, as the button is spinning very fast! How, then, can we control the pace of an animation? Glad you asked.

Controlling the Pace of an Animation

By default, an animation will take approximately one second to transition between the values assigned to the From and To properties. Therefore, our button has one second to spin around a full 360-degree angle, while the button will fade away to invisibility (when clicked) over the course of one second.

If you want to define a custom amount of time for an animation’s transition, you may do so via the animation object’s Duration property, which can be set to an instance of a Duration object. Typically, the time span is established by passing a TimeSpan object to the Duration’s constructor. Consider the following update that will give the button a full four seconds to rotate:

private void btnSpinner_MouseEnter(object sender, MouseEventArgs e)
{
  if (!isSpinning)
  {
    isSpinning = true;

    // Make a double animation object, and register
    // with the Completed event.
    DoubleAnimation dblAnim = new DoubleAnimation();
    dblAnim.Completed += (o, s) => { isSpinning = false; };

    // Button has four seconds to finish the spin!
    dblAnim.Duration = new Duration(TimeSpan.FromSeconds(4));
...
  }
}

With this adjustment, you should have a fighting chance to click the button while it is spinning, at which point it will fade away.

Images Note The BeginTime property of an Animation class also takes a TimeSpan object. Recall that this property can be set to establish a wait time before starting an animation sequence.

Reversing and Looping an Animation

You can also tell Animation objects to play an animation in reverse at the completion of the animation sequence by setting the AutoReverse property to true. For example, if you want to have the button come back into view after it has faded away, you could author the following:

private void btnSpinner_Click(object sender, RoutedEventArgs e)
{
  DoubleAnimation dblAnim = new DoubleAnimation();
  dblAnim.From = 1.0;
  dblAnim.To = 0.0;

  // Reverse when done.
  dblAnim.AutoReverse = true;
  btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}

If you’d like to have an animation repeat some number of times (or to never stop once activated), you can do so using the RepeatBehavior property, which is common to all Animation classes. If you pass in a simple numerical value to the constructor, you can specify a hard-coded number of times to repeat. On the other hand, if you pass in a TimeSpan object to the constructor, you can establish an amount of time the animation should repeat. Finally, if you want an animation to loop ad infinitum, you can simply specify RepeatBehavior.Forever. Consider the following ways we could change the repeat behaviors of either of the DoubleAnimation objects used in this example:

// Loop forever.
dblAnim.RepeatBehavior = RepeatBehavior.Forever;

// Loop three times.
dblAnim.RepeatBehavior = new RepeatBehavior(3);

// Loop for 30 seconds.
dblAnim.RepeatBehavior = new RepeatBehavior(TimeSpan.FromSeconds(30));

That wraps up our investigation about how to animate aspects of an object using C# code and the WPF animation API. Next, we will learn how to do the same using XAML.

ImagesSource Code The SpinningButtonAnimationApp project can be found under the Chapter 30 subdirectory.

Authoring Animations in XAML

Authoring animations in markup is similar to authoring them in code, at least for simple, straightforward animation sequences. When you need to capture more complex animations, which may involve changing the values of numerous properties at once, the amount of markup can grow considerably. Even if you use a tool to generate XAML-based animations, it is important to know the basics of how an animation is represented in XAML, as this will make it easier for you to modify and tweak tool-generated content.

Images Note You will find a number of XAML files in the XamlAnimations folder of the downloadable source code. As you go through the next several pages, copy these markup files into your custom XAML editor, or into the Kaxaml editor, to see the results.

For the most part, creating an animation is similar to what you have already seen. You still configure an Animation object and associate it to an object’s property. One big difference, however, is that WPF is not function call–friendly. As a result, instead of calling BeginAnimation(), you use a storyboard as a layer of indirection.

Let’s walk through a complete example of an animation defined in terms of XAML, followed by a detailed breakdown. The following XAML definition will display a window that contains a single label. As soon as the Label object loads into memory, it begins an animation sequence in which the font size increases from 12 points to 100 over a period of four seconds. The animation will repeat for as long as the Window object is loaded in memory. You can find this markup in the GrowLabelFont.xaml file, so copy it into your MyXamlPad.exe application (or Kaxaml) and observe the behavior.

<Window
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Height="200" Width="600" WindowStartupLocation="CenterScreen" Title="Growing Label Font!">
  <StackPanel>
    <Label Content = "Interesting...">
      <Label.Triggers>
        <EventTrigger RoutedEvent = "Label.Loaded">
          <EventTrigger.Actions>
            <BeginStoryboard>
              <Storyboard TargetProperty = "FontSize">
                <DoubleAnimation From = "12" To = "100" Duration = "0:0:4"
                                 RepeatBehavior = "Forever"/>
              </Storyboard>
            </BeginStoryboard>
          </EventTrigger.Actions>
        </EventTrigger>
      </Label.Triggers>
    </Label>
  </StackPanel>
</Window>

Now, lets break this example down, bit by bit.

The Role of Storyboards

Working from the innermost element outward, we first encounter the <DoubleAnimation> element, which makes use of the same properties we set in procedural code (From, To, Duration, and RepeatBehavior).

<DoubleAnimation From = "12" To = "100" Duration = "0:0:4"
                 RepeatBehavior = "Forever"/>

As mentioned, Animation elements are placed within a <Storyboard> element, which is used to map the animation object to a given property on the parent type via the TargetProperty property—which in this case is FontSize. A <Storyboard> is always wrapped in a parent element named <BeginStoryboard>.

<BeginStoryboard>
  <Storyboard TargetProperty = "FontSize">
    <DoubleAnimation From = "12" To = "100" Duration = "0:0:4"
                     RepeatBehavior = "Forever"/>
  </Storyboard>
</BeginStoryboard>

The Role of Event Triggers

After the <BeginStoryboard> element has been defined, we need to specify some sort of action that will cause the animation to begin executing. WPF has a few different ways to respond to runtime conditions in markup, one of which is termed a trigger. From a high level, you can consider a trigger a way of responding to an event condition in XAML, without the need for procedural code.

Typically, when you respond to an event in C#, you author custom code that will execute when the event occurs. A trigger, however, is just a way to be notified that some event condition has happened (“I’m loaded into memory”, “The mouse is over me!”, “I have focus!”).

Once you’ve been notified that an event condition has occurred, you can start the storyboard. In this example, we are responding to the Label being loaded into memory. Because it is the Label’s Loaded event we are interested in, the <EventTrigger> is placed in the Label’s trigger collection.

<Label Content = "Interesting...">
  <Label.Triggers>
    <EventTrigger RoutedEvent = "Label.Loaded">
      <EventTrigger.Actions>
        <BeginStoryboard>
          <Storyboard TargetProperty = "FontSize">
            <DoubleAnimation From = "12" To = "100" Duration = "0:0:4"
                             RepeatBehavior = "Forever"/>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger.Actions>
    </EventTrigger>
  </Label.Triggers>
</Label>

Let’s see another example of defining an animation in XAML, this time using a key frame animation.

Animation Using Discrete Key Frames

Unlike the linear interpolation animation objects, which can only move between a starting point and an ending point, the key frame counterparts allow us to create a collection of specific values for an animation that should take place at specific times.

To illustrate the use of a discrete key frame type, assume you want to build a Button control that animates its content so that over the course of three seconds, the value “OK!” appears, one character at a time. You’ll find the following markup in the StringAnimation.xaml file. Copy this markup into your MyXamlPad.exe program (or Kaxaml) and view the results.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Height="100" Width="300"
  WindowStartupLocation="CenterScreen" Title="Animate String Data!">
  <StackPanel>
    <Button Name="myButton" Height="40"
            FontSize="16pt" FontFamily="Verdana" Width = "100">
      <Button.Triggers>
        <EventTrigger RoutedEvent="Button.Loaded">
          <BeginStoryboard>
            <Storyboard>
              <StringAnimationUsingKeyFrames RepeatBehavior = "Forever"
                  Storyboard.TargetName="myButton"
                  Storyboard.TargetProperty="Content"
                  Duration="0:0:3">
                <DiscreteStringKeyFrame Value="" KeyTime="0:0:0" />
                <DiscreteStringKeyFrame Value="O" KeyTime="0:0:1" />
                <DiscreteStringKeyFrame Value="OK" KeyTime="0:0:1.5" />
                <DiscreteStringKeyFrame Value="OK!" KeyTime="0:0:2" />
              </StringAnimationUsingKeyFrames>
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </Button.Triggers>
    </Button>
  </StackPanel>
</Window>

First, notice that we have defined an event trigger for our button to ensure that our storyboard executes when the button has loaded into memory. The StringAnimationUsingKeyFrames class is in charge of changing the content of the button, via the Storyboard.TargetName and Storyboard.TargetProperty values.

Within the scope of the <StringAnimationUsingKeyFrames> element, we defined four DiscreteStringKeyFrame elements, which change the button’s Content property over the course of two seconds (note that the duration established by StringAnimationUsingKeyFrames is a total of three seconds, so we will see a slight pause between the final “!” and looping “O”).

Now that you have a better feel for how to build animations in C# code and XAML, let’s turn our attention of the role of WPF styles, which make heavy use of graphics, object resources, and animations.

Images Source Code These loose XAML files can be found under the XamlAnimations subdirectory of Chapter 30.

Understanding the Role of WPF Styles

When you are building the UI of a WPF application, it is not uncommon for a family of controls to require a shared look and feel. For example, you might want all button types have the same height, width, background color, and font size for their string content. Though you could handle this by setting each button’s individual properties to identical values, such an approach makes it difficult to implement changes down the road, as you’d need to reset the same set of properties on multiple objects for every change.

Thankfully, WPF offers a simple way to constrain the look and feel of related controls using styles. Simply put, a WPF style is an object that maintains a collection of property/value pairs. Programmatically speaking, an individual style is represented using the System.Windows.Style class. This class has a property named Setters, which exposes a strongly typed collection of Setter objects. It is the Setter object that allows you to define the property/value pairs.

In addition to the Setters collection, the Style class also defines a few other important members that allow you to incorporate triggers, restrict where a style can be applied, and even create a new style based on an existing style (think of it as “style inheritance”). In particular, be aware of the following members of the Style class:

  • Triggers: Exposes a collection of trigger objects, which allow you to capture various event conditions within a style.
  • BasedOn: Allows you to build a new style based on an existing style.
  • TargetType: Allows you to constrain where a style can be applied.

Defining and Applying a Style

In almost every case, a Style object will be packaged as an object resource. Like any object resource, you can package it at the window or application level, as well as within a dedicated resource dictionary (this is great, because it makes the Style object easily accessible throughout your application). Now recall that the goal is to define a Style object that fills (at minimum) the Setters collection with a set of property/value pairs.

Create a new WPF application named WpfStyles using Visual Studio. Let’s build a style that captures the basic font characteristics of a control in our application. Open your App.xaml file and define the following named style:

<Application x:Class="WpfStyles.App"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  StartupUri="MainWindow.xaml">

  <Application.Resources>
    <Style x:Key ="BasicControlStyle">
      <Setter Property = "Control.FontSize" Value ="14"/>
      <Setter Property = "Control.Height" Value = "40"/>
      <Setter Property = "Control.Cursor" Value = "Hand"/>
    </Style>
  </Application.Resources>

</Application>

Notice that our BasicControlStyle adds three Setter objects to the internal collection. Now, let’s apply this style to a few controls in our main window. Because this style is an object resource, the controls that want to use it still need to use the {StaticResource} or {DynamicResource} markup extension to locate the style. When they find the style, they will set the resource item to the identically named Style property. Consider the following <Window> definition:

<Window x:Class="WpfStyles.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="A Window with Style!" Height="229"
  Width="525" WindowStartupLocation="CenterScreen">

  <StackPanel>
    <Label x:Name="lblInfo" Content="This style is boring..."
           Style="{StaticResource BasicControlStyle}" Width="150"/>
    <Button x:Name="btnTestButton" Content="Yes, but we are reusing settings!"
           Style="{StaticResource BasicControlStyle}" Width="250"/>
  </StackPanel>

</Window>

If you run this application, you’ll find that both controls support the same cursor, height, and font size.

Overriding Style Settings

Here we have a Button and Label that have both opted in to the constraints enforced by our style. Of course, if a control wants to apply a style and then change some of the defined settings, that’s fine. For example, the Button will now use the Help cursor (rather than the Hand cursor defined in the style):

<Button x:Name="btnTestButton" Content="Yes, but we are reusing settings!"
        Cursor="Help" Style="{StaticResource BasicControlStyle}" Width="250" />

Styles are processed before the individual property settings of the control using the style; therefore, controls can “override” settings on a case-by-case basis.

Automatically Applying a Style with TargetType

Currently, our style is defined in such a way that any control can adopt it (and has to do so explicitly by setting the control’s Style property), given that each property is qualified by the Control class. For a program that defines dozens of settings, this would entail a good amount of repeated code. One way to clean this style up a bit is to use the TargetType attribute. When you add this attribute to a Style’s opening element, you can mark exactly once where it can be applied.

<Style x:Key ="BasicControlStyle" TargetType="Control">
  <Setter Property = "FontSize" Value ="14"/>
  <Setter Property = "Height" Value = "40"/>
  <Setter Property = "Cursor" Value = "Hand"/>
</Style>

Images Note When you are building a style that is using a base class type, you needn’t be concerned if you assign a value to a dependency property not supported by derived types. If the derived type does not support a given dependency property, it is ignored.

This is somewhat helpful, but we still have a style that can apply to any control. The TargetType attribute is more useful when you want to define a style that can be applied to only a particular type of control. Add the following new style to the application’s resource dictionary:

<Style x:Key ="BigGreenButton" TargetType="Button">
  <Setter Property = "FontSize" Value ="20"/>
  <Setter Property = "Height" Value = "100"/>
  <Setter Property = "Width" Value = "100"/>
  <Setter Property = "Background" Value = "DarkGreen"/>
  <Setter Property = "Foreground" Value = "Yellow"/>
</Style>

This style will work only on Button controls (or a subclass of Button) and if you apply it on an incompatible element, you will get markup and compiler errors. If the Button uses this new style, like so:

<Button x:Name="btnTestButton" Content="OK!"
        Cursor="Help" Style="{StaticResource BigGreenButton}" Width="250"  />

you’d see the output like that shown in Figure 30-12.

images

Figure 30-12. Controls with different styles

Subclassing Existing Styles

You can also build new styles using an existing style, via the BasedOn property. The style you are extending must have been given a proper x:Key in the dictionary, as the derived style will reference it by name using the {StaticResource} markup extension. Here is a new style based on BigGreenButton, which rotates the button element by 20 degrees:

<!-- This style is based on BigGreenButton. -->
<Style x:Key ="TiltButton" TargetType="Button" BasedOn = "{StaticResource BigGreenButton}">
  <Setter Property = "Foreground" Value = "White"/>
  <Setter Property = "RenderTransform">
    <Setter.Value>
      <RotateTransform Angle = "20"/>
    </Setter.Value>
  </Setter>
</Style>

This time, the output appears as in Figure 30-13.

images

Figure 30-13. Using a derived style

The Role of Unnamed Styles

Assume you need to ensure that all TextBox controls have the same look and feel. Now assume you have defined a style as an application-level resource, so all windows in the program have access to it. While this is a step in the right direction, if you have numerous windows with numerous TextBox controls, you’ll need to set the Style property numerous times!

WPF styles can be implicitly applied to all controls within a given XAML scope. To create such a style, you use the TargetType property but you don’t assign the Style resource an x:Key value. This “unnamed style” now applies to all controls of the correct type. Here is another application-level style that will apply automatically to all TextBox controls in the current application:

<!-- The default style for all text boxes. -->
<Style TargetType="TextBox">
  <Setter Property = "FontSize" Value ="14"/>
  <Setter Property = "Width" Value = "100"/>
  <Setter Property = "Height" Value = "30"/>
  <Setter Property = "BorderThickness" Value = "5"/>
  <Setter Property = "BorderBrush" Value = "Red"/>
  <Setter Property = "FontStyle" Value = "Italic"/>
</Style>

We can now define any number of TextBox controls and they will automatically get the defined look. If a given TextBox does not want this default look and feel, it can opt out by setting the Style property to {x:Null}. For example, txtTest will get the default unnamed style, while txtTest2 is doing things its own way:

<TextBox x:Name="txtTest"/>
<TextBox x:Name="txtTest2" Style="{x:Null}" BorderBrush="Black"
                 BorderThickness="5" Height="60" Width="100" Text="Ha!"/>

Defining Styles with Triggers

WPF styles can also contain triggers, by packaging up Trigger objects within the Triggers collection of the Style object. Using triggers in a style allows you to define certain <Setter> elements in such a way that they will be applied only if a given trigger condition is true. For example, perhaps you want to increase the size of a font when the mouse is over a button. Or maybe you want to make sure that the text box with the current focus is highlighted with a given color. Triggers are very useful for these sorts of situations, in that they allow you to take specific actions when a property changes, without the need to author explicit C# code in a code-behind file.

Here is an update to the TextBox style that ensures that when a TextBox has the input focus, it will receive a yellow background:

<!-- The default style for all text boxes. -->
<Style TargetType="TextBox">
  <Setter Property = "FontSize" Value ="14"/>
  <Setter Property = "Width" Value = "100"/>
  <Setter Property = "Height" Value = "30"/>
  <Setter Property = "BorderThickness" Value = "5"/>
  <Setter Property = "BorderBrush" Value = "Red"/>
  <Setter Property = "FontStyle" Value = "Italic"/>
  <!-- The following setter will be applied only when the text box is
       in focus. -->
  <Style.Triggers>
    <Trigger Property = "IsFocused" Value = "True">
      <Setter Property = "Background" Value = "Yellow"/>
    </Trigger>
  </Style.Triggers>
</Style>

If you test this style, you’ll find that as you tab between various TextBox objects, the currently selected TextBox has a bright yellow background (provided it has not opted out by assigning {x:Null} to the Style property).

Property triggers are also very smart, in that when the trigger’s condition is not true, the property automatically receives the default assigned value. Therefore, as soon as a TextBox loses focus, it also automatically becomes the default color without any work on your part. In contrast, event triggers (examined when we looked at WPF animations) do not automatically revert to a previous condition.

Defining Styles with Multiple Triggers

Triggers can also be designed in such a way that the defined <Setter> elements will be applied when multiple conditions are true (similar to building an if statement for multiple conditions). Let’s say we want to set the background of a TextBox to Yellow only if it has the active focus and the mouse is hovering within its boundaries. To do so, we can make use of the <MultiTrigger> element to define each condition, like so:

<!-- The default style for all text boxes. -->
<Style TargetType="TextBox">
  <Setter Property = "FontSize" Value ="14"/>
  <Setter Property = "Width" Value = "100"/>
  <Setter Property = "Height" Value = "30"/>
  <Setter Property = "BorderThickness" Value = "5"/>
  <Setter Property = "BorderBrush" Value = "Red"/>
  <Setter Property = "FontStyle" Value = "Italic"/>
  <!-- The following setter will be applied only when the text box is
  in focus AND the mouse is over the text box. -->
  <Style.Triggers>
    <MultiTrigger>
      <MultiTrigger.Conditions>
          <Condition Property = "IsFocused" Value = "True"/>
          <Condition Property = "IsMouseOver" Value = "True"/>
        </MultiTrigger.Conditions>
      <Setter Property = "Background" Value = "Yellow"/>
    </MultiTrigger>
  </Style.Triggers>
</Style>

Animated Styles

Styles can also incorporate triggers that kick off an animation sequence. Here is one final style that, when applied to Button controls, will cause the controls to grow and shrink in size when the mouse is inside the button’s surface area:

<!-- The growing button style! -->
<Style x:Key = "GrowingButtonStyle" TargetType="Button">
  <Setter Property = "Height" Value = "40"/>
  <Setter Property = "Width" Value = "100"/>
  <Style.Triggers>
    <Trigger Property = "IsMouseOver" Value = "True">
      <Trigger.EnterActions>
        <BeginStoryboard>
          <Storyboard TargetProperty = "Height">
            <DoubleAnimation From = "40" To = "200"
                             Duration = "0:0:2" AutoReverse="True"/>
          </Storyboard>
        </BeginStoryboard>
      </Trigger.EnterActions>
    </Trigger>
  </Style.Triggers>
</Style>

Here, our triggers collection is on the lookout for the IsMouseOver property to return true. When this occurs, we define a <Trigger.EnterActions> element to execute a simple storyboard that forces the button to grow to a Height value of 200 (and then return to a Height of 40) over two seconds. If you want to perform other property changes, you could also define a <Trigger.ExitActions> scope to define any custom actions to take when IsMouseOver is false.

Assigning Styles Programmatically

Recall that a style can be applied at runtime as well. This can be helpful if you want to let end users choose how their UI looks and feels, or if you need to enforce a look and feel based on security settings (e.g., the DisableAllButton style) or what have you.

During this project, you have defined a number of styles, many of which can apply to Button controls. So, let’s retool the UI of our main window to allow the user to pick from some of these styles by selecting names in a ListBox. Based on the user’s selection, we will apply the appropriate style. Here is the new (and final) markup for the <Window> element:

<Window x:Class="WpfStyles.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Height="350" Title="A Window with Style!"  
  Width="525" WindowStartupLocation="CenterScreen">

  <DockPanel >
    <StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
      <Label Content="Please Pick a Style for this Button" Height="50"/>
      <ListBox x:Name ="lstStyles" Height ="80" Width ="150" Background="LightBlue"
               SelectionChanged ="comboStyles_Changed" />    
    </StackPanel>
    <Button x:Name="btnStyle" Height="40" Width="100" Content="OK!"/>
  </DockPanel>

</Window>

The ListBox control (named lstStyles) will be filled dynamically within the window’s constructor, like so:

public MainWindow()
{
  InitializeComponent();

  // Fill the list box with all the Button
  // styles.
  lstStyles.Items.Add("GrowingButtonStyle");
  lstStyles.Items.Add("TiltButton");
  lstStyles.Items.Add("BigGreenButton");
  lstStyles.Items.Add("BasicControlStyle");
}

The final task is to handle the SelectionChanged event in the related code file. Notice in the following code how we are able to extract the current resource by name, using the inherited TryFindResource() method:

private void comboStyles_Changed(object sender, SelectionChangedEventArgs e)
{
  // Get the selected style name from the list box.    
  Style currStyle = (Style)
    TryFindResource(lstStyles.SelectedValue);
  if (currStyle != null)
  {
    // Set the style of the button type.
    this.btnStyle.Style = currStyle;
  }
}

When you run this application, you can pick from one of these four button styles on the fly. Figure 30-14 shows our completed application.

images

Figure 30-14. Controls with different styles

Images Source Code The WpfStyles project can be found under the Chapter 30 subdirectory.

Summary

The first part of this chapter examined the resource management system of WPF. We began by looking at how to work with binary resources, then quickly turned our attention of the role of object resources. As you learned, object resources are named blobs of XAML that can be stored at various locations, in order to reuse content.

Next, you learned about WPF’s animation framework. Here you had a chance to create some animations using C# code, as well as with XAML. You learned that if you define an animation in markup, you use <Storyboard> elements and triggers to control execution. We wrapped up by looking at the WPF style mechanism, which makes heavy use of graphics, object resources, and animations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.35.247