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.
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.
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.
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 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 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.
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).
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.
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.
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).
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.
Source Code The BinaryResourcesApp can be found under the Chapter 30 subdirectory.
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 Button
s.
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.
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, Window
s, Page
s (used when building navigation applications or XBAP programs), and UserControl
s 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.
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).
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).
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 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>
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);
}
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.
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)
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).
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>
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 New Project menu option of Visual Studio (see Figure 30-10).
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.
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.
Source Code The ObjectResourcesApp project can be found under the Chapter 30 subdirectory.
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.
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.
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
.
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).
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.
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.
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.
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.
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.
Source Code The SpinningButtonAnimationApp project can be found under the Chapter 30 subdirectory.
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.
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.
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>
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.
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.
Source Code These loose XAML files can be found under the XamlAnimations
subdirectory of Chapter 30.
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.
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.
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.
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>
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.
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.
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!"/>
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.
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>
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
.
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.
Source Code The WpfStyles project can be found under the Chapter 30 subdirectory.
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.
3.145.35.247