Adding a Progress Bar to Your Web Service Client Application

 

Matt Powell
Microsoft Corporation

November 20, 2002

Summary: Matt Powell shows how to intercept the Web service message stream using SOAP extensions in order to provide solutions to problems such as implementing progress bar support for Web service client applications. (8 printed pages)

Sometimes the processing and data transfer for a Web service message may take a long time to complete, particularly if the data is large and the throughput is limited. It would be great for user-friendly applications to provide a progress bar to indicate to users how the message transfer is going. The problem is that in order to update a progress bar, you have to have some sort of incremental notification about how things are going with the request. The basic Microsoft® .NET Framework support for Web services, however, only notifies an application when the request has been completed. The trick is to somehow hook into the underlying request handling in order to get the incremental notifications needed to update the progress bar. We can do this by using the streaming capabilities of SOAP extensions.

The Problem

Progress bars are well suited to reading data from a data stream. Data streams are nice, because you can read as little or as much of the stream as you want, and each read will complete separately. So if you have 200 bytes of data to read from a data stream, you can break that up into ten 20-byte reads, and then increment a progress bar accordingly for each of the reads.

If you're familiar with sending HTTP requests and reading HTTP responses using the HttpWebRequest.GetRequestStream and HttpWebResponse.GetResponseStream methods, then you are aware of the capabilities for streaming request and response data in the underlying support that Web methods use. For instance, the underlying TCP connections that the HTTP and SOAP payloads are being transferred across are streaming connections.

The problem is that it is much easier to send a SOAP message and read the response if the entire mechanism is exposed through a simple method call on an object. The .NET Framework provides this ease of use by creating a proxy class that exposes individual methods to your application. The Web service support serializes the parameters into a SOAP request before it sends it across the wire. Similarly the response is deserialized from the SOAP stream into the response information to the method call. It doesn't make sense to only serialize or deserialize a portion of the data. (Would that mean that some parameters exist and others do not?)

The Solution

The solution is to hook into the request and response at a lower level. On the server side, Microsoft® ASP.NET provides a plethora of interfaces for hooking into the request-handling mechanism. On the client side, however, we are limited. Luckily the built-in support for sending and receiving SOAP requests for Web method calls provides functionally equivalent to the support provided on the server for hooking into SOAP requests and responses. That's right—the solution is to use a SOAP extension.

Writing a SOAP extension on the client is similar to writing a SOAP extension on the server. The only difference is the order in which things are called. On the server, a request on the wire is deserialized and then the response is serialized before it is sent back to the client. On the client, the request must first be serialized to send on the wire, and then the response received must be deserialized. For our particular problem, we want to capture the response as it is being returned as close to the wire as possible.

A SOAP extension is simply a class that derives from the System.Web.Services.Protocols.SoapExtension class. There are a number of initialization methods that you must override in your class, but other than the default override, they can largely be ignored for our purposes. Basically, we are concerned with just two overrideable methods from the SoapExtension class: ChainStream and ProcessMessage.

ChainStream is the mechanism used to add your own stream to the stream "chain." This provides your SOAP extension the ability to see and potentially to modify the incoming or outgoing data. ChainStream is called twice per request—once during the creation of the request stream, and once during the creation of the response stream. For the sake of updating a progress bar as a SOAP response message is received, we are specifically interested in hooking into the response stream.

The ProcessMessage override in your SOAP extension will be called at four stages during the course of handling a single SOAP request and response. The BeforeSerialize and AfterSerialize stages occur around the serialization of the request. The BeforeDeserialize and AfterDeserialize stages occur around the deserialization of the response. We are interested in the response. We want to monitor the data as it comes across the network, instead of after it has already been buffered and deserialized. We will therefore be doing the majority of our work in the BeforeDeserialize stage.

One of the unique requirements about our SOAP extension is that it will need to interact with the user interface of our application. In particular, it will need to update the progress bar, which must happen on the specific thread that owns the control's underlying window handle. In normal, multi-threaded Microsoft® Windows applications, you simply use a delegate function and the control's Invoke method to launch the delegate in the control's thread—and have it do the control interaction work. In our case, the SOAP extension is implemented in a separate class from the user interface, so we need a way to pass a reference for the particular instance of the control to our SOAP extension class.

To do this, I created a special class that is derived from the Web service proxy class that was created through the Add Web Reference capability of Microsoft® Visual Studio® .NET. You can get at the instance of the proxy class associated with the request by first casting the SoapMessage parameter passed to the ProcessMessage function to a SoapClientMessage, and by then accessing the Client property. I simply add a public member that holds any information I need to the class I created (the one derived from the original Web service proxy class). In this case, I added a reference to the process bar object as a public member. I use the same mechanism for transferring information from the application, like the size of the data I am expecting and the delegate function to use for communicating with the progress bar. Here is the code for my special proxy class:

internal class ProgressClient : localhost.Service1
{
    public ProgressBar Progress;
    public int TransferSize;
    public Form1.UpdateDelegate ProgressDelegate;
}

Here is the code for my SoapExtension class:

