Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / Win32

HiRes Backgroundworker

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
20 Aug 2014CPOL6 min read 17.4K   725   15   4
Cut and Paste BackgroundWorker.

Screenshot

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.

C#
// The Controller Function with sender.ToString() values:
// "Start", "Stop", "Pause", "Resume", "Status", etc. 
private void workerControl(object sender, EventArgs e)
{
    #region Start/Stop
    if (sender.ToString() == "Start" && (this.hiResWorker == null || this.hiResWorker.CanStart))
    {
        // (optional) clear form progress controls from previous job 
        // if hiResWorker != null && hiResWorker.HasReportedProgress
        if (this.hiResWorker != null) this.hiResWorker.Dispose();   // dispose of the old memory 
        // enable/disable Progress Events and assign the same EventHandler to all Events
        this.hiResWorker = new HiResBackgroundWorker(true, HiResWorkerEventHandler);
        this.hiResWorker.PauseReportRate = new TimeSpan(0, 0, 1);   // set the report rate during pause
        // create HiResEventArgs with argument (ms per iteration in this case)
        HiResEventArgs newArgs = new HiResEventArgs(33);            // can add start(0) and total(1000)
        this.hiResWorker.Start(newArgs);                            // start with the HiResEventArgs
    }
    else if (this.hiResWorker == null)
        System.Diagnostics.Debug.Print("Not Available");    // can't do anythng but start a null worker
    else if (sender.ToString() == "Stop")
        this.hiResWorker.Stop();                            // response based on DoWork iteration time
    #endregion
    #region Pause/Resume
    else if (sender.ToString() == "Pause")
        this.hiResWorker.Pause();                           // response based on DoWork iteration time
    else if (sender.ToString() == "Resume" || (sender.ToString() == "Start" && this.hiResWorker.IsPaused))
        this.hiResWorker.Resume();                          // report based on DoWork iteration time  
    #endregion
    #region Reporting Control
    else if (sender.ToString() == "Reporting On")
        this.hiResWorker.WorkerReportsProgress = true;      // report based on DoWork iteration time  
    else if (sender.ToString() == "Reporting Off")
        this.hiResWorker.WorkerReportsProgress = false;     // Events may be in the message queue  
    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.

C#
// The Event(s) Handler
private void HiResWorkerEventHandler(object sender, EventArgs e)
{
    // cast the parameters into their respective objects
    HiResBackgroundWorker bw = sender as HiResBackgroundWorker;
    HiResEventArgs hrea;    // will be cast acording to the event type
    #region DoWork Event Handler
    if (e.GetType() == typeof(System.ComponentModel.DoWorkEventArgs))
    {
        hrea = (HiResEventArgs)((DoWorkEventArgs)e).Argument;   // get the HiResEventArgs
        try                                                     // (not optional) trap exceptions
        {
            #region Setups
            int blockSize = 1000;                               // example iteration time in ms
                                                                // (optional) pick up any non-null argument
            int.TryParse((hrea.Argument != null ? hrea.Argument.ToString() : ""), out blockSize);
            hrea.RealUserState = "UserState For First Progress";// (optional) init data for FirstProgress
            hrea.RealResult = "Result For First Progress";      // (optional) init data for FirstProgress
            hrea.ReportRate = new TimeSpan(0, 0, 1);            // (optional) 0 if critical progress data
                                                                // (not optional) some kind of loop control
            Int64 loopTotal = (hrea.IsUsingTotal ? hrea.TotalCount : 1000);
                                                                // (not optional) initial ReportProgress 
            bw.ReportProgress(hrea.InitTotalCount(loopTotal),   // (not optional) unless set when creating 
                    HiResEventArgs.CopyOfHiResEventArgs(hrea)); // (not optional) starting copy  
            #endregion
            #region Work Loop
            // bw.CanContinue checks cancel/stop; 
            // hrea.CanContinue checks counter (always true if hrea.TotalCount == 0)
            while (bw.CanContinue && hrea.CanContinue           // (not optional) bw state and counter 
            && true)                                            // other exit condition check(s)
            {
                //
                System.Threading.Thread.Sleep(blockSize);       // do whatever work intended here
                //
                hrea.RealUserState = "UserState For Progress";  // (optional) data for ProgressChanged 
                hrea.RealResult = "Result For Progress";        // (optional) data for ProgressChanged 
                //
                                                
                #region (not optional) Cycle Counting, Reporting and Pause/Resume Request Handling
                if (hrea.WillPause = bw.CanContinuePaused) ;    // close open connections/files, etc.
                bw.ReportProgress(hrea.IncrementCycle(),        // (not optional) counter, timing, pause 
                    HiResEventArgs.CopyOfHiResEventArgs(hrea)); // (not optional) latest copy  
                if (hrea.WasPaused) ;                           // re-open connections/files, etc.
                // if you now know what your loopTotal is, enable display of percentage and remaining time
                if (!hrea.IsUsingTotal && loopTotal > 0         /*DEBUG/TEST*/ && hrea.CycleCount >= 100)
                    hrea.InitTotalCount(loopTotal);             // gets called once if you have a loopTotal
                #endregion
            }
            #endregion
        }
        #region Wrapups
        catch (Exception ex)
        {
            hrea.Error = ex;                                    // return the error
        }
        finally
        {
            hrea.RealUserState = "UserState For Completion";    // (optional) data for RunWorkerCompleted
            hrea.RealResult = "Result For Completion";          // (optional) data for RunWorkerCompleted
            if (bw.StopRequested) hrea.Cancel = true;           // flag Cancel if StopRequested 
            if (hrea.Error == null && true) hrea.Error = null;  // custom .Error for non-optimum condition
            ((DoWorkEventArgs)e).Result =
                    HiResEventArgs.CopyOfHiResEventArgs(hrea);  // (not optional) final copy 
        }
        #endregion
    }
    #endregion
    #region ProgressChanged Event Handler (if assigned and enabled)
    else if (e.GetType() == typeof(System.ComponentModel.ProgressChangedEventArgs))
    {
        hrea = (HiResEventArgs)((ProgressChangedEventArgs)e).UserState; // get the HiResEventArgs
        System.Diagnostics.Debug.Print(hrea.LastEvent.ToString() + ": " + hrea.CycleStatus(true));
        // (optional) update form status label - hrea.CycleStatus(null=short, false=medium, true=long)
        // (optional) update form progress bar - hrea.Percentage; - hrea.IsUsingTotal ? .Blocks : .Marquee
    }
    #endregion
    #region RunWorkerCompleted Event Handler (if assigned)
    else if (e.GetType() == typeof(System.ComponentModel.RunWorkerCompletedEventArgs))
    {
        hrea = (HiResEventArgs)((RunWorkerCompletedEventArgs)e).Result; // get the HiResEventArgs
        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);
        // (optional) update form status label if bw.WorkerReportsProgress hrea.CycleStatus(false)
        // (optional) update form progress bar if bw.WorkerReportsProgress hrea.Percentage and .Blocks
        // use what you want from this.hiResWorker.RealUserState and this.hiResWorker.RealResult
    }
    #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.

C#
DataTable toBeUpdated = new DataTable();
private void MainControlProgram()
{
    // ... some other part of your process collected up DataTable toBeUpdated
    // only a DoWork Event Handler
    HiResBackgroundWorker newWorker = new HiResBackgroundWorker(DoWorkFunction);
    // argument and totalCount
    HiResEventArgs newArgs = new HiResEventArgs(toBeUpdated, toBeUpdated.Rows.Count);
    this.hiResWorker.Start(newArgs);    // start the worker

    // ...  at some other point in your main control program
    if (newWorker.IsCompleted)
        ;   // flag the next collection action is ok to start
    else if (newWorker.IsRunning)
        ;   // display the progress on a local control
    // move on to check other background tasks you've started
}

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.

C#
private void DoWorkFunction(object sender, EventArgs e)
{
    HiResBackgroundWorker bw = (HiResBackgroundWorker)sender;             // HiResBackgroundWorker
    HiResEventArgs hrea = (HiResEventArgs)((DoWorkEventArgs)e).Argument;  // HiResEventArgs

    try                                                       // trap exceptions
    {
        DataTable toBeUpdated = hrea.Argument as DataTable;   // get argument
        // then open connection(s), file(s), etc.

        while (bw.CanContinue && hrea.CanContinue             // bw state and counter < total
        && true)                                              // check connection(s) alive, etc.
        {
            // 
            // update each record from the table to the remote database
            // 

            if (hrea.WillPause = bw.CanContinuePaused) ;      // close down open connections, etc. 
            bw.ReportProgress(hrea.IncrementCycle(),          // counter, timing, stats and pause
                HiResEventArgs.CopyOfHiResEventArgs(hrea));   // send latest copy as e.UserState 
            if (hrea.WasPaused) ;                             // re-open connections, etc. 
        }
    }
    catch (Exception ex)
    {
        hrea.Error = ex;                                      // return the error
    }
    finally
    {
        if (bw.StopRequested) hrea.Cancel = true;             // flag Cancel if StopRequested
        if (hrea.Error == null && true) hrea.Error = null;    // .Error if lost connection, etc.
        ((DoWorkEventArgs)e).Result = 
                HiResEventArgs.CopyOfHiResEventArgs(hrea);    // final copy as e.Result 
    }
}

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. ;)

