Preface
There are a lot of advanced articles on WPF, and their desktop applications reflect a lot of experience. But sometimes, the learning curve can be steep, as there may be a large gap between those at the advanced level and those at the beginning level. This article is an attempt to step through the building process. In any form of programming, the best way to learn how to program is to read well-written programs. This paper will dive into a seemingly complicated WPF application. The XAML code will appear long-winded and the code-behind relatively simple. So we'll start by using either Expression Blend or Visual Studio 2008 (or 2010) to make and develop this application. Once we are finished, we will examine how it works, and more importantly, why it works. So we begin by starting a new C# WPF application, and call it the default, WPFApp4. We then modify the code in MainWindow.xaml:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WpfApp4.MainWindow"
Title="Color Spinner" Height="370"
Width="270">
<Window.Resources>
<Storyboard x:Key="Spin">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ellipse1"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(
TransformGroup.Children)[0].(RotateTransform.Angle)"
RepeatBehavior="Forever">
<SplineDoubleKeyFrame KeyTime="00:00:10" Value="360"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ellipse2"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(TransformGroup.Children)[0]
.(RotateTransform.Angle)"
RepeatBehavior="Forever">
<SplineDoubleKeyFrame KeyTime="00:00:10" Value="-360"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ellipse3"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(TransformGroup.Children)[0]
.(RotateTransform.Angle)"
RepeatBehavior="Forever">
<SplineDoubleKeyFrame KeyTime="00:00:05"
Value="360"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ellipse4"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(TransformGroup.Children)[0]
.(RotateTransform.Angle)"
RepeatBehavior="Forever">
<SplineDoubleKeyFrame KeyTime="00:00:05"
Value="-360"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="ellipse1"/>
</Window.Resources>
<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource Spin}"
x:Name="Spin_BeginStoryboard"/>
<BeginStoryboard Storyboard="{StaticResource ellipse1}"/>
</EventTrigger>
<EventTrigger RoutedEvent="ButtonBase.Click"
SourceName="goButton">
<ResumeStoryboard BeginStoryboardName="Spin_BeginStoryboard"/>
</EventTrigger>
<EventTrigger RoutedEvent="ButtonBase.Click"
SourceName="stopButton">
<PauseStoryboard
BeginStoryboardName="Spin_BeginStoryboard"/>
</EventTrigger>
</Window.Triggers>
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1"
StartPoint="0.5,0">
<GradientStop Color="#FFFFFFFF" Offset="0"/>
<GradientStop Color="#FFFFC45A" Offset="1"/>
</LinearGradientBrush>
</Window.Background>
<Grid>
<Ellipse Margin="50,50,0,0" Name="ellipse5"
Stroke="Black" Height="150"
HorizontalAlignment="Left"
VerticalAlignment="Top" Width="150">
<Ellipse.Effect>
<BlurEffect Radius="10"/>
</Ellipse.Effect>
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color="#FF000000"
Offset="1"/>
<GradientStop Color="#FFFFFFFF"
Offset="0.306"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Margin="15,85,0,0" Name="ellipse1"
Stroke="{x:Null}"
Height="80" HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="120" Fill="Red" Opacity="0.5"
RenderTransformOrigin="0.92,0.5" >
<Ellipse.Effect>
<BlurEffect/>
</Ellipse.Effect>
<Ellipse.RenderTransform>
<TransformGroup>
<RotateTransform Angle="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<Ellipse Margin="85,15,0,0" Name="ellipse2"
Stroke="{x:Null}"
Height="120" HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="80" Fill="Blue" Opacity="0.5"
RenderTransformOrigin="0.5,0.92" >
<Ellipse.Effect>
<BlurEffect/>
</Ellipse.Effect>
<Ellipse.RenderTransform>
<TransformGroup>
<RotateTransform Angle="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<Ellipse Margin="115,85,0,0" Name="ellipse3"
Stroke="{x:Null}"
Height="80" HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="120" Opacity="0.5" Fill="Yellow"
RenderTransformOrigin="0.08,0.5" >
<Ellipse.Effect>
<BlurEffect/>
</Ellipse.Effect>
<Ellipse.RenderTransform>
<TransformGroup>
<RotateTransform Angle="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<Ellipse Margin="85,115,0,0" Name="ellipse4"
Stroke="{x:Null}"
Height="120" HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="80" Opacity="0.5" Fill="Green"
RenderTransformOrigin="0.5,0.08" >
<Ellipse.Effect>
<BlurEffect/>
</Ellipse.Effect>
<Ellipse.RenderTransform>
<TransformGroup>
<RotateTransform Angle="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
<Button Height="23" HorizontalAlignment="Left"
Margin="20,0,0,56"
Name="goButton" VerticalAlignment="Bottom"
Width="75" Content="Go"/>
<Button Height="23" HorizontalAlignment="Left"
Margin="152,0,0,56"
Name="stopButton" VerticalAlignment="Bottom" Width="75"
Content="Stop"/>
<Button Height="23" HorizontalAlignment="Left"
Margin="85,0,86,16"
Name="toggleButton" VerticalAlignment="Bottom"
Width="75"
Content="Toggle"/>
</Grid>
</Window>
Double-click the Toggle button in the Design view and modify the code in MainWindow.xaml.cs as follows (both the using
statement and the new code in the toggleButton_Click()
event handler that was added when you double-clicked the button):
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace WpfApplication4
{
public partial class MainWindow : Window
{
public MainWindow()
{
}
private void toggleButton_Click(object sender, RoutedEventArgs e)
{
Storyboard spinStoryboard = Resources["Spin"] as Storyboard;
if (spinStoryboard != null)
{
if (spinStoryboard.GetIsPaused(this))
{
spinStoryboard.Resume(this);
}
else
{
spinStoryboard.Pause(this);
}
}
}
}
}
Now press F5 and run the application. Here is a view of how it should look:
First, look at the XAML for the desktop application, MainWindow.xaml, and the top-level element of this code:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WpfApp4.MainWindow"
Title="Color Spinner" Height="370"
Width="270">
...
</Window>
The <Window>
element is used, unsurprisingly, to define a window. An application might consist of several windows, each of which would be contained in a separate XAML file. This isn't to say that a XAML file always defines a window, though; XAML files can contain user controls, brushes, and other resources, Web pages, and more. There is even a XAML file in the WpfApp4 project that defines the application itself - App.xaml. Back to MainWindow.xaml, notice that the <Window>
element contains some fairly self-explanatory attributes. There are two namespace declarations, one for the global namespace to be used for the XML, and one for the x
namespace. Both of these are essential for WPF functionality, and define the vocabulary for the XAML syntax. Next is a Class
attribute, taken from the x
namespace. This attribute links the XAML <Window>
element to a partial class definition in the code-behind, in this case, WpfApp4.Window
. This is similar to the way things work in ASP.NET, with a class used for a page, and enables code-behind to share the same code model as the XAML file, including controls defined by XAML elements, and so on. Note that the x:Class
attribute can be used only on the root element of a XAML file.
Three other attributes, Title
, Height
, and Width
, specify the text to display in the title of the window, and the dimensions (in pixels) to use for the window. These attributes map to properties of the System.Windows.Window
class, from which the WpfApp4.Window
class is derived. Several other properties of the System.Windows.Window
class enable you to define additional functionality. Many of these properties are more complex than the three used on the <Window>
element - that is, they aren't, for example, simple strings or numbers. XAML syntax enables you to use nested elements to specify values for these properties. For example, the Background
property is defined in this code with a nested <window.background>
element as follows:
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFFFFFFF" Offset="0"/>
<GradientStop Color="#FFFFC45A" Offset="1"/>
</LinearGradientBrush>
</Window.Background>
This code sets the Background
property to an instance of the LinearGradientBruch
class. In this case, the brush defines a gradient that changes from white to a peach-like color from top to bottom. There are two other ''complex'' properties defined in nested elements in this code: <Window.Resources>
, which defines the animation, and <Window.Triggers>
, which defines triggers that control the animation. Before looking at the implementation of these properties, it's worth jumping ahead to the <Grid>
element. The <Grid>
element defines an instance of the System.Windows.Controls.Grid
control. This is one of several controls that you can use for layout in a WPF application. It enables you to position nested controls using coordinates that can be relative to any of the four edges of a rectangle.
The <Grid>
element contains five <Ellipse>
elements (System.Windows.Shapes.Ellipse
controls) and three <Button>
elements (System.Windows.Controls.Button
controls). These elements define the ellipses used to display the spinning graphics in the application and the buttons used to control the application, respectively. The first <Ellipse>
element is as follows:
<Ellipse Margin="50,50,0,0" Name="ellipse5"
Stroke="Black" Height="150"
HorizontalAlignment="Left"
VerticalAlignment="Top" Width="150">
<Ellipse.Effect>
<BlurEffect Radius="10"/>
</Ellipse.Effect>
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color="#FF000000" Offset="1"/>
<GradientStop Color="#FFFFFFFF" Offset="0.306"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
This element defines an instance of the System.Windows.Shapes.Ellipse
class, which is used to display an ellipse shape, and sets several properties of this instance as follows:
Name
: An identifier to use for the control.
Margin
: Indicates the location of the shape defined by the Ellipse
control in the grid that contains it by specifying the margin around the shape. These measurements are given in pixels in this code. How this property maps to the actual location of the shape depends on the HorizontalAlignment
and VerticalAlignment
properties.
HorizontalAlignment
and VerticalAlignment
: Used to specify which edges of the rectangle defined by Grid
are used to lay out the shape. For example, values of Left
and Bottom
cause the shape to be positioned relative to the bottom left of the grid.
Height
and Width
: The dimensions of the shape.
Stroke
: The brush to use for the outline of the shape defined by the Ellipse
control.
Fill
: The brush to use for the interior of the shape defined by the Ellipse
control.
Effect
: A special effect to use when displaying the Ellipse
control.
Four more <Ellipse>
elements in the code are very similar. Each of these elements defines one of the four colored ellipses that are animated. The first of these elements is as follows:
<Ellipse Margin="15,85,0,0" Name="ellipse1"
Stroke="{x:Null}" Height="80" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="120"
Fill="Red" Opacity="0.5"
RenderTransformOrigin="0.92,0.5" >
<Ellipse.Effect>
<BlurEffect/>
</Ellipse.Effect>
<Ellipse.RenderTransform>
<TransformGroup>
<RotateTransform Angle="0"/>
</TransformGroup>
</Ellipse.RenderTransform>
</Ellipse>
This code looks a lot like the code for the previous ellipse, with the following differences:
- The
Stroke
property is set to {x:null}
. In XAML, values enclosed in curly braces such as this are called markup extensions, and are used to provide values for properties that cannot be reduced to simple strings in the XAML syntax. In this case, {x:null}
specifies a null value for the property, meaning that no brush is to be used for Stroke
.
- An
Opacity
property is specified with a value of 0.5. This specifies that the ellipse is semitransparent.
- The
Effect
property uses a BlurEffect
without a Radius
attribute; in this case, the default value of 5 is used for Radius
.
- A
RenderTransform
property is specified. This property is set to a TransformGroup
object with a single transformation: RotateTransform
. This transformation is used when the ellipse is animated. It has a single property specified, Angle
. This is the angle, in degrees, through which the ellipse is rotated, and is initially set to 0.
RenderTransformOrigin
is used to set a center point around which the ellipse will be rotated by the RotateTransform
transformation.
These last two properties relate to the animation defined in the XAML, which is defined by a System.Windows.Media.Animation.Storyboard
object. This object is defined in the <window.resources>
element, meaning that the Storyboard
object will be available through the Resources
collection of the window. The code also defines an x:Key
attribute, which enables the Storyboard
object to be referenced through Resources
using a key:
<Window.Resources>
<Storyboard x:Key="Spin">
...
</Storyboard>
</Window.Resources>
The Storyboard
object contains four DoubleAnimationUsingKeyFrames
objects. These objects enable you to specify that a property containing a double value should change over time, along with details to further define this behavior. Each of the elements in this code defines the animation used by one of the colored ellipses. For example, the animation for the ellipse1
ellipse examined earlier is as follows:
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ellipse1"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(TransformGroup.
Children)[0].(RotateTransform.Angle)"
RepeatBehavior="Forever">
<SplineDoubleKeyFrame KeyTime="00:00:10" Value="360"/>
</DoubleAnimationUsingKeyFrames>
Without going into this element too deeply at this point, this specifies that the Angle
property of the RotateTransform
transformation described previously should change from its initial value to a value of 360 over a time period of 10 seconds, and that this change should be repeated once complete.
After the ellipse definitions, there are three <Button>
elements that define buttons (note that the Click
attribute was not shown in the code in the example; it was added by the IDE when you double-clicked the button ):
<Button Height="23" HorizontalAlignment="Left"
Margin="20,0,0,56" Name="goButton"
VerticalAlignment="Bottom" Width="75"
Content="Go"/>
<Button Height="23" HorizontalAlignment="Left"
Margin="152,0,0,56" Name="stopButton"
VerticalAlignment="Bottom" Width="75"
Content="Stop"/>
<Button Height="23" HorizontalAlignment="Left"
Margin="85,0,86,16" Name="toggleButton"
VerticalAlignment="Bottom" Width="75"
Content="Toggle" Click="toggleButton_Click"/>
Each of these elements specifies the name, position, and dimensions of a Button
object using the same properties as the <Ellipse>
elements shown earlier. They also have Content
properties that determine what is displayed in the content of the button - in this case, the string to display for the text on the button. Buttons aren't limited to displaying simple strings in this way, though; you could use embedded shapes or other graphical content if you prefer. You'll look at this in more detail in the ''Control Styling'' section later in this chapter. The Click
attribute on the toggleButton
button defines an event handler method for the Click
event. This method, toggleButton_Click()
, is actually a routed event handler. For now, you need to know that this event fires when you click the button, and the event handler is then called. In the event handler code, you start by obtaining a reference to the Storyboard
object that defines the animation. Earlier, you saw that this object is contained in the Resources
property of the containing Window
object, and that it uses the key Spin
. The code that retrieves the storyboard should therefore come as no surprise:
private void toggleButton_Click(object sender, RoutedEventArgs e)
{
Storyboard spinStoryboard = Resources["Spin"] as Storyboard;
Once obtained, and if the previous code doesn't obtain a null value, the Storyboard.GetIsPaused()
method is used to determine whether the animation is currently paused or not. If it is, then a call to Resume()
is made; otherwise, Pause()
is called. These methods resume or pause the animation, respectively:
if (spinStoryboard != null)
{
if (spinStoryboard.GetIsPaused(this))
{
spinStoryboard.Resume(this);
}
else
{
spinStoryboard.Pause(this);
}
}
}
Note that all these methods require a reference to the object that contains the storyboard. This is because storyboards themselves do not keep track of time. The window that contains a storyboard has its own clock, which is used by the storyboard. By passing a reference to the window (using this), the storyboard is able to gain access to this clock. The other two buttons, goButton
and stopButton
, are not linked to any event handler methods in the code-behind. Instead, their functionality is determined by triggers. In this example, three triggers are defined as follows:
<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource Spin}"
x:Name="Spin_BeginStoryboard"/>
</EventTrigger>
<EventTrigger RoutedEvent="ButtonBase.Click"
SourceName="goButton">
<ResumeStoryboard BeginStoryboardName="Spin_BeginStoryboard"/>
</EventTrigger>
<EventTrigger RoutedEvent="ButtonBase.Click"
SourceName="stopButton">
<PauseStoryboard BeginStoryboardName="Spin_BeginStoryboard"/>
</EventTrigger>
</Window.Triggers>
The first of these is a trigger that links the FrameworkElement.Loaded
event (which fires when the application is loaded) with a BeginStoryboard
action. This action starts the Spin
animation. Notice how the Spin
animation is referenced by using markup extension syntax with the code {StaticResource Spin}
. The BeginStoryboard
action is given the name Spin_BeginStoryboard
, and is referenced in the other two triggers, which link up the Click
events of goButton
and stopButton
, respectively. These triggers use the ResumeStoryboard
and PauseStoryboard
actions, which do exactly what their names suggest.
Where is the Explanation About this XAML?
The basic structure of a XAML file uses object element syntax to describe a hierarchy of objects with a single root object that contains everything else. Object element syntax, as its name suggests, describes an object (or struct) represented by an XML element. For example, you saw in the example how the <Button>
element was used to represent a System.Windows.Controls.Button
object. The root element of a XAML file always uses object element syntax, although as you saw in the earlier example, the class used for the root object is defined not by the element name (<Window>
or <Page>
) but by the x:Class
attribute. This syntax is only used in the root element. For desktop applications, the root element must inherit from System.Windows.Window
, and for Web applications, it must inherit from System.Windows.Controls.Page
.
You have seen that in many cases where an element is used to represent an object (using object element syntax), attributes are used to specify properties and events. For example, the <Button>
element shown earlier used attributes as follows:
<Button Height="23" HorizontalAlignment="Left"
Margin="85,0,86,16" Name="toggleButton"
VerticalAlignment="Bottom" Width="75"
Content="Toggle" Click="toggleButton_Click"/>
Here, each attribute sets the value of a property of the toggleButton
object, apart from Click
, which assigns a routed event handler to the Click
event of toggleButton
, and Name
. These are all examples of attribute syntax. The Name
attribute used here is a special case: it defines the identifier for the control so that you can reference it from code-behind and other XAML code. Attributes may be qualified with the base class that they refer to by using a period. For example, the Button
control inherits its Click
event from ButtonBase
, so you could rewrite the previous code as follows:
<Button Height="23" HorizontalAlignment="Left"
Margin="85,0,86,16" Name="toggleButton"
VerticalAlignment="Bottom" Width="75"
Content="Toggle" ButtonBase.Click="toggleButton_Click"/>
Often, we'll require something a little more complicated than a simple string to initialize the value of a property. In our example application, that was the case for the Fill
properties we used, which you set to various brush objects:
<Ellipse ...>
...
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color="#FF000000" Offset="1"/>
<GradientStop Color="#FFFFFFFF" Offset="0.306"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
Here, the property is set by a child element that is named according to the following convention: [Parent Element Name].[Property Name]. This is referred to as property element syntax.