Introduction
This is somewhat of an update to the Background Cycle Timing Utility; but realistically, it's a different utility. I sill used the same basic High Resolution Timing Function from Microsoft. After cleaning up the timer code a bit, I wanted to have a simple backgrounder that I could easily cut and paste. The result of this desire is a BackgroundWorker inheriting class utilizing a little more extensive HiResTimer class (Inherited by HiResEventArgs) than was in that article. The HiResBackgroundWorker class maintains a copy of the latest HiResEventArgs as they pass though from the DoWork Event Handler to the ProgressChanged and RunWorkerCompleted Events so that after the work is done, the data/state/result of the work is in the HiResBackgroundWorker class, available via a number of read-only properties. I put this all in a class library for easy inclusion.
Background
I've added generic Short, Medium and Long Status Line Methods to the HiResTimer, as well as implemented pause functionality that integrates with the HiResBackgroundWorker, providing background task pause/resume while maintaining the actual running time as different from its total time and its lifetime. The reasoning behind this was to maintain as acurate a remaing time estimate as possible. The HiResBackgroundWorker can be run quiet and status can be obtained from it without necessity of EventHandlers at all, if desired (except the DoWork Event Handler doing the work). So, basically, it can be a One-Function "Silent" BackgroundWorker that I can launch and check in on every now and then until it completes. With a List<HiResBackgroundWorker>, as tasks come up, they can be added, named, and as they are complete, the data can be extracted and the HiResBackgroundWorker destroyed.
Most of the Properties and Methods of the Inherited BackgroundWorker are hidden so I can maintain control over it's settings and flow of information. Only the RunWorkerCompleted Event is implemented as an override. The whole thing really pivots on three actions being taken in the main loop of whatever work is being done. 1. The .WillPause property of the HiResEventArgs needs to be assigned from the .PauseRequested property of the HiResBackgroundWorker. 2. The .IncrementCycle() of the HiResEventArgs call needs to update the loop counte and timing stats. And 3. the .ProgressChanged() of the HiResBackgroundWorker call needs to be made to Report Progress, hand a copy of the current HiResEventargs to the HiResBackgroundWorker and responsd to .PauseRequested. Normally, the .ProgressChanged call can't be made if .WorkerReportsProgress property is false, but because it's a hidden property I can maintain a channel through that call. I've been able to implement the Pause functionality this way. So as long as these three actions are taken in the DoWork work loop, pause/resume control, cycle timing, timing stats and up-to-date UserState and Result details can be kept by the HiResBackgroundWorker.
Using the code
For demonstration purposes, the first step is to declare a form-level HiResBackgroundWorker object that will be accessible for querying regarding its status and data. Then, create the HiResBackgroundWorker, specifying whether or not to fire ProgressChanged Events and the EventHandler function where (at a minimum) the DoWork Event will go. Next, create a HiResEventArgs object that will act as the argument when the BackgroundWorker is started. Since HiResEventArgs inherits the HiResTimer, there are some counting options that can be supplied on creation if they are known at that time. In it's general intended format, it is created with the Argument object that will be passed to DoWork and the TotalCount of items that your work loop will perform (again, this may not be known). Finally, Start the worker with the HiResEventArgs.
These steps and the other control functions for the HiResBackgroundWorker can be put in the same function for demonstration purposes. But really, they will just be called from the code that is utilizing and checking in on the background task(s) it has created. The .ToggleReporting() method (or changing the .WorkerReportsProgress property) can be useful when your application has a single status label and progress bar and the task to be reported on is based on the context of what screen/tab the user is looking at.
private void workerControl(object sender, EventArgs e)
{
#region Start/Stop
if (sender.ToString() == "Start" && (this.hiResWorker == null || this.hiResWorker.CanStart))
{
if (this.hiResWorker != null) this.hiResWorker.Dispose(); this.hiResWorker = new HiResBackgroundWorker(true, HiResWorkerEventHandler);
this.hiResWorker.PauseReportRate = new TimeSpan(0, 0, 1); HiResEventArgs newArgs = new HiResEventArgs(33); this.hiResWorker.Start(newArgs); }
else if (this.hiResWorker == null)
System.Diagnostics.Debug.Print("Not Available"); else if (sender.ToString() == "Stop")
this.hiResWorker.Stop(); #endregion
#region Pause/Resume
else if (sender.ToString() == "Pause")
this.hiResWorker.Pause(); else if (sender.ToString() == "Resume" || (sender.ToString() == "Start" && this.hiResWorker.IsPaused))
this.hiResWorker.Resume(); #endregion
#region Reporting Control
else if (sender.ToString() == "Reporting On")
this.hiResWorker.WorkerReportsProgress = true; else if (sender.ToString() == "Reporting Off")
this.hiResWorker.WorkerReportsProgress = false; else if (sender.ToString() == "Toggle Reporting")
this.hiResWorker.ToggleReporting();
#endregion
#region Status
else if (sender.ToString().StartsWith("Status"))
{
if (sender.ToString() == "Status Short")
MessageBox.Show(this, this.hiResWorker.StatusShort, Application.ProductName,
MessageBoxButtons.OK, MessageBoxIcon.Information);
else if (sender.ToString() == "Status Medium")
MessageBox.Show(this, this.hiResWorker.StatusMedium, Application.ProductName,
MessageBoxButtons.OK, MessageBoxIcon.Information);
else if (sender.ToString() == "Status Long")
MessageBox.Show(this, this.hiResWorker.StatusLong, Application.ProductName,
MessageBoxButtons.OK, MessageBoxIcon.Information);
else
MessageBox.Show(this, this.hiResWorker.StatusMultiline, Application.ProductName,
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
#endregion
}
The HiResWorkerEventHandler specified when creating the HiResBackgroundWorker is illustrated below. Again, assuming you know how many iterations the job will take; this is the shell of the EventHandler. If the job to be done is not based on a count but based on some other condition of completion, the "while" clause would have to evaluate your completion condition. This example covers receiving all three Events (DoWork, ProgressChanged and RunWorkerCompleted); but in many cases this is overkill.
private void HiResWorkerEventHandler(object sender, EventArgs e)
{
HiResBackgroundWorker bw = sender as HiResBackgroundWorker;
HiResEventArgs hrea; #region DoWork Event Handler
if (e.GetType() == typeof(System.ComponentModel.DoWorkEventArgs))
{
hrea = (HiResEventArgs)((DoWorkEventArgs)e).Argument; try {
#region Setups
int blockSize = 1000; int.TryParse((hrea.Argument != null ? hrea.Argument.ToString() : ""), out blockSize);
hrea.RealUserState = "UserState For First Progress"; hrea.RealResult = "Result For First Progress"; hrea.ReportRate = new TimeSpan(0, 0, 1); Int64 loopTotal = (hrea.IsUsingTotal ? hrea.TotalCount : 1000);
bw.ReportProgress(hrea.InitTotalCount(loopTotal), HiResEventArgs.CopyOfHiResEventArgs(hrea)); #endregion
#region Work Loop
while (bw.CanContinue && hrea.CanContinue && true) {
System.Threading.Thread.Sleep(blockSize); hrea.RealUserState = "UserState For Progress"; hrea.RealResult = "Result For Progress";
#region (not optional) Cycle Counting, Reporting and Pause/Resume Request Handling
if (hrea.WillPause = bw.CanContinuePaused) ; bw.ReportProgress(hrea.IncrementCycle(), HiResEventArgs.CopyOfHiResEventArgs(hrea)); if (hrea.WasPaused) ; if (!hrea.IsUsingTotal && loopTotal > 0 && hrea.CycleCount >= 100)
hrea.InitTotalCount(loopTotal); #endregion
}
#endregion
}
#region Wrapups
catch (Exception ex)
{
hrea.Error = ex; }
finally
{
hrea.RealUserState = "UserState For Completion"; hrea.RealResult = "Result For Completion"; if (bw.StopRequested) hrea.Cancel = true; if (hrea.Error == null && true) hrea.Error = null; ((DoWorkEventArgs)e).Result =
HiResEventArgs.CopyOfHiResEventArgs(hrea); }
#endregion
}
#endregion
#region ProgressChanged Event Handler (if assigned and enabled)
else if (e.GetType() == typeof(System.ComponentModel.ProgressChangedEventArgs))
{
hrea = (HiResEventArgs)((ProgressChangedEventArgs)e).UserState; System.Diagnostics.Debug.Print(hrea.LastEvent.ToString() + ": " + hrea.CycleStatus(true));
}
#endregion
#region RunWorkerCompleted Event Handler (if assigned)
else if (e.GetType() == typeof(System.ComponentModel.RunWorkerCompletedEventArgs))
{
hrea = (HiResEventArgs)((RunWorkerCompletedEventArgs)e).Result; System.Diagnostics.Debug.Print(hrea.LastEvent.ToString() + ":\r\n" + hrea.CycleStatus(true));
if (hrea.Error != null && bw.WorkerReportsProgress)
MessageBox.Show(this, hrea.CycleStatus(true), Application.ProductName,
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
#endregion
}
The more streamlined and usual way to use this is from whatever Main Control Program you are running that is managing the overall work. After collecting some data of whatever sort from the user, a HiResBackgroundWorker can be fired off to complete tasks that don't require user interaction. Your Main Control Program will keep an eye on the background process for completion, error, etc. If you are not using a "Main Control Program" this could also be accomplished by a Timer firing off every now and then for you to check in on your background processes.
DataTable toBeUpdated = new DataTable();
private void MainControlProgram()
{
HiResBackgroundWorker newWorker = new HiResBackgroundWorker(DoWorkFunction);
HiResEventArgs newArgs = new HiResEventArgs(toBeUpdated, toBeUpdated.Rows.Count);
this.hiResWorker.Start(newArgs);
if (newWorker.IsCompleted)
; else if (newWorker.IsRunning)
; }
The DoWorkFunction is only expecting DoWorkEventArgs because the HiResBackgroundWorker was started up with only a DoWork Event Handler assigned. Changing .WorkerReportsProgress or calling .ToggleReporting() will have no effect, since there has been no handler assigned to the ProgressChanged Event (or RunWorkerComplete Event for that matter). This is a level of cut and paste I can live with. There will usually only be two areas that I have to address. I need to 1. write the code that will actually do the work and 2. add any extra exit conditions to the "while" clause. There may be an additional few areas, depending on the work being done: 1. add code (when applicable) to prepare for .WillPause condition; 2. add code for .WasPaused condition (when applicable); and 3. create any custom Exceptions for non-optimum exit conditions that are not actually Exceptions that will fail the "try" block.
private void DoWorkFunction(object sender, EventArgs e)
{
HiResBackgroundWorker bw = (HiResBackgroundWorker)sender; HiResEventArgs hrea = (HiResEventArgs)((DoWorkEventArgs)e).Argument;
try {
DataTable toBeUpdated = hrea.Argument as DataTable;
while (bw.CanContinue && hrea.CanContinue && true) {
if (hrea.WillPause = bw.CanContinuePaused) ; bw.ReportProgress(hrea.IncrementCycle(), HiResEventArgs.CopyOfHiResEventArgs(hrea)); if (hrea.WasPaused) ; }
}
catch (Exception ex)
{
hrea.Error = ex; }
finally
{
if (bw.StopRequested) hrea.Cancel = true; if (hrea.Error == null && true) hrea.Error = null; ((DoWorkEventArgs)e).Result =
HiResEventArgs.CopyOfHiResEventArgs(hrea); }
}
A note about this type of activity is that if you are really honking on something in the background and sending ProgressChanged Events every millisecond and updating ListViews, DataGridViews and Graphic representations of your progress;, the message queue can get filled up with pending (undelievered as yet) ProgressChanged messages. The BackgroundWorker will complete, but the progress status will only reflect the last ProgreesChanged Event that the queue has had the time and resources to send. It will eventually catch up; but that may be too late if the user decides to exit the program. Controls (like a progress bar) that your ProgressChanged Event are updating could get destroyed before you are actually finished with them. There are probably many ways of handling this, but ignoring everything except null and brute force is what I use. Which is only slightly different than ignorance and brute force. ;)
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (hiResWorker != null)
{
hiResWorker.WorkerReportsProgress = false; hiResWorker.Stop(); Application.DoEvents();
System.Threading.Thread.Sleep(250);
do
{
hiResWorker.Stop();
Application.DoEvents();
System.Threading.Thread.Sleep(250);
} while (hiResWorker.CanContinue);
hiResWorker.Dispose();
}
base.OnFormClosing(e);
}
Sean O'Leary
Points of Interest
I learned a lot about the difference between overriding something and hiding it. And how the slight difference between the sequence of the two can have an influence on the payload state.
History
This is somewhat of an update to the Background Cycle Timing Utility; but realistically, it's a different utility.