Wp7 - BackgroundWorker and ProgressBar

Windows Phone 7 – BackgroundWorker & ProgressBar

在之前幾篇文章有提到Dispatcher、HttpRequest這類的東西,讓程式可以進行非同步運作,或是透過

控制不同的Thread(UI Thread)來完成資料的呈現或是後端邏輯的處理。此時,我腦中有閃過一些想法,

我在背景非同步處理邏輯時,我要怎麼讓用戶可以了解,或是提供一些Tip讓它明白,那麼,在WP7

提供了「ProressBar」控制項來提供呈現,如:Loading…、Sync…這些效果。

 

介紹ProgressBar是一部分,但其實我想介紹的還有「BackgroundWorker」這個類別是比較重要的。

ProgressBar

注意在UI Design and Interaction Guide specifies(P.59)中有提到二個重要的Progress Indicator:

a. Indeterminate

如果在使用的目標是在未能明確指出需要的時間,只要求最後一定要做完任務,建議使用Indeterminate模式。

image

 

b. Determinate

如果執行任務所需的時間是在可得知的範圍內,建議使用Determinate模式。

image

 

二者的差異在於「是否在可得知的時間範圍」,那二者在設計上有那些差別呢?從上方二張圖的呈現可以了解:

Determinate模式時,程式在執行任務是是透過「bar」的呈現方式;Indeterminate模式,則是使用「…」的模式

如果把Determinate模式想成Download的情境,把Indeterminate想成背景執行取得資料,這樣就比較容易對應起來。

 

至於在ProgressBar裡要怎麼識別呢?其實可以直接透過「ProgressBar.IsIndeterminate」來設定此時要使用的情境是何種類型。

 

在Windows Phone 7的開發裡,其實針對ProgressBar的實作,非常容易上手的,下方就簡單舉個例子:

〉範例說明

(1) 實作一個UserControl,裡面包括了:ProgressBar(設定IsIndeterminate為true)、TextBlock與Popup類別。

主要透過Popup類別將該UserControl的內容呈現於主畫面上。裡面要支援的任務很簡單,就是提供可以設定ProgressBar.Value、

ProgressBar.IsIndeterminate與顯示/隱藏UserControl的方法。如下之程式碼:

XAML Code:

   1: <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
   2:         <StackPanel Height="30" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal">
   3:             <TextBlock Text="Loading..."/>
   4:             <ProgressBar Height="25" HorizontalAlignment="Left" Name="progressBar1" VerticalAlignment="Top" Width="350"
   5:                          IsIndeterminate="true" />
   6:         </StackPanel>
   7:     </Grid>

Code:

   1: #region 屬性
   2: internal Popup PopupControl
   3: {
   4:     get;
   5:     private set;
   6: }
   7:  
   8: //設定/取得ProgressBar.IsIndeterminate值。
   9: public bool IsIndeterminate
  10: {
  11:     get
  12:     {
  13:         return progressBar1.IsIndeterminate;
  14:     }
  15:     set
  16:     {
  17:         progressBar1.IsIndeterminate = value;
  18:     }
  19: }
  20:  
  21: //設定/取得ProgressBar.Value的值
  22: public int ProgressValue
  23: {
  24:     set
  25:     {
  26:         progressBar1.Value = value;
  27:     }
  28: }
  29: #endregion
  30:  
  31: //顯示ProgressBar UserControl
  32: public void ShowProgress()
  33: {
  34:     progressBar1.Value = 0;
  35:     if (PopupControl == null)
  36:     {
  37:         PopupControl = new Popup();
  38:         PopupControl.Child = this;
  39:     }
  40:     if (PopupControl != null)
  41:     {
  42:         //顯示畫面
  43:         this.PopupControl.IsOpen = true;
  44:         //修改顯示位置
  45:         this.PopupControl.VerticalOffset = 20;
  46:         this.PopupControl.HorizontalOffset = 5;
  47:     }
  48: }
  49:  
  50: //隱藏ProgressBar UserControl
  51: public void HideProgress()
  52: {
  53:     this.progressBar1.IsIndeterminate = false;
  54:     this.PopupControl.IsOpen = false;
  55: }
  56: }

