需求分析
經常用到的耗時操作,例如:
1、文件下載和上載(包括點對點應用程序傳輸文件,從網絡下載文件、圖像等)
2、數據庫事務(從數據庫讀到大量的數據到WinForm界面中的DataGridview里呈現)
3、復雜的本地計算
4、本地磁盤文件訪問(讀寫文件,磁盤文件列表)
……
這些操作在長時間運行時會導致用戶界面 (UI) 處於停止響應狀態,用戶在這操作期間無法進行其他的操作,造成非常差的用戶體驗,為了不使UI層處於停止響應狀態,則可以使用 BackgroundWorker 類方便地解決這類問題。這個后台的線程處理,可以很好的實現常規操作的同時,還可以及時通知UI當前處理信息的進度等。
MSDN的介紹
BackgroundWorker是.NET Framework 里用來執行多線程任務的控件,它允許開發人員在一個單獨的線程上執行一些操作。耗時的操作(如下載和數據庫事務)在長時間運行時可能會導致用戶界面 (UI) 似乎處於停止響應狀態。 如果您需要能進行響應的用戶界面,而且面臨與這類操作相關的長時間延遲,則可以使用 BackgroundWorker 類方便地解決問題。
若要在后台執行耗時的操作,請創建一個 BackgroundWorker,偵聽那些報告操作進度並在操作完成時發出信號的事件。 可以通過編程方式創建 BackgroundWorker,也可以將它從“工具箱”的“組件”選項卡中拖到窗體上。 如果在 Windows 窗體設計器中創建 BackgroundWorker,則它會出現在組件欄中,而且它的屬性會顯示在“屬性”窗口中。
若要為后台操作做好准備,請添加 DoWork 事件的事件處理程序。 在此事件處理程序中調用耗時的操作。 若要開始此操作,請調用 RunWorkerAsync。 若要收到進度更新的通知,請處理 ProgressChanged 事件。 若要在操作完成時收到通知,請處理 RunWorkerCompleted 事件。
有2點需要注意的:
1、由於DoWork事件內部的代碼運行在非UI線程之上,確保在 DoWork 事件處理程序中不操作任何用戶界面對象。 而應該通過 ProgressChanged 和 RunWorkerCompleted 事件與用戶界面進行通信。
2、BackgroundWorker 事件不跨 AppDomain 邊界進行封送處理。 請不要使用 BackgroundWorker 組件在多個 AppDomain 中執行多線程操作。
最簡單示例
准備材料:一個耗時的操作
代碼如下,這個就不多解釋了:
int iSum = 0; private void button1_Click(object sender, EventArgs e) { for (int i = 0; i <= 100; i++) { iSum+=i; System.Threading.Thread.Sleep(300); } }
運行一下,拖動程序界面看看,直接卡死了,假死,一會兒,運算完了,就又可以拖動了。現在用BackgroundWorker來解決這個問題。
為此,我們新建一個WindowsForm命名為bgwA,拖入一個Label命名為lblPercent,一個ProgressBar命名為pgbPercent,一個Button命名為btnStart。
然后,代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace bgwA { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void BtnStart_Click(object sender, EventArgs e) { BackgroundWorker bgwA = new BackgroundWorker(); bgwA.WorkerReportsProgress = true; bgwA.DoWork += bgwA_DoWork; bgwA.ProgressChanged += bgwA_ProgressChanged; bgwA.RunWorkerCompleted += bgwA_Completed; bgwA.RunWorkerAsync(); } private void bgwA_DoWork(object sender, DoWorkEventArgs e) { var bgworker = sender as BackgroundWorker; for (int i = 0; i <= 100; i++) { bgworker.ReportProgress(i); System.Threading.Thread.Sleep(200); } } private void bgwA_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.pgbPercent.Value = e.ProgressPercentage; this.lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%"; } private void bgwA_Completed(object sender, RunWorkerCompletedEventArgs e) { this.lblPercent.Text = "后台操作結束(可能是程序100%完成,也可能是用戶取消或程序異常導致結束)。"; } } }
現在運行一下,點擊btnStart,進度條跑起來了,再拖動一下程序界面,這下不會沒有響應了,不卡,不假死了吧。good.
進階一:
重復上面說到的注意點:由於DoWork事件內部的代碼運行在非UI線程之上,確保在 DoWork 事件處理程序中不操作任何用戶界面對象。 而應該通過 ProgressChanged 和 RunWorkerCompleted 事件與用戶界面進行通信。
如何在程序運行途中取消正在進行的運算?
要實現在這個功能,我們首先在程序界面上添加一個可以隨時中止后台進程的按鈕BtnCancel,允許用戶在執行過程中取消當前的操作:
與WorkerReportsProgress屬性一樣,如果要支持取消操作我們需要設置 WorkerSupportsCancellation屬性為 true,還要在DoWork方法中進行支持。
接下來對上面的程序進行擴展一下,我們再新建一個WindowsForm命名為bgwB,這次,我們不再通過代碼來寫BackgroundWorker實例,我們通過從VS的工具箱中直接拉一個BackgroundWorker出來,然后從屬性窗口中命名為bgWorker,然后再拖入一個Label命名為lblPercent,一個ProgressBar命名為pgbPercent,一個Button命名為btnStart。
DoWork,ProgressChanged,RunWorkerCompleted三個事件都是通過在bgWorker1屬性窗口中雙擊出來的,下面看代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace bgwB { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { bgWorker1.WorkerReportsProgress = true; bgWorker1.WorkerSupportsCancellation = true; } private void BgWorker1_DoWork(object sender, DoWorkEventArgs e) { for (int i = 0; i <= 100; i++) { if (bgWorker1.CancellationPending == true) { e.Cancel = true; break; } else { bgWorker1.ReportProgress(i); System.Threading.Thread.Sleep(100); } } } private void BgWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.pgbPercent.Value = e.ProgressPercentage; this.lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%"; } private void BgWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { this.lblPercent.Text = "后台操作結束(可能是程序100%完成,也可能是用戶取消或程序異常導致結束)。"; } private void BtnStart_Click(object sender, EventArgs e) { bgWorker1.RunWorkerAsync(); } private void BtnCancel_Click(object sender, EventArgs e) { bgWorker1.CancelAsync(); } } }
現在,我們可以按BtnStart啟動后台任務,也可以隨時按BtnCancel隨時中止后台任務。
發現一個問題沒有?在進度條還沒跑完100%時,再次按btnStart會怎么樣,前后兩個例子是不一樣,第一個例子中,再次按btnStart,會感覺有兩個重疊的進度條在跑,而第二個例子中,則直接拋出了異常:
為什么會這樣?第一個例子中,每次按btnStart,都是BackgroundWorker bgwA = new BackgroundWorker();而第二個例子中每次按btnStart,還是那個bgWorker1,只是再次RunWorkerAsync(),解決的方法是先判斷一下后台操作是否還在運行中:IsBusy
代碼:
private void BtnStart_Click(object sender, EventArgs e) { if (!bgWorker1.IsBusy) { bgWorker1.RunWorkerAsync(); } }
小結一下:
BackgroundWorker的屬性:
IsBusy:獲取一個值(true/false),指示 BackgroundWorker 是否正在運行異步操作。
WorkerReportsProgress:獲取或設置一個值(true/false),該值指示 BackgroundWorker 能否報告進度更新。
WorkerSupportsCancellation:獲取或設置一個值(true/false),該值指示 BackgroundWorker 是否支持異步取消。
BackgroundWorker事件:
DoWork:調用 RunWorkerAsync 時發生。(要在后台進行的耗時操作,不能操作任何用戶界面對象。)
ProgressChanged:調用 ReportProgress 時發生。(后台操作進行時,隨時向用戶界面報告進度,可以操作用戶界面對象,如進度條顯示,文本顯示等。)
RunWorkerCompleted:當后台操作已完成、被取消或引發異常時發生。(可以操作用戶界面對象,如顯示完成的結果報告。)
進階二:前后台交互,參數傳遞
private void BgWorker1_DoWork(object sender, DoWorkEventArgs e) { for (int i = 0; i <= 50; i++) { if (bgWorker1.CancellationPending == true) { e.Cancel = true; break; } else { bgWorker1.ReportProgress(i*2); System.Threading.Thread.Sleep(100); } } }
看for (int i = 0; i <= 50; i++)以及bgWorker1.ReportProgress(i*2); 原因就自己琢磨一下了。
private void BtnStart_Click(object sender, EventArgs e) { if (!bgWorker1.IsBusy) { int intStart = 1000000; bgWorker1.RunWorkerAsync(intStart); } }
DoWork事件通過參數 e 的Argument屬性來獲取傳遞過來的參數。
private void BgWorker1_DoWork(object sender, DoWorkEventArgs e) { int intStart = 0; if (e.Argument != null) { intStart = (int)e.Argument; } }
記得傳過來的是個object類型的參數,使用時一定不要忘記了轉換一下。
private void BgWorker1_DoWork(object sender, DoWorkEventArgs e) { int intStart = 0; if (e.Argument != null) { intStart = (int)e.Argument; } int intSum = intStart; for (int i = 0; i <= 50; i++) { if (bgWorker1.CancellationPending == true) { e.Cancel = true; break; } else { intSum += i; bgWorker1.ReportProgress(i*2,"循環求和結果:"+intSum.ToString()); System.Threading.Thread.Sleep(100); } } } private void BgWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.pgbPercent.Value = e.ProgressPercentage; this.lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%"; this.txtStatus.Text += e.UserState.ToString() + System.Environment.NewLine; }
BackgroundWorker的DoWork中ReportProgress加一參數,把需要傳出去的參數通過 bgWorker1.ReportProgress(完成百分比, 傳出參數) 報告出去,ProgressChanged中把這個傳出來的參數顯示到UI上:this.txtStatus.Text += e.UserState.ToString() ...。
現在運行一下程序試試看。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace bgwB { public partial class Form1 : Form { public Form1() { InitializeComponent(); } string strBgw = ""; private void Form1_Load(object sender, EventArgs e) { bgWorker1.WorkerReportsProgress = true; bgWorker1.WorkerSupportsCancellation = true; } private void BgWorker1_DoWork(object sender, DoWorkEventArgs e) { int intStart = 0; if (e.Argument != null) { intStart = (int)e.Argument; } int intSum = intStart; for (int i = 0; i <= 50; i++) { if (bgWorker1.CancellationPending == true) { e.Cancel = true; break; } else { strBgw = "times:" + i.ToString() + " " + DateTime.Now.ToString(); intSum += i; bgWorker1.ReportProgress(i*2,"循環求和結果:"+intSum.ToString()); System.Threading.Thread.Sleep(100); } } } private void BgWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.pgbPercent.Value = e.ProgressPercentage; this.lblPercent.Text = @"已完成:" + e.ProgressPercentage.ToString() + @"%"; this.txtStatus.Text += e.UserState.ToString() + System.Environment.NewLine; this.txtStatus.Text += strBgw + System.Environment.NewLine; } private void BgWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { this.lblPercent.Text = "后台操作結束(可能是程序100%完成,也可能是用戶取消或程序異常導致結束)。"; } private void BtnStart_Click(object sender, EventArgs e) { if (!bgWorker1.IsBusy) { this.txtStatus.Text = ""; int intStart = 1000000; bgWorker1.RunWorkerAsync(intStart); } } private void BtnCancel_Click(object sender, EventArgs e) { bgWorker1.CancelAsync(); } } }
結果如圖:
總結