I’ve had the chance to play around a bit with SL 2.0 recently and thought I’d share some discoveries using the ProgressBar control. It’s not quite so easy to use as you might initially think.

To illustrate this I’ve created a simple Page.xaml user control,

<UserControl x:Class="ProgressBarTest.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <StackPanel Background="White">
        <ProgressBar x:Name="progressBar" Minimum="0" Maximum="100" IsIndeterminate="False" Height="20" Margin="5"></ProgressBar>
        <TextBlock x:Name="progressText" Height="20" Text="" Margin="5" ></TextBlock>
        <Button Name="startButton" Content="Start" Width="100" Height="30" Click="Button_Click"></Button>
    </StackPanel>
</UserControl>

Which produces something that looks like,

image

There are a few articles (and in fact Microsoft’s help documentation itself) that simple state that all you need to do is to set the ProgressBar’s Value property and away you go. I’ve seen code snippets like this,

public partial class Page : UserControl
{
    public Page()
    {
        InitializeComponent();
    }
 
    private int _progressValue = 0;
 
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        progressBar.Value = _progressValue;
        progressText.Text = _progressValue + "% complete";
 
        _progressValue += 10;
        if (_progressValue > 100) _progressValue = 0;
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

And, sure enough, each time you click the Start button the ProgressBar increments nicely. But try something a bit more realistic like,

public partial class Page : UserControl
{
    public Page()
    {
        InitializeComponent();
    }
 
    private void DoSomethingHard()
    {
        for (int i = 0; i <= 100; i = i + 10)
        {
            progressBar.Value = i;
            progressText.Text = i + "% complete";
 
            Thread.Sleep(500);
        }
    }
 
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        DoSomethingHard();
    }
 
}

 

 

and you find that nothing happens to the ProgressBar until the end of the DoSomethingHard loop when the ProgressBar suddenly shows 100% complete. Not so good.

It turns out that any (long running or otherwise) user code on the main UI thread will essentially suspend any updates to any user interface elements on the page (not only ProgressBar’s). Only when control has been returned to Silverlight will it process the batched UI updates. I’m sure there’s a great explanation for this but it’s a real pain. I remember this problem when I used to code in VB (old school, that is) and the way round it then was to call the DoEvents function that would given the application a chance to process things like UI updates during intensive operations. Unfortunately, there is no such equivalent in Silverlight. In fact,

The only way to update the user interface during an operation is to have that operation performed in a different thread to the user interface.

Updating ProgressBar with BackgroundWorker

The simplest way to achieve this is to use the BackgroundWorker class. It handles spawning a new thread for you and allowing you to respond to changes to progress.

public partial class Page : UserControl
{
    BackgroundWorker _worker;
 
    public Page()
    {
        InitializeComponent();
 
        // Set up the BackgroundWorker class to do the Hard work in another thread
        _worker = new BackgroundWorker();
 
        // For some bizarre reason we need to tell the BackgoundWorker that we will be
        // reporting progress!
        _worker.WorkerReportsProgress = true;
        
        // Wire up an event handler to respond to progress changes during the operation.
        _worker.ProgressChanged += new ProgressChangedEventHandler(_worker_ProgressChanged);
 
        // Let the BackgoundWorker know what operation to call when it's kicked off.
        _worker.DoWork += new DoWorkEventHandler(_worker_DoWork);
    }
 
    void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // This is the opportunity to update the controls on the main thread
        progressBar.Value = e.ProgressPercentage;
        progressText.Text = e.UserState.ToString();
    }
 
    void _worker_DoWork(object sender, DoWorkEventArgs e)
    {
        DoSomethingHard();
    }
 
    private void DoSomethingHard()
    {
        for (int i = 0; i <= 100; i = i + 10)
        {
            // Report some progress - this will result in the ProgressChanged event being
            // raised
            _worker.ReportProgress(i, i + "% complete");
 
            Thread.Sleep(500);
        }
    }
 
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // Start the BackgoundWorker thread to do the Hard Work
        _worker.RunWorkerAsync();
    }
 
}

Of course, in a real world scenario you probably aren’t running the long running process in the code behind of your control. So you won’t necessarily have access to the BackgroundWorker instance within your loop. Consider an example where you have implemented an MVP pattern.

 

Updating ProgressBar within an MVP Implementation

Consider you have a View interface defined as,

public interface IPageView
{
    event EventHandler DoSomethingHard;
}

This will be used subscribed to by the Presenter to know when to DoSomethingHard.

The Presenter is defined as,

public class PagePresenter
{
    // Define an event for the View to subscribe to to receive progress notifications
    public event EventHandler<ProgressChangedEventArgs> ReportProgressFromPresenter;
 
    public PagePresenter(IPageView view)
    {
        View = view;
        View.DoSomethingHard += new EventHandler(View_DoSomethingHard);
    }
 
    void View_DoSomethingHard(object sender, EventArgs e)
    {
        for (int i = 0; i <= 100; i = i + 10)
        {
            // If the View is listening...
            if (ReportProgressFromPresenter != null)
            {
                ReportProgressFromPresenter(this, new ProgressChangedEventArgs(i, i+"% complete"));
            }
 
            Thread.Sleep(500);
        }
    }
 
    private IPageView View {get; set;}
 
}

 

Note the Presenter has also defined an event that will be raised to notify the View of changes to it’s progress.

Our Page now provides a concrete implementation of the IPageView interface

 

 

 

 

 

public partial class Page : UserControl, IPageView
{
    BackgroundWorker _worker;
 
    PagePresenter Presenter { get; set; }
 
    public Page()
    {
        InitializeComponent();
 
        // Set up the presenter and subscribe to it's ReportProgressFromPresenter event
        Presenter = new PagePresenter(this);
        Presenter.ReportProgressFromPresenter += new EventHandler<ProgressChangedEventArgs>(Presenter_ReportProgressFromPresenter);
 
        _worker = new BackgroundWorker();
        _worker.WorkerReportsProgress = true;
        _worker.DoWork += new DoWorkEventHandler(_worker_DoWork);
        _worker.ProgressChanged += new ProgressChangedEventHandler(_worker_ProgressChanged);
    }
 
    /// <summary>
    /// When the Presenter tells us there's a change in progress we pass this on to the
    /// BackgroundWorker.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void Presenter_ReportProgressFromPresenter(object sender, ProgressChangedEventArgs e)
    {
        _worker.ReportProgress(e.ProgressPercentage, e.UserState.ToString());
    }
 
    void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // This is the opportunity to update the main UI's controls
        progressBar.Value = e.ProgressPercentage;
        progressText.Text = e.UserState.ToString();
    }
 
    /// <summary>
    /// Raise the DoSomethingHard event for the Presenter to respond to.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void _worker_DoWork(object sender, DoWorkEventArgs e)
    {
        // If our presenter is listening then let it know to start doing something Hard.
        if (DoSomethingHard != null)
        {
            DoSomethingHard(this, new EventArgs());
        }
 
    }
 
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // Start the backgroundworker thread to do the Hard Work
        _worker.RunWorkerAsync();
    }
 
    #region IPageView Members
 
    public event EventHandler DoSomethingHard;
    
    #endregion
}

So we’ve now separated the DoSomethingHard operation away from the user interface and it’s BackgroundWorker class. This makes the Presenter code easier to test against a mock View that may or may not even have a BackgroundWorker.

Remember that the consequence of using a separate thread to perform lengthy operations is that other user interface elements (like buttons) on the main UI thread will still be accessible while the operation is being performed. Unless you disable other controls appropriately you could have unexpected results.

kick it on DotNetKicks.com