Domanda [vb.net] Usare progressbar con http client e progress T class

Matty80

Utente Bronze
18 Settembre 2020
45
11
0
25
Ciao a tutti, sto provando a spulciare nel web per creare un downloader veramente asincrono. Tutti i codici o sample program che sto provando non sono completamente asincroni, e dunque, tutti freezano o meglio dire, non aggiornano il valore di una progress bar. O la aggiornano alla fine del download o nel caso di file di 20 o più mb, non si muove affatto!!
Sto provando ad usare la classe Progress <T> e la sua interfaccia IProgress<T>.
Il codice per scaricare funziona, ma vorrei implementare una progress bar funzionante e completamente asincrona, nel senso che anche se scaricassi un file prova di 1 giga, la progress bar resti efficiente e mostri il reale progresso del download.
Questo è il mio codice:
Codice:
Private downloader As MyDownloader = Nothing

Private Sub btnStartDownload_Click(sender As Object, e As EventArgs) Handles btnStartDownload.Click
    Dim progress = New Progress(Of String)(
        Sub(data)
            ' We're on the UI Thread here
            ListBox1.Items.Clear()
            ListBox1.Items.AddRange(Split(data, vbLf))
            RichTextBox1.SelectionStart = RichTextBox1.TextLength
        End Sub)

    Dim url As Uri = New Uri("https://SomeAddress.com")
    downloader = New MyDownloader()
    ' Download from url every 1 second and report back to the progress delegate
    downloader.StartDownload(progress, url, 1)

Private Async Sub btnStopDownload_Click(sender As Object, e As EventArgs) Handles btnStopDownload.Click
    Await downloader.StopDownload()
End Sub

La classe d'aiuto:
Codice:
Imports System.Diagnostics
Imports System.Net
Imports System.Net.Http
Imports System.Text.RegularExpressions

Public Class MyDownloader
    Private Shared ReadOnly client As New HttpClient()
    Private ReadOnly cts As CancellationTokenSource = New CancellationTokenSource()
    Private interval As Integer = 0

    Public Sub StartDownload(progress As IProgress(Of String), url As Uri, intervalSeconds As Integer)
        interval = intervalSeconds * 1000
        Task.Run(Function() DownloadAsync(progress, url, cts.Token))
    End Sub

    Private Async Function DownloadAsync(progress As IProgress(Of String), url As Uri, token As CancellationToken) As Task
        Dim pattern As String = "<(?:[^>=]|='[^']*'|=""[^""]*""|=[^'""][^\s>]*)*>"
        Dim downloadTimeWatch As Stopwatch = New Stopwatch()
        downloadTimeWatch.Start()
        Do
            If cts.IsCancellationRequested Then Return
            Try
                Dim response = Await client.GetAsync(url, HttpCompletionOption.ResponseContentRead, token)
                Dim data = Await response.Content.ReadAsStringAsync()

                data = WebUtility.HtmlDecode(Regex.Replace(data, pattern, ""))
                progress.Report(data)

                Dim delay = interval - CInt(downloadTimeWatch.ElapsedMilliseconds)
                Await Task.Delay(If(delay <= 0, 10, delay), token)
                downloadTimeWatch.Restart()
            Catch tcEx As TaskCanceledException
                ' Don't care - catch a cancellation request
                Debug.Print(tcEx.Message)
            Catch wEx As WebException
                ' Internet connection failed? Internal server error? See what to do
                Debug.Print(wEx.Message)
            End Try
        Loop
    End Function

    Public Async Function StopDownload() As Task
        Try
            cts.Cancel()
            client?.CancelPendingRequests()
            Await Task.Delay(interval)
        Finally
            client?.Dispose()
            cts?.Dispose()
        End Try
    End Function
End Class

Probabilmente scaricare da url ogni secondo non è il mio obbiettivo, ma avendo usato un codice non mio, proverò a rimuovere quella funzione successivamente.
Il mio obbiettivo per adesso è inserire una progressbar, e magari anche delle label con i bytes ricevuto e quelli totali da ricevere.
Grazie
 
Un piccolo esempio usando WebClient, completamente asincrono, con percentuale e numero bytes ricevuti / totale:

Codice:
Imports System.Net
Public Class Form1

    Protected Friend WithEvents _WClient As New WebClient()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ProgressBar1.Minimum = 0
        ProgressBar1.Maximum = 100
        _WClient.DownloadFileAsync(New Uri("https://speed.hetzner.de/100MB.bin"), "xyz.bin")
    End Sub

    Private Sub _WClient_DownloadProgressChanged(sender As Object, e As DownloadProgressChangedEventArgs) Handles _WClient.DownloadProgressChanged
        ProgressBar1.Value = e.ProgressPercentage
        StatusLabel.Text = e.BytesReceived & "B / " & e.TotalBytesToReceive & "B"
    End Sub

    Private Sub _WClient_DownloadFileCompleted(sender As Object, e As AsyncCompletedEventArgs) Handles _WClient.DownloadFileCompleted
        MsgBox("Download Completed!")
    End Sub
End Class
 
Ultima modifica:
Ciao Junk,
Ho provato il tuo codice con questo url: link ottenuto da link2.
Questo crea il problema che dicevo prima, la progress bar non funziona e i bytes totali sono pari a -1B
Edit 1: nel caso del link1, i bytes ricevuti e la progress bar si "sbloccano" una volta terminato il download, il che mi fa pensare non è completamente asincrono.
Inoltre, il codice non funziona per file come 512 mb file zip test. Ecco perchè volevo optare per tenere quel codice principale e usare la classe Progress T, che a quanto pare ci viene in aiuto per mostrare il reale progresso nella progressbar
Grazie

