Windows Phone 7 – BackgroundWorker & ProgressBar
在之前幾篇文章有提到Dispatcher、HttpRequest這類的東西,讓程式可以進行非同步運作,或是透過
控制不同的Thread(UI Thread)來完成資料的呈現或是後端邏輯的處理。此時,我腦中有閃過一些想法,
我在背景非同步處理邏輯時,我要怎麼讓用戶可以了解,或是提供一些Tip讓它明白,那麼,在WP7
提供了「ProressBar」控制項來提供呈現,如:Loading…、Sync…這些效果。
介紹ProgressBar是一部分,但其實我想介紹的還有「BackgroundWorker」這個類別是比較重要的。
注意在UI Design and Interaction Guide specifies(P.59)中有提到二個重要的Progress Indicator:
a. Indeterminate:
如果在使用的目標是在未能明確指出需要的時間,只要求最後一定要做完任務,建議使用Indeterminate模式。
b. Determinate:
如果執行任務所需的時間是在可得知的範圍內,建議使用Determinate模式。
二者的差異在於「是否在可得知的時間範圍」,那二者在設計上有那些差別呢?從上方二張圖的呈現可以了解:
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: }
範例畫面:
Determinate Indeterminate 範例程式:
看完上述的例子,是否有發現一個很熟悉的類別:「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,常用於手動修改控件的呈現方式。對於動態調整畫面控件非常好用。
〉VerticalAlignment與HorizontalAlignment屬性
這二個屬性分別用於定義控件的垂直或水平對齊於相關的控件項目,相關的控件項目代表就是被設定該屬性控件的父控件,
在生成該控件時,會先參考父控件的相關Width/Hieght來組合。
〉Popup類別 (System.Windows.Controls.Primitives)
根據MSDN上的定義:「覆蓋現有 Silverlight 內容的上方來顯示內容 (在 Silverlight 控制項的界限內)。」
使用Popup類別可被用於顯示暫時完成特定工作的內容,也有可能是引入其他外部畫面來啟動背景作業或是呼叫服務。
要透過Popup呈現的內容,透過把建立好的使用者介面(UIElement)指定給Clinet屬性即可,最後透過IsOpen = true的方式,
將Popup呈現出來。另外,有時如果Popup出現的位置或畫面太小時,可以使用 VerticalOffset 和 HorizontalOffset 即可將
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.
‧Silverlight 事件概觀 (必讀,有助於在看一些文件時,可以了解Silverlight事件運作的方式)