在 Visual Basic .NET 中完成后臺(tái)進(jìn)程(二)
發(fā)表時(shí)間:2024-02-12 來(lái)源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]架構(gòu)設(shè)計(jì)要實(shí)現(xiàn)我們討論的行為,顯然需要實(shí)現(xiàn) Controller 類(lèi)。為了使此架構(gòu)能夠在多數(shù)方案中應(yīng)用,我們還會(huì)定義一些正式接口,可以由 Controller 在與 UI(或客戶端)和輔助線程交互時(shí)使用。通過(guò)為客戶端和輔助線程定義正式接口,我們可以在不同的情況下使用相同的 Controller 對(duì)...
架構(gòu)設(shè)計(jì)
要實(shí)現(xiàn)我們討論的行為,顯然需要實(shí)現(xiàn) Controller 類(lèi)。為了使此架構(gòu)能夠在多數(shù)方案中應(yīng)用,我們還會(huì)定義一些正式接口,可以由 Controller 在與 UI(或客戶端)和輔助線程交互時(shí)使用。
通過(guò)為客戶端和輔助線程定義正式接口,我們可以在不同的情況下使用相同的 Controller 對(duì)象,還可以根據(jù)需要使用不同的 UI 要求和不同的 Worker 對(duì)象。
下面的 UML 類(lèi)圖表顯示了 Controller 類(lèi)以及 IClient 和 IWorker 接口。它還顯示了 IController 接口,輔助代碼將通過(guò)它與 Controller 對(duì)象交互。
圖 5:Controller 和相關(guān)接口的類(lèi)圖表
IClient 接口定義的方法將由 Controller 對(duì)象調(diào)用,用于向客戶端 UI 通報(bào) Worker 的開(kāi)始時(shí)間、結(jié)束時(shí)間和任何中間狀態(tài)消息。它還包含一個(gè)指示輔助代碼失敗的方法。
多數(shù)情況下,我們可以將這些方法作為由 Controller 對(duì)象發(fā)出而由 UI 處理的事件來(lái)實(shí)現(xiàn)。但是,從輔助線程發(fā)出事件然后由 UI 線程正確處理并非易事,因而我們將其作為一組方法來(lái)進(jìn)行實(shí)現(xiàn)。
使控制器代碼(在輔助代碼上運(yùn)行)調(diào)用 UI 中的這些方法并由 UI 線程進(jìn)行處理,這樣相對(duì)要簡(jiǎn)單得多。
同樣,IWorker 接口定義了由 Controller 對(duì)象調(diào)用的、使其可以與輔助代碼交互的方法。使用 Initialize 方法可以為輔助代碼提供對(duì) Controller 對(duì)象的引用,而使用 Start 方法可以啟動(dòng)后臺(tái)線程上的操作。
由于線程的工作方式,Start 方法無(wú)法包含任何參數(shù)。啟動(dòng)新線程時(shí),必須將不接受任何參數(shù)的方法的地址傳遞給線程。
請(qǐng)注意,IWorker 接口中不存在 Cancel 或 Stop 方法。我們不能強(qiáng)制輔助代碼停止,同時(shí)也沒(méi)有這個(gè)必要;但是輔助代碼可以使用 IController 接口詢問(wèn) Controller 對(duì)象是否存在取消請(qǐng)求。
IController 接口定義了輔助代碼可以在 Controller 對(duì)象上調(diào)用的方法。它允許輔助代碼檢查 Running 標(biāo)志。如果存在取消請(qǐng)求,Running 標(biāo)志即為 False。它還允許輔助代碼在工作完成或無(wú)法完成時(shí)告訴 Controller,并允許使用狀態(tài)消息和完成百分比值(0 到 100 之間的 Integer)更新 Controller。
最后我們定義了 Controller 對(duì)象。該對(duì)象中包含一些可以被 UI 代碼調(diào)用的方法。其中包括 Start 方法,該方法可以通過(guò)為 Controller 對(duì)象提供對(duì) Worker 對(duì)象的引用來(lái)啟動(dòng)后臺(tái)操作。還包括 Cancel 方法,該方法用于請(qǐng)求取消操作。UI 也可以檢查 Running 屬性,查看是否存在取消請(qǐng)求;還可以檢查 Percent 屬性,查看任務(wù)完成的百分比。
Controller 類(lèi)中包含的 constructor 方法接受 IClient 作為參數(shù),還允許 UI 為 Controller 提供對(duì)窗體(用于處理 Worker 中的顯示消息)的引用。
為了實(shí)現(xiàn)一系列動(dòng)畫(huà)點(diǎn)來(lái)顯示線程的活動(dòng),我們將創(chuàng)建一個(gè)簡(jiǎn)單 Windows 窗體控件,該控件使用計(jì)時(shí)器以更改一系列 PictureBox 控件中的顏色。
實(shí)現(xiàn)方案
我們將在 Class Library(類(lèi)庫(kù))項(xiàng)目中實(shí)現(xiàn)此架構(gòu),使其可用于需要運(yùn)行后臺(tái)進(jìn)程的應(yīng)用程序。
打開(kāi) Visual Studio .NET,然后創(chuàng)建一個(gè)名為 Background 的新 Class Library(類(lèi)庫(kù))應(yīng)用程序。由于此庫(kù)將包含 Windows 窗體控件和窗體,因此需要使用 Add References(添加引用)對(duì)話框引用 System.Windows.Forms.dll 和 System.Windows.Drawing.dll。此外,我們可以使用項(xiàng)目的屬性對(duì)話框在這些項(xiàng)目范圍內(nèi)導(dǎo)入命名空間,如圖 6 所示。
圖 6:使用項(xiàng)目屬性添加項(xiàng)目范圍內(nèi)的命名空間 Imports
此操作完成后,就可以開(kāi)始編碼了。讓我們先從創(chuàng)建接口開(kāi)始。
定義接口
在名為 IClient 的項(xiàng)目中添加一個(gè)類(lèi),并用以下代碼替換其代碼:
Public Interface IClient Sub Start(ByVal Controller As Controller) Sub Display(ByVal Text As String) Sub Failed(ByVal e As Exception) Sub Completed(ByVal Cancelled As Boolean)End Interface
然后添加名為 IWorker 的類(lèi),并用以下代碼替換其代碼:
Public Interface IWorker Sub Initialize(ByVal Controller As IController) Sub Start()End Interface
最后添加名為 IController 的類(lèi),代碼如下:
Public Interface IController ReadOnly Property Running() As Boolean Sub Display(ByVal Text As String) Sub SetPercent(ByVal Percent As Integer) Sub Failed(ByVal e As Exception) Sub Completed(ByVal Cancelled As Boolean)End Interface
至此,我們已定義了本文前面所述的所有類(lèi)圖表中的接口。現(xiàn)在可以實(shí)現(xiàn) Controller 類(lèi)了。
Controller 類(lèi)
現(xiàn)在,我們可以實(shí)現(xiàn)架構(gòu)的核心,Controller 類(lèi)。此類(lèi)中包含的代碼可用于啟動(dòng)輔助線程,以及在輔助線程完成之前充當(dāng) UI 線程和輔助線程之間的媒介。
在名為 Controller 的項(xiàng)目中添加一個(gè)新類(lèi)。首先添加 Imports,并聲明一些變量:
Imports System.ThreadingPublic Class Controller Implements IController Private mWorker As IWorker Private mClient As Form Private mRunning As Boolean Private mPercent As Integer
然后需要聲明一些委托。委托是方法的正式指針,而且方法的委托必須具有與方法本身相同的方法簽名(參數(shù)類(lèi)型等)。
委托的用途很廣。在我們的示例中,委托非常重要,因?yàn)槲惺刮覀兛梢宰屢粋(gè)線程調(diào)用窗體上的方法,使其在該窗體的 UI 線程上運(yùn)行。正如 IClient 所定義的那樣,要在窗體上調(diào)用的三個(gè)方法都需要委托:
' 此委托簽名與 IClient.Completed ' 中的簽名相匹配,并用于安全地 ' 調(diào)用 UI 線程上的方法 Private Delegate Sub CompletedDelegate(ByVal Cancelled As Boolean) ' 此委托簽名與 IClient.Display ' 中的簽名相匹配,并用于安全地 ' 調(diào)用 UI 線程上的方法 Private Delegate Sub DisplayDelegate(ByVal Text As String) ' 此委托簽名與 IClient.Failed ' 中的簽名相匹配,并用于安全地 ' 調(diào)用 UI 線程上的方法 Private Delegate Sub FailedDelegate(ByVal e As Exception)
IClient 還定義了 Start 方法,但是該方法可以從 UI 線程調(diào)用,因此不需要委托。
下面編寫(xiě)將從 UI 線程調(diào)用的代碼。代碼中包括 constructor 方法、Start 和 Cancel 方法以及 Percent 屬性。我將這些內(nèi)容放入 Region 中,便于大家清楚地了解它們是從 UI 線程調(diào)用的。
#Region " 從 UI 線程調(diào)用的代碼 " ' 使用客戶端初始化 Controller Public Sub New(ByVal Client As IClient) mClient = CType(Client, Form) End Sub ' 此方法由 UI 調(diào)用,因此在 ' UI 線程上運(yùn)行。此處我們將 ' 啟動(dòng)輔助線程 Public Sub Start(Optional ByVal Worker As IWorker = Nothing) ' 如果輔助線程已經(jīng)啟動(dòng),將產(chǎn)生錯(cuò)誤 If mRunning Then Throw New Exception("Background process already running") End If mRunning = True ' 存儲(chǔ)對(duì)輔助對(duì)象的引用,并 ' 初始化輔助對(duì)象,使其包含 ' 對(duì) Controller 的引用 mWorker = Worker mWorker.Initialize(Me) ' 創(chuàng)建后臺(tái)線程 ' 以進(jìn)行后臺(tái)操作 Dim backThread As New Thread(AddressOf mWorker.Start) ' 開(kāi)始后臺(tái)工作 backThread.Start() ' 告訴客戶端后臺(tái)工作已開(kāi)始 CType(mClient, IClient).Start(Me) End Sub ' 此代碼由 UI 調(diào)用,因此在 UI ' 線程上運(yùn)行。它只設(shè)置了請(qǐng)求 ' 取消的標(biāo)志 Public Sub Cancel() mRunning = False End Sub ' 返回完成百分比值,并且 ' 只被 UI 線程調(diào)用 Public ReadOnly Property Percent() As Integer Get Return mPercent End Get End Property#End Region
此處唯一比較特殊的代碼位于 Start 方法中,我們可以在該方法中創(chuàng)建輔助線程然后啟動(dòng)該線程:
Dim backThread As New Thread(AddressOf mWorker.Start) backThread.Start()
要?jiǎng)?chuàng)建線程,需要在 Worker 對(duì)象的 IWorker 接口上傳遞 Start 方法的地址。然后,只需調(diào)用線程對(duì)象的 Start 方法即可開(kāi)始操作。此時(shí)我們要特別注意,UI 不應(yīng)直接與 Worker 交互,Worker 也不應(yīng)直接與 UI 交互。
請(qǐng)注意,Cancel 方法只設(shè)置一個(gè)標(biāo)志,表明我們不希望繼續(xù)運(yùn)行。輔助代碼應(yīng)定期查看此標(biāo)志,以確定是否應(yīng)該停止運(yùn)行。
現(xiàn)在,我們可以實(shí)現(xiàn) Worker 對(duì)象運(yùn)行時(shí)將由輔助線程調(diào)用的代碼。此代碼比較有趣,因?yàn)樗仨殞?Display 和 Completed 從輔助線程中轉(zhuǎn)至 UI 線程,同時(shí)還要在 UI 線程上完成此操作。
要完成此操作,我們可以使用 Form 對(duì)象的 Invoke 方法。此方法接受窗體應(yīng)該調(diào)用的方法的委托指針,以及包含該方法的參數(shù)的 Object 類(lèi)型數(shù)組。
Invoke 方法不直接調(diào)用窗體上的方法,而是請(qǐng)求窗體返回并使用窗體的 UI 線程調(diào)用該方法。此操作可通過(guò)向窗體發(fā)送 Windows 消息在后臺(tái)完成。這說(shuō)明窗體獲得這些方法調(diào)用的方式與從操作系統(tǒng)中獲得 click 或 keypress 事件的方式基本相同。
通常,這些細(xì)節(jié)不會(huì)影響大局。結(jié)果由 Invoke 方法觸發(fā)一個(gè)進(jìn)程,通過(guò)該進(jìn)程窗體將終止其 UI 線程上運(yùn)行的方法,這就是我們要實(shí)現(xiàn)的目標(biāo)。
再次重申,此代碼位于 Region 內(nèi),目的是為了明確它將在輔助線程上調(diào)用:
#Region " 從輔助線程調(diào)用的代碼 " ' 從輔助線程調(diào)用,以更新顯示 ' 這將觸發(fā)對(duì)包含狀態(tài)文本的 UI 的 ' 方法調(diào)用 - 該調(diào)用是在 UI 線程上 ' 進(jìn)行的 Private Sub Display(ByVal Text As String) _ Implements IController.Display Dim disp As New DisplayDelegate( _ AddressOf CType(mClient, IClient).Display) Dim ar() As Object = {Text} ' 調(diào)用 UI 線程上的客戶端窗體 ' 以更新顯示 mClient.BeginInvoke(disp, ar) End Sub ' 從輔助線程調(diào)用,以表明出現(xiàn)故障 ' 這將觸發(fā)對(duì)包含異常對(duì)象的 UI 的 ' 方法調(diào)用 - 該調(diào)用是在 UI 線程上 ' 進(jìn)行的 Private Sub Failed(ByVal e As Exception) _ Implements IController.Failed Dim disp As New FailedDelegate(_ AddressOf CType(mClient, IClient).Failed) Dim ar() As Object = {e} ' 在 UI 線程上調(diào)用客戶端窗體 ' 以表明出現(xiàn)故障 mClient.Invoke(disp, ar) End Sub ' 從輔助線程上調(diào)用,以指出完成的百分比 ' 值將轉(zhuǎn)到 Controller,由 UI 在需要時(shí)讀取 Private Sub SetPercent(ByVal Percent As Integer) _ Implements IController.SetPercent mPercent = Percent End Sub ' 從輔助線程調(diào)用,以表明已完成 ' 我們還傳遞參數(shù),以表明是否真正完成, ' 以及是否取消在 UI 線程上進(jìn)行的對(duì) UI ' 的調(diào)用 Private Sub Completed(ByVal Cancelled As Boolean) _ Implements IController.Completed mRunning = False Dim comp As New CompletedDelegate( _ AddressOf CType(mClient, IClient).Completed) Dim ar() As Object = {Cancelled} ' 調(diào)用 UI 線程上的客戶端窗體 ' 以表明已完成 mClient.Invoke(comp, ar) End Sub ' 表明是否仍在運(yùn)行或是否已請(qǐng)求取消 ' 這將在輔助線程上進(jìn)行調(diào)用,因此 ' 輔助代碼可以查看它是否應(yīng)該正常 ' 退出 Private ReadOnly Property Running() As Boolean _ Implements IController.Running Get Return mRunning End Get End Property#End Region
Failed 和 Completed 方法利用窗體的 Invoke 方法。例如,Failed 方法可以執(zhí)行以下操作:
Dim disp As New FailedDelegate(_ AddressOf CType(mClient, IClient).Failed) Dim ar() As Object = {e} ' 調(diào)用 UI 線程上的客戶端窗體 ' 以表明出現(xiàn)故障 mClient.Invoke(disp, ar)
首先創(chuàng)建一個(gè)委托,從 IClient 接口指向客戶端窗體的 Failed 方法。然后聲明包含向方法傳遞參數(shù)值的 Object 類(lèi)型數(shù)組。最后調(diào)用客戶端窗體的 Invoke 方法,將委托指針和參數(shù)數(shù)組傳遞給窗體。
窗體將在 UI 線程(窗體在這里可以安全運(yùn)行以更新顯示)上使用這些參數(shù)調(diào)用此方法。
整個(gè)進(jìn)程是同步進(jìn)行的,即對(duì)窗體進(jìn)行調(diào)用時(shí)輔助線程將停止。盡管可以在顯示錯(cuò)誤消息或完成消息時(shí)停止輔助線程,但我們并不希望顯示每個(gè)小狀態(tài)時(shí)都停止輔助線程。
為了避免顯示狀態(tài)時(shí)停止輔助線程,Display 方法將使用 BeginInvoke,而不使用 Invoke。BeginInvoke 使窗體上的方法調(diào)用異步進(jìn)行,這樣輔助線程可以一直保持運(yùn)行狀態(tài),不需要等待窗體上的顯示方法完成:
Dim disp As New DisplayDelegate( _ AddressOf CType(mClient, IClient).Display) Dim ar() As Object = {Text} ' 調(diào)用 UI 線程上的客戶端窗體 ' 以更新顯示 mClient.BeginInvoke(disp, ar)
以這種方式使用 BeginInvoke 可以防止輔助線程停止,使輔助線程具有盡可能高的性能。
ActivityBar 控件
最后,我們來(lái)創(chuàng)建顯示動(dòng)畫(huà)點(diǎn)的 ActivityBar 控件。
在名為 ActivityBar 的項(xiàng)目中添加一個(gè)用戶控件。
將該控件的寬度調(diào)整為約 110,高度調(diào)整為約 20。可以通過(guò)拖動(dòng)邊界進(jìn)行調(diào)整,也可以通過(guò)在 Properties(屬性)窗口中設(shè)置 Size 屬性進(jìn)行調(diào)整。
其余的操作將通過(guò)代碼完成。要?jiǎng)?chuàng)建一系列在顯示時(shí)不停閃爍的動(dòng)畫(huà)“燈”,可以使用帶有 Timer 控件的一系列 PictureBox 控件。每次 Timer 控件關(guān)閉時(shí),我們將使下一個(gè) PictureBox 呈綠色顯示,并將已經(jīng)呈綠色顯示的 PictureBox 更改為窗體的背景色。
將 Windows Forms(Windows 窗體)選項(xiàng)卡中的 Timer 控件放入窗體中,然后將其名稱更改為 tmAnim。同時(shí)將 Interval 屬性設(shè)置為 300,以獲得較好的動(dòng)畫(huà)速度。
順便說(shuō)一句,Components(組件)選項(xiàng)卡中有一個(gè)不同的 Timer 控件。它是一個(gè)多線程計(jì)時(shí)器。也就是說(shuō),該計(jì)時(shí)器將在后臺(tái)線程中引發(fā) Elapsed 事件,而不是象 Windows 窗體計(jì)時(shí)器那樣在 UI 線程上引發(fā) Elapsed 事件。建立 UI 時(shí)這種方法通常會(huì)產(chǎn)生相反的效果,因?yàn)?Elapsed 事件中的代碼顯然不能直接與我們的 UI 進(jìn)行交互。
現(xiàn)在,在控件中添加以下代碼:
Private mBoxes As New ArrayList() Private mCount As Integer Private Sub ActivityBar_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Dim index As Integer If mBoxes.Count = 0 Then For index = 0 To 6 mBoxes.Add(CreateBox(index)) Next End If mCount = 0 End Sub Private Function CreateBox(ByVal index As Integer) As PictureBox Dim box As New PictureBox() With box SetPosition(box, index) .BorderStyle = BorderStyle.Fixed3D .Parent = Me .Visible = True End With Return box End Function Private Sub GrayDisplay() Dim index As Integer For index = 0 To 6 CType(mBoxes(index), PictureBox).BackColor = Me.BackColor Next End Sub Private Sub SetPosition(ByVal Box As PictureBox, ByVal Index As Integer) Dim left As Integer = CInt(Me.Width / 2 - 7 * 14 / 2) Dim top As Integer = CInt(Me.Height / 2 - 5) With Box .Height = 10 .Width = 10 .Top = top .Left = left + Index * 14 End With End Sub Private Sub tmAnim_Tick(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles tmAnim.Tick CType(mBoxes((mCount + 1) Mod 7), PictureBox).BackColor = _ Color.LightGreen CType(mBoxes(mCount Mod 7), PictureBox).BackColor = Me.BackColor mCount += 1 If mCount > 6 Then mCount = 0 End Sub Public Sub Start() CType(mBoxes(0), PictureBox).BackColor = Color.LightGreen tmAnim.Enabled = True End Sub Public Sub [Stop]() tmAnim.Enabled = False GrayDisplay() End Sub Private Sub ActivityBar_Resize(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Resize Dim index As Integer For index = 0 To mBoxes.Count - 1 SetPosition(CType(mBoxes(index), PictureBox), index) Next End Sub
窗體的 Load 事件創(chuàng)建 PictureBox 控件并將它們放入數(shù)組,這樣便于我們?cè)谒鼈冎g循環(huán)。Timer 控件的 Tick 事件循環(huán)顯示,使各個(gè)控件依次呈綠色。
所有操作由 Start 方法開(kāi)始,由 Stop 事件結(jié)束。由于 Stop 是一個(gè)保留字,因此把這個(gè)方法名放在方括號(hào)內(nèi):[Stop]。Stop 方法不僅可以停止計(jì)時(shí)器,還可以灰顯所有框,告訴用戶這些框中當(dāng)前沒(méi)有活動(dòng)。
創(chuàng)建 Worker 類(lèi)
本文前面已簡(jiǎn)單介紹了 Worker 類(lèi)。因?yàn)槲覀円呀?jīng)定義了 IWorker 接口,所以可以增強(qiáng)該類(lèi),以利用我們創(chuàng)建的 Controller。
首先創(chuàng)建 Background.dll 文件。此步驟很重要,因?yàn)槿绻煌瓿纱瞬襟E,ActivityBar 控件將無(wú)法在我們建立測(cè)試窗體時(shí)顯示在工具箱上。
在解決方案中添加名為 bgTest 的 Windows Forms Application(Windows 窗體應(yīng)用程序)。在 Solution Explorer(解決方案資源瀏覽器)中用右鍵單擊該項(xiàng)目并選擇相應(yīng)的菜單項(xiàng),將該程序設(shè)置為啟動(dòng)項(xiàng)目。
然后使用 Add References(添加引用)對(duì)話框中的 Projects(項(xiàng)目)選項(xiàng)卡,添加對(duì) Background 項(xiàng)目的引用。
現(xiàn)在,在名為 Worker 的項(xiàng)目中添加一個(gè)類(lèi)。其中部分代碼與前面所述的代碼相同,但還包含一些不同的代碼,用以實(shí)現(xiàn) IWorker 接口(此處突出顯示的部分):
Imports Background
Public Class Worker Implements IWorker
Private mController As IController
Private mInner As Integer Private mOuter As Integer Public Sub New(ByVal InnerSize As Integer, ByVal OuterSize As Integer) mInner = InnerSize mOuter = OuterSize End Sub ' 由 Controller 調(diào)用,以便獲取
' Controller 的引用
Private Sub Init(ByVal Controller As IController) _
Implements IWorker.Initialize
mController = Controller
End Sub
Private Sub Work() Implements IWorker.Start
Dim innerIndex As Integer Dim outerIndex As Integer Dim value As Double Try
For outerIndex = 0 To mOuter If mController.Running Then
mController.Display("Outer loop " & outerIndex & " starting")
mController.SetPercent(CInt(outerIndex / mOuter * 100))
Else
' 它們請(qǐng)求取消
mController.Completed(True)
Exit Sub
End If
For innerIndex = 0 To mInner ' 此處進(jìn)行一些有意思的計(jì)算 value = Math.Sqrt(CDbl(innerIndex - outerIndex)) Next Next mController.SetPercent(100)
mController.Completed(False)
Catch e As Exception
mController.Failed(e)
End Try
End SubEnd Class
我們添加了能夠?qū)崿F(xiàn) IWorker.Initialize 的 Init 方法。Controller 將調(diào)用此方法,因此以后我們可以引用 Controller 對(duì)象。
我們還將 Work 方法更改為 Private,只是為了實(shí)現(xiàn) IWorker.Start 方法。此方法將在輔助線程上運(yùn)行。
我們?cè)鰪?qiáng)了 Work 方法,使其可以使用 Try..Catch 塊。這樣我們可以使用 Controller 上的 Failed 方法捕捉任何錯(cuò)誤并將其返回給 UI。
假設(shè)代碼正在運(yùn)行,我們調(diào)用 Controller 對(duì)象的 Display 和 SetPercent 方法,使它們隨著代碼的運(yùn)行更新其狀態(tài)和完成的百分比。
我們還定期檢查 Controller 對(duì)象的 Running 屬性,查看是否存在取消請(qǐng)求。如果存在取消請(qǐng)求,則停止進(jìn)程,并指示由于取消請(qǐng)求而停止操作。