Causally Connectable

Calling Web Services Asynchronously

VBUG Spotlight

LANGUAGES: C# | VB.NET

ASP.NET VERSIONS: 1.1

 

Causally Connectable

Calling Web Services Asynchronously

 

By Rob MacDonald

 

We ve all bought into the Web services message by now, and fully understand the place that Web services can have in distributed system designs. We ve also learned that interacting with Web services is best done in a chunky rather than chatty way. One reason for this is that it is always best to assume there is some latency when talking to Web services there s always the chance that it will take some time to respond. Even if the latency is only a second or so, a really solid user interface shouldn t hang waiting for its response. It should allow the user to carry on interacting with the application.

 

In this article, I ll present some of the techniques that are available for calling Web services asynchronously. For broad coverage, I ll use .NET 1.1 for all my examples. .NET 2.0 makes things slightly easier, but the same principles apply.

 

Reality or Perception?

When we talk to Web services, we communicate over HTTP, which is a synchronous (request/response) protocol. So how do we make it appear to be asynchronous? The easiest way is to make sure the call to the Web service is performed on a background thread in our client. That way, the Web service needs no modification, and in the client only a background thread is kept waiting the main thread serving the user interface carries on doing what it is supposed to do, giving the perception that the Web service is being called asynchronously.

 

Don t worry if, like many programmers, you re not too familiar with multi-threaded programming; with some help from .NET and a little bit of discipline, you can avoid all the complex threading issues.

 

Getting Started

I m going to focus on how Windows Forms programmers can work with a Web service asynchronously. To do this, I need to set up a Web service (all code examples are available for download in VB.NET and C#; see end of article for details). I have created a Web service named Slow with a single Web method, coded as follows:

 

_

Public Function HelloWorld() As String

 System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5))

 Return "Hello World at " & DateTime.Now.ToString()

End Function

 

This code is enough to simulate a long-running Web method that takes five seconds to return. From now on, everything I do will be on the client side.

 

To help demonstrate the options available, I ve written a Windows Form application that calls the Slow Web service s HelloWorld method four different ways. You can see what this looks like in Figure 1. Note that the application has four sections; each one calls the Web service using a different technique. Note also the bright red bar at the bottom. When the application s user interface is responsive (that is, it isn t hanging while awaiting a response from the Web service), this bar flashes red and blue at regular intervals. Consider it as a heartbeat all is well while the heart is beating; when it s locked, so too is the application.

 


Figure 1: The AsyncTest Web service client.

 

The Synchronous Call

Let s look first at the code for the synchronous call. Behind the synchronous call s Call the Server button is the following code:

 

'pxy = New Slow.Service1

SyncCallThreadLabel.Text = getThreadType()

Dim s As String = pxy.HelloWorld()

SyncResponseThreadLabel.Text = getThreadType()

SyncResponseLabel.Text = s

 

No surprises here. The proxy variable (pxy) is actually initialised to a Web reference object in the application s Form_Load event handler, so here the code simply calls HelloWorld and assigns the response to s before displaying it in a Label control. This is easy enough; the problem is that the entire user interface (including the heartbeat) freezes for five seconds as soon as HelloWorld is called.

 

Before seeing how to remedy this situation, it s worth taking a look at the code in the getThreadType method, which you can see is called twice in the previous code. Here it is:

 

' returns whether the current thread is a UI or a worker

' (background) thread

Private Function getThreadType() As String

 Dim currentThreadID As String = _

   AppDomain.GetCurrentThreadId().ToString()

 If Thread.CurrentThread.IsBackground Then

   Return "Background (" + currentThreadID + ")"

 Else

   Return "UI (" + currentThreadID + ")"

 End If

End Function 'getThreadType

 

This code returns a string containing the Thread ID of the currently executing thread, and also whether this thread is a background or UI thread. You ll see why this matters later.

 

You can see the result of executing this code in Figure 2. Note that both the call and the response are handled on the same thread.

 


Figure 2: The result of a synchronous call.

 

The Asynchronous Call (Blocking)

Let s now look at blocking, the first of three different ways of calling the Web service asynchronously (each approach has its strengths and weaknesses).

 

In the blocking approach, I make the call to the Web service asynchronously and then carry on doing some useful work. When I ve finished doing the useful work, I then block until the Web service returns. Here s the code (slightly simplified):

 

BlockingCallThreadLabel.Text = getThreadType()

Dim ar As IAsyncResult = _

 pxy.BeginHelloWorld(Nothing, Nothing)

doYieldingWork(blockTime)

Dim s As String = pxy.EndHelloWorld(ar)

BlockingResponseThreadLabel.Text = getThreadType()

BlockingResponseLabel.Text = s

 

The first thing to note is that I m not calling HelloWord! Whenever you create a Web Reference, the proxy code that is generated for you contains three methods for each Web method in the Web service. One of these is the method we use for synchronous calls; the other two (prefixed Begin and End) are for asynchronous calls.

 

