[VB.net] 如何實現「非同步下載」檔案?(五)做成「使用者控制項」。

上篇貼文已經做到了利用多執行緒手法完成了 FTP 下載部分的非同步作業,並且解決了 WebClient 的 DownloadFileAsync() 中向 FTP 下載時的幾個問題:
1.FTP 下載前先行取回檔案長度的問題(使用 Socket 直接向 Server 查詢來解決)。
2.FTP 下載時 ProgressBar 顯示的問題。(經由取得檔案長度得到解決)。
3.第一次下載初始等待時間較長的問題。(使用 Socket 做為主要元件得到解決)。
4.等待間 UI 被鎖住,開始下載後才進入非同步作業。(使用多執行緒解決)。

 

上篇貼文已經做到了利用多執行緒手法完成了 FTP 下載部分的非同步作業,並且解決了 WebClient 的 DownloadFileAsync() 中向 FTP 下載時的幾個問題:

  1. FTP 下載前先行取回檔案長度的問題(使用 Socket 直接向 Server 查詢來解決)。
  2. FTP 下載時 ProgressBar 顯示的問題。(經由取得檔案長度得到解決)。
  3. 第一次下載初始等待時間較長的問題。(使用 Socket 做為主要元件得到解決)。
  4. 等待間 UI 被鎖住,開始下載後才進入非同步作業。(使用多執行緒解決)。

 

接下來是把這個功能做成「使用者控制項」類別讓使用更為便利,關於使用者控制項的前置準備工程可參考(這篇)。

以下是測試10條連線(大陸稱為多線程)同時下載的模樣:

image image

 

簡介製做過程:

  1. 命名空間就用 System.Windows.Forms,這可使每次開發 WindowsForm 專案時會自動加入參考。
  2. 讓控制項擁有自己的 ProgressBar,這個 Bar 就用之前做的(自製 ProgressBar)取其色彩變化較有彈性,且在 XP 或 Windows Server 平台上皆有一致的外觀。
  3. 雖說有自己的 ProgressBar,但也要有事件通知呼叫端的能力,以便呼叫端也能建立自己的 UI(如果不喜歡內建 UI 的話)。
  4. 整合 WebClient 類別的 DownloadFileAsync() 的回呼事件,和 FTP 下載時共用相同的事件傳回呼叫端。

 

程式碼重點(這裡只貼和 UserControl 相關的 Code,其他的之前已貼過了):

  1. 事件部分(其中下載進度事件要傳回的數據較多,所以採用和 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
    


  2. 屬性部分(其實可以用繼承 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
    


  3. 回呼(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
    


  4. 建構式設計了二種,除了基本的不帶參數外再加一個直接傳入(下載檔案)和(儲存位置)的,讓使用起來更方便一些。

    
        '---建構式---
        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
    


  5. 解決跨執行緒回呼到最上層更新 UI 時發生的例外情形。
    1. 在控制項裡使用了 Delegate 和 Invoke 更新「自己的」 ProgressBar 不會有問題,但從控制項再傳事件給更上層做 UI 更新時還是會引發該例外事件。
    2. 雖然前端也可以再度使用 Delete 和 Invoke 解決,但考慮到控制項是給別人用的應避免複雜化,所以在控制項 Initialize 時用了 Control.CheckForIllegalCrossThreadCalls = False。
    3. 這個做法可能不是正規手法,但至少可以解決當下的問題,所以就先將就著用了。
    4. 工具箱裡的 BackgroundWorker Control 和 WebClient 的 DownloadFileAsync() 都可以直接更新 UI,不曉得是用了什麼手法。
  6. 測試表單的程式碼(分別找了各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
    
    
    • 設計階段控制項使用時的擷圖:

   image

 

控制項 Dll 及 demo 程式下載:非同步下載_05_demo.rar

專案原始碼:非同步下載_05_製做成控制項.rar


ku3