edit2: Non è mio interesse scaricare file superiori ai 50mb, ma usando file grandi mi aiuta a capire i limiti del codice con cui sto lavorando.
 
Ciao Junk,
Ho provato il tuo codice con questo url: link ottenuto da link2.
Questo crea il problema che dicevo prima, la progress bar non funziona e i bytes totali sono pari a -1B
Edit 1: nel caso del link1, i bytes ricevuti e la progress bar si "sbloccano" una volta terminato il download, il che mi fa pensare non è completamente asincrono.
Inoltre, il codice non funziona per file come 512 mb file zip test. Ecco perchè volevo optare per tenere quel codice principale e usare la classe Progress T, che a quanto pare ci viene in aiuto per mostrare il reale progresso nella progressbar
Grazie

edit2: Non è mio interesse scaricare file superiori ai 50mb, ma usando file grandi mi aiuta a capire i limiti del codice con cui sto lavorando.

Il metodo e' asincrono e funziona, non dipende dalla grandezza, il link 1, se guardi in e.Error.Message nella procedura DownloadFileCompleted da' errore:
"The request was aborted: Could not create SSL/TLS secure channel."

Mentre per il 512 mb file zip test ritorna errore 403 forbidden.

Probabilmente devi giocare con gli header perche' quei siti non collaborano pensando che il tuo non sia un browser valido.
 
Ciao Junk,
il link1 scarica il file mp3, ma i total bytes ricevuti sono -1B dunque la progressbar non funziona. Devo utilizzare headers anche per questo?

Potrebbe anche essere che negli header di risposta di quel sito non sia presente la Content-Length, come spesso capita con servizi del genere che scaricano da remoto e convertono il file. Per cui il webclient non e' in grado di stabilire quanto e' lungo il file finche' non finisce di scaricarlo, puoi appurarlo anche dal browser che al posto di una percentuale mostra una progressbar che carica in modo indefinito, con la dicitura "Unknown time left". Non c'e' soluzione visto che e' il sito a doverti dare questa informazione a monte dell'invio del file.
 
Potrebbe anche essere che negli header di risposta di quel sito non sia presente la Content-Length, come spesso capita con servizi del genere che scaricano da remoto e convertono il file. Per cui il webclient non e' in grado di stabilire quanto e' lungo il file finche' non finisce di scaricarlo, puoi appurarlo anche dal browser che al posto di una percentuale mostra una progressbar che carica in modo indefinito, con la dicitura "Unknown time left". Non c'e' soluzione visto che e' il sito a doverti dare questa informazione a monte dell'invio del file.

Hmm capisco..
ho aggiunto:
Codice:
 _WClient.Headers.Add(HttpRequestHeader.UserAgent, "")
e la situazione migliora per alcuni e resta la stessa per tutti gli altri.
Non capisco perchè link1 restituisce i totalbytes Size: 12.47 MB
mentre link2 ( Size: 14.99 MB ) restituisce total bytes -1B
Non riesco a capire..Posso chiederti come posso verificare che negli header di risposta del sito non è presente la Content-Lenght?
Ti ringrazio

EDIT1: link3 Size: 11.22 MB / Total bytes -1B, che strano, anche per i file più piccoli ! Sembra quasi vada alla cieca, no?
 
E' come ho detto, lui non ti da la lunghezza quindi devi scaricare finche' non hai finito, esempio: hai scaricato 5 MB fin ora, che percentuale e' 5MB su potenzialmente infinito?

Richiesta con Content-Length: (hai i totalbytes, quindi anche percentuale)
withheader.png


Richiesta senza Content-Length: (non li hai, scarichi alla cieca)
noheader.png
 
Ultima modifica:
E' come ho detto, lui non ti da la lunghezza quindi devi scaricare finche' non hai finito, esempio: hai scaricato 5 MB fin ora, che percentuale e' 5MB su potenzialmente infinito?

Richiesta con Content-Length: (hai i totalbytes, quindi anche percentuale)
Visualizza allegato 45669

Richiesta senza Content-Length: (non li hai, scarichi alla cieca)
Visualizza allegato 45670

Ti ringrazio per spiegarti con immagini illustrative!!
Dunque nulla da fare, mi tengo solo I bytes ricevuti dunque.
Grazie!
EDIT1:
Forse ho un'idea. Se noti, il sito mi da comunque la dimensione del file link .
Potrei fare un parsing con (webclient sarebbe possibie??..) webbrowser sulla label e usarla come total bytes ricevuti, così facendo la progress bar funzionerà sempre.. che ne pensi?
 
Un piccolo esempio usando WebClient, completamente asincrono, con percentuale e numero bytes ricevuti / totale:

Codice:
Imports System.Net
Public Class Form1

    Protected Friend WithEvents _WClient As New WebClient()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ProgressBar1.Minimum = 0
        ProgressBar1.Maximum = 100
        _WClient.DownloadFileAsync(New Uri("https://speed.hetzner.de/100MB.bin"), "xyz.bin")
    End Sub

    Private Sub _WClient_DownloadProgressChanged(sender As Object, e As DownloadProgressChangedEventArgs) Handles _WClient.DownloadProgressChanged
        ProgressBar1.Value = e.ProgressPercentage
        StatusLabel.Text = e.BytesReceived & "B / " & e.TotalBytesToReceive & "B"
    End Sub

    Private Sub _WClient_DownloadFileCompleted(sender As Object, e As AsyncCompletedEventArgs) Handles _WClient.DownloadFileCompleted
        MsgBox("Download Completed!")
    End Sub
End Class
C'è la possibilità di creare un handler per il download cancel? Grazie