Click here to Skip to main content
Click here to Skip to main content

Asynchronous processing of functions and webservice calls.

By , 14 Jul 2004
 

Sample Image - backgroundworker.png

Introduction

A project I have been working on has required the implementation of asynchronous processing, particularly for long running calls to web services, so I started to investigate the Microsoft Application Block for asynchronous processing, and reading articles on multi-threading and so on. One of the articles I came across was Roy Osherove's blog on implementing the Background Worker process from .NET 2.0 in .NET 1.1. This seemed exactly what I was after, especially once I had read the original article by Joval Löwy, except for three things. There was no mention of how to use this to call a webservice, it was only possible to pass in one argument, so for execution of more complex functions you would have to pass in the arguments as an array, or hashtable, and it was written in C# so I couldn't put it into my code, as the project is in VB.NET. Yes, I know I could reference a C# assembly or project in VB.NET, but it would be cleaner for my solution to port the entire code to VB.NET.

Background

The asynchronous call pattern has been explored many times now in .NET, and is described in detail in such articles as Creating a Simplified Asynchronous Call Pattern for Windows Forms Applications by David Hill.

Whidbey will introduce a component in the System.ComponentModel namespace called BackgroundWorker which encapsulates the complexities of asynchronous calls in a simple component which can be invoked from code, or dropped onto Form in the designer. It exposes methods and events for executing the asynchronous function, updating progress, canceling the execution, and completion of the execution.

As mentioned above, Joval Löwy and Roy Osherove have implemented a version of this class in C# for .NET v1.1, which will be sufficient for many people. I have ported in into VB.NET, and added a few tweaks to throw similar exceptions to the Whidbey version, when it is sub-classed. Then I extended it, and demonstrates how to use this to call webservices asynchronously.

Converting to VB.NET

This was not without its quirks.

The best practice for raising events tells us that we should always test that the event is not null before raising the event. That is to say, the event must have handlers. All the articles then give a code sample something like this:

         if(SomeEvent != null)
         {
            SomeEvent(this, args);
         }

The C# implementations of BackgroundWorker both used this standard. But how do I do that in VB.NET? If you try the statement SomeEvent != Nothing, you get the error: 'Public Event SomeEvent(sender As Object, e As System.EventArgs)' is an event, and cannot be called directly. Use a 'RaiseEvent' statement to raise an event. Not very useful!

It turns out that in VB.NET, to get at the delegate derived object to test if it is Nothing, you just add "Event" to the end of the event name. Don't go looking for it in intellisense though, because it won't be there! The resulting VB.NET code is:

         if Not SomeEventEvent is Nothing Then
            SomeEventEvent(Me, args)
         End If

Why have I used SomeEventEvent(Me, args) instead of RaiseEvent SomeEvent(Me, args)?

The default method of a delegate is the Invoke method, and RaiseEvent in VB.NET just calls this method on the delegate, and it makes my code less language specific. Personal preference really.

NB//The source code download includes the VB.NET port and the sub-class I created.

Sub-Classing the BackgroundWorker

This serves two purposes. Firstly, when Whidbey is released, changing over to calling the native BackgroundWorker will require just eight code changes in the sub-class, and not a swathe of changes across all my application. Secondly, it enables us to add functionality to the BackgroundWorker, in this case, the ability to pass multiple parameters to the asynchronous function.

Firstly, the three event argument classes, DoWorkEventArgs, ProgressChangedEventArgs, and RunWorkerCompletedEventArgs must all be sub-classed, along with their associated handler delegates. Then the BackgroundWorker class itself can be sub-classed.

We must handle the underlying events and throw our sub-classed versions.

    Public Shadows Event DoWork As DoWorkEventHandler
    Public Shadows Event ProgressChanged As ProgressChangedEventHandler
    Public Shadows Event RunWorkerCompleted As RunWorkerCompletedEventHandler

