I’m going to sing the doom song now -OR- Staying responsive during a long running process in VB
Many times in one’s life, one must stop and seek guidance. Whether it is looking for more information or factoring the 643rd Fibonnacci number one will stop what one is doing causing everything around to freeze, much to the annoyance of the impatient user. I am talking, of course about backgrounding long running tasks in Visual Basic.
Backgrounding tasks is a pain. Suddenly, the programmer must relinquish control of program flow to the computer’s task scheduler. Many things can and quite unexpectedly will go wrong. Its like sitting down when someone else has grabbed the chair that was just behind you. I digress; This blog post aims to supply a simple pattern to follow when a program needs to do something that takes a long time (in the user’s mind, any way).
Take the following simple example. The form is designed simply with a text box named TextBox1
and a button named Button1
. Here is the logic code:
Imports System.Threading Public Class Form1 Public Sub New() ' This call is required by the designer. InitializeComponent() TextBox1.ReadOnly = True End Sub Private Sub Button1_Click(sender As Object, _ e As System.EventArgs) _ Handles Button1.Click TextBox1.Text = "processing ..." TextBox1.Text = longRunningFunction() End Sub Private Function longRunningFunction() As String Thread.Sleep(5000) Return "longRunningFunction() completed" End Function End Class
Quite simply, the program sets the text box to the value returned from the longRunningFunction
. Unfortunately, this function takes over 5 seconds to return! The entire form is frozen the entire time. The user continues hammering at the button trying to get a response, then tries moving, and finally closing the window. As the form again begins processing events, it jumps and hangs, jumps and hangs until it finally disappears. How frustrating. (Sure, we could disable the button before the function call and enable it after… but that wouldn’t very well illustrate my point, the form still does nothing!)
Using a background worker allows us to launch the function and continue on serving the user (that ought to make CLU fume). To use a background worker, create a background handler and two Sub
methods. One will handle the background worker’s DoWork
event. The other will handle the RunWorkerCompleted
event.
Dim WithEvents bg As New BackgroundWorker Private Sub longRunningFunction(ByVal sender As Object, _ ByVal e As DoWorkEventArgs) _ Handles bg.DoWork Private Sub setResult(ByVal sender As Object, _ ByVal e As RunWorkerCompletedEventArgs) _ Handles bg.RunWorkerCompleted
The thread is provided an object in the DoWorkEventArgs
called result in which the method can shove anything it needs to provide the callback method (which runs on the parent thread). The final result of this makeover leaves our code as follows. It is certainly more complicated, but the user will now never have to wait on our longRunningFunction
.
Imports System.Threading Imports System.ComponentModel Public Class Form1 Dim WithEvents bg As New BackgroundWorker Public Sub New() ' This call is required by the designer. InitializeComponent() TextBox1.ReadOnly = True End Sub Private Sub Button1_Click(sender As Object, _ e As System.EventArgs) _ Handles Button1.Click TextBox1.Text = "processing ..." Button1.Enabled = False bg.RunWorkerAsync() End Sub Private Sub longRunningFunction(ByVal sender As Object, _ ByVal e As DoWorkEventArgs) _ Handles bg.DoWork Thread.Sleep(5000) e.Result = "longRunningFunction() completed" End Sub Private Sub setResult(ByVal sender As Object, _ ByVal e As RunWorkerCompletedEventArgs) _ Handles bg.RunWorkerCompleted TextBox1.Text = e.Result Button1.Enabled = True End Sub End Class