#眉標=Windows Form #副標=Windows Form UI優化入門課 #大標=實作非同步作業 #作者=文/圖 李明儒 ============= 程式1 private void Form1_Load(object sender, EventArgs e) { //預設為暫存目錄 txtPath.Text = Path.GetTempPath(); } private int fileCount = 0; private long totalSize = 0; private void btnGO_Click(object sender, EventArgs e) { //重設統計數據 fileCount = 0; totalSize = 0; //統計檔案數及大小 explore(txtPath.Text); //顯示結果 lblResult.Text = string.Format( "{0:#,##0} files, {1:#,##0} bytes", fileCount, totalSize); } private void explore(string path) { //使用遞迴搜索所有目錄 foreach (string dir in Directory.GetDirectories(path)) explore(dir); foreach (string file in Directory.GetFiles(path)) { fileCount++; //加總檔案大小 FileInfo fi = new FileInfo(file); totalSize += fi.Length; } } private void timer1_Tick(object sender, EventArgs e) { //每隔50ms顯示最新時間 toolStripStatusLabel1.Text = DateTime.Now.ToString("HH:mm:ss.fff"); }================ ============= 程式2 private void btnGO_Click(object sender, EventArgs e) { //將統計作業排入ThreadPool Queue //Windows會另開Thread處理,UI Thread可以 //繼續處理UI畫面顯示並與使用者互動 ThreadPool.QueueUserWorkItem( new WaitCallback(startJob), txtPath.Text); } private void startJob(object path) { fileCount = 0; totalSize = 0; explore(path.ToString()); //***以下這行有點問題*** lblResult.Text = string.Format( "{0:#,##0} files, {1:#,##0} bytes", fileCount, totalSize); }================ ==<灰>=========== InvalidOperationException was unhandled Cross-thread operation not valid: Control 'lblResult' accessed from a thread other than the thread it was created on. ================ ==<灰>=========== "Thou shall not access UI controls from a thread other than the one that created the UI control in the first place" “除了建立UI Control的那條執行緒外,你不可以用其他執行緒去存取該UI Control!” ================ ============= 程式3 private void startJob(object path) { fileCount = 0; totalSize = 0; explore(path.ToString()); string text = string.Format( "{0:#,##0} files, {1:#,##0} bytes", fileCount, totalSize); //透過Invoke,強制以UI Thread執行 this.Invoke( new UpdateLableHandler(printResult), new object[] { text }); } //宣告一個delegate delegate void UpdateLableHandler(string text); //透過Invoke,printReulst會以UI Thread執行 //故可隨意存取UI Control private void printResult(string text) { lblResult.Text = text; }================ ============= 程式4 private void btnGO_Click(object sender, EventArgs e) { if (btnGO.Text == "GO") { //按鈕功能變更為取消 btnGO.Text = "Cancel"; cancel = false; ThreadPool.QueueUserWorkItem( new WaitCallback(startJob), txtPath.Text); } else { //設定取消旗標 cancel = true; //在完全停止前,停用按鈕 btnGO.Enabled = false; } } private void startJob(object path) { fileCount = 0; totalSize = 0; explore(path.ToString()); string text = //取消時顯示”Canceled”,不顯示統計 (!cancel) ? string.Format( "{0:#,##0} files, {1:#,##0} bytes", fileCount, totalSize) : "Canceled"; this.Invoke( new UpdateLableHandler(printResult), new object[] { text }); } delegate void UpdateLableHandler(string text); private void printResult(string text) { lblResult.Text = text; //作業完成,恢復按鈕功能 btnGO.Text = "GO"; btnGO.Enabled = true; } private void updateProgress(string text) { lblResult.Text = text; } private bool cancel = false; private void explore(string path) { foreach (string dir in Directory.GetDirectories(path)) { //使用者要求中止 if (cancel) return; explore(dir); } foreach (string file in Directory.GetFiles(path)) { //使用者要求中止 if (cancel) return; //雖然在本例中非必要,但在此還是簡單示範利用 //Interlocked.Increment增加fileCounter值的做法 //防止多個Thread同時更動資料時發生衝突 //另一種做法是用lock(C#)或SyncLock(VB.NET) //請參考MSDN獲得更詳細的介紹 Interlocked.Increment(ref fileCount); //每100個檔案回報一下進度 if (fileCount % 100 == 0) this.Invoke( new UpdateLableHandler(updateProgress), string.Format( "{0:#,##0} files processed...", fileCount) ); FileInfo fi = new FileInfo(file); totalSize += fi.Length; } }================ ============= 程式5 private void btnGO_Click(object sender, EventArgs e) { if (btnGO.Text == "GO") { btnGO.Text = "Cancel"; fileCount = 0; totalSize = 0; //啟動非同步呼叫 backgroundWorker1.RunWorkerAsync( txtPath.Text); } else { btnGO.Text = "GO"; btnGO.Enabled = false; //取消非同步作業 backgroundWorker1.CancelAsync(); } } private void explore(string path, BackgroundWorker worker) { foreach (string dir in Directory.GetDirectories(path)) { //以CancellationPending判別是否要取消 if (worker.CancellationPending) return; explore(dir, worker); } foreach (string file in Directory.GetFiles(path)) { if (worker.CancellationPending) return; fileCount++; if (fileCount % 100 == 0) //利用ReportProgress回報進度 backgroundWorker1.ReportProgress( 0, //Progress在此未使用,設0 string.Format("{0:#,##0} files processed...", fileCount) ); FileInfo fi = new FileInfo(file); totalSize += fi.Length; } } //非同步作業 private void backgroundWorker1_DoWork( object sender, DoWorkEventArgs e) { //e.Argument接入RunWorkerAsync傳入的參數 explore((string)e.Argument, sender as BackgroundWorker); //執行完成,設執行定結果 if (backgroundWorker1.CancellationPending) e.Cancel = true; else e.Result = string.Format( "{0:#,##0} files, {1:#,##0} bytes", fileCount, totalSize); } //即時執行進度回報 private void backgroundWorker1_ProgressChanged( object sender, ProgressChangedEventArgs e) { lblResult.Text = e.UserState.ToString(); } //作業完畢處理 private void backgroundWorker1_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e) { lblResult.Text = (e.Cancelled) ? "Canceled" : //取消 e.Result.ToString(); //結果 //恢復按鈕功能 btnGO.Text = "GO"; btnGO.Enabled = true; }================ ============= 參考資料 Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads http://msdn.microsoft.com/msdnmag/issues/03/02/multithreading/ ================