Friday, December 21, 2007

WPF Progress Bars

Note: I've posted an updated, even easier to use version of this code here:

WPF Progress Bars Revisited

Implementing a progress bar display for long-running tasks is a commonly occurring task. WPF has a simple ProgressBar control which works as you would expect. It has a floating point Value property that is used to display progress within a range (set using the Minimum and Maximum properties). Here is some XAML to implement a progress dialog:
<Window x:Class="MyNamespace.ProgressDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Progress Dialog" Width="300" SizeToContent="Height">
<Grid>
<StackPanel Margin="10">
<ProgressBar Name="Progress" Width="200" Height="20" Minimum="0" Maximum="1" Margin="10" />
<TextBlock Name="StatusText" Margin="10" Height="50"/>
<StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
<Button Name="CancelButton">Cancel</Button>
</StackPanel>
</StackPanel>
</Grid>
</Window>

The progress bar is set to take progress values from 0 to 1, and has a TextBlock for displaying a status message.

It might seem like we are finished, but it is only part of what you need for a functional progress dialog. In trying to use the dialog, you quickly run into a problem - how to avoid blocking the UI thread while your operation proceeds. In Windows Forms, a common cheat was to periodically call Application.DoEvents() to allow the UI to update. While you can do something similar in WPF, it is ugly and best avoided.

Instead, you should do your work asynchronously in a separate thread. Given that, the next question becomes how to update the UI from your second thread, as WPF UI operations are not thread-safe, and you will get an exception if you try to interact with a control outside of the thread in which it was created. The solution? Use the Dispatcher. The Dispatcher basically lets you queue up function calls on the UI thread from your background thread.

To manage this communication, we will first create an interface for interacting with a progress dialog:
public interface IProgressContext
{
void UpdateProgress(double progress);
void UpdateStatus(string status);
void Finish();
bool Canceled { get; }
}

The UpdateProgress() method allows us to set the current progress value, UpdateStatus() allows us to display a text status message, Finish() lets us signal that our operation has completed, and the Canceled property allows us to check if the user has canceled the operation.

Now, the code-behind for the dialog:
public partial class ProgressDialog : Window, IProgressContext
{
private bool canceled = false;

public bool Canceled
{
get { return canceled; }
}

public ProgressDialog()
{
InitializeComponent();

CancelButton.Click += new RoutedEventHandler(CancelButton_Click);
}

void CancelButton_Click(object sender, RoutedEventArgs e)
{
canceled = true;
CancelButton.IsEnabled = false;
}

public void UpdateProgress(double progress)
{
Dispatcher.BeginInvoke(DispatcherPriority.Background,
(SendOrPostCallback)delegate { Progress.SetValue(ProgressBar.ValueProperty, progress); }, null);
}

public void UpdateStatus(string status)
{
Dispatcher.BeginInvoke(DispatcherPriority.Background,
(SendOrPostCallback)delegate { StatusText.SetValue(TextBlock.TextProperty, status); }, null);
}

public void Finish()
{
Dispatcher.BeginInvoke(DispatcherPriority.Background,
(SendOrPostCallback)delegate { Close(); }, null);
}
}

As you can see, we are basically just implementing the IProgressContext interface. We use the dispatcher to send along our progress updates to the UI thread. To use the dialog, launch a background thread to do your work, passing it a progress dialog as IProgressContext. Your work loop will look something like this:
for (int i = 0; i < 100; i++)
{
if (myProgressContext.Canceled)
break;

myProgressContext.UpdateProgress((double)i / 100.0);
myProgressContext.UpdateStatus("Doing Step " + i);
}

myProgressContext.Finish();

You can obviously get more fancy with your dialog - this is just a simple implementation to get started with.

26 comments:

  1. Great tip! I finally managed to get a background thread do something to an object in WPF by using Dispatcher.BeginInvoke! Just one question, are Dispatcher.BeginInvoke or Dispatcher in general also the right choice when doing heavy background worker threads?

    I have seen a lot of difficulties with Callcontext using WPF, and use a static Dictionary instead. Anybody else also seen difficulties using Callcontext in WPF?

    ReplyDelete
  2. Nice one!

    Somehow I can't get it working with a
    while ((input = sr.ReadLine()) != null)
    { update progressbar during loop} - loop, (sr is a StringReader) no matter what priority I set.

    ReplyDelete
  3. What exactly isn't working? Did you maybe forget to .Show() the dialog?

    ReplyDelete
  4. Sorry, I didn't explain much there :)
    Ok, In my loop I fill a ListView with data gathered from an Oracle database. The progress bar is shown in the beginning, but the progress is not updated until my loop terminates. Then the progress bar goes to max and closes.
    I have a .Show() before the loop, a .UpdateProgress inside the loop and .Finish() and .Close after the loop. I've tried the UpdateProgress with all possible priorities.

    ReplyDelete
  5. Are you doing your work in a background thread? That is the key, since the main thread needs to be available to update the UI.

    ReplyDelete
  6. I am having a headache with this...
    when i code:
    private void imprimir_onClick(object sender, MouseButtonEventArgs e)
    {
    MessageBoxResult resultadoAdv = MessageBoxResult.Yes;
    if (tablaResultados.Items.Count > 2000)
    {
    resultadoAdv = MessageBox.Show("El número de registros es muy grande. Su impresión será de más de 100 páginas y algunas computadoras con poca memoria podrían tener problemas para generarla. ¿Desea Continuar?",
    "Advertencia", MessageBoxButton.YesNo, MessageBoxImage.Exclamation);
    }
    if (resultadoAdv == MessageBoxResult.Yes)
    {
    documentoImpresion=null;
    impresionViewer.Document = null;
    EsperaBarra.Value = 0;
    EsperaBarra.Visibility = Visibility.Visible;
    FlowDocumentReader documentoAEscribir = new FlowDocumentReader();
    Dispatcher.BeginInvoke(DispatcherPriority.Render,
    (SendOrPostCallback)delegate { impresion(documentoAEscribir); }, null);
    int contador = 0;
    while (documentoAEscribir.Document == null)
    {
    this.WindowTitle="Por favor Espere: "+contador;
    EsperaBarra.Value = contador % 100;
    contador++;
    Thread.Sleep(100);
    }
    impresionViewer.Document = documentoAEscribir.Document;
    imprimePapel.Visibility = Visibility.Visible;
    tablaResultados.Visibility = Visibility.Hidden;
    EsperaBarra.Visibility = Visibility.Hidden;
    }
    }
    private void impresion(Object objetoEstado)
    {
    if (impresionViewer.Visibility == Visibility.Hidden)
    {
    FlowDocumentReader lector = (FlowDocumentReader)objetoEstado;
    try
    {
    documentoImpresion = new DocumentoListadoTesis(tablaResultados.Items);
    lector.Document = documentoImpresion.Documento;
    lector.Visibility = Visibility.Visible;
    lector.Background = Brushes.Bisque;
    lector.ViewingMode = FlowDocumentReaderViewingMode.Scroll;
    }
    catch (Exception exc)
    {
    MessageBox.Show("Ocurrio un problema al generar el documento: "
    + exc.Message + ". Puede reportarlo a soporteius@mail.scjn.gob.mx");
    //lector.Document = null;
    //lector.Visibility = Visibility = Visibility.Hidden;
    //imprimePapel.Visibility = Visibility.Hidden;
    //tablaResultados.Visibility = Visibility.Visible;
    }
    }
    else
    {
    tablaResultados.Visibility = Visibility.Visible;
    impresionViewer.Visibility = Visibility.Hidden;
    imprimePapel.Visibility = Visibility.Hidden;
    impresionViewer.Document = null;
    }

    }

    The impresion method is never called.
    So the loop in the progressBar (EsperaBar) is infinite.
    What am i doing wrong?
    Greetings
    Carlos de Luna

    ReplyDelete
  7. Hi Carlos - your problem is that your progress bar loop is running in the main UI thread. It blocks the thread so that the dispatcher never has a chance to run your impresion() method.

    ReplyDelete
  8. Hey Mike, I got all that you did up to the point where i create the for loop. I am a little confused as to how and what i do to invoke this action. Can you explain that part a little more, just connect the dots. Thanks

    ReplyDelete
  9. Hi mate,

    Thanks heaps! I was looking everywhere trying to get this to work. After reading through your post it all works perfectly. Thanks again!

    ReplyDelete
  10. Hi Aidan - glad it helped you - the inherent issue of threading still seems to be causing people problems, though. Maybe I'll do an updated version one of these days that makes that easier...

    ReplyDelete
  11. Can you please show us how to create the background thread and pass it a progress dialog as IProgressContext as this is the vital part missing from the above code example.

    Many thanks

    ReplyDelete
  12. Ok, given all of the people who were having trouble with threading, I've posted an updated version of the ProgressDialog class that handles it for you. I've added a link to the new version at the top of the post.

    ReplyDelete
  13. This line isn't working for me:

    Progress.SetValue(ProgressBar.ValueProperty

    ReplyDelete
  14. to fir . i had the same problem . it was because my namespace was called progressbar ))

    ReplyDelete
  15. its a nice article

    ReplyDelete
  16. Thanks a bunch! =o)

    ReplyDelete
  17. please upload a completed example somewhere (codeproject/codeplex/krugle)

    ReplyDelete
  18. I've always been shocked when people use Application.DoEvents in Windows Forms applications. I've always done it with threads, and I think it's insane to do it any other way.

    ReplyDelete
  19. please give explanation about last for loop and myProgressContext;

    ReplyDelete
  20. Hi manoj - the loop is just meant as a simple example of how to use the code.

    ReplyDelete
  21. Lucas - from São Paulo - BrazilSeptember 16, 2010 at 7:09 PM

    Hi Mike,
    My framework version is 4! The overload from method Dispatcher.BeginInvoke is a little diferent of yours. He gets following parameters Dispatcher.BeginInvoke(Delegate, DispatcherPriority, object[])
    I have a error in mscorlib and i not have reference about this error.
    Code:
    private void IniciaTimer()
    {
    countProgress++;
    Thread.Sleep(2000);
    Dispatcher.BeginInvoke((SendOrPostCallback)delegate{pgLoadStart.SetValue(ProgressBar.ValueProperty, countProgress);}, null);

    }

    void timer_Elapsed(object sender, ElapsedEventArgs e)
    {
    new Thread(delegate() { IniciaTimer(); }).Start();
    }

    Error:
    System.Reflection.TargetParameterCountException was unhandled
    Message=The counting of parameters does not match
    Source=mscorlib
    InnerException:

    Thanks!

    ReplyDelete
  22. Hi Lucas - I haven't yet used WPF4, so I'm afraid I can't be of much help. It looks to me, though, like you need to add a dispatcher priority as the second argument to BeginInvoke.

    ReplyDelete
  23. Lucas - from São Paulo - BrazilSeptember 17, 2010 at 7:04 PM

    Thanks Mike,
    I discovered that for some reason VS2010 not listed the four overloads that are available as spoken by the MSDN Web site. I forced the same entry in your example and it worked. Thank you very much

    ReplyDelete
  24. Thanks a lot! It's very useful dicision!

    ReplyDelete
  25. Thank you, great post!

    ReplyDelete