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

A pattern for “cancellable” background task

While working on Nym’s fancy downloader I stumbled upon a common problem: a task (the download to be precise) going on in a background which needs to be cancelled. Usually this sort of task is implemented by means of a loop executed on a background thread which periodically checks a “cancelled” flag. Exactly as illustrated below:

Code Snippet
  1. public void DoWork()
  2. {
  3.     while ( !Cancelled )
  4.     {
  5.         // Problem if DoSomeStuff() takes long time to complete...
  6.         DoSomeStuff();
  7.     }
  8. }

 

This is pretty much how the BackgroundWorker goes about it’s business. The only difference is that Cancelled and DoSomeStuf() are replaced by event’s. The major problem with this approach is the fact that when you want to cancel such a loop and wait for the thread to actually finish it’s job, you may be in for a very long wait. The method being executed on the background thread prevents it from checking Cancelled flag and thus terminating. In the context of the “fancy downloader” the problem was the method which actually reads the network stream and saves it to the file on your hard drive: if  it blocks due to poor network performance, busy site, etc the user would be facing a “hang”  in the app. The alternative solution is presented below:

Code Snippet
  1. protected virtual void CopyStream()
  2. {
  3.     var buffer = new byte[BlockSize];
  4.  
  5.     IAsyncResult result = Source.BeginRead(buffer, 0, BlockSize, null, null);
  6.  
  7.     while (WaitHandle.WaitAny(new[] { result.AsyncWaitHandle, _haltEvent }) == 0)
  8.     {
  9.         int bytesRead = Source.EndRead(result);
  10.  
  11.         if (bytesRead == 0)
  12.             return;
  13.  
  14.         Destination.Write(buffer, 0, bytesRead);
  15.  
  16.         result = Source.BeginRead(buffer, 0, BlockSize, null, null);
  17.     }
  18. }
  19.  
  20. public virtual void Cancel()
  21. {
  22.     _haltEvent.Set();
  23.     Thread.Join();
  24. }

In this case the loop simply continues as long as one of the two wait handles are signalled. If the first one gets signalled as the result of async I/O being completed, the program writes the data to the file. If the second handle gets signalled (due to Cancel method being called) the thread terminates. This allows for a Cancel() method to be pretty much instantaneous. The same approach can be used as well with any other operation but delegates may need to be used to execute method in question. What we also need to remember is the fact that the background operation does not simply “go away” because of our task being cancelled, so it would be wise to test the program and ensure that it does not crash because of some resources being disposed etc.

March 27 2010

Session downloader for MIX 2010

I’ve rehashed the session downloader to be able to download the Mix 2010 sessions just gone by.

Thanks to Marcin Kaluza for helping out and giving the application some much needed design love.

image

As usual you can pick up the source at bitbucket: http://bitbucket.org/naeem.khedarun/fancypcddownloader2009/

And the download is now available via click-once: http://sharpfellows.com/SessionDownloader/publish.htm

Please let me know if you have any trouble with the application!

March 25 2010

WCF Bindings, Security and Bandwidth Utilisation

Good things come to those who measure! I have been recently asked to investigate ways to reduce bandwidth utilisation of a WCF service and thought that it would not be a bad idea to share the results. The test was a relatively simple method call (search operation) performed on the client which would return ~500 objects from the server side. I tested the app using various WCF bindings and the table below contains the sizes of the payloads for the bindings I have tested. (the results have been obtained using Wireshark):

Binding

Payload size (bytes)

Relative size

WS HTTP with message security

2747082

100%

NET-TCP with message security

2068024

75%

NET-TCP with transport level security and encryption

393205

14%

NET-TCP with transport level security and no encryption

392948

14%

Your mileage will obviously vary, this example however illustrates that you can get substantial reduction of payload sizes by changing a couple of configuration files. This is what I call a result! :)

November 13 2009
Older Posts