I\'m trying to prevent the GUI from freezing, because of a low timer interval and too much to process in the Timer.Tick
event handler.
I\'ve been goo
You have different ways to update UI elements from a Thread other than the UI Thread.
You can use the InvokeRequired/Invoke()
pattern (meh), call the asynchronous BeginInvoke()
method, Post() to the SynchronizationContext, maybe mixed with an AsyncOperation + AsyncOperationManager (solid BackGroundWorker style), use an async callback etc.
There's also the Progress
This class provides a quite simplified way to capture the SynchronizationContext
where the class object is created and Post()
back to the captured execution context.
The Progress
delegate created in the UI Thread is called in that context. We just need to pass the Progress
object and handle the notifications we receive through the Action
delegate.
You're downloading and handling a string, so your Progress
object will be a Progress(Of String)
: so, it will return a string to you.
The Timer is replaced by a Task that executes your code and also delays its actions by a Interval that you can specify, as with a Timer, here using Task.Delay([Interval]) between each action. There's a StopWatch that measures the time a download actually takes and adjusts the Delay based on the Interval specified (it's not a precision thing, anyway).
► In the sample code, the download Task can be started and stopped using the StartDownload()
and StopDownload()
methods of a helper class.
The StopDownload()
method is awaitable, it executes the cancellation of the current tasks and disposes of the disposable objects used.
► I've replace WebClient with HttpClient, it's still quite simple to use (at least in its basics), it provides async methods that support a CancellationToken
(though a download in progress requires some time to cancel, but it's handled here).
► A Button click initializes and starts the timed downloads and another one stops it (but you can call the StopDownload()
method when the Form closes, or, well, whenever you need to).
► The Progress
delegate is just a Lambda here: there's not much to do, just fill a ListBox and scroll a RichTextBox. You can initialize the helper class object (it's named MyDownloader
: of course you will pick another name, this one is ridiculous) and call its StartDownload()
method, passing the Progress object, the Uri and the Interval between each 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
The helper class:
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