Wednesday, October 18, 2006

Showing Download Progress with .NET webservice clients

Let's say you were interested in displaying a progress bar in your .NET application form while a BIG SOAP message was coming down the line. There's an MSDN Article by Matt Powell explaining how you can do exactly that.

I managed to finally get it to twist and turn and
actually work with my Delphi (Win32) "Showing SOAP Download progress" article code.

The Microsoft article expects your client application to already know the transfer size, which honestly is not usually available beforehand. You would expect that the size of the content would vary and your progress bar should work for all transfer sizes.

But then I see why Matt has done that. You don't get any knowledge of the transfer size in your SoapExtension class (which gets you a non-seekable stream meaning you can't get Stream.Length from it)

So how do you get to the length then? Response.ContentLength is your
answer; since the data you are deserializing is the entire Response stream. But where can you see the Response that is actually received?

Answer: NOWHERE.

I had to use Reflector to back up the call stack a little bit and find a reasonable place, and that's another overridden method in the original article. Heck, let me not bore you with all of this. Here's the entire Extension code in one class:

namespace DotNetProgress
{
internal class ProgressClient :
DotNetProgress.localhost.IIDownloaderservice
{
public ProgressBar Progress;
public int TransferSize;
public Form1.UpdateDelegate ProgressDelegate;
public WebResponse _Response = null;

protected override WebResponse GetWebResponse(WebRequest
request)
{
_Response = base.GetWebResponse(request);
return _Response;
}
}

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;
private int m_totalSize;
// 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_readSize = 8192;
m_totalSize = ((int)
proxy._Response.ContentLength) / 100;
m_progressDelegate = proxy.ProgressDelegate;
}
int CurRead = 0;
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
CurRead += bytesRead;
m_Progress.Invoke(m_progressDelegate, new
object[] { CurRead, m_totalSize });
}
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; }
}
}

and in the Main Form, where I have a button and progress bar:


private void btnDownload_Click(
object sender, EventArgs e)
{
ProgressClient srv = new ProgressClient();
srv.Progress = this.progressBar1;
srv.TransferSize = 8192 * 100;
srv.ProgressDelegate = new
UpdateDelegate(ProgressBarUpdate);
srv.DownloadFile(edtFileName.Text);
}

public delegate void UpdateDelegate(
int CurRead, int TotalSize);

private void ProgressBarUpdate(
int CurRead, int TotalSize)
{
progressBar1.Value = CurRead / TotalSize;
Application.DoEvents();
}


That works for responses of all sizes, and works with the Delphi Win32 server I'd created in the earlier mentioned article.

Note: If you're using the Delphi server code, you'll have to comment out the Delphi TSoapAttachment based code, because MIME attachments aren't supported by .NET. Commenting it out in the interface should work just fine.