最近在项目中遇到一个这样的问题,要求点击计算的时候进度条随着计算的进行而改变,直至完成。但是这个地方点击计算按钮是调用的另一个类中的各个方法,无法精确地计算进度的增进情况。为此,颇费脑筋,最终使用BackgroundWorker组件进行实现。
1.描述
点击计算按钮的时候,后台大量数据进行计算,同时进度条增进,直至同时完成。
2.BackgroundWorker的属性,方法及原理
(参考:http://kb.cnblogs.com/a/1611918/)
BackgroundWorker类中主要用到的有这列属性、方法和事件:
重要属性:
1、CancellationPending获取一个值,指示应用程序是否已请求取消后台操作。通过在DoWork事件中判断CancellationPending属性可以认定是否需要取消后台操作(也就是结束线程);
2、IsBusy获取一个值,指示 BackgroundWorker 是否正在运行异步操作。程序中使用IsBusy属性用来确定后台操作是否正在使用中;
3、WorkerReportsProgress获取或设置一个值,该值指示BackgroundWorker能否报告进度更新
4、WorkerSupportsCancellation获取或设置一个值,该值指示 BackgroundWorker 是否支持异步取消。设置WorkerSupportsCancellation为true使得程序可以调用CancelAsync方法提交终止挂起的后台操作的请求;
重要方法:
1、CancelAsync请求取消挂起的后台操作
2、RunWorkerAsync开始执行后台操作
3、ReportProgress引发ProgressChanged事件
重要事件:
1、DoWork调用 RunWorkerAsync 时发生
2、ProgressChanged调用 ReportProgress 时发生
3、RunWorkerCompleted当后台操作已完成、被取消或引发异常时发生
另外还有三个重要的参数是RunWorkerCompletedEventArgs以及DoWorkEventArgs、ProgressChangedEventArgs。
BackgroundWorker的各属性、方法、事件的调用机制和顺序:
从上图可见在整个生活周期内发生了3次重要的参数传递过程:
参数传递1:此次的参数传递是将RunWorkerAsync(Object)中的Object传递到DoWork事件的DoWorkEventArgs.Argument,由于在这里只有一个参数可以传递,所以在实际应用往封装一个类,将整个实例化的类作为RunWorkerAsync的Object传递到DoWorkEventArgs.Argument;
参数传递2:此次是将程序运行进度传递给ProgressChanged事件,实际使用中往往使用给方法和事件更新进度条或者日志信息;
参数传递3:在DoWork事件结束之前,将后台线程产生的结果数据赋给DoWorkEventArgs.Result一边在RunWorkerCompleted事件中调用RunWorkerCompletedEventArgs.Result属性取得后台线程产生的结果。
另外从上图可以看到DoWork事件是在后台线程中运行的,所以在该事件中不能够操作用户界面的内容,如果需要更新用户界面,可以使用ProgressChanged事件及RunWorkCompleted事件来实现。
3.代码实现及详解
首先,创建一个BackgroundWorker组件,并设置WorkerSupportsCancellation和WorkerReportsProgress为true;
其次,我们设定一个常量MaxRecords = 100。点击计算按钮,执行下面的计算过程,RunWorkerAsync方法被调用,自定义参数被传入,触发DoWork事件,事件处理如下。
private void btnJS_Click(object sender, EventArgs e)
{
//业务处理
string strSQL = "";
DataTable dt = GetDataTable(strSQL);
if(dt.Rows.Count > 0)
{
if (dt.Rows[0][0].ToString().Trim() == "0")
{
this.btnJS.Enabled = true;
//错误提示
return;
}
else
{
if (this.backgroundWorkerJS.IsBusy)
{
return;
}
this.backgroundWorkerJS.RunWorkerAsync(MaxRecords);
this.btnJS.Enabled = false;
}
}
}
private void backgroundWorkerJS_DoWork(object sender, DoWorkEventArgs e)
{
try
{
e.Result = this.GetData(this.backgroundWorkerJS, e);
}
catch (Exception ex)
{
//异常处理
throw;
}
}
调用GetData方法去处理后台计算。
private int RetrieveData(BackgroundWorker worker, DoWorkEventArgs e)
{
//日期
string[] strArr = { this.cmbNF.SelectedValue.ToString(), this.cmbYF.SelectedValue.ToString() };
int maxRecords = (int)e.Argument;
int percent = 0;
for (int i = 1; i <= maxRecords; i++)
{
if (worker.CancellationPending)
{
return i;
}
percent = (int)(((double)i / (double)maxRecords) * 100);
worker.ReportProgress(percent, strArr);
Thread.Sleep(100);
}
return maxRecords;
}
通过e.Argument,获得最大数据获取量之后,进行一个for循环,在每次迭代中,如何worker.CancellationPending==true,代表异步操作被显示取消,则直接返回;否则,调用BackgroundWorker的ReportProgress方法。ReportProgress具有两个重载:
public void ReportProgress(int percentProgress);
public void ReportProgress(int percentProgress, object userState);
percentProgress代表当前进度,从0-100。userState便于传入一些额外的参数。这里需要传入年份和月份,以便进行相关的计算。所以定制了一个string类型数组。ReportProgress的调用将会导致ProgressChanged事件被触发。这个方法将会处理进度和数据计算。wnform自带了进度框控件ProgressBar,只要对它的属性Value进行赋值,就可以控制它的增长。
private void backgroundWorkerJS_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
string[] tempArr = (string[])e.UserState;
this.lblMsg.Text = string.Format("已完成{0}%,请稍候...", e.ProgressPercentage);
this.progressBarJS.Value = e.ProgressPercentage;
//复杂计算部分处理
}
最后,不管正常与非正常,只要结束,BackgroundWorker的RunWorkerCompleted就会被触发,可以进行一些结束释放资源的处理操作。
private void backgroundWorkerJS_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
try
{
//相关的一些处理
}
catch (TargetInvocationException ex)
{
//捕获异常
}
}
BackgroundWorker主要是利用各种EventArgs的参数传递来实现异步,比起直接新建一个线程来执行工作要方便简单。
4.深入讨论
(参考:http://www.cnblogs.com/inforasc/archive/2009/10/21/1587756.html;
http://www.cnblogs.com/net66/archive/2005/08/03/206132.html)
BackgroundWorker是微软开发出来的一个组件,它的开发原理还是基于线程,委托等方面。异步委托提供以异步方式调用同步方法的能力。当同步调用委托时,Invoke()方法直接对当前线程调用目标方法;当异步调用委托时,CLR将对请求进行排队并立即返回到调用方,将对来自线程池的线程调用该目标方法,提交请求的原始线程继续与目标方法并行执行,该目标方法是对线程池线程运行的。主要用到两个方法:
1)、BeginInvoke()方法
BeginInvoke()方法启动异步调用,它与需要异步执行的方法具有相同的参数。另外,还有两个可选参数:第一个参数是AsyncCallback委托,该委托引用在异步调用完成时要调用的方法;第二个参数是用户定义的对象,该对象可向回调方法传递信息;BeginInvoke立即返回,不等待异步调用完成;BeginInvoke返回IAsyncResult,这个结果可用于监视异步调用的进度;
2)、EndInvoke()方法
EndInvoke()方法检索异步调用的结果;在调用BeginInvoke()方法后,可以随时调用EndInvoke()方法,如果异步调用尚未完成,则EndInvoke()方法将一直阻止调用线程,直到异步调用完成后才允许调用线程执行;EndInvoke()的参数需要异步执行的方法的out和ref参数以及由BeginInvoke()返回的IAsyncResult。
委托实现上述情况,但是无法同步进行。这样处理的结果就是复杂计算处理完成后,才进行进度框的增长。给用户呈现“假死”的状态,然后是几乎一瞬间进度框完成。
private Thread fThread;
private delegate void SetPos(int ipos);
private void SetTextMessage(int ipos)
{
if (this.InvokeRequired)
{
this.Invoke(new SetPos(SetTextMessage), new object[] { ipos });
}
else
{
if (fThread.IsAlive)
{
this.lblMsg.Text = ipos.ToString() + "%";
this.progressBarJS.Value = Convert.ToInt32(ipos);
}
}
}
private void SleepT()
{
for (int i = 0; i < 500; i++)
{
System.Threading.Thread.Sleep(10);
SetTextMessage(100 * i / 500);
}
}
点击计算按钮时执行
private void btnJS_Click(object sender, EventArgs e)
{
fThread = new Thread(new ThreadStart(SleepT));
fThread.Start();
//复杂计算过程
fThread.Abort();
this.lblMsg.Text = "计算完成100%";
this.progressBarJS.Value = 100;
}
由此可以看出BackgroundWorker组件还是相对来说很好用的。