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.
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.
See Also
- For a full list of the articles on Tek Eye see the website's index.
Author:Daniel S. Fowler Published: