CHAPTER 28
WPF Resources, Animations, Styles, and Templates
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 will see later in this chapter).
You will then explore the role of WPF styles and templates. 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. The final example will teach you how to build custom control templates.
Understanding the WPF Resource System
Your 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 to be resources in the traditional sense (embedded image files or sound clips, icons used by the application, etc.).
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.
© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_28
1233
Working with Binary Resources
Before getting 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 would like to follow along, create a new WPF application named BinaryResourcesApp. Update the markup for your initial window to handle the Window Loaded event and to use a DockPanel as the layout root, like so:
<Window x:Class="BinaryResourcesApp.MainWindow"
Title="Fun with Binary Resources" Height="500" Width="649" Loaded="MainWindow_OnLoaded">
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 display not only a typical image file (.bmp,
.gif, .ico, .jpg, .png, .wdp, or *.tiff) but also data in a DrawingImage (as you saw in Chapter 27). 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, like so:
Next, add the following empty event handlers:
private void MainWindow_OnLoaded( object sender, RoutedEventArgs e)
{
}
private void btnPreviousImage_Click( object sender, RoutedEventArgs e)
{
}
private void btnNextImage_Click( object sender, RoutedEventArgs e)
{
}
When the window loads, images will be added to a collection that the Next and Previous buttons will cycle through. Now that the application framework is in place, let’s examine the different options for implementing this.
Including Loose Resource Files in a Project
One option is to ship your image files as a set of loose files in a subdirectory of the application install path. Start by adding a new folder (named Images) to your project. Into this folder add some images by right- clicking and selecting Add ➤ Existing Item. Make sure to change the file filter in the Add Existing Item dialog to . so the image files show. You can add your own image files or use the three image files named Deer. jpg, Dogs.jpg, and Welcome.jpg from the downloadable code.
Configuring the Loose Resources
To copy the content of your \Images folder to the \bin\Debug folder when the project builds, begin by selecting all the images in Solution Explorer. Now, with these images still selected, right-click and select Properties to open the Properties window. Set the Build Action property to Content, and set the Copy to Output Directory property to “Copy always” (see Figure 28-1).
Figure 28-1. Configuring the image data to be copied to your output directory
■Note you could also select Copy if newer, which will save you time if you’re building large projects with a lot of content. For this example, “Copy always” works.
If you build your project, you can now click the Show All Files button of Solution Explorer and view the copied Image folder under your \bin\Debug directory (you might need to click the Refresh button).
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. Add a List
// A List of BitmapImage files.
List
// Current position in the list.
private int _currImage=0;
In the Loaded event of your window, fill the list of images and then set the Image control source to the first image in the list.
private void MainWindow_OnLoaded( object sender, RoutedEventArgs e)
{
try
{
string path=Environment.CurrentDirectory;
// Load these images from disk when the window loads.
_images.Add(new BitmapImage(new Uri($@"{path}\Images\Deer.jpg")));
_images.Add(new BitmapImage(new Uri($@"{path}\Images\Dogs.jpg")));
_images.Add(new BitmapImage(new Uri($@"{path}\Images\Welcome.jpg")));
// Show first image in the List.
imageHolder.Source=_images[_currImage];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Next, implement the previous and next handlers to loop through the images. If the user gets to the end of the list, start them back at the beginning and vice versa.
private void btnPreviousImage_Click( object sender, RoutedEventArgs e)
{
if (--_currImage < 0)
{
_currImage=_images.Count - 1;
}
imageHolder.Source=_images[_currImage];
}
private void btnNextImage_Click( object sender, RoutedEventArgs e)
{
if (++_currImage >=_images.Count)
{
_currImage=0;
}
imageHolder.Source=_images[_currImage];
}
At this point, you can run your program and flip through each picture.
Embedding Application Resources
If you would 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 \bin\Debug\Images folder). Change the Build Action property to Resource and set the Copy to Output Directory property to “Do not copy” (see Figure 28-2).
Figure 28-2. 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 \bin\Debug\Images and then rebuild your project. Refresh Solution Explorer and observe the absence of data in your \bin\Debug\Images 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. This ensures that the resources exist but also increases the size of your compiled assembly.
You need to modify your code to load these images into your list by extracting them from the compiled assembly.
// Extract from the assembly and then load images
_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)));
In this case, you no longer need to determine the installation path and can simply list the resources by name, which considers the name of the original subdirectory. Also notice, when you create your Uri objects, you specify a UriKind value of Relative. At this point, your executable is a stand-alone entity that can be run from any location on the machine because all the compiled data is within the binary.
Working with Object (Logical) Resources
When you are building a WPF application, it is 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 Button controls.
The worst thing you could do is to copy and paste the XAML to every control. Clearly, this would be a nightmare to maintain, as you would need to make numerous changes any time you wanted to tweak the look and feel of the brush.
Thankfully, object resources allow you 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 do not need to tinker with the Build Action property to do so. If 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 can define a XAML-based animation, a 3D rendering, a custom control style, a data template, a 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 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 because 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), 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 and change the initial Grid to a horizontally aligned StackPanel layout manager. Into this StackPanel, define two Button controls like so (you really do not need much to illustrate the role of object resources, so this will do):
Now, click the OK button and set the Background color property to a custom brush type using the integrated Brushes editor (discussed in Chapter 27). After you have done so, notice how the brush is embedded within the scope of the tags, as shown here:
To allow the Cancel button to use this brush as well, you should promote the scope of your
When you need to define a resource, you use the 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 its respective Properties window.
To do so, first identify the property that has the complex object you want to package as a resource (the Background property, in this example). To the right of the property is a small square that, when clicked, will open a pop-up menu. From it, select the Convert to New Resource option (see Figure 28-3).
Figure 28-3. Moving a complex object into a resource container
You are 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 28-4).
Figure 28-4. Naming the object resource
When you are done, you will see the brush has been moved inside the Window.Resources tag.
And the Button control’s Background has been updated to use a new resource.
The Create Resource Wizard creates the new resource as a DynamicResource. You will learn about
DynamicResources later in the text, but for now, change it to a StaticResource, like this:
To see the benefit, update the Cancel Button’s Background property to the same StaticResource, and you can see the reuse in action.
The {StaticResource} Markup Extension
The {StaticResource} markup extension applies the resource only once (on initialization) and stays “connected” to the original object during the life of the application. Some properties (such as gradient stops) will update, but if you create a new Brush, for example, the control will not be updated. To see this in action, add a Name and Click event handler to each Button control, as follows:
Next, add the following code to the Ok_OnClick() event handler:
private void Ok_OnClick(object sender, RoutedEventArgs e)
{
// Get the brush and make a change.
var b=(RadialGradientBrush)Resources["myBrush"]; b.GradientStops[1]=new GradientStop(Colors.Black, 0.0);
}
■Note you 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 cannot be found. you could also use the
TryFindResource() method, which will not throw a runtime error; it will simply return null if the specified resource cannot be located.
When you run the program and click the OK Button, you see the gradients change appropriately. Now add the following code to the Cancel_OnClick() event handler:
private void Cancel_OnClick(object sender, RoutedEventArgs e)
{
// Put a totally new brush into the myBrush slot. Resources["myBrush"]=new SolidColorBrush(Colors.Red);
}
Run the program again, click the Cancel Button, and nothing happens!
The {DynamicResource} Markup Extension
It is also possible for a property to use the DynamicResource markup extension. To see the difference, change the markup for the Cancel Button to the following:
This time, when you click the Cancel Button, the background for the Cancel Button changes, but the background for the OK Button remains the same. This is because the {DynamicResource} markup extension can 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. The solution to share resources across your application is to define the object resource at the application level, rather than at the window level. There is no way to automate this within Visual Studio, so simply cut the current brush object out of the
Now any additional window or control in your application is free to use this same brush. If you want to set the Background property for a control, the application-level resources are available for selection, as shown in Figure 28-5.
Figure 28-5. Applying application-level resources
■Note placing the resources at the application level and assigning it to a control’s property will freeze the resource, preventing changing values at runtime. the resource can be cloned, and the clone can be updated.
Defining Merged Resource Dictionaries
Application-level resources are a quite often good enough, but they do not help reuse across projects. In this case, you want to define what is known as a merged resource dictionary. Think of it as a class library for WPF resources; it is nothing more than a XAML file that contains a collection of resources. A single project can have as many of these files as required (one for brushes, one for animations, etc.), each of which can be inserted using the Add New Item dialog box activated via the Project menu (see Figure 28-6).
Figure 28-6. Inserting a new merged resource dictionary
In the new MyBrushes.xaml file, cut the current resources in the Application.Resources scope and move them into your dictionary, like so:
Even though this resource dictionary is part of your project, all resource dictionaries must be merged (typically at the application level) into an existing resource dictionary to be used. To do this, use the following format in the App.xaml file (note that multiple resource dictionaries can be merged by adding multiple
The problem with this approach is that each resource file must be added to each project that needs the resources. A better approach for sharing resources is to define a .NET class library to share between projects, which you will do next.
Defining a Resource-Only Assembly
The easiest way to build a resource-only assembly is to begin with a WPF User Control Library project. Add such a project (named MyBrushesLibrary) to your current solution via the Add ➤ New Project menu option of Visual and add a project reference to it from the ObjectResourcesApp project.
Now, delete the UserControl1.xaml file from the project. Next, drag and drop the MyBrushes.xaml file into your MyBrushesLibrary project and delete it from the ObjectResourcesApp project. Finally, open MyBrushes.xaml in the MyBrushesLibrary project and change the x:local namespace in the file to clr- namespace:MyBrushesLibrary. Your MyBrushes.xaml file should look like this:
Compile your User Control Library project. Now, merge these binary resources into the application- level resource dictionary of the ObjectResourcesApp project. Doing so, however, requires some rather funky syntax, shown here:
First, be aware that this string is space sensitive. If you have extra whitespace around your semicolon or forward slashes, you will generate errors. The first part of the string is the friendly name of the external library (no file extension). After the semicolon, 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 the examination of WPF’s resource management system. You will make good use of these techniques for most (if not all) of your applications. Next up, let’s investigate the integrated animation API of Windows Presentation Foundation.
Understanding WPF’s Animation Services
In addition to the graphical rendering services you examined in Chapter 27, 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. A more business application–centric use is to fade in error messages on an application screen to improve the user experience. 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 is 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, you can build an animation entirely using XAML, entirely using C# code, or using a combination of the two.
■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, Blend for Visual studio (the companion product that ships with Visual studio 2019) 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, you must begin by examining the animation classes within the System.Windows.Media.Animation namespace of PresentationCore.dll. Here, you will find more than 100 different class types that are named using the Animation token.
These classes can be placed into one of three broad categories. First, any class that follows the name convention DataTypeAnimation (ByteAnimation, ColorAnimation, DoubleAnimation, Int32Animation, 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 you 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 clear, these Animation classes can be connected to any dependency property of a given object that
matches the underlying types. As explained in Chapter 26, 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 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 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
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 several additional properties that control the pacing of the animation, as described in Table 28-1.
Table 28-1. Key Members of the Timeline Base Class
Properties Meaning in Life
AccelerationRatio, DecelerationRatio, SpeedRatio These properties can be used to control the overall pacing of the animation sequence.
AutoReverse This property gets or sets a value that indicates whether the timeline plays in reverse after it completes a forward iteration (the default value is false).
BeginTime This property gets or sets the time at which this timeline should begin. The default value is 0, which begins the animation immediately.
Duration This property allows you to set a duration of time to play the timeline.
FillBehavior, RepeatBehavior These properties are used to control what should happen once the timeline has completed (repeat the animation, do nothing, etc.).
Authoring an Animation in C# Code
Specifically, you 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. Update the initial markup to the following (note you are handling the button’s MouseEnter event):
In the code-behind file, import the System.Windows.Media.Animation namespace and add the following code in the window’s C# code file:
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.
var 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.
var rt=new RotateTransform(); btnSpinner.RenderTransform=rt;
// Now, animation the RotateTransform object.
rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim);
}
}
private void btnSpinner_OnClick( object sender, RoutedEventArgs e)
{
}
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 you 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, you don’t “reset” it to start again.
Next, you create a RotateTransform object that is connected to the RenderTransform property of your Button control (btnSpinner). Finally, you inform the RenderTransform object to begin animating its Angle property using your DoubleAnimation object. When you are authoring animations in code, you typically do so by calling BeginAnimation() and then 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, add the following code in the Click event handler:
private void btnSpinner_OnClick( object sender, RoutedEventArgs e)
{
var dblAnim=new DoubleAnimation
{
From=1.0, To=0.0
};
btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}
Here, you 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 you 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, your 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.
var 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 of clicking the button while it is spinning, at which point it will fade away.
■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_OnClick(object sender, RoutedEventArgs e)
{
DoubleAnimation dblAnim=new DoubleAnimation
{
From=1.0, To=0.0
};
// Reverse when done.
dblAnim.AutoReverse=true; btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}
If you would 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 you 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 your investigation about how to animate aspects of an object using C# code and the WPF animation API. Next, you will learn how to do the same using XAML.
Authoring Animations in XAML
Authoring animations in markup is like 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 because this will make it easier for you to modify and tweak tool-generated content.
■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 like what you saw already. 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 Kaxaml (make sure to press F5 to show the window) and observe the behavior.
Now, let’s break this example down, bit by bit.
The Role of Storyboards
Working from the innermost element outward, you first encounter the
As mentioned, Animation elements are placed within a
The Role of Event Triggers
After the
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!” or “The mouse is over me!” or “I have focus!”).
Once you have been notified that an event condition has occurred, you can start the storyboard. In this example, you are responding to the Label being loaded into memory. Because it is the Label’s Loaded event you are interested in, the
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 you 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 will find the following markup in the AnimateString.xaml file. Copy this markup into your MyXamlPad.exe program (or Kaxaml) and view the results:
First, notice that you have defined an event trigger for your button to ensure that your storyboard executes when the button has loaded into memory. The StringAnimationUsingKeyFrames class oversees changing the content of the button, via the Storyboard.TargetProperty value.
Within the scope of the
Now that you have a better feel for how to build animations in C# code and XAML, let’s look at the role of WPF styles, which make heavy use of graphics, object resources, and animations.
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 to have the same height, width, background color, and font size for their string content. Although 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 because you would 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”). 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.
Let’s build a style that captures the basic font characteristics of a control in your application. Start by creating a new WPF application named WpfStyles. Open your App.xaml file and define the following named style:
Notice that your BasicControlStyle adds three Setter objects to the internal collection. Now, let’s apply this style to a few controls in your 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. Replace the default Grid control with the following markup:
If you view the Window in the Visual Studio designer (or run the application), you will find that both controls support the same cursor, height, and font size.
Overriding Style Settings
While both of your controls have opted into the style, if a control wants to apply a style and then change some of the defined settings, that is fine. For example, the Button will now use the Help cursor (rather than the Hand cursor defined in the style).
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.
The Effect of TargetType on Styles
Currently, your style is defined in such a way that any control can adopt it (and must 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 (in this example, in App.XAML).
■Note When you build a style that uses 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 you 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:
This style will work only on Button controls (or a subclass of Button). If you apply it to an incompatible element, you will get markup and compiler errors. Add a new Button that uses this new style, as follows:
You will see output like that shown in Figure 28-7.
Figure 28-7. Controls with different styles
Another effect of TargetType is that the style will get applied to all elements of that type within the scope of the style definition if the x:Key property does not exist.
Here is another application-level style that will apply automatically to all TextBox controls in the current application:
You 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.
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} or {DynamicResource} markup extension. Here is a new style based on
BigGreenButton, which rotates the button element by 20 degrees:
To use this new style, update the markup for the button to this:
This changes the appearance to the image shown in Figure 28-8.
Figure 28-8. Using a derived style
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
focus is highlighted with a given color. Triggers are 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:
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 you 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
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:
Here, the Triggers collection is on the lookout for the IsMouseOver property to return true. When this occurs, you define a
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 several styles, many of which can apply to Button controls. So,
let’s retool the UI of your 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, you will apply the appropriate style. Here is the new (and final) markup for the
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 you can 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.
var currStyle=(Style)TryFindResource(lstStyles.SelectedValue); if (currStyle==null) return;
// 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 28-9 shows your completed application.
Figure 28-9. Controls with different styles
Logical Trees, Visual Trees, and Default Templates
Now that you understand styles and resources, there are a few more preparatory topics to investigate before you begin learning how to build custom controls. Specifically, you need to learn the distinction between a logical tree, a visual tree, and a default template. When you are typing XAML into Visual Studio or a tool such as kaxaml.exe, your markup is the logical view of the XAML document. As well, if you author C# code that adds new items to a layout control, you are inserting new items into the logical tree. Essentially, a logical view represents how your content will be positioned within the various layout managers for a main Window (or another root element, such as Page or NavigationWindow).
However, behind every logical tree is a much more verbose representation termed a visual tree, which is used internally by WPF to correctly render elements onto the screen. Within any visual tree, there will be full details of the templates and styles used to render each object, including any necessary drawings, shapes, visuals, and animations.
It is useful to understand the distinction between logical and visual trees because when you are building a custom control template, you are essentially replacing all or part of the default visual tree of a control and inserting your own. Therefore, if you want a Button control to be rendered as a star shape,
you could define a new star template and plug it into the Button’s visual tree. Logically, the Button is still
of type Button, and it supports the properties, methods, and events as expected. But visually, it has taken on a whole new appearance. This fact alone makes WPF an extremely useful API, given that other toolkits would require you to build a new class to make a star-shaped button. With WPF, you simply need to define new markup.
■Note WpF controls are often described as lookless. this refers to the fact that the look and feel of a WpF control is completely independent (and customizable) from its behavior.
Programmatically Inspecting a Logical Tree
While analyzing a window’s logical tree at runtime is not a tremendously common WPF programming activity, it is worth mentioning that the System.Windows namespace defines a class named LogicalTreeHelper, which allows you to inspect the structure of a logical tree at runtime. To illustrate the connection between logical trees, visual trees, and control templates, create a new WPF application named TreesAndTemplatesApp.
Replace the Grid with the following markup that contains two Button controls and a large read-only TextBox with scrollbars enabled. Make sure you use the IDE to handle the Click event of each button. The following XAML will do nicely:
Within your C# code file, define a string member variable named _dataToShow. Now, within the Click handler for the btnShowLogicalTree object, call a helper function that calls itself recursively to populate the string variable with the logical tree of the Window. To do so, you will call the static GetChildren() method of LogicalTreeHelper. Here is the code:
private string _dataToShow=string.Empty;
private void btnShowLogicalTree_Click(object sender, RoutedEventArgs e)
{
_dataToShow=""; BuildLogicalTree(0, this); txtDisplayArea.Text=_dataToShow;
}
void BuildLogicalTree(int depth, object obj)
{
// Add the type name to the dataToShow member variable.
_dataToShow +=new string(' ', depth) + obj.GetType().Name + "\n";
// If an item is not a DependencyObject, skip it. if (!(obj is DependencyObject))
return;
// Make a recursive call for each logical child.
foreach (var child in LogicalTreeHelper.GetChildren((DependencyObject)obj))
{
BuildLogicalTree(depth + 5, child);
}
}
private void btnShowVisualTree_Click( object sender, RoutedEventArgs e)
{
}
If you run your application and click this first button, you will see a tree print in the text area, which is just about an exact replica of the original XAML (see Figure 28-10).
Figure 28-10. Viewing a logical tree at runtime
Programmatically Inspecting a Visual Tree
A Window’s visual tree can also be inspected at runtime using the VisualTreeHelper class of System. Windows.Media. Here is a Click implementation of the second Button control (btnShowVisualTree), which performs similar recursive logic to build a textual representation of the visual tree:
using System.Windows.Media;
private void btnShowVisualTree_Click(object sender, RoutedEventArgs e)
{
_dataToShow=""; BuildVisualTree(0, this); txtDisplayArea.Text=_dataToShow;
}
void BuildVisualTree(int depth, DependencyObject obj)
{
// Add the type name to the dataToShow member variable.
_dataToShow +=new string(' ', depth) + obj.GetType().Name + "\n";
// Make a recursive call for each visual child.
for (int i=0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
BuildVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));
}
}
As you can see in Figure 28-11, the visual tree exposes several lower-level rendering agents such as
ContentPresenter, AdornerDecorator, TextBoxLineDrawingVisual, and so forth.
Figure 28-11. Viewing a visual tree at runtime
Programmatically Inspecting a Control’s Default Template
Recall that a visual tree is used by WPF to understand how to render a Window and all contained elements. Every WPF control stores its own set of rendering commands within its default template. Programmatically speaking, any template can be represented as an instance of the ControlTemplate class. As well, you can obtain a control’s default template by using the aptly named Template property, like so:
// Get the default template of the Button. Button myBtn=new Button();
ControlTemplate template=myBtn.Template;
Likewise, you could create a new ControlTemplate object in code and plug it into a control’s Template
property as follows:
// Plug in a new template for the button to use. Button myBtn=new Button();
ControlTemplate customTemplate=new ControlTemplate();
// Assume this method adds all the code for a star template. MakeStarTemplate(customTemplate); myBtn.Template=customTemplate;
While you could build a new template in code, it is far more common to do so in XAML. However, before you start building your own templates, let’s finish the current example and add the ability to view the default template of a WPF control at runtime. This can be a useful way to look at the overall composition of a template. Update the markup of your window with a new StackPanel of controls docked to the left side of the master DockPanel, defined as so (placed just before the
Add an empty event handler function for the btnTemplate_Click() event like this:
private void btnTemplate_Click( object sender, RoutedEventArgs e)
{
}
The upper-left text area allows you to enter in the fully qualified name of a WPF control located in the PresentationFramework.dll assembly. Once the library is loaded, you will dynamically create an instance of the object and display it in the large square in the bottom left. Finally, the control’s default template will be displayed in the right-hand text area. First, add a new member variable to your C# class of type Control, like so:
private Control _ctrlToExamine=null;
Here is the remaining code, which will require you to import the System.Reflection, System.Xml, and
System.Windows.Markup namespaces:
private void btnTemplate_Click( object sender, RoutedEventArgs e)
{
_dataToShow="";
ShowTemplate();
txtDisplayArea.Text=_dataToShow;
}
private void ShowTemplate()
{
// Remove the control that is currently in the preview area. if (_ctrlToExamine !=null)
stackTemplatePanel.Children.Remove(_ctrlToExamine); try
{
// Load PresentationFramework, and create an instance of the
// specified control. Give it a size for display purposes, then add to the
// empty StackPanel.
Assembly asm=Assembly.Load("PresentationFramework, Version=4.0.0.0," + "Culture=neutral, PublicKeyToken=31bf3856ad364e35");
_ctrlToExamine=(Control)asm.CreateInstance(txtFullName.Text);
_ctrlToExamine.Height=200;
_ctrlToExamine.Width=200;
_ctrlToExamine.Margin=new Thickness(5); stackTemplatePanel.Children.Add(_ctrlToExamine);
// Define some XML settings to preserve indentation.
var xmlSettings=new XmlWriterSettings{Indent=true};
// Create a StringBuilder to hold the XAML.
var strBuilder=new StringBuilder();
// Create an XmlWriter based on our settings.
var xWriter=XmlWriter.Create(strBuilder, xmlSettings);
// Now save the XAML into the XmlWriter object based on the ControlTemplate.
XamlWriter.Save(_ctrlToExamine.Template, xWriter);
// Display XAML in the text box.
_dataToShow=strBuilder.ToString();
}
catch (Exception ex)
{
_dataToShow=ex.Message;
}
}
The bulk of the work is just tinkering with the compiled BAML resource to map it into a XAML string. Figure 28-12 shows your final application in action, displaying the default template of the System.Windows. Controls.DatePicker control. The image shows the Calendar, which is accessed by clicking the button on the right side of the control.
Figure 28-12. Investigating a ControlTemplate at runtime
Great! You should have a better idea about how logical trees, visual trees, and control default templates work together. Now you can spend the remainder of this chapter learning how to build custom templates and user controls.
Building a Control Template with the Trigger Framework
When you build a custom template for a control, you could do so with nothing but C# code. Using this approach, you would add data to a ControlTemplate object and then assign it to a control’s Template property. Most of the time, however, you will define the look and feel of a ControlTemplate using XAML and add bits of code (or possibly quite a bit of code) to drive the runtime behavior.
In the remainder of this chapter, you will examine how to build custom templates using Visual Studio. Along the way, you will learn about the WPF trigger framework and the Visual State Manager (VSM), and you will see how to use animations to incorporate visual cues for the end user. Using Visual Studio alone to build complex templates can entail a fair amount of typing and a bit of heavy lifting. To be sure, production- level templates will benefit from the use of Blend for Visual Studio, the (now) free companion application installed with Visual Studio. However, given that this edition of the text does not include coverage of Blend, it is time to roll up your sleeves and pound out some markup.
To begin, create a new WPF application named ButtonTemplate. For this project, you are more interested in the mechanics of creating and using templates, so replace the Grid with the following markup:
In the Click event handler, simply display a message box (via MessageBox.Show()) to show a message confirming the clicking of the control. Remember, when you build custom templates, the behavior of the control is constant, but the look may vary.
Currently, this Button is rendered using the default template, which, as the previous example illustrated, is a BAML resource within a given WPF assembly. When you want to define your own template, you essentially replace this default visual tree with your own creation. To begin, update the definition of the
Here, you have defined a template that consists of a named Grid control containing a named Ellipse and a Label. Because your Grid has no defined rows or columns, each child stacks on top of the previous control, which centers the content. If you run your application now, you will notice that the Click event will fire only when the mouse cursor is within the bounds of the Ellipse! This is a great feature of the WPF template architecture: you do not need to recalculate hit-testing, bounds checking, or any other low-level
detail. So, if your template used a Polygon object to render some oddball geometry, you can rest assured that the mouse hit-testing details are relative to the shape of the control, not the larger bounding rectangle.
Templates as Resources
Currently, your template is embedded to a specific Button control, which limits reuse. Ideally, you would place your template into a resource dictionary so you can reuse your round button template between projects or, at minimum, move it into the application resource container for reuse within this project. Let’s move the local Button resource to the application level by cutting the template definition from the Button and pasting it into the Application.Resources tag in the App.xaml file. Add in a Key and a TargetType, as follows:
Update the Button markup to the following:
Now, because this resource is available for the entire application, you can define any number of round buttons just by simply applying the template. Create two additional Button controls that use this template for testing purposes (no need to handle the Click event for these new items).
Incorporating Visual Cues Using Triggers
When you define a custom template, the visual cues of the default template are removed as well. For example, the default button template contains markup that informs the control how to look when certain UI events occur, such as when it receives focus, when it is clicked with the mouse, when it is enabled (or disabled), and so on. Users are quite accustomed to these sorts of visual cues because it gives the control somewhat of a tactile response. However, your RoundButtonTemplate does not define any such markup, so the look of the control is identical regardless of the mouse activity. Ideally, your control should look slightly different when clicked (maybe via a color change or drop shadow) to let the user know the visual state has changed.
This can be done using triggers, as you have already learned. For simple operations, triggers work perfectly well. There are additional ways to do this that are beyond the scope of this book, but there is more information available at https://docs.microsoft.com/en-us/dotnet/desktop-wpf/themes/how-to- create-apply-template.
By way of example, update your RoundButtonTemplate with the following markup, which adds two
triggers. The first will change the color of the control to blue and the foreground color to yellow when the mouse is over the surface. The second shrinks the size of the Grid (and, therefore, all child elements) when the control is pressed via the mouse.
The Role of the {TemplateBinding} Markup Extension
The problem with the control template is that each of the buttons looks and says the same thing. Updating the markup to the following has no effect:
This is because the control’s default properties (such as BackGround and Content) are overridden in the template. To enable them, they must be mapped to the related properties in the template. You can solve these issues by using the {TemplateBinding} markup extension when you build your template. This allows
you to capture property settings defined by the control using your template and use them to set values in the template itself.
Here is a reworked version of RoundButtonTemplate, which now uses this markup extension to map the Background property of the Button to the Fill property of the Ellipse; it also makes sure the Content of the Button is indeed passed to the Content property of the Label:
With this update, you can now create buttons of various colors and textual values. Figure 28-13 shows the result of the updated XAML.
Figure 28-13. Template bindings allow values to pass through to the internal controls.
The Role of ContentPresenter
When you designed your template, you used a Label to display the textual value of the control. Like the Button, the Label supports a Content property. Therefore, given your use of {TemplateBinding}, you could define a Button that contains complex content beyond that of a simple string.
However, what if you need to pass in complex content to a template member that does not have a Content property? When you want to define a generalized content display area in a template, you can use the ContentPresenter class as opposed to a specific type of control (Label or TextBlock). There is no need to do so for this example; however, here is some simple markup that illustrates how you could build a custom template that uses ContentPresenter to show the value of the Content property of the control using the template:
Incorporating Templates into Styles
Currently, your template simply defines a basic look and feel of the Button control. However, the process of establishing the basic properties of the control (content, font size, font weight, etc.) is the responsibility of the Button itself.
If you want, you could establish these values in the template. By doing so, you can effectively create a default look and feel. As you might have already realized, this is a job for WPF styles. When you build a style (to account for basic property settings), you can define a template within the style! Here is your updated application resource in the application resources in App.xaml, which has been rekeyed as RoundButtonStyle:
With this update, you can now create button controls by setting the Style property as so:
While the rendering and behavior of the button are identical, the benefit of nesting templates within styles is that you can provide a canned set of values for common properties. That wraps up your look at how to use Visual Studio and the trigger framework to build custom templates for a control. While there is still much more about the Windows Presentation Foundation API than has been examined here, you should be in a solid position for further study.
Summary
The first part of this chapter examined the resource management system of WPF. You began by looking at how to work with binary resources, and then you examined the role of object resources. As you learned, object resources are named blobs of XAML that can be stored at various locations 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
You examined the relationship between a logical tree and a visual tree. The logical tree is basically a one- to-one correspondence of the markup you author to describe a WPF root element. Behind this logical tree is a much deeper visual tree that contains detailed rendering instructions.
The role of a default template was then examined. Remember, when you are building custom templates, you are essentially ripping out all (or part) of a control’s visual tree and replacing it with your own custom implementation.