Click here to Skip to main content
Click here to Skip to main content

Creating a Vista Busy Cursor Like Silverlight Control Step by Step

By , 1 Dec 2008
Rate this:
Please Sign up or sign in to vote.

Screen Capture of Busy Icon

Introduction

Often in your Silverlight application, you need to download something, or do something for a period of time. During this time, you may want to display an indicator to show the user your application is busy. You may want to create a control for this that can be used in your application. In this article, I'll show step by step how to create a Vista busy cursor-like control.

Part 1. Creating a Silverlight App

Why are we first creating a Silverlight application? The reason is Expression Blend 2 (with SP1) can't directly design the Silverlight control's default template. So we first create an application using Blend to design the visual of the control.

Step 1.1 Creating a Silverlight Application in VS 2008

Open Visual Studio 2008, creating a Silverlight application. Right click the Page.xaml file, select "Open in Expression Blend", and the project will be opened with Blend 2.

Step 1.2 Designing the Visual of the Control

In Blend, the Page.xaml is opened. In the "Objects and Timeline" panel, select the LayoutRoot element. Double clicking it puts it in the selected state.

Select the LayoutRoot

Then, selecting a Grid control from the "ToolBox", double click it to add it to the LayoutRoot. In the "Properties" panel, set the Grid's Width and Height to Auto, and HorizontalAlignment and VerticalAlignment to Center.

Set Grid properties

In the "Objects and Timeline" panel, double clicking the Grid just added selects it. Add an Ellipse from the "ToolBox" panel inside it. Set the Width and Height properties to 20, Fill to None, and Stroke to a GradientBrush and the StrokeThickness to 6. Set Opacity to 0.

Ellipse Properties

The final Visual of the control looks like:

Final Visual of the Control

Step 1.3 Creating the VisualStates

Now we create the VisualStates. VisualState "represents the visual appearance of the control when it is in a specific state" (from MSDN). Our control can be in one of two states: BusyState or IdleState. In BusyState, the control will be visible and an animation will be displayed. In IdleState, the control will be hidden.

In the "States" panel, click the "Add states group" button to add a states group.

Add states group

Rename "VisualStateGroup" to "BusyIdleStates". Click the "Add State" button to the right of "BusyIdleStates", and add two VisualStates and name them "BusyState" and "IdleState".

Add two state

Select "BusyState" in the "States" panel. Then, open the Timeline panel and in the "Objects and Timeline" panel, select the Ellipse element. Then, select the Stroke property of the Ellipse element. In "ToolBox", select the "Brush Transform" tool, move the key timeline to "0:00.300", and using the "Brush Transform" tool, rotate the Stroke brush to 45 degree.

Selecting the ellipseThe stroke property

Select the "Brush Transform" tool and use it.

The Brush Transform tool

Move the timeline to "0:00.600", and using the "Brush Transform" tool, rotate the Brush to 90 degrees. Repeat this step until you rotate a whole circle.

In the end, select the Opacity property, move the Timeline to "0:00.000", and set it to 100%. This makes the control visible. After this step, close the "BusyState" by clicking "Base" in the State Panel.

Step 1.4 Testing

Add the following code to the Page.xaml.cs file.

public Page()
{
   InitializeComponent();
   // add a event handle to Loaded event
   this.Loaded += new RoutedEventHandler(Page_Loaded);
}

void Page_Loaded(object sender, RoutedEventArgs e)
{
    // Go to BusyState
   VisualStateManager.GoToState(this, "BusyState", false);
}

Right click the project in VS2008, select "Debug", then "Running a new instance". You will see the effects of the application.

Part 2. Creating the Control

Step 2.1 File Structure of our Control

In this part, we change our just created application to a control, so you can easily use this control in any application. First, add a "Silverlight Class Library" project to the current solution, and name it "WaitingIcon". Rename class1.cs to WaitingIcon.cs, also rename class1 to WaitingIcon, and derive it from the Control class. Add a folder to the project named "themes", add a new text file to the themes folder, and name it "generic.xaml". Our default control template is ready and available here:

Project Art

Selecting the Generic.xaml file and right click it. Then, select the Property menu item. In the Property window, set the Build Action to Resource and delete the text in the Custom Tool box.

Generic file property

Step 2.2 Creating a Default Control Template of our Control

Open the Generic.xaml file, and add the following code to the file:

<ResourceDictionary  xmlns="http://schemas.microsoft.com/client/2007"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows" 
                     xmlns:controls="clr-namespace:Cokkiy.Display">       
    <Style TargetType="controls:WaitingIcon">        
        <Setter Property="Template">
            <Setter.Value>
                <--Control Template for the WaitingIcon-->
                <ControlTemplate TargetType="controls:WaitingIcon">   
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Open the Page.xaml file we created in Part 1, copy all the code between "<Grid>" and "</Grid>" to the Generic.xaml file and insert that after "<ControlTemplate TargetType="controls:WaitingIcon">".

