Como deixar o código funcionar sem problemas usando timers e diferentes threads
Estou tentando evitar que a GUI congele , por causa de um intervalo de timer baixo e muito para processar no Timer.Tickmanipulador de eventos.
Estou pesquisando no Google há algum tempo e entendi que não posso atualizar a IU a partir de qualquer outro thread que não seja o thread da IU
Então, e se você estiver usando muitos controles sob Timer1.Tick?
Como posso atualizar um rótulo quando os dados são baixados com WebClient com um cronômetro, você não quer diminuir muito o intervalo e manter a IU responsiva ao mesmo tempo?
Eu recebo exceções de violação de Cross Thread quando acesso elementos da interface do usuário, um ListBox1 e um RichTextBox.
Qual é a maneira correta de atualizar a IU com um cronômetro e / ou Thread sem causar exceções de ameaças cruzadas?
Respostas
Você tem maneiras diferentes de atualizar os elementos da UI a partir de um Thread diferente do UI Thread.
Você pode usar o InvokeRequired/Invoke()padrão ( meh ), chamar o método assíncrono BeginInvoke() , Post()para o SynchronizationContext , talvez misturado com um AsyncOperation + AsyncOperationManager (estilo BackGroundWorker sólido), usar um retorno de chamada assíncrono etc.
Também existe a Progress
Esta classe fornece uma maneira bastante simplificada de capturar SynchronizationContextonde o objeto de classe é criado e de Post()volta ao contexto de execução capturado.
O Progress<T>delegado criado no UI Thread é chamado nesse contexto. Precisamos apenas passar o Progress<T>objeto e lidar com as notificações que recebemos por meio do Action<T>delegado.
Você está baixando e manipulando uma string, então seu Progress<T>objeto será um Progress(Of String): portanto, ele retornará uma string para você.
O Timer é substituído por uma Tarefa que executa seu código e também atrasa suas ações por um Intervalo que você pode especificar, como com um Timer, usando Task.Delay ([Interval]) entre cada ação . Existe um StopWatch que mede o tempo que um download realmente leva e ajusta o Delay com base no intervalo especificado (não é uma coisa de precisão , de qualquer maneira).
► No código de amostra, a Tarefa de download pode ser iniciada e interrompida usando os métodos StartDownload()e StopDownload()de uma classe auxiliar.
O StopDownload()método está aguardando, executa o cancelamento das tarefas atuais e descarta os objetos descartáveis usados.
► Substituí WebClient por HttpClient, ainda é bastante simples de usar (pelo menos em seu básico), fornece métodos assíncronos que suportam um CancellationToken(embora um download em andamento exija algum tempo para cancelar, mas é tratado aqui).
► Um clique de Botão inicializa e inicia os downloads cronometrados e outro para interrompê-lo (mas você pode chamar o StopDownload()método quando o Form fechar, ou, bem, sempre que precisar).
► O Progress<T>delegado é apenas um Lambda aqui: não há muito o que fazer, basta preencher um ListBox e rolar um RichTextBox. Você pode inicializar o objeto da classe helper (é nomeado MyDownloader: claro que você vai escolher outro nome, esse aqui é ridículo) e chamar o seu StartDownload()método, passando o objeto Progress, o Uri e o Interval entre cada 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
A classe auxiliar:
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
Os seus acessos à caixa de listagem e richtextbox devem ser executados no thread de IU A maneira mais fácil de fazer isso é assim.
Me.Invoke(Sub()
ListBox1.Items.Clear()
ListBox1.Items.AddRange(Split(clientdecode, vbLf))
RichTextBox1.SelectionStart() = RichTextBox1.TextLength
RichTextBox1.ScrollToCaret()
End Sub)