public class ProgressExtension : SoapExtension
{
    // Holds the original stream
    private Stream m_oldStream;
    // The new stream
    private Stream m_newStream;
    // The buffer for reading from the old stream
    // and writing to the new stream
    private byte[] m_bufferIn;
    // The progress bar we will be incrementing
    private ProgressBar m_Progress;
    // The size of each read
    private int m_readSize;
    // The delegate we will invoke for updating the
    // progress bar.
    private Form1.UpdateDelegate m_progressDelegate;
    // Used to keep track of which stream we are trying
    // to chain into
    private bool m_isAfterSerialization;
    public override void ProcessMessage(SoapMessage message)
    {
        switch(message.Stage)
        {
            case SoapMessageStage.AfterSerialize:
                // To let us know that the next ChainStream call
                // will let us hook in where we want.
                m_isAfterSerialization = true;
                break;
            case SoapMessageStage.BeforeDeserialize:
                // This is where we stream through the data
                SoapClientMessage clientMessage 
                    = (SoapClientMessage)message;
                if (clientMessage.Client is ProgressClient)
                {
                    ProgressClient proxy 
                        = (ProgressClient)clientMessage.Client;
                    m_Progress = proxy.Progress;
                    // Read 1/100th of the request at a time.
                    // This will give the progress bar 100 
                    // notifications.
                    m_readSize = proxy.TransferSize / 100;
                    m_progressDelegate = proxy.ProgressDelegate;
                }
                while (true)
                {
                    try
                    {
                        int bytesRead 
                            = m_oldStream.Read(m_bufferIn, 
                            0, 
                            m_readSize);
                        if (bytesRead == 0) 
                        {
                            // end of message...rewind the
                            // memory stream so it is ready
                            // to be read during deserial.
                            m_newStream.Seek(0, 
                                System.IO.SeekOrigin.Begin);
                            return;
                        }
                        m_newStream.Write(m_bufferIn, 
                            0, 
                            bytesRead);
                        // Update the progress bar
                        m_Progress.Invoke(m_progressDelegate);
                    }
                    catch
                    {
                        // rewind the memory stream
                        m_newStream.Seek(0, 
                            System.IO.SeekOrigin.Begin);
                        return;
                    }
                }
        }
    }

    public override Stream ChainStream(Stream stream)
    {
        if (m_isAfterSerialization)
        {
            m_oldStream = stream;
            m_newStream = new MemoryStream();
            m_bufferIn = new Byte[8192];
            return m_newStream;
        }
        return stream;
    }
    // We don't have an initializer to be shared across streams
    public override object GetInitializer(Type serviceType)
    {
        return null;
    }

    public override object GetInitializer(
        LogicalMethodInfo methodInfo, 
        SoapExtensionAttribute attribute)
    {
        return null;
    }

    public override void Initialize(object initializer) 
    {m_isAfterSerialization = false;}
}

In order for a SOAP extension to be invoked for a client application, the SoapExtension class must be configured appropriately. For a Microsoft® Windows® Form application, this involves modifying the application's configuration file. For my application called WebServiceProgress.exe, my configuration file, WebServiceProgress.exe.config looks like this:

<configuration>
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <add 
    type="WebServiceProgress.ProgressExtension, WebServiceProgress"
    priority="1" group="0" />
      </soapExtensionTypes>
    </webServices>
 </system.web>
</configuration>
    

The type attribute of the add element indicates the type of my SoapExtension class, and the assembly it is in. The priority is set to 1 with a group setting of 0 in order for this to be the highest priority level of SoapExtension. We want the stream we read to be as close to the wire as possible, so it is important that no other streams are inserted between our SoapExtension's stream and the network. Therefore, we set our priority to the maximum. It is still possible that a SOAP extension could be inserted in the chain before our extension. If so, then the stream we read from will only consist of the buffered data, which will not give an accurate progress of our network throughput.

The last piece of code needed to make all this work is the delegate function in the main class for my form-based application. This is the function that will actually update the progress bar control. It must be declared as a delegate and called using the Invoke method in order to allow it to run on the same thread that owns the window message pump that the control lives on. First you must declare the type of the delegate:

public delegate void UpdateDelegate();

Then you must create a function with the same signature that actually performs the interaction. In my case, the Maximum property of the progress bar is set to 100 at design time. In my SOAP extension class, I read 1/100th of the expected data at a time. With the completion of each read, the progress bar can be incremented by 1 so that it will reach its maximum of 100 with the final read. The code for my progress bar update function simply looks like the following:

    private void ProgressBarUpdate()
    {
        progressBar1.Increment(1);
    }

Another potential problem is that a lot of Web service toolkits buffer the response on the server until the Web service code has completed. This is certainly the case if you are dealing with a server written with ASP.NET Web methods. If you hope to provide progress-bar functionality that tracks the progress of the Web service code as it generates the elements in a large array, well, forget it. If, however, you want your progress bar to track the lengthy process of transmitting a rather large array across the network, then you are in luck. This solution will suit your needs well.

A hurdle common to many progress-bar scenarios is knowing the size of the data that is being transmitted. My sample application takes a user's input on how much data will be sent, but there are many scenarios where an application will not know the size of the data. For instance, your response may be based on the size of the result set of a database query, but you may have no idea how many records might be returned. In a scenario like this, you simply have to estimate the expected size and figure out how to deal with discrepancies as you update your progress bar.

Where Do We Go From Here?

SOAP extensions can be used for many purposes, including instances where you may want to treat your Web service calls in more of a stream-like fashion. A good example of this is when you need to update a progress bar where incremental reads of the incoming data are key to providing appropriate notifications to the user interface control.

You might also consider other reasons for taking a streaming approach to the Web service data. For instance, when the data is very large and cannot be easily buffered, when you want to log the incoming and outgoing data, or when incremental manipulation of the data might make sense (like when performing compression or encryption). In all of these cases, SOAP extensions provide the control you may require for your Web service applications.

 

At Your Service

Matt Powell is a member of the MSDN Architectural Samples team, where he helped develop the groundbreaking SOAP Toolkit 1.0. Matt's other achievements include co-authoring Running Microsoft Internet Information Server from Microsoft Press, writing numerous magazine articles, and having a beautiful family to come home to every day.