65.9K
CodeProject 正在变化。 阅读更多。
Home

HiRes Backgroundworker

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2014 年 8 月 18 日

CPOL

6分钟阅读

viewsIcon

18963

downloadIcon

842

复制粘贴 BackgroundWorker。

Screenshot

引言

这在某种程度上是对 Background Cycle Timing Utility 的更新;但实际上,它是一个不同的工具。我仍然使用了微软的同一个基本 High Resolution Timing Function。在稍微清理了一下计时器代码后,我想要一个我可以轻松复制粘贴的简单后台程序。这个愿望的结果是一个 BackgroundWorker 继承类,它利用了一个比那篇文章中更广泛的 HiResTimer 类(由 HiResEventArgs 继承)。HiResBackgroundWorker 类维护了最新的 HiResEventArgs 的一个副本,这些副本从 DoWork Event Handler 传递到 ProgressChanged 和 RunWorkerCompleted Events,这样在工作完成后,工作的数据/状态/结果就保存在 HiResBackgroundWorker 类中,可以通过一系列只读属性访问。我将所有这些都放入了一个类库中,以便于包含。

背景

我为 HiResTimer 添加了通用的 Short, Medium 和 Long Status Line 方法,并实现了与 HiResBackgroundWorker 集成的暂停功能,可以在保持实际运行时间与总时间和生命周期不同的情况下,提供后台任务的暂停/恢复。这样做的原因是维护尽可能准确的剩余时间估算。如果需要,HiResBackgroundWorker 可以静默运行,并且可以在根本不需要 EventHandlers 的情况下获取状态(除了执行工作的 DoWork Event Handler)。所以,基本上,它可以是一个“无声”的 BackgroundWorker,我可以在它完成后时不时地启动并检查它。使用 List<HiResBackgroundWorker>,当出现任务时,可以添加它们,命名它们,并在它们完成后,可以提取数据并销毁 HiResBackgroundWorker。

继承的 BackgroundWorker 的大多数属性和方法都被隐藏了,这样我就可以控制它的设置和信息流。只有 RunWorkerCompleted Event 被实现为重写。整个过程实际上依赖于在正在进行的任何工作的主循环中采取三个动作。1. HiResEventArgs 的 .WillPause 属性需要从 HiResBackgroundWorker 的 .PauseRequested 属性进行赋值。2. HiResEventArgs 的 .IncrementCycle() 调用需要更新循环计数器和计时统计。3. HiResBackgroundWorker 的 .ProgressChanged() 调用需要被调用来报告进度,将当前 HiResEventArgs 的副本传递给 HiResBackgroundWorker 并响应 .PauseRequested。通常,如果 .WorkerReportsProgress 属性为 false,则不能调用 .ProgressChanged 调用,但由于它是一个隐藏属性,我可以维护一个通过该调用的通道。我就是这样实现了暂停功能的。所以,只要在 DoWork 工作循环中采取这三个动作,就可以通过 HiResBackgroundWorker 保持暂停/恢复控制、循环计时、计时统计以及最新的 UserState 和 Result 详细信息。

使用代码

出于演示目的,第一步是声明一个窗体级别的 HiResBackgroundWorker 对象,以便查询其状态和数据。然后,创建 HiResBackgroundWorker,指定是否触发 ProgressChanged Events 以及(至少)包含 DoWork Event 的 EventHandler 函数。接下来,创建一个 HiResEventArgs 对象,该对象将在启动 BackgroundWorker 时作为参数。由于 HiResEventArgs 继承了 HiResTimer,因此在创建时可以提供一些计数选项,如果这些选项是已知的。在其一般的预期格式中,它会与传递给 DoWork 的 Argument 对象和您的工作循环将执行的总项数(同样,这可能未知)一起创建。最后,使用 HiResEventArgs 启动工作线程。

这些步骤和 HiResBackgroundWorker 的其他控制函数可以放在同一个函数中进行演示。但实际上,它们将只是从使用和检查它创建的后台任务的代码中调用。当您的应用程序只有一个状态标签和一个进度条,并且要报告的任务基于用户正在查看的屏幕/选项卡时,.ToggleReporting() 方法(或更改 .WorkerReportsProgress 属性)可能很有用。

// 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
}

创建 HiResBackgroundWorker 时指定的 HiResWorkerEventHandler 如下所示。同样,假设您知道作业需要多少次迭代;这是 EventHandler 的框架。如果要完成的工作不是基于计数而是基于某个其他完成条件,则“while”子句将必须评估您的完成条件。此示例涵盖了接收所有三个 Events(DoWork、ProgressChanged 和 RunWorkerCompleted);但在许多情况下,这是过度使用了。

// 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
}

更精简且常用的使用方式是从您正在运行的任何主控制程序中进行,该程序负责管理整体工作。在从用户那里收集了某种数据后,可以启动一个 HiResBackgroundWorker 来完成不需要用户交互的任务。您的主控制程序将密切关注后台进程的完成、错误等。如果您不使用“主控制程序”,也可以通过一个 Timer 每隔一段时间触发一次,以便您检查后台进程。

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
}

DoWorkFunction 只期望 DoWorkEventArgs,因为 HiResBackgroundWorker 是用只分配了 DoWork Event Handler 的方式启动的。更改 .WorkerReportsProgress 或调用 .ToggleReporting() 将不会有任何效果,因为没有为 ProgressChanged Event(或 RunWorkerComplete Event)分配处理程序。这是我能接受的复制粘贴的级别。通常只需要处理两个区域。我需要 1. 编写实际工作的代码,2. 向“while”子句添加任何额外的退出条件。根据工作内容,可能还有其他几个区域:1. 添加代码(如适用)为 .WillPause 条件做准备;2. 添加代码处理 .WasPaused 条件(如适用);3. 创建任何非标准但实际上不是导致“try”块失败的异常的自定义异常。

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 
    }
}

关于此类活动的一个注意事项是,如果您在后台进行了大量的操作,并且每毫秒发送 ProgressChanged Events 并更新 ListViews、DataGridViews 和进度的图形表示,消息队列可能会被挂起的(尚未传递的)ProgressChanged 消息填满。BackgroundWorker 会完成,但进度状态只会反映队列有时间和资源发送的最后一个 ProgressChanged Event。它最终会赶上,但如果用户决定退出程序,那可能就太晚了。您正在更新的控件(如进度条)可能在您完成使用它们之前就被销毁了。处理此问题的方法有很多,但我使用的是忽略除 null 和强制执行之外的所有内容。这只比无知和强制执行略有不同。;)

// 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

 

关注点

我学到了很多关于重写和隐藏之间的区别。以及两者之间顺序的微小差异如何影响有效载荷状态。

历史

这在某种程度上是对 Background Cycle Timing Utility 的更新;但实际上,它是一个不同的工具。

© . All rights reserved.