WPF Wait Indicator (aka Spinner)

UPDATE: New version of the spinner is available here:

Yet another leftover form Nym’s fancy downloader project is the “spinning doughnut of wait”. The downloader uses BackgroundWorker to get the initial list of videos available for download as doing so on the UI thread could cause the application to freeze for way too long. When we first put the BackgroundWorker in place, there was an “uneasy” period of time when the user would be staring at an empty screen with no visual clues as to what was going on. So I came up with a concept of a “spinner” show below (not very original, I know):

image 

The idea here is to display a spinning “doughnut” which gives the user a clue that something is indeed going on. As this is quite a common issue with background processing, I though that  wrapping the spinner in a reusable user control may be useful. The logic behind the control is simple: as soon as it becomes visible it starts spinning and stops when it’s being hidden. Parent control may then control visibility of the spinner (through data trigger etc) and that’s all that is required to use it. This behaviour is implemented through a bit of code behind as changes in visibility are advertised through IsVIsibleChanged event which is a standard non-routed .NET event. This sadly means that there is no way to react to it from the XAML event triggers and a piece of code was required. It is important to note here that it is necessary to stop the animation when it is no longer required. If you leave it running, it will keep spinning in the background, despite of the fact that it is not visible, consuming processor resources in the process. Get a couple of those running off screen and you will soon notice the difference in your computer’s performance.

Code Snippet
  1. public partial class Spinner : UserControl
  2. {
  3.     private Storyboard _storyboard;
  4.  
  5.     public Spinner()
  6.     {
  7.         InitializeComponent();
  8.  
  9.         this.IsVisibleChanged += OnVisibleChanged;
  10.     }
  11.  
  12.     private void OnVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
  13.     {
  14.         if ( IsVisible )
  15.         {
  16.             StartAnimation();
  17.         }
  18.         else
  19.         {
  20.             StopAnimation();
  21.         }
  22.     }
  23.  
  24.     private void StartAnimation()
  25.     {
  26.         _storyboard = (Storyboard)FindResource("canvasAnimation");
  27.         _storyboard.Begin(canvas, true);
  28.     }
  29.  
  30.     private void StopAnimation()
  31.     {
  32.         _storyboard.Remove(canvas);
  33.     }
  34. }

 

I have tested a couple of approaches to animate the spinner but settled on a version which simply rotates the canvas on which the rectangles are drawn. This means that I have just one thing to animate and when compared with phase-shift-animating opacities of sixteen individual rectangles it substantially easier :) It is also substantially cheaper (processor wise) when compared to animating opacities. The XAML of the spinner is shown below and I hope it is self explanatory. In order to achieve on/off effect for each rectangle I settled on key frame animation which is “jerky” by design, but it means that rectangles maintain their position but become lighter/darker as the doughnut spins thus creating the impression of them being momentarily turned on and then dimming slowly down. Think cheap 80’s disco effect :)

