다음을 통해 공유


Download / Send Request AsyncWithProgress with HttpClient (WinRT)

Windows 8 Metro Runtime comes with many innovations to be used by us developers. There are quite a number of new features also regarding the Web and Http namespaces.

Imagine you were dealing with a long lasting http communication scenario. It involves an HttpRequest and a response that takes quite a while to download. In windows runtime spirit, you want to have a download progress (since this falls into the determinate progress category).

If we were not dealing with a custom HttpRequest, the perfect fit for this scenario would actually be the Background Transfer Api. However, you want to have the IAsyncOperationWithProgress using the HttpClient . (i.e. You want to have an awaitable operation that gives you progress updates and that you can easily cancel with a cancellation token.

So we start by defining our function signature:

 

      public static  IAsyncOperationWithProgress<string, int> GetStringAsyncWithProgress(this System.Net.Http.HttpClient client, HttpRequestMessage request, CancellationToken cancelToken)

We define it as an extension method to HttpClient. The function takes in two parameters:

  • HttpRequest request               : The request that we are going to send to the server
  • CancellationToken token        : The cancellation token reference that we are going to use to be able to cancel the asynchronous call.

We could have as well declared the progress callback function here as a parameter, but this is not a very common pattern.

In our quest, we will be using the AsynInfo.Run<TResult,TProgress> method to create our IAsyncOperationWithProgress. For this scenario, result type would be string (i.e. we are downloading a string) and let’s say the progress type is integer (i.e. the number of bytes downloaded already). This method has a single parameter (so-called task provider) that is the delegate to create and start the task. In our delegate method (from AsyncInfo.Run<TResult, TProgress>)

*The started task is represented by the Windows Runtime asynchronous action that is returned. The function is passed a cancellation token that the task can monitor to be notified of cancellation requests, and an interface for reporting progress; you can ignore either or both of these arguments if your task does not support progress reporting or cancellation.

*So if we were to create a stub, it would look something like (Task.Run part is just a place holder):

var operation = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancelToken);
 
return AsyncInfo.Run<string, int>((token, progress) =>
{
    if (cancelToken != CancellationToken.None) token = cancelToken;
 
    // TODO: Do something here and return Task<string>
 
    return Task.Run(() => string.Empty);
});

So we create the send request operation (however, not executing it), and returning our async operation with progress. Crucial point here is that the completion option for the http request is set to ResponseHeadersRead, meaning, the operation should complete as soon as the headers are received from the remote server. We want to handle the body of the response with our task provider.

Notice that if the cancellation token that comes as a parameter from our method is not an empty one, we are assigning it to the current synchronization context’s token.

Next step is to actually declare our task provider. Under normal circumstances, the task provider would simply use a cancellation token (i.e. of type CancellationToken) and the progress callback (i.e. of type IProgress<int> in our case). However, we also need the reference to the send request operation.

static async Task<string> GetStringTaskProvider(Task<HttpResponseMessage> httpOperation, CancellationToken token, IProgress<int> progressCallback)

So we basically have to slowly execute our request (i.e. after the headers are downloaded, use a small enough buffer to go through the response stream, so we can send back progress updates), and return the result in async manner.

var responseBuffer = new  byte[500];
 
// Execute the http request and get the initial response
// NOTE: We might receive a network error here
var httpInitialResponse = await httpOperation;
 
using (var responseStream = await httpInitialResponse.Content.ReadAsStreamAsync())
{
    int read;
 
    do
    {
        read = await responseStream.ReadAsync(responseBuffer, 0, responseBuffer.Length);
        result += Encoding.UTF8.GetString(responseBuffer, 0, read);
        offset += read;
 
        progressCallback.Report(offset);
 
    } while  (read != 0);
}
 
return result;

In (not so) short, we have a buffer of 500 bytes, and initial offset of 0. Then we

  • Execute our http operation (the operation will anyways complete as soon as we receive the header response)
  • Open the stream to read the body of the response.
  • In each buffer read, we send a progress callback reporting the current offset.