You will see that BeginHelloWorld takes two arguments. These can be used to help me control the asynchronous operation; I ll explain them later. In this example, I ve passed Nothing for both of these arguments. (If the HelloWorld method took arguments, they would appear in BeginHelloWorld directly after these two control arguments.) You will also see that BeginHelloWorld doesn t return a string. Instead, it returns an object that implements the IAsyncResult interface, which I ve assigned to a variable named ar.

 

When you call BeginHelloWord, .NET immediately takes a background thread from its thread pool and executes the real call to HelloWorld on this thread. You don t need to worry about the thread management. It then immediately returns the IAsyncResult object so that your code can carry on without waiting for the Web service.

 

In my example, I have a method called doYieldingWork, which actually doesn t do anything interesting (but the point is, it could). It carries on doing uninteresting things for blockTime number of seconds (which is controllable from my user interface, and defaults for two seconds).

 

When I have finished doing other things and am ready for my Web service results, I call EndHelloWorld, passing it the ar variable that identifies which set of results I want. If the results are already available, EndHelloWorld simply returns them. If not, it blocks, waiting for the background thread to receive the return value from the Web service. EndHelloWorld returns exactly the same type as HelloWorld (in this case, a string). I handle the return from EndHelloWorld in exactly the same way as I did the synchronous result. Figure 3 shows my user interface after the call has returned.

 


Figure 3: The result of a blocking call.

 

Just as for the synchronous call, all my code has run on the main user interface thread. This is handy, as I don t have to worry about thread management or data marshalling. However, the blocking technique isn t that much better than the synchronous technique. It can be useful if you want to call a Web service at the beginning of a complex task, because you can carry on with the rest of the task while waiting for the Web service to return but it can still involve locking the UI.

 

The Asynchronous Call (Polling)

Polling provides a more flexible async technique than blocking. With polling, you make the call on a background thread, then check from time to time to see if the results are ready. My polling code is very similar to the blocking code:

 

PollingCallThreadLabel.Text = getThreadType()

Dim ar As IAsyncResult = _

 pxy.BeginHelloWorld(Nothing, Nothing)

While Not ar.IsCompleted

 doYieldingWork(pollInterval)

End While

Dim s As String = pxy.EndHelloWorld(ar)

PollingResponseThreadLabel.Text = getThreadType()

PollingResponseLabel.Text = s

 

I ve used BeginHelloWorld and EndHelloWorld in much the same way as before. The difference is that I now have a loop. In the loop, I check the IsCompleted property on the ar variable. If IsCompleted returns false, I carry on doing other things; but if it returns true, I can retrieve the results using EndHelloWorld knowing that I will never be blocked, because the async operation has completed. Figure 4 shows my user interface for using the polling technique. Note that, again, the call and reply are handled on the same thread.

 


Figure 4: Using the polling technique.

 

Polling is a much more useful approach than blocking in most circumstances. Rather than using a loop, as I have here, a more general approach uses a Timer control firing every second or two, checking for returns from all your Web service calls. You can design a polling approach that will handle all your Web service requests asynchronously, without ever locking the user interface. In fact, many applications work exactly this way. The main problem is one of control. If you only call one or two Web methods, polling is fine. But for more complex requirements, it can require some effort to handle all the different responses through a Timer control.

 

The Asynchronous Call (Callback)

In many ways, the most convenient async approach is to use callbacks. As you can see in the following code, it looks simple enough and in many ways, it is but it does create one problem that you need to resolve with considerable care. Here s the code behind the callback Call the Server button:

 

' define an ansync callback delegate that will call

' callbackFunction

Dim cb As New AsyncCallback(AddressOf callbackFunction)

CallbackCallThreadLabel.Text = getThreadType()

pxy.BeginHelloWorld(cb, Nothing)

 

Start by looking at the last line of code. I m calling BeginHelloWorld again, but this time I am supplying a value for the first argument. This argument is a delegate (basically, a managed pointer to a method) of type AsyncCallback, which is pointing to a method called callbackFunction. There s nothing special about callbackFunction, it s simply the name of a method I ve written (any name will do). Here s its definition:

 

Private Sub callbackFunction(ByVal ar As IAsyncResult)

 Dim s As String = pxy.EndHelloWorld(ar)

 CallbackResponseThreadLabel.Text = getThreadType()

 CallbackResponseLabel.Text = s

End Sub 'callbackFunction

 

The callbackFunction must take an argument of type IAsyncResult, which you saw earlier. .NET takes care of calling callbackFunction, as well as passing it an object that implements IAsyncResult, when the Web service returns. Using IAsyncResult, I can call EndHelloWorld and retrieve its return value, as you can see. So the callback technique does look convenient: I call BeginHelloWorld and pass it a reference to the code I would like it to call when the Web service returns. In the meantime, my code running on the main user interface thread can carry on. No blocking or frozen UI, and no concerns over complex control code all I need to do is handle the response in a different method from the one that makes it (callbackFunction is basically an event handler). Take a look at Figure 5 though, and you will see there s a little bit more to it than that.

 


Figure 5: Using a callback.

 

