A Better UX: Running the Progress bar on a Second UI Thread

Introduction

 

Generally, we know that you should never use the UI thread to perform a time consuming operation, but there are several cases in which you are forced to make the user wait for the application to finish:

  • The application is updating / logging in and the user must not intervene
  • The application is doing something that for technical reasons is unable to run in another thread (mostly legacy code / COM)
  • Updating the UI takes a long time, and you are unwilling / unable to change the UI to accommodate less information (i.e. using paging)

In these cases a progress bar can go a long way towards alleviating the user’s edginess, and ensure him that the program is still responding and is advancing towards the finish line. These bars come in two flavors:

  1. Advancing Progress bars – actually showing a percent of completion for the task.
  2. Throbber bars (Marquees) – giving the general feeling of movement, and stating to the user that the program is still alive.

 Progress bar.png

Each of these can also be circular.

 

Whenever possible I usually prefer the first, since it gives more data about the process and includes a sense of time. However, if you can’t accurately gauge the process completion throughout the work, opt to use a marquee style bar instead of having the program display inaccurate progress data, which may make the UI seem less trustworthy.

 

An example of the wrong choice of progress bar, is the incredibly inaccurate time estimates that are provided when copying files in Windows XP shell (although, in this specific case, copying files should be very predictable).

 

Implementing a progress bar:

Anybody who has ever tried to implement a progress bar has found out that if the UI thread is busy doing the actual work (as we said this is not good practice, but it exists) the ProgressBar won’t be advanced, since it relies on the UI thread to redraw it.

There are 3 solutions to this problem:

 

1. The progress bar will be on the UI thread and the work will be intermittently interrupted by a DoEvents call.

     a. This gives the UI a chopppy feel, since it is not really getting all the CPU attention it needs

     b. Both the progress bar window and main window tend to freeze and thaw.

 

2. The Progress bar will be displayed by another process and will be activated and incremented by some Inter process communication (IPC).

     a. It is hard to sync two processes properly (opening ports / pipes, dealing with permissions, disconnections etc.)
     b. This method may cause the Progress bar to stay alive after application closes.

 

3. Running the progress bar on another thread

     a. It has to be a UI thread
     b. Can there be more than one UI thread per application?

 

Surprisingly, the third option is very feasible, although it seems like a hack at first.

The following code is a static class that manages an instance of a form containing a progress bar, and runs it on a new UI thread.

 

A Windows forms implementation:

 

using System.Threading;
using System.Windows.Forms;
 
namespace WinFormsProgressBarDemo
{
    public static class ProgressBarManager
    {
        static long m_intFullProgressBarValue = 100;
 
        public static long FullProgressBarValue
        {
            get { return m_intFullProgressBarValue; }
            set { m_intFullProgressBarValue = value; }
        }
        static int m_intProgressSteps = 100;
        static long m_intIntermediateValue = 0;
        static FrmProgressBar m_frm = null;
        static int m_intPrevValue = 0;
 
        private static void CreateInThread()
        {
            m_frm = new FrmProgressBar();
            m_frm.SetLableText(m_labelText);
            m_frm.SetTotalProgressSteps(m_intProgressSteps);
            Thread t = new Thread((ThreadStart)delegate
            {
                Application.Run(m_frm);
            });
 
            //running UI requires a STA Apartment thread
            t.SetApartmentState(ApartmentState.STA);
            
            //background threads close when main thread closes
            t.IsBackground = true;
 
            //start the ProgressBar personal UI thread
            t.Start();
 
            //wait to see that the thread is running ok and the form has become visible
            while (m_frm.Visible == false)
                Thread.Sleep(50);
        }
 
        public static void ShowProgressBar(long intFullProgressBarValue)
        {
            if (m_frm == null)
                CreateInThread();
 
            m_intFullProgressBarValue = intFullProgressBarValue;
 
            m_frm.Invoke((ThreadStart)delegate
            {
                if (!m_frm.Visible)
                    m_frm.Show();
                m_frm.ProgressValue = 0;
            });
        }
 
        static string m_labelText = "Adding Files";
        public static void SetLableText(string text)
        {
            m_labelText = text;
            if (m_frm != null)
            {
                if (m_frm.InvokeRequired)
                {
                    m_frm.Invoke((ThreadStart)delegate
                    {
                        m_frm.SetLableText(text);
                    });
                }
                else
                {
                    m_frm.SetLableText(text);
                }
            }
        }
 
