最近在項目中遇到一個這樣的問題,要求點擊計算的時候進度條隨着計算的進行而改變,直至完成。但是這個地方點擊計算按鈕是調用的另一個類中的各個方法,無法精確地計算進度的增進情況。爲此,頗費腦筋,最終使用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組件還是相對來說很好用的。