Code Snippet
  1. <UserControl x:Class="FancyPDCDownload2009.Modules.Main.Views.Spinner"
  2.    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  4.   <UserControl.Resources>
  5.         <SolidColorBrush x:Key="SpinnerRectangleBrush" Color="Blue"/>
  6.         <Style TargetType="Rectangle">
  7.             <Setter Property="RadiusX" Value="5"/>
  8.             <Setter Property="RadiusY" Value="5"/>
  9.             <Setter Property="Width" Value="50"/>
  10.             <Setter Property="Height" Value="20"/>
  11.             <Setter Property="Fill" Value="{StaticResource SpinnerRectangleBrush}"/>
  12.             <Setter Property="Canvas.Left" Value="220"/>
  13.             <Setter Property="Canvas.Top" Value="140"/>
  14.             <Setter Property="Opacity" Value="0.1"/>
  15.         </Style>
  16.  
  17.         <Storyboard x:Key="canvasAnimation">
  18.             <DoubleAnimationUsingKeyFrames
  19.                RepeatBehavior="Forever"
  20.                SpeedRatio="16"
  21.                Storyboard.TargetProperty="RenderTransform.(RotateTransform.Angle)">
  22.                 <DiscreteDoubleKeyFrame KeyTime="0:0:1" Value="22.5"/>
  23.                 <DiscreteDoubleKeyFrame KeyTime="0:0:2" Value="45"/>
  24.                 <DiscreteDoubleKeyFrame KeyTime="0:0:3" Value="67.5"/>
  25.                 <DiscreteDoubleKeyFrame KeyTime="0:0:4" Value="90"/>
  26.                 <DiscreteDoubleKeyFrame KeyTime="0:0:5" Value="112.5"/>
  27.                 <DiscreteDoubleKeyFrame KeyTime="0:0:6" Value="135"/>
  28.                 <DiscreteDoubleKeyFrame KeyTime="0:0:7" Value="157.5"/>
  29.                 <DiscreteDoubleKeyFrame KeyTime="0:0:8" Value="180"/>
  30.                 <DiscreteDoubleKeyFrame KeyTime="0:0:9" Value="202.5"/>
  31.                 <DiscreteDoubleKeyFrame KeyTime="0:0:10" Value="225"/>
  32.                 <DiscreteDoubleKeyFrame KeyTime="0:0:11" Value="247.5"/>
  33.                 <DiscreteDoubleKeyFrame KeyTime="0:0:12" Value="270"/>
  34.                 <DiscreteDoubleKeyFrame KeyTime="0:0:13" Value="292.5"/>
  35.                 <DiscreteDoubleKeyFrame KeyTime="0:0:14" Value="315"/>
  36.                 <DiscreteDoubleKeyFrame KeyTime="0:0:15" Value="337.5"/>
  37.                 <DiscreteDoubleKeyFrame KeyTime="0:0:16" Value="360"/>
  38.             </DoubleAnimationUsingKeyFrames>
  39.         </Storyboard>
  40.     </UserControl.Resources>
  41.     <Grid>
  42.       <TextBlock
  43.          HorizontalAlignment="Center"
  44.          VerticalAlignment="Center"
  45.          Text="Loading..."/>
  46.         <Canvas Height="300" Width="300"
  47.                 Background="Transparent"
  48.                 Name="canvas">
  49.         <!-- First quadrant -->
  50.           <Rectangle Opacity="1"/>
  51.             <Rectangle>
  52.           <Rectangle.RenderTransform>
  53.             <RotateTransform Angle="22.5" CenterX="-70" CenterY="10"/>
  54.           </Rectangle.RenderTransform>
  55.         </Rectangle>
  56.         <Rectangle >
  57.           <Rectangle.RenderTransform>
  58.             <RotateTransform Angle="45" CenterX="-70" CenterY="10"/>
  59.           </Rectangle.RenderTransform>
  60.         </Rectangle>
  61.         <Rectangle >
  62.           <Rectangle.RenderTransform>
  63.             <RotateTransform Angle="67.5" CenterX="-70" CenterY="10"/>
  64.           </Rectangle.RenderTransform>
  65.         </Rectangle>
  66.        
  67.         <!-- Second quadrant -->
  68.         <Rectangle >
  69.           <Rectangle.RenderTransform>
  70.             <RotateTransform Angle="90" CenterX="-70" CenterY="10"/>
  71.           </Rectangle.RenderTransform>
  72.         </Rectangle>
  73.         <Rectangle >
  74.           <Rectangle.RenderTransform>
  75.             <RotateTransform Angle="112.5" CenterX="-70" CenterY="10"/>
  76.           </Rectangle.RenderTransform>
  77.         </Rectangle>
  78.         <Rectangle Name="r7">
  79.           <Rectangle.RenderTransform>
  80.             <RotateTransform Angle="135" CenterX="-70" CenterY="10"/>
  81.           </Rectangle.RenderTransform>
  82.         </Rectangle>
  83.         <Rectangle >
  84.           <Rectangle.RenderTransform>
  85.             <RotateTransform Angle="157.5" CenterX="-70" CenterY="10"/>
  86.           </Rectangle.RenderTransform>
  87.         </Rectangle>
  88.        
  89.         <!-- Third quadrant -->
  90.         <Rectangle Opacity="0.2">
  91.           <Rectangle.RenderTransform>
  92.             <RotateTransform Angle="180" CenterX="-70" CenterY="10"/>
  93.           </Rectangle.RenderTransform>
  94.         </Rectangle>
  95.             <Rectangle Opacity="0.3">
  96.           <Rectangle.RenderTransform>
  97.             <RotateTransform Angle="202.5" CenterX="-70" CenterY="10"/>
  98.           </Rectangle.RenderTransform>
  99.         </Rectangle>
  100.             <Rectangle Opacity="0.4" >
  101.           <Rectangle.RenderTransform>
  102.             <RotateTransform Angle="225" CenterX="-70" CenterY="10"/>
  103.           </Rectangle.RenderTransform>
  104.         </Rectangle>
  105.             <Rectangle Opacity="0.5">
  106.           <Rectangle.RenderTransform>
  107.             <RotateTransform Angle="247.5" CenterX="-70" CenterY="10"/>
  108.           </Rectangle.RenderTransform>
  109.         </Rectangle>
  110.        
  111.         <!-- Fourth quadrant -->
  112.             <Rectangle Opacity="0.6">
  113.           <Rectangle.RenderTransform>
  114.             <RotateTransform Angle="270" CenterX="-70" CenterY="10"/>
  115.           </Rectangle.RenderTransform>
  116.         </Rectangle>
  117.             <Rectangle Opacity="0.7">
  118.           <Rectangle.RenderTransform>
  119.             <RotateTransform Angle="292.5" CenterX="-70" CenterY="10"/>
  120.           </Rectangle.RenderTransform>
  121.         </Rectangle>
  122.             <Rectangle Opacity="0.8">
  123.           <Rectangle.RenderTransform>
  124.             <RotateTransform Angle="315" CenterX="-70" CenterY="10"/>
  125.           </Rectangle.RenderTransform>
  126.         </Rectangle>
  127.             <Rectangle Opacity="0.9">
  128.           <Rectangle.RenderTransform>
  129.             <RotateTransform Angle="337.5" CenterX="-70" CenterY="10"/>
  130.           </Rectangle.RenderTransform>
  131.         </Rectangle>
  132.         <Canvas.RenderTransform>
  133.             <RotateTransform Angle="0" CenterX="150" CenterY="150"/>
  134.         </Canvas.RenderTransform>
  135.                     </Canvas>
  136.     </Grid>
  137. </UserControl>
       

The source code for the downloader including the spinner can be downloaded from here.

March 30 2010
blog comments powered by Disqus