(2) 使用BackgroundWorker,讓做好的UserControl出現於畫面上短暫的時間。

在啟動BackgroundWorker之前,先把做好的UserControl產生出來,並且顯示它再啟動RunWorkerAsync()。

在DoWork事件裡,撰寫模擬處理時間的部分。(如果實際要用於驗證例如像站台是否活著時,建議把處理ProgressBar在Determinate下

針對ProgressBar.Value控件的部分,移動到原有由執行緒下,透過DispatcherTimer來控制也是不錯的選擇)。

程式碼如下:

   1: //初始化BackgroundWorker元件與實作的UserControl
   2: private void Init()
   3: {
   4:     gProgress = new UserControls.UCProgress();
   5:     gBackWorker = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true };
   6:     gBackWorker.DoWork += new DoWorkEventHandler(gBackWorker_DoWork);
   7:     gBackWorker.ProgressChanged += new ProgressChangedEventHandler(gBackWorker_ProgressChanged);
   8:     gBackWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(gBackWorker_RunWorkerCompleted);
   9: }
  10:  
  11: void gBackWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  12: {
  13:     Dispatcher.BeginInvoke(() =>
  14:     {
  15:         gProgress.HideProgress();
  16:     });
  17: }
  18:  
  19: void gBackWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
  20: {
  21:     Dispatcher.BeginInvoke(() =>
  22:     {
  23:         gProgress.ProgressValue = e.ProgressPercentage;
  24:     });
  25: }
  26:  
  27: void gBackWorker_DoWork(object sender, DoWorkEventArgs e)
  28: {
  29:     if (gBackWorker.CancellationPending == false)
  30:     {
  31:         if (gIsIndeterminate == false)
  32:         {
  33:             for (int i = 0; i < 100; i++)
  34:             {
  35:                 i += 10;
  36:                 gBackWorker.ReportProgress(i);
  37:                 Thread.Sleep(1000);
  38:             }
  39:         }
  40:         else
  41:         {
  42:             Thread.Sleep(5000);
  43:         }
  44:     }
  45: }
  46:  
  47: private void btnCancel_Click(object sender, RoutedEventArgs e)
  48: {
  49:     if (gBackWorker.IsBusy)
  50:     {
  51:         gBackWorker.CancelAsync();
  52:     }
  53:     gProgress.HideProgress();
  54: }
  55:  
  56: private void btnCheck_Click(object sender, RoutedEventArgs e)
  57: {
  58:     //配合ProgressChanged使用的話,需要把IsIndeterminate設為false。
  59:     if (radioButton1.IsChecked == true)
  60:     {
  61:         gProgress.IsIndeterminate = false;
  62:     }
  63:     else
  64:     {
  65:         gProgress.IsIndeterminate = true;
  66:     }
  67:     gIsIndeterminate = gProgress.IsIndeterminate;
  68:     gUri = textBox1.Text;
  69:     gProgress.ShowProgress();
  70:     if (gBackWorker.IsBusy == false)
  71:     {
  72:         gBackWorker.RunWorkerAsync();
  73:     }
  74: }

範例畫面:

DeterminateDeterminate Indeterminate Indeterminate  範例程式:

 

看完上述的例子,是否有發現一個很熟悉的類別:「BackgroundWorker」。這一個元件在開發非同步(背景執行)應用或邏輯運算

時是很常見的,因為它的好處在於容易實作之外,重點它還包括了一些特性,讓開發時就簡單達到功能,又能有效的管理它。

 

BackgroundWorker

根據之前撰寫過的一篇文章:<撰寫Windows Phone 7程式之前 – 了解WPF Thread Model的概念>,裡面的內容也有大略提及相關

BackgroundWorker的介紹,讓我們先來回憶一下:

BackgroundWorker元件適用於WPF,因為它實際上使用AsyncOperationManager類別,該類別使用SynchronizationContext

類別來處理同步化的工作。然而該SynchronizationContext類別在不同的應用上有不同的衍生類別,例如:在Windows Forms上,有衍生的:

