Pro C#10 CHAPTER 28 WPF Resources, Animations, Styles, and Templates

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:



tags, as shown here:

To allow the Cancel button to use this brush as well, you should promote the scope of your

to a parent element’s resource dictionary. For example, if you move it to the
, both buttons can use the same brush because they are child elements of the layout manager. Even better, you could package the brush into the resource dictionary of the Window itself so the window’s content can use it.
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.



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 element, you define 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 you 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 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:


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:

发表评论