<ControlTemplate TargetType="controls:WaitingIcon">                    
    <Grid>                        
        <Ellipse StrokeThickness="{TemplateBinding StrokeThickness}" 
           x:Name="ellipse" 
           Stroke="{TemplateBinding Background}" 
           Opacity="0">
        </Ellipse>
    </Grid>
</ControlTemplate>

In the application, we directly set the Ellipse StrokeThickness to 6. But in our control, we set it to TemplateBinding so the end user can set the width. Change the Stroke property to TemplatingBinding so the end user can set a different Brush for our control. We can add a default Brush for the Stroke by adding the following code after <Style TargetType="controls:WaitingIcon"> and a default width for the StrokeThickness property.

<Style TargetType="controls:WaitingIcon">
   <Setter Property="StrokeThickness" Value="6"/>
   <Setter Property="Background">
      <Setter.Value>
         <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
             <GradientStop Color="#FF0A0E94" Offset="0.576"/>
             <GradientStop Color="#FF0FFF1B" Offset="1"/>
         </LinearGradientBrush>
      </Setter.Value>
   </Setter>

Copy the "VisualStates" definition from Page.xaml to Generic.xaml and insert it after <Grid>.

<Grid>
   <vsm:VisualStateManager.VisualStateGroups>
         <vsm:VisualStateGroup x:Name="BusyIdleStates">
               <vsm:VisualState x:Name="BusyState">
                    <Storyboard AutoReverse="False" 
                           RepeatBehavior="Forever">
                        <PointAnimationUsingKeyFrames BeginTime="00:00:00" 
                               Storyboard.TargetName="ellipse" 
                               Storyboard.TargetProperty=
                                "(Shape.Stroke).(LinearGradientBrush.StartPoint)">
                            <SplinePointKeyFrame KeyTime="00:00:00.25" 
                               Value="0.868,0.161"/>
                            <SplinePointKeyFrame KeyTime="00:00:00.5" 
                               Value="0.997,0.44"/>
                            <SplinePointKeyFrame KeyTime="00:00:00.75" 
                               Value="0.845,0.863"/>
                            <SplinePointKeyFrame KeyTime="00:00:01" 
                               Value="0.545,0.999"/>
                            <SplinePointKeyFrame KeyTime="00:00:01.2500000" 
                               Value="0.166,0.873"/>
                            <SplinePointKeyFrame KeyTime="00:00:01.5" 
                               Value="0.001,0.536"/>
                            <SplinePointKeyFrame KeyTime="00:00:01.7500000" 
                               Value="0.084,0.222"/>
                            <SplinePointKeyFrame KeyTime="00:00:02" 
                               Value="0.462,0.001"/>
                        </PointAnimationUsingKeyFrames>
                        <PointAnimationUsingKeyFrames BeginTime="00:00:00" 
                               Storyboard.TargetName="ellipse" 
                               Storyboard.TargetProperty=
                                 "(Shape.Stroke).(LinearGradientBrush.EndPoint)">
                             <SplinePointKeyFrame KeyTime="00:00:00.25" 
                                Value="0.132,0.839"/>
                             <SplinePointKeyFrame KeyTime="00:00:00.5" 
                                Value="0.003,0.56"/>
                             <SplinePointKeyFrame KeyTime="00:00:00.75" 
                                Value="0.155,0.137"/>
                             <SplinePointKeyFrame KeyTime="00:00:01" 
                                Value="0.455,0.001"/>
                             <SplinePointKeyFrame KeyTime="00:00:01.2500000" 
                                Value="0.834,0.127"/>
                             <SplinePointKeyFrame KeyTime="00:00:01.5" 
                                Value="0.999,0.464"/>
                             <SplinePointKeyFrame KeyTime="00:00:01.7500000" 
                                Value="0.916,0.778"/>
                             <SplinePointKeyFrame KeyTime="00:00:02" 
                                Value="0.538,0.999"/>
                        </PointAnimationUsingKeyFrames>
                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                 Storyboard.TargetName="ellipse"
                                 Storyboard.TargetProperty="Opacity">
                             <SplineDoubleKeyFrame KeyTime="00:00:00" 
                                 Value="1"/>
                        </DoubleAnimationUsingKeyFrames>
                    </Storyboard>
               </vsm:VisualState>
           <vsm:VisualState x:Name="IdleState"/>
       </vsm:VisualStateGroup>
   </vsm:VisualStateManager.VisualStateGroups>

Step 2.3 Creating the Code for our Control

We have just created the file structure and the Control Template for our control. Now we should add code to it. In the control template, we bind the Ellipse's StrokeThickness to a property named StrokeThickness. So, we first add StrokeThickness to our control code.

#region StrokeThickness Property
/// <summary>
/// Gets or sets the width of the <see cref="WaitingIcon"/> stroke outline. 
/// </summary>
/// <value>The width of the <see cref="WaitingIcon"/>  outline, in pixels. 
/// The default value is 0. </value>
public double StrokeThickness
{
    get { return (double)GetValue(StrokeThicknessProperty); }
    set { SetValue(StrokeThicknessProperty, value); }
}

/// <summary>
/// Identifies the <see cref="StrokeThickness"/> dependency property. 
/// </summary>
public static readonly DependencyProperty StrokeThicknessProperty =
      DependencyProperty.Register("StrokeThickness", typeof(double), 
        typeof(WaitingIcon), new PropertyMetadata(6.0)); 
#endregion

The purpose of creating this control is to be able to indicate the application is busy doing something, so our control should have a property indicating whether it is busy or not.

#region IsBusy Property
/// <summary>
/// Gets or sets a value indicating is busy or not
/// </summary>
/// <value>A value indicating whether the control is in busy state or not.
/// <para>The default value is <c>false</c>.</para></value>
public bool IsBusy
{
    get { return (bool)GetValue(IsBusyProperty); }
    set { SetValue(IsBusyProperty, value); }
}

/// <summary>
/// Identifies the <see cref="IsBusy"/> dependency property. 
/// </summary>
public static readonly DependencyProperty IsBusyProperty =
       DependencyProperty.Register("IsBusy", typeof(bool), 
          typeof(WaitingIcon),
            new PropertyMetadata(false, IsBusyPropertyChanged));

/// <summary>
/// The <see cref="IsBusy"/> property changed callback function.
/// </summary>
/// <param name="d">The <see cref="WaitingIcon"/>
/// control whosevsee cref="IsBusy"/> property changed.</param>
/// <param name="e">The DependencyPropertyChangedEventArgs
///     contains old and new value.</param>
private static void IsBusyPropertyChanged(DependencyObject d, 
                    DependencyPropertyChangedEventArgs e)
{
     WaitingIcon wi = d as WaitingIcon;
     wi.IsBusyChanged((bool)e.OldValue, (bool)e.NewValue);
} 
#endregion

When the IsBusy property set to true, our control should be visible and display the animation we created. We simply go to the "BusyState".

/// <summary>
/// The <see cref="IsBusy "/> property changed.
/// </summary>
/// <param name="oldValue">The old value of the
///    <see cref="IsBusy"/> property.</param>
/// <param name="newValue">The new  value of the
///   <see cref="IsBusy"/> property.</param>
protected virtual void IsBusyChanged(bool oldValue, bool newValue)
{
    if (newValue)
    {
         VisualStateManager.GoToState(this, WaitingIcon.BusyStateName, false);
    }
    else
    {
         VisualStateManager.GoToState(this, WaitingIcon.IdleStateName, false);
    }
}

The final step is to apply the default control template to our control. In the constructor, set the DefaultStyleKey to the type of our control.

/// <summary>
/// Initialize a new instance of <see cref="WaitingIcon"/> class.
/// </summary>
public WaitingIcon()
{
    // The default style key
    this.DefaultStyleKey = typeof(WaitingIcon);
}

/// <summary>
/// Apply new template
/// </summary>
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    
    if (this.IsBusy)
    {
         // if set to busy in XAML, we must go to BusyState here
         VisualStateManager.GoToState(this, WaitingIcon.BusyStateName, false);
    }
}

Compiling the project of we just created, our control is ready to use.

Part 3. Using the Control

Create a new Silverlight application, and add a reference to our control assembly. Then, in the Page.xaml file, put your control where you want, and set the background and stroke thickness, or just use the default.

<UserControl x:Class="WaitingTest.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:cdc="clr-namespace:Cokkiy.Display;assembly=Cokkiy.Display.WaitingIcon"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="#FF090808">
        <cdc:WaitingIcon Width="20" Height="20" IsBusy="True">
            <cdc:WaitingIcon.Background>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FF070B9C" Offset="0.57599997520446777"/>
                    <GradientStop Color="#FFFFFFFF" Offset="1"/>
                </LinearGradientBrush>
            </cdc:WaitingIcon.Background>
        </cdc:WaitingIcon>
   </Grid>

The IsBusy property is set in code when your application is in busy state.

Points of Interest

You may notice that in both the IsBusyChanged and OnApplyTemplate functions, I do the same checking: the IsBusy property value is checked, and go to "BusyState" when it set to true. The reason is when you set IsBusy to true in XAML, the IsBusyChanged function is called before the Template is applied. At that time, the "BusyState" VisualState does not exist at all and nothing will happen. So you need to recheck, when the template is applied, and if the value is true, you should go to the "BusyState" here.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

cokkiy
Team Leader
China China
The God created the world.
The programer made the world easy.
I am a programer like c# and c++, so I am writting application with it.
Creating beautiful application make life easy.
If you have a project and looking for a man, I'll be say "hi, I am just the man you are looking for."

Comments and Discussions

 
GeneralWorks great Pinmemberdefwebserver25-Oct-09 10:46 
GeneralVery Cool PinmemberPaul Conrad1-Dec-08 7:14 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 1 Dec 2008
Article Copyright 2008 by cokkiy
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid