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.
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
.
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.
The final Visual
of the control looks like:
Step 1.3 Creating the VisualStates
Now we create the VisualState
s. 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.
Rename "VisualStateGroup" to "BusyIdleStates". Click the "Add State" button to the right of "BusyIdleStates", and add two VisualState
s and name them "BusyState" and "IdleState".
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.
Select the "Brush Transform" tool and use it.
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();
this.Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
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:
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.
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
public double StrokeThickness
{
get { return (double)GetValue(StrokeThicknessProperty); }
set { SetValue(StrokeThicknessProperty, value); }
}
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
public bool IsBusy
{
get { return (bool)GetValue(IsBusyProperty); }
set { SetValue(IsBusyProperty, value); }
}
public static readonly DependencyProperty IsBusyProperty =
DependencyProperty.Register("IsBusy", typeof(bool),
typeof(WaitingIcon),
new PropertyMetadata(false, IsBusyPropertyChanged));
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".
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.
public WaitingIcon()
{
this.DefaultStyleKey = typeof(WaitingIcon);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (this.IsBusy)
{
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.