Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

The Anatomy of a WPF Application

Rate me:
Please Sign up or sign in to vote.
4.86/5 (7 votes)
11 Aug 2010CPOL11 min read 30K   29   4
An article that both explains the Hows and Whys behind WPF.

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:

XML
<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):

C#
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:

Capture.JPG

First, look at the XAML for the desktop application, MainWindow.xaml, and the top-level element of this code:

XML
<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:

XML
<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:

XML
<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:

XML
<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:

XML
<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:

XML
<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 ):

XML
<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:

C#
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:

C#
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:

XML
<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:

XML
<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:

XML
<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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Monroe Community
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
John Adams21-Oct-10 3:39
John Adams21-Oct-10 3:39 
GeneralMy vote of 4 Pin
louislong12-Aug-10 4:49
louislong12-Aug-10 4:49 
You could have one button, and on each click, change the button's content to be what the next action will be. I agree indentation makes things a little easier ont he eye. Good job and I enjoyed reading.
GeneralNeeds indenting Pin
Ashley Davis10-Aug-10 23:29
Ashley Davis10-Aug-10 23:29 
GeneralRe: Needs indenting Pin
Ashley Davis10-Aug-10 23:31
Ashley Davis10-Aug-10 23:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.