The DoWork event is the easiest, although you need to be careful to reassign the sub-classed event arguments back to the original event arguments.

    Private Sub BackgroundWorker_DoWork(ByVal sender As Object, _
        ByVal e As VS2005.DoWorkEventArgs) Handles MyBase.DoWork
        If Not DoWorkEvent Is Nothing Then
            Dim args As New DoWorkEventArgs(e.Argument)
            DoWorkEvent(sender, args)
            e.Cancel = args.Cancel
            If Not e.Cancel Then
                e.Result = args.Result
            End If
        End If
    End Sub

The ProgressChanged event, however, must call a copy of the ProcessDelegate function from our port of the BackgroundWorker.

    Private Sub BackgroundWorker_ProgressChanged(ByVal sender As Object, _
        ByVal e As VS2005.ProgressChangedEventArgs) Handles MyBase.ProgressChanged
        If Not ProgressChangedEvent Is Nothing Then
            Me.ProcessDelegate(ProgressChangedEvent, Me, _
                New ProgressChangedEventArgs(e.ProgressPercentage, Nothing))
        End If
    End Sub

This is because the Whidbey version does not have this method, so if we call the base class function, then we will lose our ability to upgrade easily.

The worst one however is the RunWorkerCompleted event. The RunWorkerCompletedEventArgs class throws an exception if the Result property is accessed and would return nothing, and we must work around this in order to pass the event arguments into our sub-classed event arguments. Microsoft has changed some of the internal workings of delegates for Whidbey.

In .NET 1.1, the exception causes the callback delegate to be called twice, if we do not handle it. The second time, a new exception is thrown because you cannot call EndInvoke twice.

In Whidbey, the exception will bubble up the call stack until it is handled.

To deal with this, we only read the value of the Result property if we have not cancelled or erred.

    Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
        ByVal e As VS2005.RunWorkerCompletedEventArgs) _
        Handles MyBase.RunWorkerCompleted
        If Not RunWorkerCompletedEvent Is Nothing Then
            Dim result As Object = Nothing
            If e.Cancelled = False AndAlso e.Error Is Nothing Then
                result = e.Result
            End If
            Me.ProcessDelegate(RunWorkerCompletedEvent, Me, _
                New RunWorkerCompletedEventArgs(result, e.Error, e.Cancelled))
        End If
    End Sub

Finally, we wanted to extend the class. We do this by simply overloading the RunWorkerAsync method and passing it a ParamArray of type Object().

    Public Overloads Sub RunWorkerAsync(ByVal ParamArray arguments As Object())
        MyBase.RunWorkerAsync(arguments)
    End Sub

Using the code

Making basic asynchronous calls with this class is a simple matter of hooking up the DoWork, ProgressChanged, and RunWorkerCompleted events, then calling the RunWorkerAsync method as I show in my demo app.

Making asynchronous webservice calls is a little odd however. You hook up the events in the same manner as normal, and in the DoWork event handler, call the webservice synchronously. This is exactly what happens under the wraps when you call the BeginWebMethod/EndWebMethod methods produced automatically in the proxy class. It spawns a new thread and makes a normal synchronous call from there.

Of course, the asynchronous thread will be locked up for the duration of the call, so you need to check if there is a cancellation pending when it returns. Your RunWorkerCompleted handler must ensure that it only does anything if cancelled is False.

It is impossible to monitor the progress of a webservice call until the data begins to return, at which point you can monitor the progress of receiving the data stream. However, for the purposes of the demo, I just set a timer running which updates the progress bar ten times per second. It's good enough to make users think something is happening, which it is, just not on their machines!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

The Man from U.N.C.L.E.
Software Developer
United Kingdom United Kingdom
Member
Unfortunately my real name was already in use as a code project login. For those of you who are wondering I am really Napoleon Solo. Sorry, I mean, Mark Jackson. Well, I do look a bit like him I think.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Questionfew questionsmemberdoran_doran12 Jun '08 - 6:55 
Great article.
 
I have a long runing process that I want to run upon clicking a button. It's webservice (soap, object oriented). I am updating local tables (update or add records, depending on some variables) using a class (where the update function is). I dont have the knowledge how long it will take to complete the job. I have to loop through each record and perform update or add task.
 
I dont mind copying the process into dowork or whereever needs to be. Please suggest.
 
button click event
Cry | :((
 
Please suggest. I am on this thing for last 4 days.
AnswerRe: few questionsmemberalhambra-eidos11 Aug '08 - 0:25 
Please, any solutionabout it ??
 
thanks
 
AE

QuestionCancellingmemberKim Bilida26 Dec '05 - 22:07 
Thanks for the great article. I have implemented asynchronous processing for report generation that can take several minutes. When the user chooses to run a report, a new window is opened showing the progress meter and messages and they can continue using the application for other tasks.
 
However, the problem I have now is when the user cancels the asynch process, it's not really cancelled. The RunWorkerCompletedEventHandler isn't called until the process is completed and the report that they 'Cancelled' is still generated.
 
I tried throwing an error in the CancelAsynch event to interrupt the processing, but that ended up throwing an unhandled exception that forced the whole application to close.
 
Basically my question is: How can I terminate the Do_Work thread when the user cancels?

AnswerRe: Cancellingmemberluke72720 Jan '06 - 6:53 
You need to do something like this:
 
private void cancelButton_Click(System.Object sender, System.EventArgs e)
{
    this.backgroundWorker.CancelAsync();
    this.cancelButton.Enabled = false;
}
 
private void backgroundWorker_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
    while(working)
    {
        if(this.backgroundWorker.CancellationPending)
        {
            e.Cancel = true;
            working = false;
        }
        else
        {
            DoSomeWork();
        }
    }
}

GeneralRe: CancellingmemberMalcolm Hall26 Oct '06 - 13:37 
No, you need to call the webservice synchronously but use the async methods. I think its called async without callbacks.
 
private WebClientAsyncResult asyncResult;
 
DoWork(){
asyncResult = serv.beginBlaWebserviceCall();
ar.WaitHandle.WaitOne();
int result = serv.endBlaWebServiceCall(asyncResult);
}
 
This means that when cancel is called you can call asyncResult.Abort() from another method and the the background worker exceptions and the webservice call stops immediately.
QuestionIf the user quits the appilcation?memberabee-fish9 Dec '05 - 6:18 
I'm trying to implement asynchronous processing to run a really long report in the background and then email the results to the user. I was wondering what happens if the user closes the application while the report is being generated. Will my thread that's generating the report be terminated?
AnswerRe: If the user quits the appilcation?memberThe Man from U.N.C.L.E.11 Dec '05 - 20:54 
If the async thread is only used sending a request to the server where the work is done then closing the App merely means that there will be no indication that the job has completed.
 
If the async thread is performing the job on the local machine then it will still be inside the current app domain and process, therefore it will be shut down when the app exits as this will clean up all threads started by the app.
 
If you have knowledge, let others light their candles at it.
Margaret Fuller (1810 - 1850)

GeneralRedesignmemberTim McCurdy6 Oct '05 - 7:14 
I've redesigned this in C# (not a simple rewrite...redesign). I didn't much care for the way 2005 does this with and with method names that make no sense.
 
Anyway, I have kinda one question. Why did you find it necessary to first code the complete class in one file (VS2005 Namespace) and then recode the entire thing again in the other class? I was able to do everything with one simple class.
 
Also, you mention in a comment that getting the result would raise an error and cause the Exception to bubble the events once again. Well, simply take out the Exception you're throwing in the CompletedEventArgs class! It doesn't make any sense anyway! If I was running some method Asynchronously, why would I want an Error message to appear on a different Thread? I would most likely handle this Error on my UI Thread where it would be easier to display it to an End User in the same Form.
 
I made your AsyncCompleteEventArgs and the DoWorkCompletedEventArgs one class to eliminate the redundancy. Anyway, this is how the Result Property ended up looking...
 
public object Result { 
     get { 
          // Throwing Error here makes absolutely no sense 
          // and it's bad practice to raise an error in a Property anyway!
          // if (this._Cancelled) throw new System.InvalidOperationException("Operation has been cancelled.");
          // if (this._Error != null) throw new InvalidCastException();
	  return this._Result; 
     }
}

GeneralRe: RedesignmemberThe Man from U.N.C.L.E.6 Oct '05 - 7:35 
My intent was to replicate the 2005 code as closely as posible so that when I migrate my code to 2005 I can get rid of all except the sub class of the background worker. Since this article I have improved on my sub classing and now use a very small sub class of the background worker, and no subclass of the eventArgs etc.
 
My newer subclass merely enables param arrays to be passed into the args in an easier manner.
 
If you are already coding in 2005 you don't need any of this, though you may like to run Reflector, or some other decompiler against the .Net assemblies and see the code for the actual background worker. I have since replicated it line for line so as to make migration easier.
 
As for the throwing of the error. I agree that it is bad practise, however that is how microsoft did it in the beta one code, and you would only ever access the result in the main thread, which is where this error would appear.
 
And the Async Args and completed Args, again this is how microsoft did it, and they are using the async args for other purposes in the framework.
 
I have been using this background worker in production code for a year now, in 2003 and it has been solid.
 
If you have knowledge, let others light their candles at it.
Margaret Fuller (1810 - 1850)
 

-- modified at 4:35 Friday 7th October, 2005
GeneralRe: RedesignmemberTim McCurdy10 Oct '05 - 3:30 
That's cool. It's not that I thought your design was bad or anything because I know you got it from MS. I agree that it is very solid and I am very impressed with it since I tend to get errors on all my other async code 1 out of 20 times...which makes it really difficult to debug.
 
I can't understand why MS would throw an Error while reading a Read-Only Property, but if you look at their samples, it makes sense why no one would have noticed it. Here's a typical example of how to handle the Completed Event:
If (e.Cancelled) Then
     MessageBox.Show("Process Cancelled")
ElseIf (Not IsNothing(e.Error)) Then
     MessageBox.Show("Errors Occured: " & ControlChars.CrLf & e.Error.ToString())
Else
     'Do something with Result
End If
 
In the MS BackgroundWorker though, you would get the error if you tried this:
If (Not e.Result Is Nothing)...
 
Why would they do that!? WTF | :WTF:
GeneralRe: RedesignmemberThe Man from U.N.C.L.E.10 Oct '05 - 4:29 
I suspect they are trying to remind us to write the error handling code.
 
I always check for cancelled or errors before processing the result, just as the MS samples do, and this is why:
 
Our main reason for using the background worker is for calls to a data access component, either via a webservice, or direct. In either case we have to trap errors such as the connection timing out, transactions rolling back, or some idiot unplugging the server!
 
In all these cases we want the errors reported, or handled on the GUI thread.
 
Alternatively we have cancelled the request due to closing the application or something, and need to do a thread cleanup before we shut down.
 
If you have knowledge, let others light their candles at it.
Margaret Fuller (1810 - 1850)

GeneralRe: RedesignmemberTim McCurdy10 Oct '05 - 5:10 
Agreed, but what about just having an e.HasErrors Property? Why is it that in every bit of documentation from MS it says "don't throw unnecessary Exceptions" because of performance reasons, yet they do it? I just believe that the less Errors throw, the better, but give me a way to see if one did happen (hence: HasErrors = True).
 
Anyway, still a great post!
GeneralEventEventmemberOktay Sarioglu15 Feb '05 - 13:48 
Wink | ;) I was dying to find out if an event from a VB component is handled or not Thanks a lot for your superb explanation in this article.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 15 Jul 2004
Article Copyright 2004 by The Man from U.N.C.L.E.
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid