vb.netmultithreadingwinformsasynchronouscross-thread

How to let the code run smoothly using timers and different threads


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 googling a while and I understood that I cannot update UI from any other thread other than the UI thread.

So, what about if you are using lots of controls under Timer1.Tick?
How can I update a Label when the data is downloaded with WebClient with a timer, you don't want to lower the interval too much and keep the UI responsive at the same time?

I receiver Cross Thread violation exceptions when I access UI elements, a ListBox1 and a RichTextBox.

What is the correct way to update the UI with a timer and/or a Thread without causing cross threat exceptions?


Solution

  • 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<T> class and its IProgress<T> interface.
    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<T> delegate created in the UI Thread is called in that context. We just need to pass the Progress<T> delegate and handle the notifications we receive.
    You're downloading and handling a string, so your Progress<T> 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 replaced WebClient with HttpClient, it's still quite simple to use, 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<T> 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<T> 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
        Implements IDisposable
    
        Private Shared client As New HttpClient()
        Private cts As CancellationTokenSource = Nothing
        Private interval As Integer = 0
        Private disposed As Boolean
    
        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
            token.ThrowIfCancellationRequested()
    
            Dim responseData As String = String.Empty
            Dim pattern As String = "<(?:[^>=]|='[^']*'|=""[^""]*""|=[^'""][^\s>]*)*>"
            Dim downloadTimeWatch As Stopwatch = New Stopwatch()
            downloadTimeWatch.Start()
            Do
                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
    
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not disposed AndAlso disposing Then
                client?.Dispose()
                client = Nothing
            End If
            disposed = True
        End Sub
    
        Public Sub Dispose() Implements IDisposable.Dispose
            Dispose(True)
        End Sub
    End Class