
Using the Background Worker Process for Long Running Processes in WPF and Silverlight
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.