The decidedly badly named LoadingCircle WPF Control Pt 1

Soooo... I've been playing around with WPF for a while now, generally building small apps for peeps at work, or my own stuff, and one of the things that happens in quite a few cases is the dreaded 'data retrieval' phase. During said phase, the app will go into a state of nothingness whilst the data is retrieved, at which point it'll come back and be useable again.

Now, we all know the way to keep the UI active is to multi-thread that bad boy, and that's where I'm at.

In the app I'm thinking of, whilst the data is loading, there really isn't anything you can do, so - you might say - why bother multi-threading it? Well the reason is that whilst on a single thread, the app will look like it's crashed, and, let's face it, that just looks rubbish. There's no indication the app is actually doing something... So, multi-thread - Check! Some way of indicating it's working? No Check!

Lets get to the (inappropriately named) loading circle.

I've been a WinForms developer for the last 3-4 years now, and that hugely influences how I design applications, and has perhaps been the biggest stumbling block in my WPF designing, as a result, my first cut of the LoadingCircle utilised old skool WinForms methods, namely threads and for loops... :)

First, the layout of the control:

<UserControl x:Class="WPF_TestNavigation.LoadingCircle"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Ellipse x:Name="_00" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_01" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="26,0,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_02" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="52,0,0,0" VerticalAlignment="Top" Height="22" Width="22"/>
        <Ellipse x:Name="_03" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="78,0,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_04" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="78,26,0,0" VerticalAlignment="Top" Width="22" Height="22"/>        
        <Ellipse x:Name="_05" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="78,52,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_06" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="78,78,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_07" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="52,78,0,0" VerticalAlignment="Top" Height="22" Width="22"/>
        <Ellipse x:Name="_08" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="26,78,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_09" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="0,78,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_10" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="0,52,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
        <Ellipse x:Name="_11" Fill="Transparent" Stroke="#FF000000" HorizontalAlignment="Left" Margin="0,26,0,0" VerticalAlignment="Top" Width="22" Height="22"/>
    </Grid>
</UserControl>

Those of you who can read the xaml in full and can get the Margin positions may have finally realised why the LoadingCircle is badly named, yes, I fear it is in fact, a square. Why? To be honest, a moment of laziness, and it was easier to position the square :) I do actually hope to make it a circle in the near future!

Anyhews, below is the screenshot of the control:


So, with this in place, I jumped into the code behind file, the basic plan being as follows:

1. New thread

2. Loop through each ellipse, filling / unfilling as we go

Pretty simple stuff I think you'll agree, so - onto the code. First thing, we know we have 12 circles, named _## where ## is a number from 00 to 11, the numbers are in order, so we want to fill them in order too. Ideally, we want to do something like:

for(int i = 0; i < 12; i++)
{
  Ellipse x = GetEllipse(i);
}

Looks like we're gonna need a helper method:

 

private Ellipse GetCircle( int circ )
{
    string name = "_" + circ.ToString( "00" );
    return _circles[name];
}

At this point you might well say - "Hang on! Where the hell is '_circles' defined?? Surely that's not a default container?????", and you'd be right, I'll be honest with you, I cheated :( I loaded all the circles into a Dictionary<string, Ellipse> named _circles. Basically because it was a quick way to get the controls. So, in my constructor for the LoadingCircle I have:

 

_circles.Add("_00", _00);
... etc

(Told you this was the non-wpfy version!)...

Anyhews, that makes our loops nice and easy, and the code works, which in essence is what the 1st cut is all about. So, before we delve into the loop and the threads etc, lets have a look at the members we need:

private readonly Dictionary<string, Ellipse> _circles = new Dictionary<string, Ellipse>();
private readonly Brush _baseBrush = Brushes.White;
private readonly Brush _highlightBrush = Brushes.Black;
private bool _animating = false;
private Ellipse _currentlyColoured;
 
_circles is a Dictionary that we've already covered here, the _baseBrush is the Brush we'll come back to, i.e. after an highlight, we'll go back to this colour. The _highlightBrush is (unsurprisingly) the brush that the currently selected circle will be brushed with. _animating is our escape bool to break out of the thread doing the animation, true means we are animating, false means otherwise. _currentlyColoured is the currently selected Ellipse that is filled with _highlightBrush.

Now we've covered those, I can paste the animation method below:

private void StartAnimation()
{
    for ( int i = 0; i < 12; i++ )
    {
        if ( !Animating )
            return;
 
        Ellipse next = GetCircle( i );
        SetEllipseFill( _currentlyColoured, _baseBrush );
        SetEllipseFill( next, _highlightBrush );
        _currentlyColoured = next;
        Thread.Sleep( 30 );
        if ( i == 11 )
            i = -1;
    }
}
 

Pretty basic stuff really, 'Animating' is a property that gets and sets the _animating bool, if set to false, then the thread will exit cleanly - if set to true, it will fire off a new thread which will run StartAnimation

 

        _animating = value;
        if ( _animating )
            new Thread( StartAnimation ).Start();

The only other new method is the 'SetEllipseFill' method, all this does is (in a thread safe manner) set the Fill property on a given Ellipse:

private delegate void SetEllipseFillDelegate( Ellipse ellipse, Brush brush );
private void SetEllipseFill( Ellipse ellipse, Brush brush )
{
    if ( !Dispatcher.CheckAccess() )
    {
        Dispatcher.Invoke( DispatcherPriority.Send, new SetEllipseFillDelegate( SetEllipseFill ), ellipse, brush );
        return;
    }
 
    ellipse.Fill = brush;
}

Ok, so now, if we test the code, set the Animating to true, we get a black dot moving around the control, ACES! Close program, and it doesn't actually exit. hmmm this is due to the thread not stopping. I've written about this before (http://geekswithblogs.net/cskardon/archive/2008/06/23/dispose-of-a-wpf-usercontrol-ish.aspx) so I won't go over it in detail here, but basically +='ing to the Dispatcher.ShutdownStarted event on the control and setting Animating = false in the resulting method does the trick.

In the end, it kind of looks like the below (you'll need to use your imagination as to the effects, but think that in .3 of a second, that black dot will be in the circle to the left, then in another .3, the circle to the left of the circle to the left etc.. etc.. :))


Ok, so here we are then, a control that does the business. Obviously there are loads of improvements:

* Make it into a circle :)
* Configurable timing for moving the dot

etc etc

But, my biggest issue is that I've written code, really I should do this all in xaml, and that is what I intend for pt 2 of this series... (which hopefully won't be too long, as I've done a bit of it already, and it looks snazzier by far!)

Print | posted @ Friday, July 4, 2008 12:44 PM

Comments on this entry:

No comments posted yet.

Post A Comment
Title:
Name:
Email:
Comment:
Verification: