Tek Eye Logo

Tek Eye

C# BackgroundWorker with Progress Bar

When developing software not only is the program's main purpose important, i.e. what task the program performs, there are other aspects to consider. These other aspects of a computer program include things like usability, security and maintainability. This article looks at an important part of usability, specifically how the interface for a C# program can keep the user updated and the User Interface (UI) responsive when running intensive tasks. In this tutorial this is done with the BackgroundWorker and ProgressBar .NET Framework classes.

BackgroundWorker with ProgressBar

The Importance of Program Responsiveness

In terms of usability the program's UI should be informative, responsive and reflect the status of the program and the data it is processing. A program should keep the user updated on what it is doing. One of the annoying problems of badly written software is when it fails to provide useful feedback and fails to respond to users actions. In such cases the user may force the program to close because it gives the impression it has stopped working.

Modern computers are extremely powerful and often come with multiple processing cores that can run many threads. It is easy for a modern PC to support multithreaded programs. Therefore, there is no excuse for software to not be responsive and user friendly. However, even popular operating systems and programs still fail to provide a good user experience. Part of the problem lies with the software writers. Whilst it is easy for a PC to run multithreaded programs, some developers are not inclined to develop them because they can be harder to debug if programming errors are made. However, for those developing Windows applications on the .NET Framework there are classes available that an application can use to run multiple threads.

BackgroundWorker Class Runs Code on Another Thread

A normal Windows Forms (WinForms) application has one thread. The main UI thread, which not only runs the main program code it also services all the screen interface elements. If intensive code, such as complex calculations, or slow code, such as heavy Internet access, runs in the main program code, then the UI can become unresponsive.

The BackgroundWorker is available from the Visual Studio Toolbox (although BackgroundWorker can be used programmatically). BackgroundWorker is straightforward to use, there are a couple of examples at the Microsoft BackgroundWorker documentation. However, it is often misused, with a few programmers thinking it doesn't work. Usually because they don't really understand its correct operation.

Using the BackgroundWorker

Any intensive or slow code can be run by the BackgroundWorker off the DoWork event. However, the slow or intensive code must be self-contained and cannot access UI elements or other methods on the UI thread. To support this self-containment and communicate with the UI thread (to update the interface) the ProgressChanged event can be used. Finally, once the task called by the DoWork event has completed, the BackgroundWorker can fire another event, RunWorkerCompleted, to let the main program know the task has finished. Finally, if necessary, the background task can be cancelled using BackgroundWorker's CancelAsync method.

In this example the BackgroundWorker is going to count the English letter characters in a text file. This task has been chosen to illustrate the workings of a BackgroundWorker, a simple example of processing that can be moved to a BackgroundWorker. Though unless it is processing a hundred megabytes plus file, it still runs very fast on a modern computer.

(Why count the English letters in a file? Counting letter frequency is a technique used in forensic and cryptography applications. Here it is just used as a file processing example.)

BackgroundWorker with ProgressBar Example Code

As shown in the image at the start, the simple app allows a text file to be chosen (using a FileOpenDialog), and then analysed, with percentage progress shown, and messages added to a list (ListBox). A BackgroundWorker is available from the Toolbox and can be dropped onto a WinForm. Note, the code for the actual WinForm construction is not shown as it standard stuff. Download the Visual Studio solution in the backgroundworker-demo.zip file to have a look at all the source. Here is the main code:

using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.IO;

namespace WorkerTest
{
    public partial class WorkerTest : Form
    {
        //Count the frequency of different characters
        long LowercaseEnglishLetter = 0;
        long LowercaseNonEnglishLetter = 0;
        long UppercaseEnglishLetter = 0;
        long UppercaseNonEnglishLetter = 0;
        long EnglishLetter = 0;
        long Digit0to9 = 0;
        long Whitespace = 0;
        long ControlCharacter = 0;
        //Count the frequency of English letters
        long[] LetterFrequency=new long[26];

        public WorkerTest()
        {
            InitializeComponent();
        }
        #region Buttons and Listbox
        //Get the file
        private void ButChooseFile_Click(object sender, EventArgs e)
        {
            DialogResult result = FileDialog.ShowDialog();
            if (result == DialogResult.OK)
            {
                TxtFile.Text = FileDialog.FileName;
                ButGo.Enabled = true;
            }
        }
        //Process the file
        private void ButGo_Click(object sender, EventArgs e)
        {
            ButCancel.Enabled = true;
            //Clear previous results
            ButClear_Click(this, null);
            if (!File.Exists(TxtFile.Text))
            {
                AddMessage("The file " + TxtFile.Text + " does not exist.");
            }
            else
            {
                AddMessage("Starting letter analysis...");
                if (BgrdWorker.IsBusy != true)
                {
                    ButGo.Enabled = false;
                    // Start the asynchronous operation.
                    BgrdWorker.RunWorkerAsync(TxtFile.Text);
                }
            }
        }
        //Cancel the processing
        private void ButCancel_Click(object sender, EventArgs e)
        {
            if (BgrdWorker.WorkerSupportsCancellation == true)
            {
                // Cancel the asynchronous operation.
                BgrdWorker.CancelAsync();
            }
        }
        //Clear the data
        private void ButClear_Click(object sender, EventArgs e)
        {
            LstStatus.Items.Clear();
            Progress.Value = 0;
            LetterFrequency = new long[26];
            LowercaseEnglishLetter = 0;
            LowercaseNonEnglishLetter = 0;
            UppercaseEnglishLetter = 0;
            UppercaseNonEnglishLetter = 0;
            EnglishLetter = 0;
            Digit0to9 = 0;
            Whitespace = 0;
            ControlCharacter = 0;
        }
        //User feedback in listbox
        int AddMessage(string MessageToAdd)
        {
            //Limit number of items
            if (LstStatus.Items.Count >= 60000)
                LstStatus.Items.RemoveAt(0);
            int ret = LstStatus.Items.Add(MessageToAdd);
            //ensure new item is visible
            LstStatus.TopIndex = LstStatus.Items.Count - 1;
            return ret;
        }
        #endregion
        #region  BackgroundWorker
        private void BgrdWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            string file;            //name of file to analyse
            long fileLength;        //store total number bytes to process
            long bytesProcessed;    //Count the characters processed
            int nextChar;           //stores each char to analyse
            int progress;           //percentage for progress reporting
            BackgroundWorker worker = sender as BackgroundWorker;   //who called us

            try
            {
                //get the file to process
                file = (string)e.Argument;
                //How many bytes to process?
                fileLength = (new FileInfo(TxtFile.Text)).Length;
                bytesProcessed = 0; //none so far
                // Create an instance of StreamReader to read from file
                // The using statement also closes the StreamReader
                using (StreamReader sr = new StreamReader(file))
                {
                    //until end of the file
                    while((nextChar = sr.Read()) != -1)
                    {
                        //has the operation been cancelled
                        if (worker.CancellationPending == true)
                        {
                            e.Cancel = true;
                            break;
                        }
                        else
                        {
                            //Now process the character
                            AnalyseChar((char)nextChar);
                            bytesProcessed += 1;
                            //Report back every 100000 chars
                            if (bytesProcessed % 100000 == 0)
                            {
                                //report progress
                                //actual percentage calculated on number of processed bytes
                                progress=(int)Math.Ceiling(((float)bytesProcessed / fileLength) * 100);
                                worker.ReportProgress(progress, bytesProcessed);
                            }
                        }
                    }
                    e.Result = bytesProcessed;
                }
            }
            catch (Exception ex)
            {
                throw new Exception ("Error analysing text file: " + ex.ToString());
            }
        }
        //Inform user of pregress
        private void BgrdWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            AddMessage("Processed: " + ((long)e.UserState).ToString() + " bytes");
            Progress.Value = e.ProgressPercentage;
            LblPercent.Text = Progress.Value.ToString() + "%";
        }
        //Finished the processing
        private void BgrdWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            ButCancel.Enabled = false;
            ButGo.Enabled = true;
            if (e.Cancelled == true)
            {
                AddMessage("Analysis aborted.");
            }
            else if (e.Error != null)
            {
                AddMessage("Analysis error: " + e.Error.Message);
            }
            else
            {
                //100% completed
                Progress.Value = 100;
                LblPercent.Text = "100%";
                //Print results
                AddMessage("Analysis completed, bytes processed: " + ((long)e.Result).ToString());
                if (LowercaseEnglishLetter > 0)
                    AddMessage("Total Lowercase English Letters=" + LowercaseEnglishLetter.ToString());
                if (UppercaseEnglishLetter > 0)
                    AddMessage("Total Uppercase English Letters=" + UppercaseEnglishLetter.ToString());
                if (LowercaseNonEnglishLetter > 0)
                    AddMessage("Total Lowercase Non-English Letters=" + LowercaseNonEnglishLetter.ToString());
                if (UppercaseNonEnglishLetter > 0)
                    AddMessage("Total Uppercase Non-english Letters=" + UppercaseNonEnglishLetter.ToString());
                if (Digit0to9 > 0)
                    AddMessage("Total Digits=" + Digit0to9.ToString());
                if (Whitespace > 0)
                    AddMessage("Total Whitespace=" + Whitespace.ToString());
                if (ControlCharacter > 0)
                    AddMessage("Total Control Characters=" + ControlCharacter.ToString());
                AddMessage("");
                //Show frequency of english letters
                if (EnglishLetter > 0)
                {
                    AddMessage("Total number of English letters:" + EnglishLetter.ToString());
                    double LetterPercentage;
                    string PrintResult;
                    for (int i = 0; i < 26; i++)
                    {
                        LetterPercentage = ((double)LetterFrequency[i] / EnglishLetter) * 100.0;
                        PrintResult = ((char)(i + 65)).ToString();
                        for (int j = 0; j < Math.Round(LetterPercentage); j++)
                            PrintResult += "-";
                        AddMessage(PrintResult + " " + LetterPercentage.ToString("n3") + "%");
                    }
                }
            }
        }
        //Analyse a single character
        void AnalyseChar(char Character)
        {
            if (char.IsLower(Character))
            {
                if (Character >= 'a' && Character <= 'z')
                {
                    LowercaseEnglishLetter++;
                    LetterFrequency[Character - 'a']++;
                }
                else
                {
                    LowercaseNonEnglishLetter++;
                }
            }
            else if (char.IsUpper(Character))
            {
                if (Character >= 'A' && Character <= 'Z')
                {
                    UppercaseEnglishLetter++;
                    LetterFrequency[Character - 'A']++;
                }
                else
                {
                    UppercaseNonEnglishLetter++;
                }
            }
            else if (char.IsDigit(Character))
            {
                Digit0to9++;
            }
            else if (char.IsWhiteSpace(Character))
            {
                Whitespace++;
            }
            else if (char.IsControl(Character))
            {
                ControlCharacter++;
            }
            EnglishLetter = LowercaseEnglishLetter + UppercaseEnglishLetter;
        }
        #endregion
    }
}

Running the BackgrounderWorker Example

Unless you have a tens of megabytes plus text file, the processing will be very quick. (If you want some big text files an Internet search will reveal several sources). Due to the fast processing the ProgressBar update lags behind the file processing, the UI is slow to update (to improve that aspect the number of characters processed before calling ReportProgress can be increased). To see the processing and ProgressBar update in action on smaller files slow down the processing by getting the background thread to wait a little longer:

//Report back every 100000 chars
if (bytesProcessed % 100000 == 0)
{
    //Add this line to slow the processing
    System.Threading.Thread.Sleep(100);

    //report progress
    .
    .
    .

Once the code for this BackgroundWorker example has been examined it will be seen that it is a useful template for other intensive or slow processing tasks that a C# program will need to do. Get code in the backgroundworker-demo.zip file or from GitHub.

English Letter Frequency

See Also

  • For a full list of the articles on Tek Eye see the website's index.

Author:  Published:  

ShareSubmit to TwitterSubmit to FacebookSubmit to Google+Submit to LinkedInSubmit to redditPrint Page

markdown CMS Small Logo Icon ↓markdown↓ CMS is fast and simple. Build websites quickly and publish easily. For beginner to expert.



Articles on:

Android Programming and Android Practice Projects, HTML, VPS, Computing, IT, Computer History, ↓markdown↓ CMS, C# Programming, Using Windows for Programming



Free Android Projects and Samples:

Android Examples, Android List Examples, Android UI Examples



Maintain your PC with Piriform:

CCleaner Professional
CCleaner for Mac
Recuva Professional
Defraggler Professional
Speccy Professional

Shop at Tek Eye for PC utilities