In a previous article WPF DataBinding using Visual Basic (VB.NET), a problem was encountered when trying to keep the User Interface active during Long Running Processes as a result of a button click. Either the whole system would hang until the process was completed or the program would crash.
This hang-up is due to the fact that both the Long Running Process and the UI are using the same thread. The solution is to run the Long Running Process in a separate thread. The BackgroundWorker component, gives you the ability to execute Long Running Processes in the background asynchronously, on a separate thread from the application’s main UI thread.
I have just started a project which is going to have quite a few of these Long Running Processes and so what I need is a simple implementation of the Background Worker process which I can expand as required.
The implementation consists of a WPF page with a start button, a cancel button and three labels to let the user know what is happening before, during and after the Long Running Process has been started by the start button.
<Page x:Class="BackGroundWorkerExample" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="BackGroundWorkerExample" x:Name="BackGroundWorkerExample" WindowTitle="WPF Luddite Test Bed: Background Worker Example" Width="1024" Height="768"> <StackPanel> <Button x:Name="btnStartBackgroundProcess" Width="300" Height="30" Margin="0 20 0 0" Content="Start Background Process"> </Button> <Label x:Name="lblStartedCompleted" Content="" Width="500" Height="30" Margin="0 20 0 0"> </Label> <Label x:Name="lblCounter" Content="" Width="500" Height="30" Margin="0 20 0 0"> </Label> <Label x:Name="lblPercentCompleted" Content="" Width="500" Height="30" Margin="0 20 0 0"> </Label> <Button x:Name="btnCancelBackgroundProcess" Width="300" Height="30" Margin="0 20 0 0" Content="Cancel Background Process" IsEnabled="False"> </Button> </StackPanel> </Page>
The following steps will be required in the code behind:
1. Define a new BackgroundWorker, create an instance and do some setup.
2. Define the LongRunningProcess – must not reference the User Interface (UI).
3. Define a subroutine to handle the BackgroundWorker DoWork event.
4. Define a subroutine to handle the BackgroundWorker ProgressChanged event.
5. Define a subroutine to handle the BackgroundWorker RunWorkerCompleted event.
6. Define a subroutine to handle the event caused by the Start button being clicked.
7. Define a subroutine to handle the event caused by the Cancel button being clicked.
Imports System.ComponentModel Partial Public Class BackGroundWorkerExample ' ' Define a new backgroung worker. ' Private WithEvents bw As New BackgroundWorker() ' ' Create new instance of BackGroundWorker and do some setup ' Public Sub New() InitializeComponent() bw.WorkerSupportsCancellation = True bw.WorkerReportsProgress = True End Sub Private Function LongRunningProcess() As String ' ' The LongRunngProcess should NOT refer to any UI objects. ' Dim iteration As Integer = CInt(100000000 / 100) Dim cnt As Long = 0 For i As Long = 0 To 100000000 ' ' Bring the LongRunningProcess to an orderly termination if the Cancel button is clicked. ' - see further comments in routine that handles the btnCancelBackgroundProcess_Click event. ' If bw.CancellationPending Then Return "" End If cnt = cnt + 1 ' ' Report Progress: ' When you need the background operation to report on its progress, ' you can call the ReportProgress method to raise the ProgressChanged event. ' The ReportProgress Method permits up to 2 parameters (ProgressChangedEventArgs): ' Paramater 1. Is of type Integer and is defined as "percentProgress" ' Paremeter 2. Is of type Object and is defined as "userState" ' I see no reason why you can't return any information you want as long as you conform to the ' parameter types integer and object. ' ' In a simple example like this, if the ReportProgress method was called for every iteration of i ' then the system would probably hang the user interface and run out of memory. Therefore, in this instance, we ' only report back on every 10,000 iterations. ' If (i Mod 10000 = 0) _ And (bw IsNot Nothing) _ AndAlso bw.WorkerReportsProgress Then ' ' Call ReportProgress ' bw.ReportProgress(i \ iteration, cnt) End If Next Return cnt.ToString() End Function Private Sub backgroundWorker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles bw.DoWork ' ' call long running process and get result ' e.Result = Me.LongRunningProcess() ' ' Cancel if cancel button was clicked. ' If bw.CancellationPending Then e.Cancel = True ' This sets the Cancelled property of the RunWorkerCompletedEventArgs Return End If End Sub Private Sub backgroundWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles bw.ProgressChanged ' ' Use the ProgressChanged Sub to give user feedback via the UI. ' ' Update UI with values contained in ProgressChangedEventArgs e.ProgressPercentage and e.UserState. ' Me.lblPercentCompleted.Content = CStr(e.ProgressPercentage) & "% Completed." lblCounter.Content = CStr(e.UserState) '' End Sub Private Sub backgroundWorker_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles bw.RunWorkerCompleted ' ' Back on primary thread, can access ui controls ' If e.Cancelled Then Me.lblStartedCompleted.Content = "Process Cancelled." Else Me.lblStartedCompleted.Content = "Background Process Completed. " End If ' Me.btnStartBackgroundProcess.IsEnabled = True Me.btnCancelBackgroundProcess.IsEnabled = False '' End Sub Private Sub btnStartBackgroundProcess_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnStartBackgroundProcess.Click ' lblStartedCompleted.Content = "Background Process Started..." Me.btnStartBackgroundProcess.IsEnabled = False Me.btnCancelBackgroundProcess.IsEnabled = True ' ' Calls DoWork on secondary thread ' bw.RunWorkerAsync() ' ' RunWorkerAsync returns immediately. '' End Sub Private Sub btnCancelBackgroundProcess_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles btnCancelBackgroundProcess.Click ' ' There are several points to note when cancelling a process: ' ' 1. The WorkerSupportsCancellation property must have been initialised to true. ' ' 2. The CancelAsync method does not immediately cancel the process, but instead sets the ' CancellationPending property to True. It is the therefore important that the LongRunningProcess ' periodically checks the CancellationPending property and if True the process should be ' terminated in an orderly manner. ' ' 3. The CancellationPending property of the background worker process should also be checked by the ' routine handling the DoWork method after the LongRunningProcess has completed. ' If the CancellationPending Property is true then the DoWork routine should set Cancel ' property of the DoWorkEventArgs to True. ' CancelAsync() method. This should be checked by the routine which is handling the RunWorkerCompleted ' event. ' ' 4. It is good practice to ensure theat the Cancel button is only enabled when the LongRunningProcess ' is actually running. Therefore the button should be disabled by default, only enabled when the ' process is started, and disable again when the process is completed or cancelled. ' bw.CancelAsync() ' btnCancelBackgroundProcess.IsEnabled = False ' End Sub End Class
Hope this helps.