Documentation

Single-threaded progress indicator in c#

Explains how the Axisbase progress indicator was written using a single thread.

I had a coding situation where I needed to do some work that may take a long time and I wanted to force the user to wait while possibly giving them a progress indicator. There are many variants of the problem and several solutions.

The code shown here is used in Axisbase  (a free .NET database server and client).

Requirements

No matter what method is chosen, the UI should never freeze; if the user goes to another application's window then goes back to the working app, it should at least re-paint. (However, calling Application.DoEvents can lead to some very bad and hard to debug problems.)

When using a progress indicator for a task that might be fast and might be slow, it is better to delay displaying it until after about a half second of the work. Users don't want to see a progress indicator flash for an instant and then go away.


Background worker threads are often discussed as a solution to this, but when the work being done is closely related to the UI, the use of multiple threads creates the need for extra code. If the user has just asked to do one thing, and the UI doesn't block (because the request is being processed in the background), then should the user be able to click on other controls? In many cases, no. So you can disable all the controls during the background work. But you don't want the controls to repaint to disabled status if the duration of the background work turns out to be very short.

Background threads are good for some things, but I wanted a progress indicator for a foreground process.

A flight of fancy

There are two distinct kinds of UI operations: blocking and non-blocking. An example of a blocking (or sequential) operation is entering a character into a text box. You want the application to complete the action before moving on to the next action. If for some reason, it takes a long time to complete the action (network delay for example), you ideally want the window to block all other actions until that is done. Of course keystrokes can be queued up, but they are still processed sequentially. The other kind of operation - non-blocking - is painting or moving windows, or some other actions that cause repainting. These actions should be done even while the window is blocked for the next blocking action.

Ideally we would have two threads to work with. One thread would allow the user to move windows and would repaint them, while the other thread would process sequential (blocking) user actions on the controls. If a long-running action was in progress, the painting thread could notice this, and change the window or cursor appearance as an indicator that the blocking thread is blocked.

If the dual-UI-thread thing was available, you could do something like this, and push all the work of displaying progress onto the other thread in a generalized way:

someControl_OnClick(...) {

window.ProgressText = "Loading data";

//load some data

window.ProgressText = "Setting up tree view";

//populate the tree view

}

But this is not available, so...

Back to reality

Here is my real-word answer: Create a progress form but don't show it when you start doing long-running work in the UI thread. Instead call it every once in a while while working with the progress status. The progress form shows itself if a half second has elapsed, and when it does, it blocks windows messages to other windows. So it isn't a dialog box, but it stays on top and blocks other windows from being used like a dialog box.

The c# code for such a form is shown below.

I made the progress form implement IMessageFilter. The filter tells windows to throw away messages that we don't want, like mouse clicks, during the work. However, it lets messages through for the cancel button and it lets all paint messages through.

The ShowProgress method of the form should be called often during the work. It calls Application.DoEvents, but since event messages are being filtered, this doesn't cause problems.

One downside of this approach is that when using the form, you must always call the Finish method to remove the message filter, and this HAS to be done in a finally clause and there MAY NOT be any dialog boxes or other interaction before Finish gets called. Otherwise the app will hang.

 

First: here is how you use the form:

 

            try
            {
                ProgressForm pf = new ProgressForm();
                pf.SetupAndShow(this, "Doing something", true, true);
                try
                {
                    for (int i = 0; i < 100; ++i)
                    {
                        //do some work
                        System.Threading.Thread.Sleep(70);

                        //report on progress in the same thread
                        pf.ShowProgress(i / 100.0, "Record #" + i);
                    }

                    //if you show a dialog box here, your app will hang
                }
                finally
                {
                    pf.Finish();
                }
            }
            catch (Exception ex)
            {
                //Note cancellation throws an exception
                MessageBox.Show(ex.Message);
            }


Here is the form code:

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace Axis1.Forms
{
    /// <summary>
    /// Allows a progress indicator to pop up, blocking the user from
    /// doing anything (except possibly canceling the operation).
    /// To use you MUST MUST MUST call SetupAndShow, then call Finish in
    /// a finally block. You may call ShowProgress any number of times
    /// in between. If you allow cancellation, then you must trap errors,
    /// as a cancellation will throw an exception.
    /// </summary>
    public partial class ProgressForm : Form, IMessageFilter
    {
        const int WM_PAINT = 0x000F, WM_NCACTIVATE = 0x086;

        private Form caller;
        private DateTime showTime; //time when form should show, UTC
        private Cursor priorCursor = Cursors.Default;
        private bool allowCancel; //controls whether a manual close should cause an exception
        private bool pleaseAbort; //set when user presses cancel

        public ProgressForm()
        {
            InitializeComponent();
        }
       
        /// <summary>
        /// Setup the form.
        /// </summary>
        /// <param name="msg1">the message describing the work being done</param>
        /// <param name="delayedDisplay">if true, the form is only shown if
        /// the operation takes longer than a half-second</param>
        public void SetupAndShow(Form caller, string msg1,
            bool delayedDisplay, bool allowCancel)
        {
            if (caller == null)
            {
                if (Application.OpenForms.Count == 0) return;
                caller = Application.OpenForms[0];
            }
            this.allowCancel = allowCancel;
            eCancel.Visible = allowCancel;
            label1.Text = msg1;
            this.caller = caller;
            eBar.Value = 0;
            if (delayedDisplay)
                showTime = DateTime.UtcNow.AddSeconds(0.5);
            else
            {
                showTime = DateTime.MinValue;
                ShowProgress(0, null);
            }
            priorCursor = caller.Cursor;
            caller.Cursor = Cursors.WaitCursor;
            Application.AddMessageFilter(this);
        }

        /// <summary>
        /// Update progress bar and labels. If enough time has passed,
        /// make form visible. Call DoEvents, and if form is canceled,
        /// throw Exception.
        /// </summary>
        /// <param name="percent">a number between 0 and 1</param>
        /// <param name="displayValue"></param>
        public void ShowProgress(double percent, string displayValue)
        {
            if (percent > 0)
            {
                try { eBar.Value = (int)(percent * 100.0); }
                catch { }
            }
            if (displayValue != null)
                label2.Text = displayValue;

            //show/paint form
            if (!Visible && showTime < DateTime.UtcNow)
            {
                Show();
                Cursor = Cursors.WaitCursor;
                if (eCancel.Visible) eCancel.Focus();
            }
        
            //allow some events to occur during the work, but only those
            //that deal with the cancel button
            Application.DoEvents();
            if (pleaseAbort)
            {
                Application.RemoveMessageFilter(this);
                Hide();
                throw new Exception("Operation canceled");
            }
        }

        //returns true if a message should be filtered out
        public bool PreFilterMessage(ref Message m)
        {
            bool isgoodtype = m.Msg == WM_PAINT;
            bool allowed = isgoodtype ||
                (Visible && (m.HWnd == this.Handle || m.HWnd == eCancel.Handle));
            return !allowed;
        }

        private void eCancel_Click(object sender, EventArgs e)
        {
            pleaseAbort = true;
        }

        /// <summary>
        /// Finish form - hides, unless there are errors, in which case it
        /// shows it as a dialog and waits for OK pressed
        /// </summary>
        public void Finish()
        {
            Application.DoEvents(); //this clears the event queue of unwanted clicks
            Hide();
            Application.RemoveMessageFilter(this);
            Dispose();
            caller.Cursor = priorCursor;
        }

    }


}
(c) 2014-2015 Divergent Labs, Inc.