Smarter Loading Animations in WPF

An example of how to build a loading control that only appears after a delay.

Any WPF application will need to handle loading delays gracefully. In most cases this involves showing a loading animation while the user waits for the operation to complete. For long delays this is a great idea and all of my code takes care to do this. But what happens if some operations complete too soon? The user gets their result straight away but the loading animation flashed on screen… and if the user triggers this many times in close succession it can be very distracting.

The code below shows how to build a re-usable view model base class that solves this problem.

The base view model implements the INotifyPropertyChanged interface, and exposes a public property LoadingVisibility which the loading control should be bound to. There are two methods to signal the start and end of an operation, LoadStarted and LoadCompleted. LoadStarted sets the _IsLoading flag indicating that work is in progress, then starts a DispatcherTimer which will issue the PropertyChanged event for the LoadingVisibility property after a delay (200 miliseconds in the example). The LoadingVisibility property looks at the flag, and returns Visible if the work is still in progress. LoadCompleted resets the flag:

public class ViewModelBase : INotifyPropertyChanged
{
    private bool _IsLoading;
    public event PropertyChangedEventHandler PropertyChanged;

    public Visibility LoadingVisibility
    {
        get
        {
            if (_IsLoading)
                return Visibility.Visible;
            else
                return Visibility.Collapsed;
        }
    }

    /// <summary>
    /// Raises the PropertyChanged event.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    public void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    /// Signals the start of some work.
    /// </summary>
    public void LoadStarted()
    {
        _IsLoading = true;

        // Delay the property change notification for a few miliseconds to
        // avoid showing it for short running tasks
        DispatcherTimer dt = new DispatcherTimer();
        dt.Interval = TimeSpan.FromMilliseconds(200);
        dt.Tick += delegate(object sender, EventArgs e)
        {
            OnPropertyChanged("LoadingVisibility");
            dt.IsEnabled = false;
        };
        dt.Start();
    }

    /// <summary>
    /// Signals the end of some work.
    /// </summary>
    public void LoadCompleted()
    {
        _IsLoading = false;
        OnPropertyChanged("LoadingVisibility");
    }

    /// <summary>
    /// Wraps the method calls required to manage the loading control
    /// and executes the specified method on a new thread.
    /// </summary>
    /// <param name="method">The method to execute.</param>
    public void LoadAsync(Action method)
    {
        LoadStarted();

        ThreadStart start = delegate()
        {
            method();
            LoadCompleted();
        };
        new Thread(start).Start();
    }
}

The LoadAsync is a helper class. It takes care of two things, firstly it ensures the LoadStarted and LoadCompleted methods are called and secondly it executes the operation on a background thread.

Since most of the code is nicely wrapped up in the base class, the main window’s view model only needs the methods to retrieve the data. Note that these methods will be called on a background thread.

public class MainWindowViewModel : ViewModelBase
{
    public void LoadDataFast()
    {
        // Simulates a short running task
        Thread.Sleep(100);
    }

    public void LoadDataSlow()
    {
        // Simulates a long running task
        Thread.Sleep(5000);
    }
}

I’m told that to use view models “properly” I should be using WPF Commanding. That may be true, but I prefer to call the view model methods directly from the code-behind file (and I’m more concerned with unit testing than keeping the layers seperate):

public partial class MainWindow : Window
{
    private MainWindowViewModel _viewModel;

    public MainWindow()
    {
        InitializeComponent();

        // Create an instance of the view model and make it available for binding
        this._viewModel = new MainWindowViewModel();
        this.DataContext = _viewModel;
    }

    private void FastTask_Click(object sender, RoutedEventArgs e)
    {
        // Load data (won't trigger loading animation)
        _viewModel.LoadAsync(_viewModel.LoadDataFast);
    }

    private void SlowTask_Click(object sender, RoutedEventArgs e)
    {
        // Load data (will trigger loading animation)
        _viewModel.LoadAsync(_viewModel.LoadDataSlow);
    }
}

Here’s the XAML for the main window. Note that the Visibility property of the loading graphic must be bound to the LoadingVisibility property of the view model:

<Grid>
    <Grid Height="16" Margin="200,120,253,0" VerticalAlignment="Top" Visibility="{Binding LoadingVisibility}">
    	<Ellipse Fill="#FFDDDDDD" HorizontalAlignment="Left" Width="16"/>
    	<Ellipse Fill="#FFDDDDDD" Margin="20,0"/>
    	<Ellipse Fill="#FFDDDDDD" HorizontalAlignment="Right" Width="16"/>
    </Grid>
    <Button Content="Slow" Height="24" HorizontalAlignment="Right" Margin="0,0,125,37" VerticalAlignment="Bottom" Width="64" Click="SlowTask_Click" />
    <Button Content="Fast" Height="24" HorizontalAlignment="Right" Margin="0,0,55,37" VerticalAlignment="Bottom" Width="64" Click="FastTask_Click" />
</Grid>
Advertisements
This entry was posted in Reference and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s