WPF Spinner – take two

The WPF wait indicator which I have published a while ago proved to be good as a starting point but somewhat limited when it came to using it in production: the major problem was the fact that changing its appearance required pretty much  total rework. As soon as I put it to use, new requirements arrived and it  was clear that we need a slightly different approach. First of all there was the issue of size, as sometimes you may need a smaller wait indicator, secondly the content was not very flexible and if it was to be changed, the key frame animation driving the whole thing needed to be reworked from scratch. So after a bit of head scratching I opted for a “lookless” approach and decided to implement a custom WPF ContentControl which would provide scaling and animation capabilities. To cut the long story short you can see the final effects below:


The control is dead easy to use as the following piece of XAML illustrates.

   1:         <Spinner:ContentSpinner Margin="10"
   2:                                  BorderBrush="Black"
   3:                                  BorderThickness="2" />


To make our life easier, the ContentSpinner provides a couple of dependency properties: first there is the Scale property which allows the user to scale the content, secondly NumberOfFrames property controls how many frames will be included in the animation and thirdly RevolutionsPerSecond drives a number of rotations the content will make each second. Also if you are not happy with the default content feel free to provide your own as in the XAML below:

   1:          <Spinner:ContentSpinner Margin="10"
   2:                                  BorderBrush="Black"
   3:                                  BorderThickness="2"
   4:                                  NumberOfFrames="8"
   5:                                  Content="{StaticResource blueDotsCanvas}">
   7:          </Spinner:ContentSpinner>


The following code fragment illustrates the most important aspects of the control. The basic idea is that the control applies RotateTransform to its content and the transform is then animated using DoubleAnimationUsingKeyFrames. The number of frames is driven by the NumberOfFrames property and duration of the animation is calculated based on the number of required RevolutionsPerSecond.

   1:  private void StartAnimation()
   2:  {
   3:      if (_content == null)
   4:          return;
   6:      var animation = GetAnimation();
   8:      _content.LayoutTransform = GetContentLayoutTransform();
   9:      _content.RenderTransform = GetContentRenderTransform();
  11:      _storyboard = new Storyboard();
  12:      _storyboard.Children.Add(animation);
  14:      _storyboard.Begin(this);
  15:  }
  17:  private void StopAnimation()
  18:  {
  19:      if (_storyboard != null)
  20:      {
  21:          _storyboard.Remove(this);
  22:          _storyboard = null;
  23:      }
  24:  }
  26:  private void RestartAnimation()
  27:  {
  28:      StopAnimation();
  29:      StartAnimation();
  30:  }
  32:  private Transform GetContentLayoutTransform()
  33:  {
  34:      return new ScaleTransform(ContentScale, ContentScale);
  35:  }
  37:  private Transform GetContentRenderTransform()
  38:  {
  39:      var rotateTransform = new RotateTransform(0, _content.ActualWidth / 2 * ContentScale, _content.ActualHeight / 2 * ContentScale);
  40:      RegisterName(ANIMATION, rotateTransform);
  42:      return rotateTransform;
  43:  }
  45:  private DoubleAnimationUsingKeyFrames GetAnimation()
  46:  {
  47:      NameScope.SetNameScope(this, new NameScope());
  49:      var animation = new DoubleAnimationUsingKeyFrames();
  51:      for (int i = 0; i < NumberOfFrames; i++)
  52:      {
  53:          var angle = i * 360.0 / NumberOfFrames;
  54:          var time = KeyTime.FromPercent(((double)i) / NumberOfFrames);
  55:          DoubleKeyFrame frame = new DiscreteDoubleKeyFrame(angle, time);
  56:          animation.KeyFrames.Add(frame);
  57:      }
  59:      animation.Duration = TimeSpan.FromSeconds(1 / RevolutionsPerSecond);
  60:      animation.RepeatBehavior = RepeatBehavior.Forever;
  62:      Storyboard.SetTargetName(animation, ANIMATION);
  63:      Storyboard.SetTargetProperty(animation, new PropertyPath(RotateTransform.AngleProperty));
  65:      return animation;
  66:  }

There are a couple of things which I need to mention though when it comes to the custom content: if you want it to be a part of resource dictionary, you may need to set the x:Shared property to False as otherwise it will be impossible to display two spinners simultaneously. Secondly the content should be symmetrical and its horizontal and vertical alignment needs to be set to Center as otherwise the spinner may get a bit wonky. Sample code and the control are available as part of the SharpFellows.Toolkit.

September 20 2010
blog comments powered by Disqus