Notice that the reply thread is different than the call thread. All the code in callbackFunction executes not on the main UI thread, but on a background thread. This has not caused a problem for my demonstration code, but only because I ve been lucky. There are in fact two issues you need to address when running on a background thread in a Windows Forms application. The first issue is one of contention. All kinds of horrible things can happen if code on two different threads accesses the same data at the same time. Consequently, you either need to ensure that code running on two different threads can t access the same variables, or you need to write synchronisation code around every access to every bit of data that may be shared between two threads. .NET provides great features for writing this kind of code but if you ve not done it before, be afraid (be very afraid).

 

The second issue is specific to Windows Forms and the way that Windows handles its user interfaces. Put simply, Windows is (currently) designed such that all user interface features (forms, controls, etc.) in a given program always run on a single thread specially allocated for UI processing. It is simply not safe to access your UI from any thread other than your program s UI thread. You will see what I mean if you download the demonstration code and try out the two TreeView buttons. One button attempts to update a TreeView control from the UI thread; the other tries to do the same from a background thread. Both buttons call exactly the same code:

 

Private Sub AddToTreeView()

 DemoTreeView.Nodes.Add(DateTime.Now.ToString())

End Sub 'AddToTreeView

It looks harmless enough, but it fails when called from the background thread. This isn t something particular to TreeView controls: it applies to all UI operations attempted from a background thread. You re not supposed to go there (remember, I was just lucky with the callback code that updates a Label control).

 

Fortunately, there is a solution to this problem that avoids the need to address complex threading issues. Accessing UI controls from a background thread is a common problem, so Windows Forms provides an easy way of making it safe. Every control in Windows Forms supports a method called Invoke. This method allows a background thread to ask the main UI thread to invoke a method and run that method on the UI thread. You can pass arguments to the method, which allows you to safely marshal data from the background thread to the UI thread without worrying about writing synchronisation code around shared data.

 

So, let me now re-write my callback code to work safely. Here goes:

 

Private Delegate Sub UpdateDelegate(ByVal s As String)

Private Sub callbackFunction(ByVal ar As IAsyncResult)

 Dim s As String = pxy.EndHelloWorld(ar)

 Me.Invoke( _

   New UpdateDelegate(AddressOf UpdateUISafely), _

   New Object() {s})

End Sub 'callbackFunction

Private Sub UpdateUISafely(ByVal s As String)

 CallbackResponseThreadLabel.Text = getThreadType()

 CallbackResponseLabel.Text = s

End Sub

 

In this code, I ve declared my own delegate (UpdateDelegate) that can point to any method that takes a single string as an argument. Now if you look at the new callbackFunction, you ll see that I call Invoke, passing it an instance of UpdateDelegate pointing at a method named UpdateUISafely. If the method you want to invoke takes arguments (which mine does), these are supplied in the form of an array as the second argument to Invoke. So now my callback function, which runs on a background thread, uses Invoke to get the UI code in UpdateUISafely executed on the main UI thread. Take a look at Figure 6 and you will see that my user interface is now being updated from the UI thread rather than a background thread.

 


Figure 6: Callback invoking code on the UI thread.

 

By using Invoke, I can overcome the difficulties that arise because the callback function executes on a background thread. Of course, there s no point calling Invoke from the main UI thread; it works, but involves more code and the overhead that .NET incurs doing the synchronisation and data marshalling for you. Although you won t see it in VB.NET s IntelliSense, every control implements a property called InvokeRequired that returns true if you call it from a background thread.

 

I ve left the lucky code in the download, so you can see that the callback gets handled on a background thread. But you ll see that I ve commented out the safe code, so it will be easy to try it both ways.

 

Conclusion

You can make your client applications into smart client applications by handling all your calls to Web services asynchronously. Although your program still makes a synchronous call to the Web service, this is handled on a background thread in your client, so your UI is always in control. You simply need to handle the synchronisation between the user interface thread and the background thread when the results become available.

 

In this article I ve looked at three different ways of doing this: blocking, polling, and callbacks, and shown that, although the callback function you supply when using this third technique runs on a background thread, you can safely use the Control.Invoke technique to marshal the callback onto the UI thread, thus avoiding any further threading issues.

 

If you re ready to start working with Windows Forms 2.0, take a look at the BackgroundWorker control. This control makes it easy for you to separate out your UI and background thread code, and handles the marshalling for you, removing the need to use a delegate and call Invoke. Otherwise, it does much the same as we have done here.

 

Whichever approach you take, there is more and more emphasis on asynchronous communications these days, and this is a trend that s going to run. Now is a great time to start getting familiar with async techniques.

 

The sample code in this article is available for download.

 

VBUG member Rob MacDonald has more than 20 years of programming experience, mostly with Microsoft technologies. He has worked exclusively with .NET for more than five years, and has spent much of that time creating learning material for Microsoft, from internal Microsoft technical courses covering topics including ADO.NET, Web services, and architecture for the benefit of Microsoft consultants and engineers, to the widely distributed Learn247 DVD Series. Prior to that, Rob authored several books and earned his spurs as project manager and lead programmer on some serious projects. When not doing IT, Rob is the UK s leading commercial stoat breeder.

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish