Windows Presentation Foundation
This chapter is excerpted from Learning C# 3.0: Master the fundamentals of C# 3.0 by Jesse Liberty, Brian MacDonald, published by O'Reilly Media
The Windows Forms techniques you learning in Chapter 18, Creating Windows Applications are great, and they represent a major improvement in user experience over the console applications you've used for most of this book. Time and familiarity have a way of changing expectations, though, and Windows Forms have been around for a very long time in programming years. As rival operating environments grow and mature, and people become accustomed to sophisticated interface design on the Web, users' expectations for interfaces have changed. Although Windows Forms are powerful, they don't leave a whole lot of flexibility for designers to show off, and those designers who do try need to be programmers as well if they want to get the most out of Windows Forms. Microsoft has responded to those concerns with the Windows Presentation Foundation (WPF).
The purpose of WPF is to provide a solution that's similar to the Windows Forms techniques you just learned, but provides greater flexibility of design. In Windows Forms, you used the visual designer to create the layout of the form, but you kept the code in a separate part of the file. WPF keeps the idea of separating the presentation (the look of the form) from the implementation (the event handler code), but it gives you direct control over the presentation. Instead of allowing access to only the Design view of your form in a visual interface, WPF represents the design in a form of XML, specifically created for WPF. This form of XML is called the XAML (pronounced "ZAM-el"), which stands for eXtensible Application Markup Language.
Tip
If you're familiar with ASP.NET, this XAML idea will sound familiar to you. WPF borrows the ASP.NET idea of having a markup file and a code-behind file, except the markup is in XAML instead of HTML. Using XAML gives the markup greater flexibility over HTML, but you lose the familiarity that many web developers have with HTML. If you're not familiar with ASP.NET, don't worry about it. You don't need to know ASP.NET to understand WPF; just know that the idea is similar.
Using XAML to define the presentation of your application opens up all sorts of design possibilities that Windows Forms just aren't capable of. You may have already seen some of the possibilities in the Aero interface that Windows Vista uses, with its animated menus and transitions, and semitransparent "Glass" appearance. The WPF elements, though, go beyond the standard Windows controls.
Even more significant than what you can do with WPF is how you can do it. XAML is robust enough that you can define some event handlers (called triggers) entirely within the XAML, without writing formal handlers for them at all. In this chapter, you'll start out very simply with a Hello World application to get the hang of using XAML. We'll also show you how to use animations and some of the other elements, and finish up with a more elaborate application that displays data interactively.
Warning
To complete the examples and exercises in this chapter, you'll need to have WPF on your machine. WPF is already installed on Windows Vista, and it's available for Windows XP Service Pack 2 by downloading the .NET Framework 3.5.
Your First WPF Application
The best way to learn WPF is to start off with a traditional Hello World application. From within C# Express or Visual Studio, create a new project. When the New Project dialog box appears, select WPF Application as the project type, and name the project "Hello WPF".
Visual Studio will create the project and open a WPF editing window for you, as you can see in Figure 19.1, "When you create a WPF project, the IDE opens with the XAML window in two views: the Design view on top, and the XAML markup below.".
Notice the split window in the middle of the IDE. It shows a visual representation of your form on the top, called the Design view, and the XAML markup on the bottom.
Before you do anything else, take a look at the code that's automatically created for you in the XAML window. It looks something like this:
<Window x:Class="Hello_WPF.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
</Grid>
</Window>
The first thing you see is the opening tag for the Window element itself. That element contains a reference to the Class for the application, created by default. The x refers to the default namespace for this application. (You can change the namespace if you like, but x is the default.) The xmlns properties define the namespace for the application, by referring to the XML schema for WPF, as defined by Microsoft. In short, this is where you'll find the specification of the elements that you can use in this file. You don't really need to worry much about this for the moment, although later, you'll see how to add your own elements to the namespace.
XML Crash Course
XAML is a form of XML (eXtensible Markup Language), which is similar in appearance to HTML, the markup language used to create web pages. The difference is that HTML tags define not only the type of content (paragraph, image, text box, and so on), but also the appearance, or presentation, of that content (bold, italic, blinking, and so on). XML takes the presentation elements out of the language, and also extends its usefulness beyond just defining web pages. XML documents can define any sort of data, as long as the document adheres to the defined schema for that document-the set of valid elements.
Although you certainly can define web pages or text documents with XML, you can also define other relationships, like this:
<schedule>
<shifts>
<shift name="morning" startTime="8:00" endTime="4:00">
<managers>
<manager>Johnson</manager>
<manager>Singh</manager>
</managers>
</shift>
<shift name="evening" startTime="4:00" endTime="12:00">
<managers>
<manager>Bradley</manager>
</managers>
</shift>
</shifts>
</schedule>
In this example, you can see several XML syntax rules at work:
All element names are enclosed in angle brackets: <>.
Each opening tag has a corresponding closing tag; closing tags start with a forward slash: /.
Hierarchical order must be maintained; elements can't overlap. In other words, you can't close the <schedule> element before you close the <shifts> element.
Element attributes can be defined within the opening tag, such as the name attribute on the <shift> element.
Some elements are self-closing; they don't have content, but they can have attributes:
<book title="Dubliners" author="Joyce" />
There's a lot more to XML than that, but you don't need to know the details to understand the XAML in this chapter. If you'd like to learn more, you can pick up XML in a Nutshell by Elliotte Rusty Harold and W. Scott Means (O'Reilly).
Figure 19.1. When you create a WPF project, the IDE opens with the XAML window in two views: the Design view on top, and the XAML markup below.
Next, you'll find three attributes for the Window itself: Title, Height, and Width. All three of these attributes are pretty self-explanatory, and they're reflected in the Design view as shown in Figure 19.1, "When you create a WPF project, the IDE opens with the XAML window in two views: the Design view on top, and the XAML markup below.". Go ahead and change the Width property from 300 to 100 and you'll see the window immediately shrink in the Design view. Change it back when you're done. You can also click on the window in the Design view, and all those properties will be available for you to change in the Properties window, just as you did with Windows Forms.
The final thing to notice in this XAML is the Grid element. The Grid is a subelement of the Window element, used for laying out the controls. For now, you'll just put the controls inside the Grid; we'll worry about positioning later.
Next, find the Label control in the Common section of the Toolbox. Drag the Label onto the window, anywhere you like. Notice that the guide lines don't appear when you're placing the control, like they did in Windows Forms. Instead, they appear after you've placed the Label. Once you've dropped the Label, the guide lines appear, and you can drag the Label around to an aesthetically pleasing spot.
As with the Window element, you can change the properties of the Label element in the Properties window, but it is easier to change them in the XAML window. When you added the Label, a line appeared in the XAML that looks something like this, depending on where you placed the Label:
<Label Height="28" Margin="77,28,81,0" Name="label1" VerticalAlignment="Top">Label</Label>
You can see the Name attribute in the middle of the line there. You won't find it in the Properties window, though. Edit the XAML window, and change the Name to lblHello. The other elements are for positioning; leave them the way they are for right now.
Now drag a Button control from the Toolbox onto the window, underneath the Label. In the XAML, change the Name to btnHello. Instead of having a Text property, as buttons did in Windows Forms, there's a Content property for the Button. You won't see the Content property directly in the XAML window; that's because the content of the button is between the <Button> and </Button> tags, where it currently says "Button". Change that text to "Say Hello", either in the XAML window or in the Properties window. Your IDE should look something like Figure 19.2, "When you place controls onto the Window element in the Design view, the appropriate XAML is automatically added to the XAML window. Changes you make to the XAML are also reflected in the Design view." at this point.
Figure 19.2. When you place controls onto the Window element in the Design view, the appropriate XAML is automatically added to the XAML window. Changes you make to the XAML are also reflected in the Design view.
So far, so good. Double-click btnHello, and you'll be taken to the code file, just as you were in Windows Forms, except this is called the code-behind file. The event handler is created for you, just like before. Add the following highlighted code to change the text of the label when the button is clicked:
private void btnHello_Click(object sender, RoutedEventArgs e)
{
lblHello.Content = "Hello WPF!";
}
Notice here that instead of setting the value of lblHello.Text, as you would have done in Windows Forms, you're changing the Content property of lblHello. That's all you need to do. Now run the application, and click the button. The content of the Label changes, just as you'd expect, as shown in Figure 19.3, "In this simple application, WPF behaves much like Windows Forms.".
Figure 19.3. In this simple application, WPF behaves much like Windows Forms.
WPF Differences from Windows Forms
What you did in Hello WPF isn't much different from what you could have done in Windows Forms. There are a few differences, however, even in this simple application, which aren't readily apparent. For example, run the program again, and try stretching out the borders of the window, as shown in Figure 19.4, "When you stretch the window in WPF, the controls stretch with it.".
Notice how the "Say Hello" button stretches as you stretch the window. That wouldn't happen in Windows Forms without some extra tweaking on your part (you can load one of the examples from Chapter 18, Creating Windows Applications if you want to check that). In fact, the label stretches too, but you can't see that because the Label control has no border, and it's the same color as the background.
Figure 19.4. When you stretch the window in WPF, the controls stretch with it.
Tip
If the button doesn't stretch for you, you probably have the button placed too far to one side of the window, so the margin is set to zero. Go back to the Design view and move the button until you see the arrows connecting the left and right sides of the button to the sides of the window.
Close the application and go back to the Window1.xaml window to see how that happened. Expand the XAML part of the window so that you can see it better. You'll find that it looks something like Example 19.1, "The XAML for your Hello WPF application is simple, but there's a lot going on here", although the actual values will probably be different in your case.
Example 19.1. The XAML for your Hello WPF application is simple, but there's a lot going on here
<Window x:Class="Example_19_1_ _ _ _Hello_WPF.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Label Height="28" Margin="77,28,81,0" Name="lblHello"
VerticalAlignment="Top">Label</Label>
<Button Height="23" Margin="77,66,126,0" Name="btnHello"
VerticalAlignment="Top" Click="btnHello_Click">
Say Hello</Button>
</Grid>
</Window>
We talked about the Window and Grid elements earlier. Within the Grid, you can see two elements: the Button and the Label that you added. The label has a handful of properties: the Name property that you changed earlier; a Height property that defines the height of the window, in units; a VerticalAlignment property that indicates which side of the window the control is aligned with; and finally, the Margin property.
The Margin property requires a bit of explanation; its value is set with four integers, separated by commas. Those integers indicate the distance between the control and each of the four sides, in a strict order: left, top, right, and bottom. If any of the values are zero, as is the case with the bottom values in this example, the distance doesn't matter. If you use a single integer, all four sides use that value as the margin. You can also use two integers, in which case the first integer sets the left and right margins and the second sets the top and bottom margins.
In our example, the button is always 77 units from the left side of the window, and 126 units from the right. It's 66 units from the top, but the bottom doesn't matter. So, if you resize the window horizontally, the button stretches to keep the distances constant (unless you make the window too small). If you resize the bottom edge, though, the button doesn't move.
Tip
Units in WPF are always 1/96 of an inch. The standard Windows resolution is 96 pixels to an inch, so at normal resolution, one unit is one pixel. These units are absolute, though, so if you're using a different resolution, 96 pixels might be more or less than an inch, but 96 units is always 1 inch.
Now you're going to alter the properties of the controls in the XAML window, just to see what it can do. First, select the Grid element. You can do this either by clicking in the window, anywhere that's not one of the controls, or by simply clicking on the <Grid> element in the XAML window. Either way, you'll see the properties of the Grid element in the Properties window. In the Brushes section, click the Background property and set it to whatever you like (we chose IndianRed for this example). You can also simply type in the XAML window: type a space after the word Grid, and IntelliSense will provide a list of all the available properties and methods. Once you select Background, IntelliSense will provide a list of all the available background colors. You could do this with a Windows form as well, but your color choices would be somewhat more limited.
Now that you've changed the background color of the Grid, your Label control may be a bit harder to read. Click on the Label and scroll to the Brushes section in the Properties window. Set the Background property to whatever you like (we used SlateGray), and set the Foreground to White. Now scroll up to the Appearance section, and set the Opacity property to 0.5. This is a property that's not available in Windows Forms. Click the Button control and set its Opacity to 0.5 as well. Run your application now, and you'll see that the two controls have a translucent property to them, which is something similar to the Aero interface in Windows Vista. Example 19.2, "You can easily edit the properties of controls in the XAML file to create effects that aren't possible in Windows Forms" shows the XAML for this application, simple as it is.
Example 19.2. You can easily edit the properties of controls in the XAML file to create effects that aren't possible in Windows Forms
<Window x:Class="Example_19_2_ _ _ _Properties_in_WPF.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid Background="IndianRed">
<Label Height="28" Margin="77,28,81,0" Name="lblHello"
VerticalAlignment="Top" Opacity="0.5"
Background="SlateGray" Foreground="White">
Label</Label>
<Button Height="23" Margin="77,66,126,0" Name="btnHello"
VerticalAlignment="Top" Click="btnHello_Click"
Opacity="0.5">Say Hello</Button>
</Grid>
</Window>
Using Resources
You've seen how you can affect the properties of a single control, but in an application with more than one control, you wouldn't want to have to set each control's properties individually. That's where resources come in. You define a resource for an entire WPF application, where it can be used by any appropriate control.
To see how this works, you'll add another Label and Button to your Hello WPF example. Drag a Label control to somewhere near the upper-left corner of the window. Drag a Button control next to the Say Hello button. You can see in the Design window that the new controls have the Windows standard appearance, not the custom appearance that you gave the two older controls. In some cases, that's fine, but most of the time, you'll want all the controls in your application to have a consistent appearance.
For the Label, change its name to lblTop, and set its Content property to "WPF Test". You won't do anything programmatically with this label; it's just for demonstration purposes. You might as well give the button something to do, though. Change its Name to btnCancel, and its Content property to "Cancel". Double-click the button to create its event handler, and add the following code:
this.Close( );
In a WPF application, you use the Close( ) method to end the application, rather than Application.Exit( ), as you would in Windows Forms.
To get the controls of each type to have the same appearance, you'll define a Style element as a resource. Resources are generally scoped to particular elements. You could create a resource for each individual control, but it's easier to do it at the scope of the Window element.
In the XAML window, after the opening Window tag, enter the following code:
<Window.Resources>
<Style x:Key="btnStyle" TargetType="Button">
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style x:Key="lblStyle" TargetType="Label">
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Background" Value="SlateGray" />
<Setter Property="Foreground" Value="White" />
</Style>
</Window.Resources>
You start off with the Window.Resources element; that's clear enough. The next line defines the Style element. As we mentioned before, the x is used to indicate the namespace for this application. Here, you're defining a Key as part of the namespace so that other elements can refer to it elsewhere in the form. We've given the Key a value of btnStyle, to make it obvious what it's referring to, but just as you saw with dictionaries back in Chapter 14, Generics and Collections, a Key can be anything you like, as long as you can find it later. The TargetType property restricts the style to being applied to Button controls only; it's not strictly necessary, but if you defined a style specifically for Button controls, without using the TargetType property, and later tried to apply that style to a TextBox, you could cause an error, depending on the specific styles you defined.
Once you've opened the Style element, you can define some Setter elements. These, as the name implies, set properties of the target. In this case, the only change you're making to the Button control is to set the Opacity property to 0.5, so you provide the Property, and then the Value.
You then close the Style element for the Button, and open one for the Label control, cleverly named lblStyle. This style has a few more properties than btnStyle does, but they're all pretty simple.
The next step is to apply those styles to the individual controls. You do that within the element for each control, with the Style attribute:
<Label Style="{StaticResource lblStyle}" Height="28"
Margin="77,83,81,0" Name="lblHello" VerticalAlignment="Top">
Label</Label>
In this case, you define the Style property with a static resource, which means that the control element will look for the style definition with the appropriate name elsewhere in the XAML file, within the scope that it can access. You defined lblStyle as a resource for the entire Window, so the Label can find and use that resource. Note that the curly braces are required.
Now apply the lblStyle to the other Label, and the btnStyle to the two Button controls. You should find that the styles are applied immediately in the Design window, and of course they stay if you run the application. The entire XAML file for this example is shown in Example 19.3, "When you define a resource for the Window element, that resource is available to all the elements in the window".
Example 19.3. When you define a resource for the Window element, that resource is available to all the elements in the window
<Window x:Class="Example_19_3_ _ _ _Classes_and_Styles.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<Style x:Key="btnStyle" TargetType="Button">
<Setter Property="Opacity" Value="0.5" />
</Style>
<Style x:Key="lblStyle" TargetType="Label">
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Background" Value="SlateGray" />
<Setter Property="Foreground" Value="White" />
</Style>
</Window.Resources>
<Grid Background="IndianRed">
<Label Style="{StaticResource lblStyle}" Height="28"
Margin="77,83,81,0" Name="lblHello"
VerticalAlignment="Top">Label</Label>
<Button Style="{StaticResource btnStyle}" Height="23"
Margin="77,0,126,105" Name="btnHello"
VerticalAlignment="Bottom" Click="btnHello_Click">
Say Hello</Button>
<Label Style="{StaticResource lblStyle}" Height="28"
HorizontalAlignment="Left" Margin="15,18,0,0"
Name="lblTop" VerticalAlignment="Top" Width="120">
WPF Test</Label>
<Button Style="{StaticResource btnStyle}" Height="23"
HorizontalAlignment="Right" Margin="0,0,26,105"
Name="btnCancel" VerticalAlignment="Bottom"
Width="75" Click="btnCancel_Click">Cancel</Button>
</Grid>
</Window>
Animations
This control and resource stuff is great, but it's not exactly exciting, we know. We promised you animations, and animations you shall have. The best way to make animations in WPF is with a tool called Expression Blend (available at https://www.microsoft.com/expression), but in this case, we'll show you how to do it the hard way so that you'll know what's going on in the XAML. In this example, you're going to start with a simple square, and perform some of the more basic animations on it. The square is in fact an instance of the built-in Rectangle control, which is part of the WPF schema. However, you can apply these animations to many other controls.
The first thing to do is define the Rectangle control. Create a new WPF project to start with. You could drag a Rectangle control out of the toolbar onto the window, but in this case, it's easier to just define the rectangle in the XAML. Add the following element inside the <Grid> element:
<Rectangle Name="myRectangle" Width="100" Height="100">
</Rectangle>
Now you should see a square in the Design window, 100 units on a side. You didn't define the Margin property, so the square will be centered in the window, which is fine. The first thing you're going to do is animate the color of the square, and to do that, you'll need the square to have some color first. If you click on the Rectangle element, you'll see it has a Fill property, which is the interior color of the square (as opposed to the outside border, which we'll leave blank). You could simply set the Fill property to a color, but you can't manipulate the Fill property directly from an event, which is what you'll want to do later. That means you have to define a Brush, which is a property of Fill that you use for applying color, and other drawing properties. Inside the Rectangle element, add this new subelement:
<Rectangle.Fill>
<SolidColorBrush x:Name="rectangleBrush" Color="Blue" />
</Rectangle.Fill>
What you've done here is define a Brush, specifically a SolidColorBrush-there are more complicated Brush types, used for gradients and other fancy drawing tools, but we'll stick with a solid color here. Notice specifically that you've used the x element to give this brush a name that can be referred to elsewhere in the code-that's critical to the animation, as you'll see in a moment.
Triggers and Storyboards
So far, you haven't animated anything. To start the animation, you'll need a trigger. Triggers are attached to WPF elements, and they're similar to event handlers in that they react to events within the application, but they don't necessarily require you to write any code. This trigger will be attached to the Rectangle element, so below your new <Rectangle.Fill> element, add another element, <Rectangle.Triggers>:
<Rectangle.Fill>
<SolidColorBrush x:Name="rectangleBrush" Color="Blue" />
</Rectangle.Fill>
<Rectangle.Triggers></Rectangle.Triggers>
Notice that IntelliSense provides the closing tag for you. This subelement will hold any triggers you define for Rectangle, and you'll add one in a moment.
Much like event handlers, triggers need events to react to. WPF defines two different kinds of events, routed events and attached events. Routed events are events raised normally by a single control; attached events are more complicated, so we won't worry about them here. You're going to use a routed event for this trigger, the Loaded event on the Rectangle element. As the name implies, this event is fired when the Rectangle element is loaded, which happens as soon as the application starts. Add that trigger element now, and then we'll define the animation to go inside it:
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
</EventTrigger>
</Rectangle.Triggers>
Animations live inside special elements called storyboards, so that's what you need to define next. The trigger element can't contain just a storyboard by itself; it has to contain a storyboard action. Therefore, you first need to add a <BeginStoryboard> element inside the EventTrigger element. Inside the BeginStoryboard element, you define the Storyboard:
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
You can also define a storyboard elsewhere in your code, as a resource. You'll see how that works in the next example, but we won't do that here.
After all these nested elements, it's finally time to get to the animation. WPF defines several different kinds of animations, depending on what you're animating. You can think of an animation as changing a property of an element over time. For example, if you define an animation to make a rectangle increase in width, you're changing its Width property, which is defined as a double data type, so you'd use the DoubleAnimation for the animation type. In this case, you're going to change the color of the square, so you'll use the ColorAnimation type. Add the following code inside the Storyboard element:
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetName="rectangleBrush"
Storyboard.TargetProperty="Color"
From="Blue" To="Red" Duration="0:0:10"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
This all breaks down pretty easily. First, you open the <ColorAnimation> tag. Then you define the TargetName of the storyboard; that's the rectangleBrush you defined earlier, and now you know why you gave it a name, so you could refer to it down here. You also have a TargetProperty to indicate what property of the target Brush you're going to change, which in this case is Color. So far, so good.
The next few elements define how the animation works. The From property indicates the starting value of the animation; in this case, the starting color. The To property indicates the value where you want to end up; in this case, Red. The Duration indicates how long it takes for the value to change from the starting value to the final value. The duration is measured in hours, minutes, and seconds, so 0:0:10 means the color change will take 10 seconds to happen. The last line just adds a bit of class to the animation: the AutoReverse property indicates that the animation will turn the square blue, then back to red, automatically. The RepeatBehavior is used to limit how many times the animation will repeat; the value of Forever means that it'll repeat until the user closes the application.
Run your application now. You'll see your square, centered in the window, slowly changing from blue through purple to red, and back again, every 10 seconds. Feel free to stop the application and play with the values to get different effects.
Tip
If you wanted to shift colors between more than two values-say, down the spectrum from red to yellow to green to blue-you'd use a different type of animation called ColorAnimationUsingKeyFrames. That's a little complex for this example, though.
The result looks like Figure 19.5, "You can't tell this on the page, but this square is slowly changing color.", and the full XAML is shown in Example 19.4, "You can create animation effects entirely in XAML, without having to write any C# code".
Figure 19.5. You can't tell this on the page, but this square is slowly changing color.
Example 19.4. You can create animation effects entirely in XAML, without having to write any C# code
<Window x:Class="Example_19_4_ _ _ _Animations.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Rectangle Name="myRectangle" Width="100" Height="100">
<Rectangle.Fill>
<SolidColorBrush x:Name="rectangleBrush"
Color="Blue" />
</Rectangle.Fill>
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetName=
"rectangleBrush"
Storyboard.TargetProperty="Color"
From="Blue" To="Red"
Duration="0:0:10"
AutoReverse="True"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
</Grid>
</Window>
It's important to note here that you haven't written a single line of C# code for this example. This entire animation happened declaratively, in the XAML file. WPF contains the code to automate all of this for you.
Animations As Resources
We mentioned earlier that you can define an animation as a resource; in this next example, we'll show you how. You'll rotate the square in the window, and you'll see how it can respond to user events.
Rotating elements in WPF is pretty easy. You'll need to add a RenderTransform property to the Rectangle, just like you added the Fill element to hold the Brush. Add the following element after the Rectangle.Fill element from earlier:
<Rectangle.RenderTransform>
<RotateTransform x:Name="rectangleRotate" Angle="0.0" />
</Rectangle.RenderTransform>
You've defined a RenderTransform element, with two properties. As with the Brush, you've given the RotateTransform a name, so you can refer to it from elsewhere in your code. You've also given it a starting angle, in this case, 0.
If you're going to rotate an element, you need to define the point on which the rotation will center; the hinge, if you want to imagine it that way. The default for a Rectangle is the upper-left corner, but we'd rather use the center point of the square instead. So, go back up to the definition of the Rectangle element, and add the following property:
<Rectangle Name="myRectangle" Width="100" Height="100"
RenderTransformOrigin="0.5,0.5">
The RenderTransformOrigin property is a nice shortcut for defining rotation points. Instead of requiring you to count pixels from the edge of the element, or the window, you can provide two coordinates between 0 and 1 that define the point in relation to the edges of the element. In this case, 0.5,0.5 indicates halfway through each dimension, or the exact center of the square.
Now that you have the necessary properties set on the rectangle, you can define the animation in a storyboard. This time, you'll define the storyboard as a resource for the window itself so that other elements in the window can use this rotate animation. Go up to the Window definition, and define a subelement, <Window.Resources>, before the <Grid> element, like this:
<Window x:Class="Example_19_5_ _ _ _More_animation.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Window.Resources>
</Window.Resources>
<Grid>
Within the Resources element, you'll define the storyboard. You don't need the <BeginStoryboard> element this time, because you're not defining an action, you're just defining the storyboard itself. Add the following code for the storyboard:
<Window.Resources>
<Storyboard x:Key="Rotate">
<DoubleAnimation
Storyboard.TargetName="rectangleRotate"
Storyboard.TargetProperty="Angle"
From="0.0" To="360.0" Duration="0:0:10"
RepeatBehavior="Forever"/>
</Storyboard>
</Window.Resources>
Notice first that you're providing the storyboard itself with a name that can be referenced elsewhere in the code; that's necessary because this is a resource for the entire window.
As before, the storyboard contains an animation. This time, you're changing the angle of the Rectangle, which is expressed as a double, so you're using a DoubleAnimation. You provide the TargetName and TargetProperty, just as you did in the previous example, but this time you're targeting the RotateTransform property instead of the Brush. The From and To elements go from zero to 360, or a full rotation, and the Duration indicates that it'll take 10 seconds to accomplish. The RepeatBehavior property indicates that the square will keep rotating until something stops it.
Now you've defined the animation, but it's not associated with any triggers. Instead of having the animation start when the Rectangle loads, you'll have it start when the user moves the mouse over the square. To do that, you'll need a new EventTrigger element, associated with a different event. Add the following code inside the <Rectangle.Triggers> element, but after the closing tag of the trigger that's already there:
<EventTrigger RoutedEvent="Rectangle.MouseEnter">
<BeginStoryboard Storyboard="{StaticResource Rotate}"
x:Name="BeginRotateStoryboard"/>
</EventTrigger>
The RoutedEvent that you're using this time is called MouseEnter, and it's raised by the Rectangle itself. Within the trigger is the BeginStoryboard element that you saw in the previous example, but instead of defining the storyboard here, you have a reference to the resource you defined earlier for the Window. You've also given the BeginStoryboard a name, so you can refer to that later; you'll see why in a minute.
For now, run the application, move the mouse over the square, and you'll see that the square rotates as it continues to change colors, as you'd see in Figure 19.6, "You still can't see it, but now the square is rotating in addition to changing color." if this book weren't black-and-white and static. You can experiment with the values in the storyboard, and with the angle and rotation origin defined in the Rectangle as well.
Figure 19.6. You still can't see it, but now the square is rotating in addition to changing color.
Let's take this animation one step further, and have the animation pause when the user moves the mouse off the square. To do that, you simply need to add another EventTrigger, after the one you just added, to handle a different event:
<EventTrigger RoutedEvent="Rectangle.MouseLeave">
<PauseStoryboard BeginStoryboardName="BeginRotateStoryboard" />
</EventTrigger>
Here, you're handling the MouseLeave event, which is raised when the cursor exits the element. Instead of the BeginStoryboard, you're using a PauseStoryboard action here, which halts the execution of the BeginStoryboard in the previous trigger-that's why you gave it a name, so you could refer to it here.
Run your application again, and you'll see that the animation stops when the mouse leaves the square. If you bring the mouse back inside the square the animation starts again, but the angle starts over from zero. Fixing that would be somewhat more complicated, and beyond the scope of this chapter. Once again, notice that you still haven't written any C# code to accomplish these animations, even with the event triggers. The full XAML file for this example is shown in Example 19.5, "This time you're creating the animation as a resource at the window level, but you still don't need to write any C# code".
Example 19.5. This time you're creating the animation as a resource at the window level, but you still don't need to write any C# code
<Window x:Class="Example_19_5_ _ _ _More_animation.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<Storyboard x:Key="Rotate">
<DoubleAnimation
Storyboard.TargetName="rectangleRotate"
Storyboard.TargetProperty="Angle"
From="0.0" To="360.0" Duration="0:0:10"
RepeatBehavior="Forever"/>
</Storyboard>
</Window.Resources>
<Grid>
<Rectangle Name="myRectangle" Width="100" Height="100"
RenderTransformOrigin="0.5,0.5">
<Rectangle.Fill>
<SolidColorBrush x:Name="rectangleBrush"
Color="Blue" />
</Rectangle.Fill>
<Rectangle.RenderTransform>
<RotateTransform x:Name="rectangleRotate"
Angle="0.0" />
</Rectangle.RenderTransform>
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetName=
"rectangleBrush"
Storyboard.TargetProperty="Color"
From="Blue" To="Red"
Duration="0:0:10"
AutoReverse="True"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="Rectangle.MouseEnter">
<BeginStoryboard
Storyboard="{StaticResource Rotate}"
x:Name="BeginRotateStoryboard"/>
</EventTrigger>
<EventTrigger RoutedEvent="Rectangle.MouseLeave">
<PauseStoryboard
BeginStoryboardName="BeginRotateStoryboard" />
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
</Grid>
</Window>
C# and WPF
So far, just about everything you've done with WPF has been declarative; that is, all the functionality has taken place in the XAML file. WPF is specifically designed that way, to be useful to designers as well as to developers. The only C# you've had to write so far has been some very rudimentary event handlers. In this section you're going to create an example that more closely resembles a production application, and that's going to involve a supporting class, and some event handlers.
In this example, you're going to grab the images of the first 20 presidents of the United States from the White House's website, and present them in a custom WPF control, a modified ListBox control. The control will not be wide enough to show all 20 images, so you'll provide a horizontal scroll bar, and as the user moves the mouse over an image, you'll provide feedback by enlarging that image (from 75 to 85) and increasing its opacity from 75% to 100%. As the user moves the mouse off the image, you'll return the image to its smaller, dimmer starting point.
This application will use some declarative animation, as you've already seen, although slightly subtler than the rotating square. In addition, when the user clicks on an image, you'll handle the click and display the name of the president using a C# event handler, and then you'll reach into the control and place the president's name into the title bar of the control.
Figure 19.7, "The Presidential Browser application makes use of some slightly subtler animations, but most of it still takes place in the XAML." shows the result of scrolling to the 16th president and clicking on the image. Note that the name of the president is displayed in the title bar and that the image of President Lincoln is both larger and brighter than the surrounding images.
Figure 19.7. The Presidential Browser application makes use of some slightly subtler animations, but most of it still takes place in the XAML.
Grids and Stack Panels
Create a new WPF application called Presidential Browser. Up until now, you've placed all your elements in the default Grid control that WPF provides. This time, though, you want two items in the grid-the text block that says "United States Presidents" and the sideways ListBox of photographs, so you can make use of WPF's layout elements.
In addition to the grid element, WPF provides a layout object called a stack panel. A stack panel lets you stack a set of objects one on top of (or next to) another set of objects. That turns out to be very useful for laying out your page. If you want a stack that is horizontal and vertical (essentially a table), that's what the grid element is for. A grid has columns and rows, both counting from zero.
You'll create a simple grid of two rows and one column, and inside each row you'll place a stack panel. The top stack panel will hold the text, and the bottom stack panel will hold the ListBox that will, in turn, hold the photos. We'll break this down for you and take it one step at a time.
To begin, set the width of the Window element to 330 and the height to 230. Next, give the grid some dimensions, by adding properties to the grid element. A width of 300 and a height of 190 should do it. Add the properties like this:
<Grid Width="300" Height="190" >
Next, you'll need to define the rows in the grid element. That's easy enough to do with the RowDefinition element, but you'll need to put that within a <Grid.RowDefinitions> element, like this:
<Grid Width="300" Height="190">
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
You know that you want the first row to be a fixed 20 units high, so that number's hardcoded. The second row, though, should be whatever space is left in the grid. You could do the math yourself (and come up with 170), but the * element lets WPF do it for you.
The next things you need to add to the Grid are the two StackPanel elements. These are relatively easy: you just add the StackPanel elements inside the <Grid> tags. Inside each StackPanel, you'll add a TextBlock element, which does what it sounds like-it holds text. The TextBlock is a flexible control for displaying text, but here we're just using it to align the text in the panel. Add the following code to the XAML:
<StackPanel Grid.Row="0">
<TextBlock FontSize="14">United States Presidents
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="1">
<TextBlock Text="Bottom Stack Panel" VerticalAlignment="Center"/>
</StackPanel>
The first thing you need to notice here is that the rows in the grid are numbered from zero, the same as with arrays. The TextBlock element has a property for FontSize; it also has font weight and font family and the other features you might expect in a text element.
Your XAML code should now look like Example 19.6, "This is the starting XAML for the Presidential Browser application, with the layout elements in place".
Example 19.6. This is the starting XAML for the Presidential Browser application, with the layout elements in place
<Window x:Class="Presidential_Browser.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid Width="300" Height="190">
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<TextBlock Text="Top Stack Panel" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1">
<TextBlock Text="Bottom Stack Panel" VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Window>
Defining ListBox styles
Your next goal is to get the pictures into a ListBox and to turn the ListBox sideways so that the pictures scroll along, as shown previously in Figure 19.7, "The Presidential Browser application makes use of some slightly subtler animations, but most of it still takes place in the XAML.".
To accomplish that, we need to do two things: we need to work with the style of the ListBox, and we need to work with its data. We'll take these one at a time to make it clear.
You've created a Brush as a resource before; now you'll make one for the entire Window element, in a Window.Resources section. This brush will be a LinearGradientBrush, which is a bit fancier than the Fill brush you used before. The gradient brush uses a nice shading effect that changes gradually through the colors identified in the GradientStop elements. The exact details aren't important, but you should note that we're giving this resource a name, as all resources have, so we can use it on the ListBox later. Create a Window.Resources section at the top of your XAML file, and add this code:
<Window.Resources>
<LinearGradientBrush x:Key="ListBoxGradient"
StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#90000000" Offset="0" />
<GradientStop Color="#40000000" Offset="0.005" />
<GradientStop Color="#10000000" Offset="0.04" />
<GradientStop Color="#20000000" Offset="0.945" />
<GradientStop Color="#60FFFFFF" Offset="1" />
</LinearGradientBrush>
</Window.Resources>
Briefly, all linear gradients are considered as occurring on a line ranging from 0 to 1. You can set the start points and endpoints (traditionally, the start point 0,0 is the upper-left corner and the endpoint 1,1 is the lower-right corner, making the linear gradient run on an angle). We've set the linear gradient to end at 0,1, making the gradient run from top to bottom, giving a horizontal gradient, moving through five colors, unevenly spaced.
The next resource you need to define is a Style object. We haven't specifically applied a style as a resource yet, but Style objects work like any other resource: they manage the properties of their targets, in this case, their style properties.
A difference in this case is that instead of defining a TargetName for the resource, you'll define a TargetType. That means that the style will be applied to all objects of a specific type (in this case, ListBox). Within the Style, you define a Template, which means that the style definition can be applied to objects, or modified by them. Within that, there's a set of properties defined for the Border element, most of which are pretty self-explanatory. Notice that for a background, the Border element uses the ListBoxGradient brush that you defined a moment ago.
Within the Border element is a ScrollViewer element. This element is what gives the ListBox a horizontal scroll bar, but not a vertical one. Within the ScrollViewer is another StackPanel element-this is where you'll keep the images of the presidents. The IsItemsHost property indicates that this StackPanel will hold other objects (you'll see how this works in a bit), and the Orientation and HorizontalAlignment properties simply orient the StackPanel inside the ScrollViewer.
Add the following Style within the Window.Resources element, right after the LinearGradiantBrush:
<Style x:Key="SpecialListStyle" TargetType="{x:Type ListBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}" >
<Border BorderBrush="Gray"
BorderThickness="1"
CornerRadius="6"
Background="{DynamicResource ListBoxGradient}" >
<ScrollViewer
VerticalScrollBarVisibility="Disabled"
HorizontalScrollBarVisibility="Visible">
<StackPanel IsItemsHost="True"
Orientation="Horizontal"
HorizontalAlignment="Left" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Triggers and animations
The style for the ListBox that you just created contains a StackPanel that contains items. The next thing you'll do is define the style for these items. The items, you'll recall, are the images of the presidents that you're displaying. We mentioned that these images will change appearance when the user moves the mouse over them. You saw earlier in the chapter how to interact with the user's mouse movements-you'll need to define some triggers. The Triggers will reside in the Style, rather than being attached to a specific instance of a control.
This style begins by setting its target type, as the last style did (ListBoxItems), and three properties: MaxHeight, MinHeight, and Opacity. The MaxHeight and MinHeight properties have the same value, which means that the size of the items is fixed, but you'll be able to change that dynamically, as you'll see shortly. The Opacity of a control is defined as a value between 0 and 1:
<Style x:Key="SpecialListItem"
TargetType="{x:Type ListBoxItem}">
<Setter Property="MaxHeight" Value="75" />
<Setter Property="MinHeight" Value="75" />
<Setter Property="Opacity" Value=".75" />
The style then sets a couple of triggers. As with the triggers you saw earlier in the chapter, these triggers associate an EventTrigger with a RoutedEvent. Specifically, the first trigger uses the MouseEnter event that you saw in an earlier example:
<EventTrigger RoutedEvent="Mouse.MouseEnter">
In this case, the event will be kicked off when the mouse enters the object that is associated with this EventTrigger (that object will be the ListBox item), as opposed to targeting a specific control as you did earlier in the chapter. Within that EventTrigger you defined an EventTrigger.Actions element. In this case, the action is BeginStoryBoard, and there is a single, unnamed Storyboard:
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="MaxHeight" To="85" />
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="Opacity" To="1.0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
The action is inside the storyboard, where you'll find two animations. These are DoubleAnimation elements, because you're changing two properties with double values. These two animations are defined to have a duration of 2/10 of a second. The TargetProperty refers to the property of the object to be animated (that is, the ListBox item)-in the first case, the height of the ListBox item, which will be animated to a height of 85 (from a starting point of 75). The second animation will change the opacity from its starting point of .75 to 1 (making it appear to brighten). The other trigger is for the MouseLeave event, and just reverses the effects.
Here's the XAML for the entire style; add this to the Windows.Resources section:
<Style x:Key="SpecialListItem"
TargetType="{x:Type ListBoxItem}">
<Setter Property="MaxHeight" Value="75" />
<Setter Property="MinHeight" Value="75" />
<Setter Property="Opacity" Value=".75" />
<Style.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="MaxHeight"
To="85" />
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="Opacity"
To="1.0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="MaxHeight" />
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="Opacity" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
Adding Data
We're now going to cheat somewhat, and rather than getting our data from a web service or from a database, we're going to put it right into our resources. The data will consist of a generic list of ImageURL objects. You haven't heard of these types of objects before, because you're going to create the class right now. Right-click on the project file in the Solution Explorer and choose Add → Class. When the Add New Item dialog box appears, the Class item will be selected automatically. Leave it selected, and give the class file a name of PhotoCatalog.cs. Visual Studio will automatically open a new class file for you. Add the code in Example 19.7, "The ImageURL class defines a new class that you'll be able to use in the XAML application namespace", and be sure to add the using System.Windows.Media.Imaging statement, because you'll need it, and also be sure to add the public modifier to the class name.
Example 19.7. The ImageURL class defines a new class that you'll be able to use in the XAML application namespace
using System;
using System.Collections.Generic;
using System.Windows.Media.Imaging;
namespace PhotoCatalog
{
public class ImageURL
{
public string Path { get; private set; }
public Uri ImageURI { get; set; }
public BitmapFrame Image { get; private set; }
public string Name { get; set; }
public ImageURL( ) { }
public ImageURL(string path, string name)
{
Path = path;
ImageURI = new Uri(Path);
Image = BitmapFrame.Create(ImageURI);
Name = name;
}
public override string ToString( )
{
return Path;
}
}
public class Images : List<ImageURL> { }
}
You've actually created two classes here. The first class, ImageURL, is designed to act as a wrapper -that is, it'll hold the properties for an image that you retrieve given the path to an image, or a URI from which we can create an image. Note that you override ToString( ) to return the Path property, even though we haven't explicitly created the backing variable.
The second class is at the very bottom: Images derives from a generic list of ImageURL objects. The implementation is empty-all you're really doing with this class is providing an alias for List<ImageURL>.
Instantiating objects declaratively
Now that you've declared these classes, you can create instances of them in the Resources section of the XAML file-you're not limited to the types defined by the XAML namespace. However, before you can use your new type, you must first include the class in your XAML file by creating a namespace for this project. At the top of the XAML file, you'll find the other namespace declarations for this project; they all start with xmlns:. Add your own xmlns statement, and call the new namespace local, like this:
xmlns:local="clr-namespace:Presidential_Browser"
As soon as you type local=, IntelliSense will help you out with the rest of the namespace. You'll probably notice an error message in the Design window at that point, and you'll need to reload the designer. Go ahead and do that, and everything will be fine.
Now that you have the local namespace, you can create an instance of the Images class in the Window.Resources section, like this:
<local:Images x:Key="Presidents"></local:Images>
This is the XAML equivalent of writing:
List<ImageURL> Presidents = new List<ImageURL>( );
You then add to that list by creating instances of ImageURL between the opening and closing tags of the Images declaration:
<local:ImageURL ImageURI="http://www.whitehouse.gov/history/presidents/images/
gw1.gif" Name="George Washington" />
Again, this is the XAML equivalent of writing:
ImageURL newImage = new ImageURL(
"http://www.whitehouse.gov/history/presidents/images/gw1.gif,"
"George Washington");
Presidents.Add(newImage);
You'll need to do that 20 times, once for each of the first 20 presidents. The URL is somewhat long, so you might want to cut and paste it, or you can download the code listing for this chapter from http://www.oreilly.com and cut and paste that part. Now you have a static data resource you can refer to in the rest of your XAML file; and that completes the Resources section.
Using the Data in the XAML
Now that you've defined the resource, the next step is to provide a way for the Grid element to access the data in that resource. To do that, provide a DataContext for the Grid:
<Grid Width="300" Height="190"
DataContext="{StaticResource Presidents}">
Every Framework object has a DataContext object, usually null. If you don't instruct the object otherwise, it will look up the object hierarchy from where it is defined until it finds an object that does have a DataContext defined, and then it will use that DataContext as its data source. You can use virtually anything as a DataContext-a database, an XML file, or, as in this case, a static resource.
Defining the ListBox
Now that you've got all the resources in place, you're finally ready to define the ListBox and the template for its contents in the second StackPanel. The first thing you need to do is set some of the properties for the ListBox element:
<StackPanel Grid.Row="1" Grid.ColumnSpan="3" >
<ListBox Style="{StaticResource SpecialListStyle}"
Name="PresPhotoListBox" Margin="0,0,0,20"
SelectionChanged="PresPhotoListBox_SelectionChanged"
ItemsSource="{Binding }"
IsSynchronizedWithCurrentItem="True" SelectedIndex="0"
ItemContainerStyle="{StaticResource SpecialListItem}" >
The first line shown here places the stack panel into the grid at row offset 1 (the second row). The ListBox itself has its style set to a StaticResource (the SpecialListStyle resource you defined earlier in the Resources section). The ListBox is named:
Name="PresPhotoListBox"
And an event handler is defined for anytime an image is clicked:
SelectionChanged="PresPhotoListBox_SelectionChanged"
The source for each item is set to Binding, indicating that you're binding to the source in the parent element (which you just defined in the Grid element's DataContext property). Finally, the ItemContainerStyle is set, again, to the style defined earlier in the Resources section.
Each item in the ListBox will be drawn from the (unknown) number of items in the data (which in this case happens to be statically placed in the Resources, but could well be dynamically drawn from a web service). To do this, we'll need a template for how to draw each item. Add the following code as a subelement of the ListBox element:
<ListBox.ItemTemplate>
<DataTemplate>
<Border VerticalAlignment="Center"
HorizontalAlignment="Center" Padding="4"
Margin="2" Background="White">
<Image Source="{Binding Path=ImageURI}" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
Within the ListBox.ItemTemplate, you place a DataTemplate; this is necessary if you want to show anything more than simple text derived from the data retrieved. In this case, you place a Border object within the DataTemplate, and within the Border object you place the Image object. It is the Image object you really care about (though the Border object helps with placement). The Image requires a source; here, you add Binding (indicating that you are binding to the current source), and you state that you'll be using the ImageURI property to set the Path. Because the source you're binding to is a list of ImageURL objects, and each ImageURL has four public properties (Path, ImageURI, Image, and Name), this is the critical piece of data required to tell the DataTemplate how to get the information necessary to create the image in the ListBox.
Event Handling
Except for defining the ImageURL class, everything you've done so far in this example has been done declaratively, in the XAML file. Now it's finally time to write some C# in this example. You may have noticed that you did create an event handler for when the user changes the selected item in the ListBox:
SelectionChanged="PresPhotoListBox_SelectionChanged"
This is typically done by clicking on an image (though you can accomplish this with the arrow keys as well). This event will fire the event handler in the code-behind file, which is, finally, C#.
The event handler is, as you would expect, in the code-behind file, Window1.xaml.cs. Switch to that file now, and add the following event handler:
private void PresPhotoListBox_SelectionChanged(
object sender, SelectionChangedEventArgs e)
{
ListBox lb = sender as ListBox;
if (lb != null)
{
if (lb.SelectedItem != null)
{
string chosenName =
(lb.SelectedItem as ImageURL).Name.ToString( );
Title = chosenName;
}
}
else
{
throw new ArgumentException(
"Expected ListBox to call selection changed in
PresPhotoListBox_SelectionChanged");
}
}
Like all event handlers in .NET, this handler receives two parameters: the sender (in this case, the ListBox) and an object derived from EventArgs.
In the code shown, you cast the sender to the ListBox type (and consider it an exception if the sender is not a ListBox, as that is the only type of object that should be sending to this event handler).
You then check to make sure that the selectedItem is not null (during startup it is possible that it can be null). Assuming it is not null, you cast the selectedItem to an ImageURL, extract the Name property, and assign it to a temporary variable, chosenName, which we then in turn assign to the title of the window.
The interim variable is useful only for debugging; there is no other reason not to write:
Title = (lb.SelectedItem as ImageURL).Name.ToString( );
Tip
You can also get at both the currently selected president's ImageURL and the previously selected ImageURL through the SelectionChangedEventArgs parameter.
The Complete XAML File
If you're not sitting in front of your computer right now, Example 19.8, "Here is the complete XAML listing for the Presidential Browser application" has the complete XAML listing. Please replace the ellipses (…) in the URLs in this listing with:
"http://www.whitehouse.gov/history/presidents/images
We omitted this long URL from the listing to save space.
Example 19.8. Here is the complete XAML listing for the Presidential Browser application
<Window x:Class="Presidential_Browser.Window1"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Presidential_Browser"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<LinearGradientBrush x:Key="ListBoxGradient"
StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#90000000" Offset="0" />
<GradientStop Color="#40000000" Offset="0.005" />
<GradientStop Color="#10000000" Offset="0.04" />
<GradientStop Color="#20000000" Offset="0.945" />
<GradientStop Color="#60FFFFFF" Offset="1" />
</LinearGradientBrush>
<Style x:Key="SpecialListStyle" TargetType="{x:Type ListBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}" >
<Border BorderBrush="Gray"
BorderThickness="1"
CornerRadius="6"
Background=
"{DynamicResource ListBoxGradient}" >
<ScrollViewer
VerticalScrollBarVisibility="Disabled"
HorizontalScrollBarVisibility="Visible">
<StackPanel IsItemsHost="True"
Orientation="Horizontal"
HorizontalAlignment="Left" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SpecialListItem"
TargetType="{x:Type ListBoxItem}">
<Setter Property="MaxHeight" Value="75" />
<Setter Property="MinHeight" Value="75" />
<Setter Property="Opacity" Value=".75" />
<Style.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="MaxHeight"
To="85" />
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="Opacity"
To="1.0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="MaxHeight" />
<DoubleAnimation Duration="0:0:0.2"
Storyboard.TargetProperty="Opacity" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
<local:Images x:Key="Presidents">
<local:ImageURL ImageURI=".../gw1.gif" Name="George Washington" />
<local:ImageURL ImageURI=".../ja2.gif" Name="John Adams" />
<local:ImageURL ImageURI=".../tj3.gif" Name="Thomas Jefferson" />
<local:ImageURL ImageURI=".../jm4.gif" Name="James Madison" />
<local:ImageURL ImageURI=".../jm5.gif" Name="James Monroe" />
<local:ImageURL ImageURI=".../ja6.gif" Name="John Quincy Adams" />
<local:ImageURL ImageURI=".../aj7.gif" Name="Andrew Jackson" />
<local:ImageURL ImageURI=".../mb8.gif" Name="Martin Van Buren" />
<local:ImageURL ImageURI=".../wh9.gif" Name="William H. Harrison" />
<local:ImageURL ImageURI=".../jt10_1.gif" Name="John Tyler" />
<local:ImageURL ImageURI=".../jp11.gif" Name="James K. Polk" />
<local:ImageURL ImageURI=".../zt12.gif" Name="Zachary Taylor" />
<local:ImageURL ImageURI=".../mf13.gif" Name="Millard Fillmore" />
<local:ImageURL ImageURI=".../fp14.gif" Name="Franklin Pierce" />
<local:ImageURL ImageURI=".../jb15.gif" Name="James Buchanan" />
<local:ImageURL ImageURI=".../al16.gif" Name="Abraham Lincoln" />
<local:ImageURL ImageURI=".../aj17.gif" Name="Andrew Johnson" />
<local:ImageURL ImageURI=".../ug18.gif" Name="Ulysses S. Grant" />
<local:ImageURL ImageURI=".../rh19.gif" Name="Rutherford B. Hayes" />
<local:ImageURL ImageURI=".../jg20.gif" Name="James Garfield" />
</local:Images>
</Window.Resources>
<Grid Width="300" Height="190"
DataContext="{StaticResource Presidents}">
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<TextBlock FontSize="14" Grid.Row="0" >
United States Presidents</TextBlock>
</StackPanel>
<StackPanel Grid.Row="1" Grid.ColumnSpan="3" >
<ListBox Style="{StaticResource SpecialListStyle}"
Name="PresPhotoListBox" Margin="0,0,0,20"
SelectionChanged="PresPhotoListBox_SelectionChanged"
ItemsSource="{Binding }"
IsSynchronizedWithCurrentItem="True"
SelectedIndex="0"
ItemContainerStyle=
"{StaticResource SpecialListItem}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Border VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="4" Margin="2" Background="White">
<Image Source="{Binding Path=ImageURI}" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</Window>
Run the application now, and you should see that it looks like Figure 19.7, "The Presidential Browser application makes use of some slightly subtler animations, but most of it still takes place in the XAML." (shown previously). The individual images animate when you mouse over them, and clicking one changes the title bar of the window to show the president's name.
Silverlight
As you've seen with WPF, the flexibility of defining the design of your interface in XML lets you include features that you wouldn't have thought possible before. In fact, you can make Windows applications that don't look like Windows applications at all, but more like web applications. The idea of separating the design of the application from the underlying functionality opens up interesting possibilities.
In Windows and WPF applications, both the interface and the functional code reside on your local machine; the separation is entirely in the design stage. On the Web, though, the application usually resides on a remote server, with the client using only a browser. ASP.NET with AJAX brings the two a bit closer together, where some of the controls are created in code that runs in the browser, reducing the number of times the browser needs to get data from the server, which makes for a faster, smoother user experience.
Silverlight narrows the gap even more, by bringing the WPF tools to run in browsers over the Web. Silverlight requires a browser plug-in that the remote user has to accept. That plug-in contains a carefully chosen subset of the Common Language Runtime (CLR), allowing Silverlight applications to use managed code languages like VB.NET or C# and to implement applications built on a subset of XAML.
The net result is that Silverlight applications are much faster, are richer, and have capabilities that are simply not possible with AJAX, but they are limited to running on IE, Firefox, and Safari on Windows, the Mac, and Linux, for now. Silverlight is in version 2.0 at the time of this writing, and a lot of development remains to be done, but the possibilities are very exciting.
Learning Silverlight is not hard, and what you've learned about XAML in this chapter gives you a significant head start, but there is quite a bit to it. A full discussion would require a book in itself. See Programming Silverlight by Jesse Liberty and Tim Heuer (O'Reilly) to learn more.
Summary
Windows Presentation Foundation (WPF) is a system intended to combine the functionality of Windows Forms with greater flexibility in interface design.
In WPF, the presentation of the application is kept in a separate file, written in XAML, which is a dialect of XML.
When you start a new WPF project, Visual Studio opens both the Design window and the XAML window. Changes made in one window are immediately reflected in the other.
The XAML file uses a Window element as its root element.
Each WPF application uses a distinct namespace, defined by the Microsoft XAML schema, and you can add your own objects to that namespace.
XAML contains several elements for positioning other elements within the application, including the Grid and Stackpanel elements.
You can set the properties of XAML elements in the Properties window, or by editing the XAML directly.
Event handlers for WPF elements are kept in a code file, written in C# or another .NET language.
Resources allow you to define properties for use by any appropriate element in the application.
Resources require you to define a key in the current namespace so that you can refer to them later in the application.
A Style element, which can be defined on an element or as a resource, can contain a number of Setter elements that define specific properties of the target element.
Routed events in WPF can be associated with triggers, which can change the properties of elements in response to events. Triggers can be defined on individual elements, or as resources.
Animation elements are contained within Storyboard elements. There are different animation elements depending on the type of the value that the animation is changing.
A trigger can contain a storyboard action, which can contain a storyboard element, but a trigger cannot contain a storyboard by itself.
Many triggers can be declared completely declaratively, without needing to write any C# code.
The DataContext property allows an element to access a data source.
Your skills have come a long way from where they were at the beginning of the book, through the various pieces of the C# language, and now into Windows applications in a couple of different ways. There's still one major piece of the development puzzle that we've left untouched, though: handling data. You saw in the Presidential Browser application in this chapter that you had to handcode the URLs for 20 images into the Resources section of your application; that's a real pain. It would have been much easier if you could have read the information directly from a repository of some kind. Fortunately, many such repositories are available, from simple XML files to full-fledged SQL databases, and C# can access many of them. We'll spend the final two chapters of this book looking at how to access data from C#, first with ADO.NET and then with the newer Language Integrated Query (LINQ) methods.
Test Your Knowledge: Quiz
Question 19-1. What is XAML?
Question 19-2. What are two ways to edit the properties of a XAML element?
Question 19-3. What does the x: refer to in element definitions?
Question 19-4. What's the syntax for the Margin property?
Question 19-5. If you had an application with 32 buttons, what's the easiest way to ensure that all the buttons are colored blue?
Question 19-6. What property should you use to ensure that your style will be applied to controls of only a certain type?
Question 19-7. What element allows you to handle certain WPF events within the XAML file?
Question 19-8. What kind of control contains an animation?
Question 19-9. What is the purpose of the BeginStoryboard element?
Question 19-10. What property do you use to enable an element to access a data source?
Test Your Knowledge: Exercises
Exercise 19-1. We'll start things off simply. Create a WPF application with several Button and TextBox controls. Set the TextBox controls to have white text on a blue background, and the Button controls to have green text on a gray background.
Exercise 19-2. Now you'll create your own animation. Create a WPF application with a single Button control (it doesn't have to do anything). Add an animation that increases the size of the button from the standard size to 300 units wide by 200 units high, and then reverses itself.
Exercise 19-3. Create a rectangle, 100 by 200. Add three buttons to the application: one to rotate the rectangle clockwise, the second to rotate it counterclockwise, and the third to stop the rotation.