C# BackgroundWorker實現WinForm非同步操作的例子
在最近的一個Smart Client專案中,為了演示非同步操作的實現,寫了一個基於BackgorundWorker的例子。由於這個理基本上實現了BackgorundWorker的大部分功能:非同步操作的啟動、操作結束後的回撥、非同步操作的撤銷和進度報告等等。儘管沒有太多的技術含量,姑且放上來與大家分享。
1、場景描述
下面是程式執行時的截圖。本程式模擬這樣的一個場景:有兩組相互獨立的資料需要逐條獲取和顯示,左邊和右邊兩個groupbox分別代表基於這兩組資料的操作,由於他們完全獨立,因此可以並行執行。當點選Start按鈕,以非同步的方式從儲存介質中逐條獲取資料,並將獲取的資料追加到對應的ListBox中,ProgressBar真實反映以獲取的資料條數和總記錄條數的百分比,同時,當前獲取的條數也會在下方的Label上隨著操作的繼續而動態變化。此外通過點選Stop按鈕,可以中止掉當前的操作。當操作被中止後,ProgressBar和Label反映中止的那一刻的狀態。
2、程式碼實現
由於介面上左右兩邊是兩個互不干擾、相互獨立的操作,所以分別建立了兩個BackgroundWorker元件來負責(如下圖:backgroundWorkerLeft和backgroundWorkerRight)。
將兩個BackgroundWorker的WorkerReportsProgress和WorkerSupportsCancellation設為true。
我們假設獲取的記錄數固定,我們為此定義一個常量:
private static int MaxRecords = 100;
下面是左邊Start按鈕的Click event handler:
private void buttonStartLeft_Click(object sender, EventArgs e)
{
if (this.backgroundWorkerLeft.IsBusy)
{
return;
}
this.listBoxLeft.Items.Clear();
this.backgroundWorkerLeft.RunWorkerAsync(MaxRecords);
this.buttonStartLeft.Enabled = false;
this.buttonCacnelLeft.Enabled = true;
}
當Start按鈕被點選後,RunWorkerAsync方法被掉呼叫,我們定義的常量(MaxRecords )當作引數被摻入。隨後,將會觸發其DoWork事件,Dowork event handler處理程式碼如下:
private void backgroundWorkerLeft_DoWork(object sender, DoWorkEventArgs e)
{
try
{
e.Result = this.RetrieveData(this.backgroundWorkerLeft, e);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
throw;
}
}
呼叫RetrieveData方法逐條獲取資料。注意該方法的兩個引數:BackgroundWorker和DoWorkEventArgs 物件,返回值是返回資料的數量。由於在buttonStartLeft_Click中,我們將常量MaxRecords 作為引數傳入了BackgroundWorker的RunWorkerAsync方法, 此時的e.Argument = MaxRecords。之所以要將這兩個引數傳入RetrieveData()方法,是因為該方法是為兩個BackgroundWorker服務的,需要通過引數來區別當前是哪個BackgroundWorker。我們再來看看RetrieveData方法的定義:
private int RetrieveData(BackgroundWorker worker, DoWorkEventArgs e)
{
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, new KeyValuePair<int,string>(i,Guid.NewGuid().ToString()));
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便於傳入一些額外的引數。在介面上,由於資料的當前數量需要實時地顯示,而記錄也是現取現加(取出一條就在ListBox上追加)。所以制定一個KeyValuePair<int,string>物件作為第二個引數。其中Key為當前記錄數,Value是一個Guid,代表取出的資料。
ReportProgress的呼叫將會導致ProgressChanged事件被觸發。ProgressChanged event handler用於顯示當前進度、當前記錄數量和顯示獲取的紀錄:
private void backgroundWorkerLeft_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
KeyValuePair<int,string> record = (KeyValuePair<int,string>) e.UserState ;
this.labelResultLeft.Text = string.Format("There are {0} records retrieved!", record.Key);
this.progressBarLeft.Value = e.ProgressPercentage;
this.listBoxLeft.Items.Add(record.Value);
}
注:這些操作需要操作UI上的控制元件,只能在Main Thread中進行。如何在RetrieveData方法進行的話,由於該方式是一個非同步方法,是會丟擲異常的。
由於操作的時間可能無法預知,在長時間不能完全獲取資料的情況下,使用者可以需要手工結束掉當前的操作。這個操作實現在Stop按鈕的Click事件中:
private void buttonCacnelLeft_Click(object sender, EventArgs e)
{
this.backgroundWorkerLeft.CancelAsync();
}
如何操作正常地結束,BackgroundWorker的RunWorkerCompleted會被觸發,下面是RunWorkerCompleted
event handler的定義:
private void backgroundWorkerLeft_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
try
{
this.labelResultLeft.Text = string.Format("Total records: {0}", e.Result);
this.buttonStartLeft.Enabled = true;
this.buttonCacnelLeft.Enabled = false;
}
catch (TargetInvocationException ex)
{
MessageBox.Show(ex.InnerException.GetType().ToString());
}
}
上面介紹的是介面左邊功能的實現,右邊部分的實現完全一致。幹興趣的朋友可以參考Source Code.