上篇貼文已經做到了利用多執行緒手法完成了 FTP 下載部分的非同步作業,並且解決了 WebClient 的 DownloadFileAsync() 中向 FTP 下載時的幾個問題:
1.FTP 下載前先行取回檔案長度的問題(使用 Socket 直接向 Server 查詢來解決)。
2.FTP 下載時 ProgressBar 顯示的問題。(經由取得檔案長度得到解決)。
3.第一次下載初始等待時間較長的問題。(使用 Socket 做為主要元件得到解決)。
4.等待間 UI 被鎖住,開始下載後才進入非同步作業。(使用多執行緒解決)。
上篇貼文已經做到了利用多執行緒手法完成了 FTP 下載部分的非同步作業,並且解決了 WebClient 的 DownloadFileAsync() 中向 FTP 下載時的幾個問題:
- FTP 下載前先行取回檔案長度的問題(使用 Socket 直接向 Server 查詢來解決)。
- FTP 下載時 ProgressBar 顯示的問題。(經由取得檔案長度得到解決)。
- 第一次下載初始等待時間較長的問題。(使用 Socket 做為主要元件得到解決)。
- 等待間 UI 被鎖住,開始下載後才進入非同步作業。(使用多執行緒解決)。
接下來是把這個功能做成「使用者控制項」類別讓使用更為便利,關於使用者控制項的前置準備工程可參考(這篇)。
以下是測試10條連線(大陸稱為多線程)同時下載的模樣:
簡介製做過程:
- 命名空間就用 System.Windows.Forms,這可使每次開發 WindowsForm 專案時會自動加入參考。
- 讓控制項擁有自己的 ProgressBar,這個 Bar 就用之前做的(自製 ProgressBar)取其色彩變化較有彈性,且在 XP 或 Windows Server 平台上皆有一致的外觀。
- 雖說有自己的 ProgressBar,但也要有事件通知呼叫端的能力,以便呼叫端也能建立自己的 UI(如果不喜歡內建 UI 的話)。
- 整合 WebClient 類別的 DownloadFileAsync() 的回呼事件,和 FTP 下載時共用相同的事件傳回呼叫端。
程式碼重點(這裡只貼和 UserControl 相關的 Code,其他的之前已貼過了):
-
事件部分(其中下載進度事件要傳回的數據較多,所以採用和 DownloadFileAsync() 相同的參數格式):
#Region "---事件宣告---" '---定義參數包--- Public Structure 下載進度_Args Dim BytesReceived As Long Dim TotalBytesToReceive As Long Dim ProgressPercentage As Integer Dim UserState As Object Dim ProgressPercentageStr As String End Structure Public Event 作業完成(sender As ku_DownLoader, ByVal 描述 As String) Public Event 發生例外(sender As ku_DownLoader, ByVal 描述 As String) Public Event 進度改變(sender As ku_DownLoader, e As 下載進度_Args) #End Region
-
屬性部分(其實可以用繼承 ProgressBar 和 Implements Interface 的方法簡化和 ProgressBar 的介接,但因為原來的 ProgressBar 在設計之初沒有想到這一點,所以屬性安排有些笨拙):
#Region "---屬性宣告---" <Description("下載所用的帳號"), Browsable(True), Category("行為")> _ Public WriteOnly Property 帳號() As String Set(ByVal value As String) new_帳號 = value End Set End Property <Description("下載所用的密碼"), Browsable(True), Category("行為")> _ Public WriteOnly Property 密碼() As String Set(ByVal value As String) new_密碼 = value End Set End Property <Description("要下載檔案的 URI"), Browsable(True), Category("行為")> _ Public Property 遠端檔案徑名() As String Get Return new_遠端檔案徑名 End Get Set(ByVal value As String) new_遠端檔案徑名 = value End Set End Property <Description("檔案儲存的位置"), Browsable(True), Category("行為")> _ Public WriteOnly Property 近端儲存位置() As String Set(ByVal value As String) new_近端儲存位置 = If(String.IsNullOrEmpty(value) Or String.IsNullOrWhiteSpace(value), 預設儲存位置, value) End Set End Property <Description("以被動模式連接 FTP"), Browsable(True), Category("行為")> _ Public Property 被動模式() As Boolean Get Return new_FTP被動模式 End Get Set(ByVal value As Boolean) new_FTP被動模式 = value End Set End Property <Description("緩衝區大小"), Browsable(True), Category("行為")> _ Public Property 緩衝區大小() As Integer Get Return new_緩衝區大小 End Get Set(ByVal value As Integer) If value < 512 Then value = 512 If value > 65536 Then value = 65536 new_緩衝區大小 = value End Set End Property <Description("決定進度圖顯示樣式"), Browsable(True), Category("外觀")> _ Public Property 進度圖條樣式() As 色塊樣式 Get Return new_進度圖顯示樣式 End Get Set(ByVal value As 色塊樣式) new_進度圖顯示樣式 = value ProgressBar1.顯示樣式 = value End Set End Property <Description("決定進圖外框樣式"), Browsable(True), Category("外觀")> _ Public Property 進度圖外框樣式() As BorderStyle Get Return ProgressBar1.BorderStyle End Get Set(ByVal value As BorderStyle) ProgressBar1.BorderStyle = value End Set End Property <Description("決定是否依據父容器的 BackColor 決定配色"), Browsable(True), Category("外觀")> _ Public Property 自動配色() As Boolean Get Return ProgressBar1.自動配色 End Get Set(ByVal value As Boolean) ProgressBar1.自動配色 = value End Set End Property <Description("非自動配色時進度列已運行區域的色彩值"), Browsable(True), Category("外觀")> _ Public Overloads Property 已運行區域色彩() As Color Get Return new_已運行區域色彩 End Get Set(ByVal value As Color) new_已運行區域色彩 = value ProgressBar1.ForeColor = value End Set End Property <Description("非自動配色時進度列未運行區域的色彩值"), Browsable(True), Category("外觀")> _ Public Overloads Property 未運行區域色彩() As Color Get Return new_未運行區域色彩 End Get Set(ByVal value As Color) new_未運行區域色彩 = value ProgressBar1.BackColor = value End Set End Property #End Region
-
回呼(Callback)及工具提示(ToolTip)部分:
#Region "---Callback 處理---" '---宣告委派程序用來更新 UI,並傳送事件至呼叫端--- Delegate Sub cb_顯示進度(bar As ku_ProgressBar, 已收到位元組 As Long, 位元組總數 As Long) Delegate Sub cb_顯示結果(e As System.ComponentModel.AsyncCompletedEventArgs) '---顯示結果(接受跨執行緒呼叫)--- Sub 顯示結果(e As System.ComponentModel.AsyncCompletedEventArgs) Try If InvokeRequired Then Dim callback As New cb_顯示結果(AddressOf 顯示結果) Invoke(callback, New Object() {e}) Else Select Case True Case e.Cancelled lbl_狀態.Text = "使用者中斷了作業!" RaiseEvent 發生例外(Me, lbl_狀態.Text) Case Not IsNothing(e.Error) lbl_狀態.Text = "下載失敗!" MsgBox(e.Error.GetBaseException.Message) RaiseEvent 發生例外(Me, lbl_狀態.Text) Case Else RaiseEvent 作業完成(Me, lbl_狀態.Text) End Select End If Catch ex As Exception Finally 工具提示() '---最後結束時的工具提示--- End Try End Sub '---顯示進度(多執行緒的做法)--- Sub 顯示進度(bar As ku_ProgressBar, 收到位元組 As Long, 位元組總數 As Long) Try If InvokeRequired Then Dim callback As New cb_顯示進度(AddressOf 顯示進度) Invoke(callback, New Object() {bar, 收到位元組, 位元組總數}) Else Dim 百分比 As Single = If(位元組總數 > 0, (收到位元組 / 位元組總數 * 100), 0) Dim 百分比整數 = CInt(百分比) 百分比字串 = "(" & Format(百分比 / 100, "percent") & ")" bar.Value = 百分比整數 lbl_下載進度.Text = Format(收到位元組, "#,### Bytes. ") & 百分比字串 lbl_狀態.Text = If(收到位元組 = 位元組總數, "下載成功。", "傳送中...") '---建立回送事件參數--- Dim e = New 下載進度_Args With e .BytesReceived = 收到位元組 .TotalBytesToReceive = 位元組總數 .ProgressPercentage = 百分比整數 .ProgressPercentageStr = 百分比字串 .UserState = Me End With RaiseEvent 進度改變(Me, e) 工具提示() '---FTP 下載中調整工具提示--- End If Catch ex As Exception End Try End Sub '---處理工具提示--- Sub 工具提示() Dim r1, r2, r3, r4, r5, r6 As String r1 = "正下載檔案:" & new_遠端檔案徑名 & vbCrLf r2 = " 儲存位置:" & new_近端儲存位置 & vbCrLf r3 = "檔案位元組:" & Format(下載檔案大小, "#,### Bytes. ") & vbCrLf r4 = "收到位元組:" & Format(已收到位元組, "#,### Bytes. ") & vbCrLf r5 = "完成百分比:" & 百分比字串 & vbCrLf r6 = " 狀態:" & lbl_狀態.Text ToolTip1.ToolTipTitle = lbl_狀態.Text Dim sb As New StringBuilder sb.Append(r1).Append(r2).Append(r3).Append(r4).Append(r5).Append(r6) ToolTip1.SetToolTip(Me, sb.ToString) ToolTip1.SetToolTip(lbl_下載進度, sb.ToString) ToolTip1.SetToolTip(lbl_狀態, sb.ToString) ToolTip1.SetToolTip(ProgressBar1.lbl_已運行區域, sb.ToString) ToolTip1.SetToolTip(ProgressBar1.lbl_未運行區域, sb.ToString) End Sub #End Region
-
建構式設計了二種,除了基本的不帶參數外再加一個直接傳入(下載檔案)和(儲存位置)的,讓使用起來更方便一些。
'---建構式--- Sub New() INIT() End Sub Sub New(遠端檔案 As String, 儲存位置 As String) INIT() 下載檔案(遠端檔案, 儲存位置) End Sub Private Sub INIT() InitializeComponent() Control.CheckForIllegalCrossThreadCalls = False 被動模式 = True ProgressBar1.Value = 0 ProgressBar1.Value = 25 Me.Width = 160 Me.Height = 40 End Sub
-
解決跨執行緒回呼到最上層更新 UI 時發生的例外情形。
- 在控制項裡使用了 Delegate 和 Invoke 更新「自己的」 ProgressBar 不會有問題,但從控制項再傳事件給更上層做 UI 更新時還是會引發該例外事件。
- 雖然前端也可以再度使用 Delete 和 Invoke 解決,但考慮到控制項是給別人用的應避免複雜化,所以在控制項 Initialize 時用了 Control.CheckForIllegalCrossThreadCalls = False。
- 這個做法可能不是正規手法,但至少可以解決當下的問題,所以就先將就著用了。
- 工具箱裡的 BackgroundWorker Control 和 WebClient 的 DownloadFileAsync() 都可以直接更新 UI,不曉得是用了什麼手法。
-
測試表單的程式碼(分別找了各5個檔案位置供測試用):
Public Class Form1 Dim 儲存位置 As String Private Sub 從HTTP下載() Handles btn_1.Click Ku_DownLoader6.下載檔案("http://db2.tspes.ntpc.edu.tw/ts9x9_v3341_setup.exe", 儲存位置 & "\HTTP\") Ku_DownLoader7.下載檔案("http://download.bittorrent.com/dl/BitTorrent-7.5.exe", 儲存位置 & "\HTTP\") Ku_DownLoader8.下載檔案("http://www.hyperionics.com/downloads/HS7Setup.exe", 儲存位置 & "\HTTP\") Ku_DownLoader9.下載檔案("http://download.skype.com/3694814915aaa38100bfa0933f948e65/partner/1/SkypeSetupFull.exe", 儲存位置 & "\HTTP\") Ku_DownLoader10.下載檔案("http://aihdownload.adobe.com/bin/install_flashplayer11x64ax_gtbp_chra_aih.exe", 儲存位置 & "\HTTP\") End Sub Private Sub 從FTP下載() Handles btn_2.Click Ku_DownLoader1.下載檔案("ftp://ftp.adobe.com/pub/adobe/flash/flash_player_10_appicon.zip", 儲存位置 & "\FTP\") Ku_DownLoader2.下載檔案("ftp://ftp.adobe.com/pub/adobe/updater/win/6.x/AdobeAUM6.0All.zip", 儲存位置 & "\FTP\") Ku_DownLoader3.下載檔案("ftp://ftp.rarlab.com/rar/FarManager170.exe", 儲存位置 & "\FTP\") Ku_DownLoader4.下載檔案("ftp://ftp.tspes.ntpc.edu.tw/ts9x9_v3341_setup.exe", 儲存位置 & "\FTP\") Ku_DownLoader5.下載檔案("ftp://ftp.microsoft.com/bussys/sql/ODBCUTIL.ZIP", 儲存位置 & "\FTP\") End Sub Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load Me.Text = Application.ProductName & " v" & Application.ProductVersion.ToString 儲存位置 = My.Computer.FileSystem.SpecialDirectories.MyDocuments LinkLabel_FTP.Tag = 儲存位置 & "\FTP" LinkLabel_HTTP.Tag = 儲存位置 & "\HTTP" End Sub Private Sub 開啟檔案位置(sender As System.Object, e As System.Windows.Forms.LinkLabelLinkClickedEventArgs) _ Handles LinkLabel_HTTP.LinkClicked, LinkLabel_FTP.LinkClicked Dim obj = CType(sender, LinkLabel) Process.Start("explorer.exe", sender.Tag) End Sub End Class
- 設計階段控制項使用時的擷圖:
控制項 Dll 及 demo 程式下載:非同步下載_05_demo.rar
專案原始碼:非同步下載_05_製做成控制項.rar