WindowsFormsSynchronizationContext在ASP.NET上,有衍生的:AspNetSynchronizationContext。

 

然而,BackgroundWorker可以用的地方非常廣,可以用於當作測試目前資料庫設定的連線或Http測試服務等,用意通常都是為了

讓用戶不需直接等待主程式的回應,而是可以透過背景執行的方式,二邊同時進行處理,完成任務。

以下將仔細介紹BackgroundWorker的相關內容:

 

〉重要屬性

名稱 描述
CancellationPending 用於識別目前應用程式是否已經取消了背景作業。該屬性通常會被用於DoWork事件中,用於識目前正在執行的背景作業是否有被取消,讓目前執行的作業可以先做被取消後要收拾的任務。
IsBusy 識別目前BackgroundWoker是否正在執行背景作業。該屬性用於識別避免重新加入背景作業,或是避免加入多個背景作業時使用。
WorkerReportsProgress 設定/取得BackgroundWorker是否可以報告進度更新。如果要使用ReportProgress方法與ProgressChanged事件要記得設定為true。
WorkerSupportsCancellation 設定/取得BackgroundWorker是否支援非同步取消。如果沒有設定為true,當呼叫CancelAsync()時會發生例外事件。

 

〉三個重要的事件處理

a. DoWork

DoWork主要處理在背景執行的邏輯運算任務,在MSDN中強調,DoWork不應該處理有關任何使用者介面溝通的部分,因此,

建議把有關於使用者介面溝通的部分,移動到ProgressChanged與RunWorkerCompleted負責

另外,如果DoWork過程裡,需要傳入參數使用的話,可以在啟動背景作業時,透過執行「RunWorkerAsync(Object)」方法,

將需要的參數(Object)傳入背景作業中,當然,進入DoWork事件裡則可以透過事件處理常式中的「DoWorkEventArgs.Argument」屬性,

來取得參數內容,如下範例:

   1: private void CallBackgroundWork()
   2: {
   3:     SqlConnection tSQLConn = new SqlConnection("server=localhost;database=Test;uid=sa;pwd=abcdef");
   4:  
   5:     //初始化BackgroundWorker 
   6:     BackgroundWorker tBgWork = new BackgroundWorker{ WorkerReportsProgress = true, WorkerSupportsCancellation = true };
   7:     tBgWork.DoWork += new DoWorkEventHandler(BackWork_DoWork);
   8:     tBgWork.ProgressChanged += new ProgressChangedEventHandler(BackWork_ProgressChanged);
   9:     tBgWork.RunWorkerAsync(tSQLConn);
  10: }
  11:  
  12: private void BackWork_DoWork(object sender, DoWorkEventArgs s)
  13: {
  14:     //取得參數中的資料
  15:     SqlConnection tSQLConn = e.Argument as SqlConnection;
  16:     tSQLConn.Open();
  17: }

 

b. ProgressChanged

在BackgroundWorker呼叫「 ReportProgress 」時發生,常用於回報目前背景工作的執行狀態給前端使用者介面。

ReportProgress方法分成二個部分:

b-1. ReportProgress(int percentProgress)

b-2. ReportProgress(int percentProgress, Object userState)

它的主要傳入:背景作業的完成百分比,從 0 到 100。但要記得設定「WorkerReportsProgress 屬性值必須是 true」才會正常執行。

另外,ReportProgress在指定完成百分比之外,第二個傳入的參數userState代表傳遞至 RunWorkerAsync 的狀態物件,可以用於處理

在ProgressChanged要通知或呈現於使用者介面上的參數,例如:

   1: private void BackWork_DoWork(object sender, DoWorkEventArgs e)
   2: {
   3:     //指定UserState
   4:     tBgWork.ReportProgress(100, new String[] { "Your State", "Busy" });
   5: }
   6:  
   7: private void BackWork_ProgressChanged(object sender, ProgressChangedEventArgs  e)
   8: {
   9:     //取得UserState內容
  10:     String[] tUserState = (String[])e.UserState;
  11: }

 