One thing we forgot is the cancellation token. In each read sequence (i.e. in the while loop), we can actually check if there is any signal from the source about the cancellation.

if (token.IsCancellationRequested)
{
    token.ThrowIfCancellationRequested();
}

This actually completes the implementation of the task provider, we can just plug it into our public function.

public static  IAsyncOperationWithProgress<string, int> GetStringAsyncWithProgress(this System.Net.Http.HttpClient client, HttpRequestMessage request, CancellationToken cancelToken)
{
    var operation = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancelToken);
 
    return AsyncInfo.Run<string, int>((token, progress) =>
    {
        if (cancelToken != CancellationToken.None) token = cancelToken;
 
        return GetStringTaskProvider(operation, token, progress);
    });
}

You can easily extend this extension class having a version that returns a Stream or IBuffer according to your needs. In this current version, you would be getting the number of bytes received as the progress information.

To use the implemented extension method we need first to implement the progress handler function (i.e. Action<int>):

// We declare our progress callback action
Action<int> reportProgress = (progresss) =>
{
    Debug.WriteLine("\t{0:o}\tCurrent Progress {1} bytes", DateTime.Now, progresss);
};

IMPORTANT: Something to note here, is the fact that, if you were to update the UI with this progress value, you would need to use the Dispatcher.RunAsync function to marshal the calls back to the ui thread.

Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
    // TODO: Do something on the UI (e.g. update text, progressbar, etc.)
});

The rest of the execution is quite straightforward:

var client = new  System.Net.Http.HttpClient();
 
// We prepare the HttpRequest to be sent
HttpRequestMessage request = new  HttpRequestMessage(HttpMethod.Get, resourceAddress);
 
m_CancellationSource = new  CancellationTokenSource();
 
// Get the token from the cancellation source
var token = m_CancellationSource.Token;
 
var operationWithProgress = client.GetStringAsyncWithProgress(request, token);
 
// We assign the progress action we defined
operationWithProgress.Progress = (result, progress) => reportProgress(progress);
 
// You can as well set a Timeout value
//m_CancellationSource.CancelAfter(2000);
 
var response = await operationWithProgress;

If the cancellation token is invoked either by the defined time frame or by the Cancel method, the task will throw OperationCanceledException, otherwise if there is a network problem, you can expect an HttpRequestException.

http://canbilgin.files.wordpress.com/2013/05/clientview.png?w=786
 Client Application Debug View - Operation Cancelled Exception

IMPORTANT: One more important note is about the fact that the server should be using the “chunked” transfer-encoding and no buffered transfer. By buffered transfer, I am referring to people (like myself) who will definitely open fiddler and try to see the transfer in chunks. This will be a big disappointment since Fiddler actually acts as a proxy (more as a middle man) and only after the whole response is downloaded, passes it on to the actual recipient.

Here is a test server implementation which would give you enough time to see what is going on and compare the reception of the stream:

Debug.WriteLine(string.Format("\t{0:o}\tStarting Write of {1} bytes", DateTime.Now, bytes.Length)); 
  
while (written != bytes.Length)
{
    var bytesToWrite = bytes.Length - written > 1000 ? 1000 : bytes.Length - written;
  
    Response.Buffer = false;
    Response.BufferOutput = false;
    Response.OutputStream.Write(bytes, written, bytesToWrite);
    Response.OutputStream.Flush();
  
    written += bytesToWrite;
  
    Debug.WriteLine(string.Format("\t{0:o}\tWritten {1} bytes", DateTime.Now, written));
  
    Thread.Sleep(50);
}  
  
Debug.WriteLine(string.Format("\t{0:o}\tEnd Write of {1} bytes", DateTime.Now, bytes.Length));

You can download the full sample from : HttpClient Sample

Happy coding everyone...

See Also An important place to find a huge amount of Visual C# related articles is the TechNet Wiki itself. The best entry point is Visual C# Resources on the TechNet Wiki