        public static void SetProgress(long intermediateValue)
        {
            int newValue = (int)(((double)intermediateValue / (double)m_intFullProgressBarValue) * (double)m_intProgressSteps);
            if (newValue > m_intProgressSteps)
                newValue = m_intProgressSteps;
 
            m_intIntermediateValue = intermediateValue;
            if (newValue > m_intPrevValue && m_frm != null)
            {
                if (m_frm.InvokeRequired)
                {
                    m_frm.Invoke((ThreadStart)delegate
                    {
                        m_frm.ProgressValue = newValue;
                        m_intPrevValue = m_frm.ProgressValue;
                        m_frm.SetLableText(m_labelText);
                        m_frm.Invalidate();
                        m_frm.Refresh();
                    });
                }
                else
                {
                    m_frm.ProgressValue = newValue;
                    m_frm.SetLableText(m_labelText);
                }
                m_intPrevValue = newValue;
            }
        }
 
        public static void IncrementProgress(long addition)
        {
            m_intIntermediateValue += addition;
            SetProgress(m_intIntermediateValue);
        }
 
        public static void ClearProgress()
        {
            m_intIntermediateValue = 0;
            m_intPrevValue = 0;
            SetProgress(m_intIntermediateValue);
        }
 
        public static void CloseProgress()
        {
            Thread t = new Thread((ThreadStart)delegate
            {
                Thread.Sleep(700);
                if (m_frm.Visible)
                    m_frm.Invoke((ThreadStart)delegate
                    {
                        m_frm.Hide();
                    });
                ClearProgress();
            });
            t.Start();
        }
    }
}

 

A WPF implementation:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
 
namespace WpfProgressBarDemo
{
    public static class ProgressBarManager
    {
        static long m_intFullProgressBarValue = 100;
        static string m_labelText = "Adding Files";
 
        public static long FullProgressBarValue
        {
            get { return m_intFullProgressBarValue; }
            set { m_intFullProgressBarValue = value; }
        }
        static int m_intProgressSteps = 100;
        static long m_intIntermediateValue = 0;
        static WndProgress m_frm = null;
        static double m_intPrevValue = 0;
        static ManualResetEvent waitForThreadCreation = new ManualResetEvent(false);
        static Dispatcher _mainThreadDispatcher = null;
        static Dispatcher _secondThreadDispatcher = null;
 
 
        private static void CreateInThread()
        {
            _mainThreadDispatcher = Dispatcher.CurrentDispatcher;
            _secondThreadDispatcher = null;
 
            Thread t = new Thread(new ThreadStart(() =>
                {
                    _secondThreadDispatcher = Dispatcher.CurrentDispatcher;
                    
                    m_frm = new WndProgress();
                    m_frm.SetLableText(m_labelText);
                    m_frm.SetTotalProgressMax(m_intProgressSteps);
                    m_frm.Visibility = Visibility.Visible;
                    Dispatcher.Run();
                }));
            
            //running UI requires a STA Apartment thread
            t.SetApartmentState(ApartmentState.STA);
 
            //background threads close when main thread closes
            t.IsBackground = true;
 
            //start the ProgressBar personal UI thread
            t.Start();
 
            //wait to see that the thread is running ok and the form has become visible
            while (m_frm == null || m_frm.Visibility != Visibility.Visible)
                Thread.Sleep(50);
        }
 
        public static void ShowProgressBar(long intFullProgressBarValue)
        {
            if (m_frm == null)
                CreateInThread();
 
            m_intFullProgressBarValue = intFullProgressBarValue;
 
            _secondThreadDispatcher.Invoke((ThreadStart)delegate
            {
                if (m_frm.Visibility != Visibility.Visible)
                    m_frm.Show();
                m_frm.ProgressValue = 0;
            });
        }
 
 
        public static void SetLableText(string text)
        {
            m_labelText = text;
            if (m_frm != null)
            {
                if (!_secondThreadDispatcher.CheckAccess())
                {
                    _secondThreadDispatcher.Invoke((ThreadStart)delegate
                    {
                        m_frm.SetLableText(text);
                    });
                }
                else
                {
                    m_frm.SetLableText(text);
                }
            }
        }
 
        public static void SetProgress(long intermediateValue)
        {
            int newValue = (int)(((double)intermediateValue / (double)m_intFullProgressBarValue) * (double)m_intProgressSteps);
            if (newValue > m_intProgressSteps)
                newValue = m_intProgressSteps;
 
            m_intIntermediateValue = intermediateValue;
            if (newValue > m_intPrevValue && m_frm != null)
            {
                if (!_secondThreadDispatcher.CheckAccess())
                {
                    _secondThreadDispatcher.Invoke((ThreadStart)delegate
                    {
                        m_frm.ProgressValue = newValue;
                        m_intPrevValue = m_frm.ProgressValue;
                        m_frm.SetLableText(m_labelText);
                    });
                }
                else
                {
                    m_frm.ProgressValue = newValue;
                    m_frm.SetLableText(m_labelText);
                }
                m_intPrevValue = newValue;
 
            }
        }
 
 
 
        public static void IncrementProgress(long addition)
        {
            m_intIntermediateValue += addition;
            SetProgress(m_intIntermediateValue);
        }
 
        public static void ClearProgress()
        {
            m_intIntermediateValue = 0;
            m_intPrevValue = 0;
            SetProgress(m_intIntermediateValue);
        }
 
        public static void CloseProgress()
        {
            Thread t = new Thread((ThreadStart)delegate
            {
                Thread.Sleep(700);
                if (m_frm.Visibility == Visibility.Visible)
                    _secondThreadDispatcher.Invoke((ThreadStart)delegate
                    {
                        m_frm.Hide();
                    });
                ClearProgress();
            });
            t.Start();
        }
    }
}

>>Download complete code samples<<

 

There are a few noteworthy functions in this class:

  • ShowProgressBar – resets progress, makes the progress bar visible, and sets the maximum amount for the progressbar (max amount = 100%)
  • IncrementProgress – adds the given number to the progress bar’s total count, and updates the progress bar UI.
  • SetProgress – sets the given number as the current progress, the progress bar will now show (precent full) = (progress/maxProgress).
  • SetLableText – allows the user to adjust the text in the form’s label

 

 

The trick 

When you run the demo you can see that the main window is frozen and can’t be moved, while the progress bar remains totally free to update and can be moved around.

What we do to get this behavior is creating a new UI thread for our progress window

  • In Windows Forms:
    • Calling the Application.Run(m_frm) function creates a new UI thread for the given form.
    • The form is stored in a member and used to invoke into the correct thread when updating the progress bar UI.
  • In WPF:
    • Calling the Dispatcher.CurrentDispatcher function creates a new Dispatcher within the calling thread
    • The dispatcher object is stored in a member and used to invoke into the correct thread when updating the progress bar UI.

Invoking into the correct thread is needed since UI objects are rooted to the thread in which they are created /displayed in, and changing a UI control’s properties from within a different thread will cause a cross thread operation exception. Therefore calls that need to update the progress bar UI, should be done on the Progress bar UI thread (our second UI thread). This is done by wrapping the calls with m_frm.Invoke / _dispatcher.Invoke

Everything else is just regular progress bar footwork.

 

 

Conclusion

Giving the user some indication about the state of the application is what UI is all about, but in some cases keeping a reliable UI look & feel can be a prime technical challenge, as well as a graphics and human interaction challenge. As in the case iPhone vs Android, a leaner, more responsive application is one of the most sought after qualities we look for in a product.

This is the first of several blog entries in which I will try to give several techniques, which can help you improve your application’s UX under real world conditions, while keeping code refactoring to a minimum.

 

Leave a Comment

We encourage you to share your comments on this post. Comments are moderated and will be reviewed
and posted as promptly as possible during regular business hours

To ensure your comment is published, be sure to follow the Community Guidelines.

Be sure to enter a unique name. You can't reuse a name that's already in use.
Be sure to enter a unique email address. You can't reuse an email address that's already in use.
Type the characters you see in the picture above.Type the words you hear.
Search
Showing results for 
Search instead for 
Do you mean 
About the Author
I've been all over the coding world since earning my degrees have worked several years in c++ and then several in java, finally setteling i...


Follow Us
The opinions expressed above are the personal opinions of the authors, not of HP. By using this site, you accept the Terms of Use and Rules of Participation