至於在WP7裡,則會透過Dispatcher.BeginInvoke方法來修改UI Thread裡的內容。例如下方的程式碼:

   1: private void BackWork_ProgressChanged(object sender, ProgressChangedEventArgs e)
   2: {
   3:     Dispatcher.BeginInvoke(() =>
   4:     {
   5:         progress.Hide();
   6:     });
   7: }

 

c. RunWorkerCompleted

代表當背景作業已完成、取消或引發例外狀況時發生。RunWokerCompleted事件處理中,有幾個重要的參數協助我們識別目前

背景處理的任務是否有錯誤或是取得結果。「RunWorkerCompletedEventArgs」是整個RunWokerCompleted事件裡很重要參數:

c-1. RunWorkerCompletedEventArgs.Result

      在DoWork事件處理完成後,可以在DoWork事件裡指定要傳遞回原本執行緒的參數:DoWorkEventArgs.Result = 結果,

      隨著DoWoker完成後執行RunWokerCompleted時,將透過RunWorkerCompletedEventArgs.Result將結果反應至使用者介面或主執行緒。

c-2. RunWorkerCompletedEventArgs.Cancelled

      如果BackgroundWoker有設定WorkerSupportsCancellation=true,用戶可以在執行過程裡呼叫CancelAsync()取消暫止的背景作業。

      當然,在執行DoWork事件時,通常會識別作業是否有取消要求(CancellationPending),如果值為true,則會配合CancelAsync()使用。

      此時,進入RunWorkerCompleted事件時,透過RunWorkerCompletedEventArgs.Cancelled會得到true的值。

c-3. RunWorkerCompletedEventArgs.Error

    當執行DoWork或ProgressChanged事件時,發生了例外的事件後,就直接進入了RunWorkerCompleted事件,則直接透過該屬性來識別,

      目前作業是否例外,通常會在執行該事件時先處理這個屬性,避免往下取得result屬性於又發生例外。

 

======

以上是分享實作ProgressBar時,針對BackgroundWorker使用時的學習心得,主要是因為很少有機會寫到背景作業的任務,

所以對背景作業不是非常熟悉,撰寫該篇文章針對目前使用到背景作業處理的方法做一些說明。

 

[補充資料]

Thickness結構:定義控件的Margin,常用於手動修改控件的呈現方式。對於動態調整畫面控件非常好用。

VerticalAlignmentHorizontalAlignment屬性

   這二個屬性分別用於定義控件的垂直或水平對齊於相關的控件項目,相關的控件項目代表就是被設定該屬性控件的父控件,

   在生成該控件時,會先參考父控件的相關Width/Hieght來組合。

Popup類別 (System.Windows.Controls.Primitives)

   根據MSDN上的定義:「覆蓋現有 Silverlight 內容的上方來顯示內容 (在 Silverlight 控制項的界限內)。」

   使用Popup類別可被用於顯示暫時完成特定工作的內容,也有可能是引入其他外部畫面來啟動背景作業或是呼叫服務。

   要透過Popup呈現的內容,透過把建立好的使用者介面(UIElement)指定給Clinet屬性即可,最後透過IsOpen = true的方式,

   將Popup呈現出來。另外,有時如果Popup出現的位置或畫面太小時,可以使用 VerticalOffsetHorizontalOffset 即可將

   Popup 設定在相對於 Silverlight 外掛程式左上角的位置。

 

References:

Creating a Splash Screen with a progress bar for WP7 applications.

多執行緒初探--使用BackgroundWorker(1) & 多執行緒初探--使用BackgroundWorker(2) (必讀)

Exiting a Windows Phone Application

BackgroundWorker 元件 & BackgroundWorker 元件概觀 & HOW TO:使用幕後背景工作

Tips for ProgressBar Performance in WP7 (使用ProgressBar要注意的事項)

The high performance ProgressBar for Windows Phone (“PerformanceProgressBar”)

Creating Progress Dialog for WP7. (必看)

WP7 Perf Tip #2: Know your ProgressBar

Customizing Picker Box dialog.

BackgoundWorker & WPF

Silverlight 事件概觀 (必讀,有助於在看一些文件時,可以了解Silverlight事件運作的方式)