Come far funzionare il codice senza problemi utilizzando timer e thread diversi
Sto cercando di impedire il blocco della GUI , a causa di un intervallo di timer basso e troppo da elaborare nel Timer.Tickgestore eventi.
Ho cercato su Google un po 'di tempo e ho capito che non posso aggiornare l'interfaccia utente da nessun altro thread diverso dal thread dell'interfaccia utente.
Quindi, cosa succede se stai usando molti controlli sotto Timer1.Tick?
Come posso aggiornare un'etichetta quando i dati vengono scaricati con WebClient con un timer, non vuoi abbassare troppo l'intervallo e mantenere allo stesso tempo reattiva l'interfaccia utente?
Ricevo eccezioni di violazione di Cross Thread quando accedo a elementi dell'interfaccia utente, un ListBox1 e un RichTextBox.
Qual è il modo corretto per aggiornare l'interfaccia utente con un timer e / o un thread senza causare eccezioni per minacce incrociate?
Risposte
Esistono diversi modi per aggiornare gli elementi dell'interfaccia utente da un thread diverso dal thread dell'interfaccia utente.
Puoi usare il InvokeRequired/Invoke()pattern ( meh ), chiamare il metodo asincrono BeginInvoke() , Post()al SynchronizationContext , magari mescolato con un AsyncOperation + AsyncOperationManager (solido stile BackGroundWorker), usare un callback asincrono ecc.
C'è anche la Progress
Questa classe fornisce un modo abbastanza semplificato per catturare il punto in SynchronizationContextcui viene creato l'oggetto della classe e Post()tornare al contesto di esecuzione acquisito.
Il Progress<T>delegato creato nel thread dell'interfaccia utente viene chiamato in quel contesto. Dobbiamo solo passare l' Progress<T>oggetto e gestire le notifiche che riceviamo tramite il Action<T>delegato.
Stai scaricando e gestendo una stringa, quindi il tuo Progress<T>oggetto sarà un Progress(Of String): quindi, ti restituirà una stringa.
Il timer viene sostituito da un'attività che esegue il codice e ritarda anche le sue azioni da un intervallo che è possibile specificare, come con un timer, qui utilizzando Task.Delay ([Interval]) tra ogni azione . C'è un StopWatch che misura il tempo effettivamente impiegato da un download e regola il ritardo in base all'intervallo specificato (non è una cosa di precisione , comunque).
► Nel codice di esempio, l'attività di download può essere avviata e interrotta utilizzando i metodi StartDownload()e StopDownload()di una classe helper.
Il StopDownload()metodo è attende, esegue l'annullamento delle attività correnti e elimina gli oggetti usa e getta utilizzati.
► Ho sostituito WebClient con HttpClient, è ancora abbastanza semplice da usare (almeno nelle sue basi), fornisce metodi asincroni che supportano a CancellationToken(anche se un download in corso richiede un po 'di tempo per essere annullato, ma è gestito qui).
► Un clic del pulsante inizializza e avvia i download a tempo e un altro lo interrompe (ma puoi chiamare il StopDownload()metodo quando il modulo si chiude, o, beh, ogni volta che ne hai bisogno).
► Il Progress<T>delegato è solo un Lambda qui: non c'è molto da fare, basta riempire un ListBox e scorrere un RichTextBox. Puoi inizializzare l'oggetto della classe helper (si chiama MyDownloader: ovviamente sceglierai un altro nome, questo è ridicolo) e chiamare il suo StartDownload()metodo, passando l'oggetto Progress, l'Uri e l'Interval tra ogni download.
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 helper:
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 responseData As String = String.Empty
Dim pattern As String = "<(?:[^>=]|='[^']*'|=""[^""]*""|=[^'""][^\s>]*)*>"
Dim downloadTimeWatch As Stopwatch = New Stopwatch()
downloadTimeWatch.Start()
Do
If cts.IsCancellationRequested Then Return
Try
Using response = Await client.GetAsync(url, HttpCompletionOption.ResponseContentRead, token)
responseData = Await response.Content.ReadAsStringAsync()
responseData = WebUtility.HtmlDecode(Regex.Replace(responseData, pattern, ""))
End Using
progress.Report(responseData)
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
Gli accessi alla casella di riepilogo e alla casella di testo RTF devono essere eseguiti sul thread dell'interfaccia utente. Il modo più semplice per farlo è così.
Me.Invoke(Sub()
ListBox1.Items.Clear()
ListBox1.Items.AddRange(Split(clientdecode, vbLf))
RichTextBox1.SelectionStart() = RichTextBox1.TextLength
RichTextBox1.ScrollToCaret()
End Sub)