Background
For those of you who remember Visual Basic, that was a time when you generally did not need to worry too much about threads and the user interface. When you pressed a button, the display would update based on what was happening (you might need to add some code to update the interface, but that was just a simple statement). Migrating to Visual Basic.NET came as a bit of a shock, when you clicked on a button, the code behind it hogged the resources and the display would freeze (and in extreme cases through up a warning about needing to be pumped or something).
This was because the user interface was under the programs control, and by default the thread that ran the UI was also the thread that ran the code behind your button.
This was fine for simple programs that did something with no user interaction (you set it up, pressed the go button, it did what it was supposed - sometimes - to do and returned a result). It was easier on the user than command line, but there was little feedback (no progress bars etc.).
For that you needed to venture into Threads. Threads appear to be similar to a processor running your code. You have the thread created to run your program, if it is busy calculating something, it does not have time to do anything else. If you want your user interface to show progress or allow you to stop your calculation, you need to create and run your calculation in another thread.
Originally Visual Studio provided a very limited framework to use threads - you could:
- Create a thread based on a method on an instance of an object
- Start a thread
- Check if it was running
Everything else you had to roll yourself. This is where Events came in.
The Event is part of the Object Orientated Programming model. An object provides Events that other objects (code) can listen out for and respond to. It provides a richer collection of options than the Interrupt model used in single thread processors, not only signalling for an interaction but passing complex data. Additionally, multiple threads can listen for the same event.
In C# there is a slight problem - the user interface thread is solely responsible for (oddly enough) the user interface, but the event response is on a separate thread. This means that any events destined for the user interface has to be sent through a delegate which invokes the call on the user interface thread.
Thread handling has been improved over time, there are now background worker threads which handle a lot of the hard work involved but do not offer the full flexibility of controlling your threads and events directly.
The following describes a test program and the required code. Some of it could have been dealt with using background worker threads.
Form
The form uses the following controls:
- Tool Strip (located at the top)
- Add a Tool Strip label, set the text to Threads
- Add a Tool Strip Combo Box. Set the text to 1, add items (one per line) 1, 2, 5, 10
- Add a button to the Tool Strip, set the text to Start and the DisplayType to Text
- Status Strip (located at the bottom). Remember to set ShowItemToolTip to true
- Panel located between the tool strip and the status strip. Set Dock to Fill.
- Split Container added to the Panel.
- Text Box named txtMessage added to the right hand split panel, Multi-Line, Vertical Scroll Bar and Dock set to Fill
Code
using System;
using System.Threading;
using System.Windows.Forms;
namespace eventtest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
// Array for instances of the Worker class
private Worker[] workers;
// Array for the matching threads
private Thread[] workerThreads;
private void toolStripButton1_Click(object sender, EventArgs e)
{
// get the number of threads from the combo box
int threads = int.Parse(tsbThreads.Text);
// Create the progress bars for the number of threads
CreateProgress(threads);
// Resize the arrays
workers = new Worker[threads];
workerThreads = new Thread[threads];
// for each intended thread
for (int ix = 0; ix < threads; ix++)
{
// Create a new Worker instance
workers[ix] = new Worker(ix, 30);
// Create a new thread
workerThreads[ix] = new Thread(workers[ix].Start);
// Connect the events to the event handlers
workers[ix].ProcessProgress += HandleProgress;
workers[ix].ProcessComplete += HandleComplete;
}
// Start each thread in turn
for (int ix = 0; ix < threads; ix++)
{
workerThreads[ix].Start();
}
}
// Event handler for a thread reaching completion
private void HandleComplete(object sender, CompleteEventArgs e)
{
SetComplete(e);
}
// Delegate for SetComplete
private delegate void SetCompleteDelegate(CompleteEventArgs e);
// If the call to this method is not on the UI thread, invoke it
private void SetComplete(CompleteEventArgs e)
{
// Check if this call is not on the UI thread
if (txtMessages.InvokeRequired)
{
// Invoke the delegate
var d = new SetCompleteDelegate(SetComplete);
Invoke(d, new object[] { e });
}
else
{
// Add the message to the message text box
txtMessages.Text = $"{txtMessages.Text}Thread {e.ID} {e.Message} {Environment.NewLine}";
}
}
// Event handling for thread progress
private void HandleProgress(object sender, ProgressEventArgs e)
{
SetProgress(e);
}
// Delegate for SetProgress
private delegate void SetProgressDelegate(ProgressEventArgs e);
// If the call to this method is not on the UI thread, invoke it
private void SetProgress(ProgressEventArgs e)
{
// Check if this call is not on the UI thread
if (txtMessages.InvokeRequired)
{
// Invoke the delegate
var d = new SetProgressDelegate(SetProgress);
Invoke(d, new object[] { e });
}
else
{
// Find the progress trip for updating
var x = (ToolStripProgressBar)statusStrip1.Items[$"Progress{e.ID.ToString("000")}"];
// Set the percentage
x.Value = e.Percentage;
// Set the tool tip
x.ToolTipText = e.Message;
}
}
private void CreateProgress(int Threads)
{
// Clear all existing status strip items
statusStrip1.Items.Clear();
// For each thread
for (int ix = 0; ix < Threads; ix++)
{
// Create a tool strip progress bar
ToolStripProgressBar newItem = new ToolStripProgressBar($"Progress{ix.ToString("000")}");
// Set the minimum and maximum values - this will be the percentage completion
newItem.Minimum = 0;
newItem.Maximum = 100;
// Add the progress bar to the status strip
statusStrip1.Items.Add(newItem);
}
}
}
// This class is a test class to show how progress and completion events can be passed to the UI
public class Worker
{
// Thread ID
private int ID;
// Count items
private int Things;
public Worker(int id, int things)
{
ID = id;
Things = things;
}
// Start the processing
public void Start()
{
// This example just runs through a number of cycles with a set period wait
// and raises an event each cycle showing the progress
for (int ix = 1; ix <= Things; ix++)
{
Thread.Sleep(500);
// Raise an event
Progress(new ProgressEventArgs(ID, (ix*100)/Things, $"{(ix * 100) / Things}%"));
}
// Raise an event indicating the thread has completed
Complete(new CompleteEventArgs(ID, "Process complete"));
}
// Raise an event showing progress, passing a ProgressEventArgs object
protected virtual void Progress(ProgressEventArgs e)
{
EventHandler<ProgressEventArgs> handler = ProcessProgress;
if (handler != null)
{
handler(this, e);
}
}
// Raise an event indicating the process has completed
protected void Complete(CompleteEventArgs e)
{
EventHandler<CompleteEventArgs> handler = ProcessComplete;
if (handler != null)
{
handler(this, e);
}
}
// Public event definition
public event EventHandler<ProgressEventArgs> ProcessProgress;
public event EventHandler<CompleteEventArgs> ProcessComplete;
}
// Process completion argument object
public class CompleteEventArgs : EventArgs
{
// Thread ID raising the event
public int ID;
// Completion message
public string Message;
// Constructor
public CompleteEventArgs(int id, string message)
{
ID = id;
Message = message;
}
}
// Process Progress argument object
public class ProgressEventArgs : EventArgs
{
// Thread ID that raised the event
public int ID;
// Process percentage
public int Percentage { get; set; }
// Process message
public string Message { get; set; }
// Constructor for progress argument
public ProgressEventArgs(int id,int percentage, string message)
{
ID = id;
Percentage = percentage;
Message = message;
}
}
}
Results
When the program runs, the form is displayed. Select the number of threads from the drop down list (or type it in) Click on Start.
This program is running with five threads. There are five progress bars, and the hover over tells you the percentage. Note that the form needed to be widened to display the five progress bars, they have a default width and the original width only displays four of the bars.
Setting the progress bars width such that all are displayed is left as an exercise for the reader (remember the bars will need to be resized if the enclosing form is resized).