C#
// if you are opening files, databases or just really honking, shutdown nicely
protected override void OnFormClosing(FormClosingEventArgs e)
{
    if (hiResWorker != null)
    {
        // releive it of further Progress Events
        hiResWorker.WorkerReportsProgress = false;  // if applicable
        hiResWorker.Stop();                         // if applicable
        // give it a chance to empty itself again...// if applicable     
        Application.DoEvents();
        System.Threading.Thread.Sleep(250);
        do
        {
            hiResWorker.Stop();
            Application.DoEvents();
            System.Threading.Thread.Sleep(250);
        } while (hiResWorker.CanContinue);
        // do whatever is necessary with the data from the hiResWorker
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Absolute Genius
Canada Canada
Design and Develop Custom Electronics and Software to control them.
Software Engineering of custom Business Software System.
Hardware Engineering of Custom Electronics Control devices.

Comments and Discussions

 
BugMy vote of 1 Pin
Jackson Savitraz18-Aug-14 14:31
professionalJackson Savitraz18-Aug-14 14:31 
GeneralRe: My vote of 1 Pin
sgoleary18-Aug-14 16:18
sgoleary18-Aug-14 16:18 
GeneralRe: My vote of 1 Pin
sgoleary18-Aug-14 16:22
sgoleary18-Aug-14 16:22 
GeneralRe: My vote of 1 Pin
John B Oliver8-Sep-14 12:10
John B Oliver8-Sep-14 12:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.