Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
302 views
in Technique[技术] by (71.8m points)

c# - Showing progress in percentage while uploading and downloading using HttpWebRequest class

I'm trying to upload and download (in the same request) to a server using HttpWebRequest in C# and since the size of data is considerable (considering network speed) I would like to show the user how far of the job is done and how much is left (not in seconds but in percentage).

I've read a couple of examples trying to implement this but none of them show any progress bar. They all just use async not to block the UI while it is uploading/downloading. And they are mostly focused on upload / download and none try including them both in the same request.

Since I'm using .Net 4 as my target framework, I can not implement an async method myself. If you are to suggest anything asynchronous, please just use Begin... methods and not await keyword! Thanks.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You're going to need to know a few things to be successful with this.

Step 0: keep the documentation for .NET 4.0 handy:

If you look at the HttpWebRequest documentation, you'll see that GetResponse() has two similarly-named methods: BeginGetResponse() and EndGetResponse(). These methods use the oldest .NET asyncronous pattern, known as "the IAsyncResult pattern". There's thousands of pages of text about this pattern, and you can read tutorials if you want detailed information. Here's the crash course:

  1. For a method Foo(), there is a BeginFoo() that may take parameters and must return an IAsyncResult implementation. This method performs the task Foo() performs, but does the work without blocking the current thread.
  2. If BeginFoo() takes an AsyncCallback parameter, it expects you to provide a delegate it will call when it is finished. (This is one of several ways to know it's finished, but happens to be the technique HttpWebRequest uses.)
  3. EndFoo() takes the IAsyncResult returned by BeginFoo() as a parameter and returns the same thing Foo() returns.

Next, there's a pitfall of which you should be aware. Controls have a property called "thread affinity", and it means it is only valid to interact with a control if you are working on the thread that created the control. This is important, because the callback method you give to BeginFoo() is not guaranteed to be called from that thread. It's actually really easy to handle this:

  • Every control has an InvokeRequired property that is the only safe property to call from the wrong thread. If it returns true, you know you are on an unsafe thread.
  • Every control has an Invoke() method that accepts a delegate parameter and will call that delegate on the thread that created the control.

With all of that out of the way, let's start looking at some code I wrote in a simple WinForms application to report the progress of downloading the Google home page. This should be similar to code that will solve your problem. It is not the best code, but it demonstrates the concepts. The form had a progress bar named progressBar1, and I called GetWebContent() from a button click.

HttpWebRequest _request;
IAsyncResult _responseAsyncResult;

private void GetWebContent() {
    _request = WebRequest.Create("http://www.google.com") as HttpWebRequest;
    _responseAsyncResult = _request.BeginGetResponse(ResponseCallback, null);           
}

This code starts the asynchronous version of GetResponse(). We need to store the request and the IAsyncResult in fields because ResponseCallback() needs to call EndGetResponse(). Everything in GetWebContent() is on the UI thread, so if you wanted to update some controls it is safe to do so here. Next, ResponseCallback():

private void ResponseCallback(object state) {
    var response = _request.EndGetResponse(_responseAsyncResult) as HttpWebResponse;
    long contentLength = response.ContentLength;
    if (contentLength == -1) {
        // You'll have to figure this one out.
    }
    Stream responseStream = response.GetResponseStream();
    GetContentWithProgressReporting(responseStream, contentLength);
    response.Close();
}

It's forced to take an object parameter by the AsyncCallback delegate signature, but I'm not using it. It calls EndGetResponse() with the IAsyncResult we got earlier, and now we can proceed as if we hadn't used asyncronous calls. But since this is an asynchronous callback, it might be executing on a worker thread, so do not update any controls directly here.

Anyway, it gets the content length from the response, which is needed if you want to calculate your download progress. Sometimes the header that provides this information isn't present, and you get -1. That means you're on your own for progress calculation and you'll have to find some other way to know the total size of the file you are downloading. In my case it was sufficient to set the variable to some value since I didn't care about the data itself.

After that, it gets the stream that represents the response and passes that along to a helper method that does the downloading and progress reporting:

private byte[] GetContentWithProgressReporting(Stream responseStream, long contentLength) {
    UpdateProgressBar(0);

    // Allocate space for the content
    var data = new byte[contentLength];
    int currentIndex = 0;
    int bytesReceived = 0;
    var buffer = new byte[256];
    do {
        bytesReceived = responseStream.Read(buffer, 0, 256);
        Array.Copy(buffer, 0, data, currentIndex, bytesReceived);
        currentIndex += bytesReceived;

        // Report percentage
        double percentage = (double)currentIndex / contentLength;
        UpdateProgressBar((int)(percentage * 100));
    } while (currentIndex < contentLength);

    UpdateProgressBar(100);
    return data;
}

This method still might be on a worker thread (since it is called by the callback) so it is still not safe to update controls from here.

The code is common for examples of downloading a file. It allocates some memory to store the file. It allocates a buffer to get file chunks from the stream. In a loop it grabs a chunk, puts the chunk in the bigger array, and calculates the progress percentage. When it's downloaded as many bytes as it expects to get, it quits. It's a bit of trouble, but if you were to use the tricks that let you download a file in one shot, you wouldn't be able to report download progress in the middle.

(One thing I feel obligated to point out: if your chunks are too small, you'll be updating the progress bar very quickly and this can still cause the form to lock up. I usually keep a Stopwatch running and try not to update progress bars more than twice a second, but there's other ways to throttle the updates.)

Anyway, the only thing left is the code that actually updates the progress bar, and there's a reason it's in its own method:

private void UpdateProgressBar(int percentage) {
    // If on a worker thread, marshal the call to the UI thread
    if (progressBar1.InvokeRequired) {
        progressBar1.Invoke(new Action<int>(UpdateProgressBar), percentage);
    } else {
        progressBar1.Value = percentage;
    }
}

If InvokeRequired returns true, it calls itself via Invoke(). If it happens to be on the correct thread, it updates the progress bar. If you happen to be using WPF, there are similar ways to marshal calls but I believe they happen via the Dispatcher object.

I know that's a lot. That's part of why the new async stuff is so great. The IAsyncResult pattern is powerful but doesn't abstract many details away from you. But even in the newer patterns, you must keep track of when it is safe to update a progress bar.

Things to consider:

  • This is example code. I haven't included any exception handling for brevity.
  • If an exception would have been thrown by GetResponse(), it will be thrown instead when you call EndGetResponse().
  • If an exception happens in the callback and you haven't handled it, it'll terminate its thread but not your application. This can be strange and mysterious to a newbie asynchronous programmer.
  • Pay attention to what I said about the content length! You may think you can ask the Stream by checking its Length property, but I've found this to be unreliable. A Stream is free to throw NotSupportedException or return a value like -1 if it's not sure, and generally with network streams if you aren't told in advance how much data to expect then you can only keep asking, "Is there more?"
  • I didn't say anything about uploading because:
    • I'm not as familiar with uploading.
    • You can follow a similar pattern: you'll probably use HttpWebRequest.BeginGetRequestStream() so you can asynchronously write the file you're uploading. All of the warnings about updating controls apply.

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...