Search code examples
c#winformslabel

Issues with updating progressbar and label inside while loop in C#/.net


I have run into a problem which is (to me) rather odd.

I have a C#/.net application in which I use a stopwatch to get accurate timing on an event. During the timing, I want to update a label with the progress. My code (simplified):

    private void backgroundWorker3_DoWork(object sender, DoWorkEventArgs e)
    {
            double stepDuration = Convert.ToDouble(textBoxPenetrationTime.Text);
            Stopwatch clock = new Stopwatch();
            long freq = Stopwatch.Frequency;
            double ticksPerStep = (double)freq * stepDuration;
            long preTick, postTick;
            clock.Restart();
            preTick = clock.ElapsedTicks;
            while (clock.ElapsedTicks < ticksPerStep)
            {
                labelPercent.BeginInvoke((Action)delegate () { labelPercent.Text = Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000)) + "%"; });
                if (backgroundWorker3.CancellationPending == true)
                {
                    return;
                }
            }
                postTick = clock.ElapsedTicks;
    }

What happens when I run start the backgroundworker is that the rest of the UI (dataplotting and labels handled in another backgroundworker freeze, as well as the entire form) hangs.

What I have tried:

Updating the label in another backgroundworker and call that backgroundworker inside the while loop (same result). Tried calling Application.DoEvents() after updating label (no difference)

One thing that works, which is the reason why I described the issue as odd, is that if I call

                Console.WriteLine(clock.ElapsedTicks);

at the beginning of the while loop, everything runs smoothly as intended. I just discovered that by chance.

So my question has two parts:

  1. Why does the UI run smoothly when writing to the console but not otherwise?
  2. How can I solve this issue without writing to the console?

Solution

  • You need to reduce the number of times you schedule work on the UI thread. Every time you call Invoke you ask for an update of the UI. If you have a tight loop you can be calling Invoke easily more than 60 times per second. Unless you're a gamer and/or Linus from LTT you won't see / notice all updates of your label, even if it would output different values on each call.

    When you added a call to Console.WriteLine you basically reduced the load on the UI thread enough to become responsive again. That is because Console.WriteLine is a call that is relatively expensive when it comes to CPU cycles / IO load. That comes at the price that your background task now takes longer to complete.

    I suggest the following changes:

    • set the property WorkerReportsProgress on your backgroundworker to True. You can do that in code or from the designer.
    • implement the ProgressChanged event, so you don't have to call invoke anymore.
    • call ReportProgress in a rate limited fashion. I have two options for that.

    The implementation of the ProgressChanged event

    backgroundWorker3.ProgressChanged += (sender, progress) => {
       labelPercent.Text = progress.ProgressPercentage.ToString("#0 \\%"); 
    };
    

    Here is what your DoWork would look like when keeping track of the last percentage so you can call ReportProgess when there is reason to do so.

    double stepDuration = Convert.ToDouble(10);
    Stopwatch clock = new Stopwatch();
    long freq = Stopwatch.Frequency;
    double ticksPerStep = (double)freq * stepDuration;      
    long preTick, postTick;
    clock.Restart();
    preTick = clock.ElapsedTicks;
    var lastPercentage = Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000));
    bgw.ReportProgress(lastPercentage);
    while (clock.ElapsedTicks < ticksPerStep)
    {
        var nextPercentage = Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000));
        if (nextPercentage != lastPercentage) {
            bgw.ReportProgress(nextPercentage);
            lastPercentage = nextPercentage;
        }
        // do work
    }
    bgw.ReportProgress(100);
    
    

    Here is what your DoWork would look like if you used a Timer to update the label 25 times per second. This comes at the cost of an extra thread and more calls to the UI thread but your worker loop is free of progress concerns.

    double stepDuration = Convert.ToDouble(10);
    Stopwatch clock = new Stopwatch();
    long freq = Stopwatch.Frequency;
    double ticksPerStep = (double)freq * stepDuration;
    long preTick, postTick;
    
    using(var timer = new System.Threading.Timer(
         (e) => {
            bgw.ReportProgress(Convert.ToInt32(clock.ElapsedTicks) / 1000000 * 100 / ((Convert.ToInt32(ticksPerStep) / 1000000)));
         }, null, 40, 40))
    {
        clock.Restart();
        preTick = clock.ElapsedTicks;
        while (clock.ElapsedTicks < ticksPerStep)
        {
           // do work
        }
    }
    bgw.ReportProgress(100);
    

    Here is a Gif of the timer solution in action:

    form with start and label progress to 100, form is responsive