Progress bar with HttpClient
Asked Answered
C

11

58

i have a file downloader function:

        HttpClientHandler aHandler = new HttpClientHandler();
        aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
        HttpClient aClient = new HttpClient(aHandler);
        aClient.DefaultRequestHeaders.ExpectContinue = false;
        HttpResponseMessage response = await aClient.GetAsync(url);
        InMemoryRandomAccessStream randomAccessStream = new InMemoryRandomAccessStream();

        // To save downloaded image to local storage
        var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
        filename, CreationCollisionOption.ReplaceExisting);
        var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);
        DataWriter writer = new DataWriter(fs.GetOutputStreamAt(0));

        writer.WriteBytes(await response.Content.ReadAsByteArrayAsync());

        await writer.StoreAsync();
        //current.image.SetSource(randomAccessStream);
        writer.DetachStream();
        await fs.FlushAsync();

How can i realize progress bar functionality? Maybe i can get the writers bytes written so far? Or something?

P.S. I cant use DownloadOperation(Background transferring) because data from server requests certificate - and this functionality doesn't exist in DownloadOperations.

Chism answered 18/12, 2013 at 15:9 Comment(4)
What about Windows.Web.Http.HttpClient. That one supports progress.Homs
Is Windows.Web.Http.HttpClient available on the desktop yet? I thought it was just for windows store apps. Which I've never actually seen anyone use in real life.Erbil
Is Windows.Web.Http.HttpClient can be used in ASP.Net?Braithwaite
this code example is exclusive for android / xamarin?Entresol
H
37

The best way to go is using Windows.Web.Http.HttpClient instead of System.Net.Http.HttpClient. The first one supports progress.

But if for some reason you want to stick to the System.Net one, you will need to implement your own progress.

Remove the DataWriter, remove the InMemoryRandomAccessStream and add HttpCompletionOption.ResponseHeadersRead to GetAsync call so it returns as soon as headers are received, not when the whole response is received. I.e.:

// Your original code.
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(
    url,
    HttpCompletionOption.ResponseHeadersRead); // Important! ResponseHeadersRead.

// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
    filename,
    CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);

// New code.
Stream stream = await response.Content.ReadAsStreamAsync();
IInputStream inputStream = stream.AsInputStream();
ulong totalBytesRead = 0;
while (true)
{
    // Read from the web.
    IBuffer buffer = new Windows.Storage.Streams.Buffer(1024);
    buffer = await inputStream.ReadAsync(
        buffer,
        buffer.Capacity,
        InputStreamOptions.None);

    if (buffer.Length == 0)
    {
        // There is nothing else to read.
        break;
    }

    // Report progress.
    totalBytesRead += buffer.Length;
    System.Diagnostics.Debug.WriteLine("Bytes read: {0}", totalBytesRead);

    // Write to file.
    await fs.WriteAsync(buffer);
}
inputStream.Dispose();
fs.Dispose();
Homs answered 18/12, 2013 at 18:0 Comment(9)
Thanks, this will do, but is there a way to get total amount of bytes i will recieve? To set ProgressBar's maximumChism
I had the filesize coming from the server, so i have managed to use that. Thanks for your working code it helped me a lotChism
Why not use ProgressMessageHandler msdn.microsoft.com/en-us/library/… ?Tauten
@Darrel, have you actually got ProgressMessageHandler working, and if so on what platform? The docs look promising, but I can't find System.Net.Http.Handlers (in WPF 4.5.1).Erbil
@Erbil You will find the handler and associated classes here nuget.org/packages/Microsoft.AspNet.WebApi.Client/5.0.0Tauten
@Homs is it possible to track the progress like this while Uploading file via HttpClient PostAsync methos..? because i saw there is no method of PostAsync available, which takes HttpCompletionOption.ResponseHeadersRead as parameter..! Help me please..Accumbent
You could use PostAsync with a System.Net.Http.StreamContent. You initialize the StreamContent with a stream implemented by you, and you can get an approximate upload progress each time Stream.Read() is called in your stream because you know how much data has been read.Homs
what if you don't have the total bytes coming from the server? is there an accurate way of calculating the total bytes from the response?Stative
You should get the total bytes by looking into the Content-Length header in the response. I.e.: response.Content.Headers.ContentLengthHoms
S
60

From .Net 4.5 onwards: Use IProgress<T>

Since .Net 4.5 you can handle asynchronous progress reporting with the IProgress<T> interface. You can write an extension method for downloading files using the HttpClient that can be called like this where progress is the implementation of IProgress<float> for your progress bar or other UI stuff:

// Seting up the http client used to download the data
using (var client = new HttpClient()) {
    client.Timeout = TimeSpan.FromMinutes(5);

    // Create a file stream to store the downloaded data.
    // This really can be any type of writeable stream.
    using (var file = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) {

        // Use the custom extension method below to download the data.
        // The passed progress-instance will receive the download status updates.
        await client.DownloadAsync(DownloadUrl, file, progress, cancellationToken);
    }
}

Implementation

The code for this extension method looks like this. Note that this extension depends on another extension for handling asynchronous stream copying with progress reporting.

public static class HttpClientExtensions
{
    public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<float> progress = null, CancellationToken cancellationToken = default) {
        // Get the http headers first to examine the content length
        using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead)) {
            var contentLength = response.Content.Headers.ContentLength;

            using (var download = await response.Content.ReadAsStreamAsync(cancellationToken)) {

                // Ignore progress reporting when no progress reporter was 
                // passed or when the content length is unknown
                if (progress == null || !contentLength.HasValue) {
                    await download.CopyToAsync(destination);
                    return;
                }

                // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%)
                var relativeProgress = new Progress<long>(totalBytes => progress.Report((float)totalBytes / contentLength.Value));
                // Use extension method to report progress while downloading
                await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
                progress.Report(1);
            }
        }
    }
}

With stream extension for the real progress reporting:

public static class StreamExtensions
{
    public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress = null, CancellationToken cancellationToken = default) {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        if (!source.CanRead)
            throw new ArgumentException("Has to be readable", nameof(source));
        if (destination == null)
            throw new ArgumentNullException(nameof(destination));
        if (!destination.CanWrite)
            throw new ArgumentException("Has to be writable", nameof(destination));
        if (bufferSize < 0)
            throw new ArgumentOutOfRangeException(nameof(bufferSize));

        var buffer = new byte[bufferSize];
        long totalBytesRead = 0;
        int bytesRead;
        while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
            await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
            totalBytesRead += bytesRead;
            progress?.Report(totalBytesRead);
        }
    }
}
Spy answered 29/9, 2017 at 22:56 Comment(5)
This doesn't allow to add a cerrificateChism
Create the HttpClient using a HttpClientHandler with correct certificate options applied like you did in your questionSpy
Great answer. The only modification I made was adding the cancellationToken to the GetAsync and CopyToAsync (non-progress support) calls inside your HttpClientExtensions DownloadAsync method.Essie
There is a much simpler way to do this: https://mcmap.net/q/328887/-progress-bar-with-httpclientBroadway
I had a the problem that progress.Report was firing to often slowing down my app, this helped me: https://mcmap.net/q/331419/-iprogress-lt-t-gt-how-often-to-report-progressMilker
J
55

Here's a self-contained class that'll do the download, and report back the progress percentage, based on code from TheBlueSky on this SO answer, and eriksendc on this GitHub comment.

public class HttpClientDownloadWithProgress : IDisposable
{
    private readonly string _downloadUrl;
    private readonly string _destinationFilePath;

    private HttpClient _httpClient;

    public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

    public event ProgressChangedHandler ProgressChanged;

    public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath)
    {
        _downloadUrl = downloadUrl;
        _destinationFilePath = destinationFilePath;
    }

    public async Task StartDownload()
    {
        _httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };

        using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
            await DownloadFileFromHttpResponseMessage(response);
    }

    private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
    {
        response.EnsureSuccessStatusCode();

        var totalBytes = response.Content.Headers.ContentLength;

        using (var contentStream = await response.Content.ReadAsStreamAsync())
            await ProcessContentStream(totalBytes, contentStream);
    }

    private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
    {
        var totalBytesRead = 0L;
        var readCount = 0L;
        var buffer = new byte[8192];
        var isMoreToRead = true;

        using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
        {
            do
            {
                var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
                if (bytesRead == 0)
                {
                    isMoreToRead = false;
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
                    continue;
                }

                await fileStream.WriteAsync(buffer, 0, bytesRead);

                totalBytesRead += bytesRead;
                readCount += 1;

                if (readCount % 100 == 0)
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
            }
            while (isMoreToRead);
        }
    }

    private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
    {
        if (ProgressChanged == null)
            return;

        double? progressPercentage = null;
        if (totalDownloadSize.HasValue)
            progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

        ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}

Usage:

var downloadFileUrl = "http://example.com/file.zip";
var destinationFilePath = Path.GetFullPath("file.zip");

using (var client = new HttpClientDownloadWithProgress(downloadFileUrl, destinationFilePath))
{
    client.ProgressChanged += (totalFileSize, totalBytesDownloaded, progressPercentage) => {
        Console.WriteLine($"{progressPercentage}% ({totalBytesDownloaded}/{totalFileSize})");
    };

    await client.StartDownload();
}

Result:

7.81% (26722304/342028776)
8.05% (27535016/342028776)
8.28% (28307984/342028776)
8.5% (29086548/342028776)
8.74% (29898692/342028776)
8.98% (30704184/342028776)
9.22% (31522816/342028776)
Jakob answered 2/4, 2017 at 15:10 Comment(7)
Had to remove the following to get progress update: if (readCount % 100 == 0)Estuary
Any idea as to why its not loading the header total size?Stanch
@Stanch only reason could be the server isn't actually returning that header. Try doing a GET on the target URL with Postman, and see if it includes the Content-Length header.Lentigo
Im unsure how to do that- though it is my own file from Google Drive if that helps in any way about fixing the issueStanch
is this using System.web.http or System.net.http ?Agential
@Stanch is it a download of multiple files? I know that Google Drive and OneDrive don't send content length headers when downloading multiple files. I think they create and output a zipped file on-the-fly, so they don't know either. Your browser also won't show the total size until it's done. gumuruh I can't remember, haven't worked with UWP in a while. Why are you asking?Lentigo
I'm curious to understand how you would attach an HttpClientHandler to this class. I have been researching examples on HttpClient file transfer examples, and thus far have found wildly different code. Thanks in advance.Thoracotomy
H
37

The best way to go is using Windows.Web.Http.HttpClient instead of System.Net.Http.HttpClient. The first one supports progress.

But if for some reason you want to stick to the System.Net one, you will need to implement your own progress.

Remove the DataWriter, remove the InMemoryRandomAccessStream and add HttpCompletionOption.ResponseHeadersRead to GetAsync call so it returns as soon as headers are received, not when the whole response is received. I.e.:

// Your original code.
HttpClientHandler aHandler = new HttpClientHandler();
aHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
HttpClient aClient = new HttpClient(aHandler);
aClient.DefaultRequestHeaders.ExpectContinue = false;
HttpResponseMessage response = await aClient.GetAsync(
    url,
    HttpCompletionOption.ResponseHeadersRead); // Important! ResponseHeadersRead.

// To save downloaded image to local storage
var imageFile = await ApplicationData.Current.LocalFolder.CreateFileAsync(
    filename,
    CreationCollisionOption.ReplaceExisting);
var fs = await imageFile.OpenAsync(FileAccessMode.ReadWrite);

// New code.
Stream stream = await response.Content.ReadAsStreamAsync();
IInputStream inputStream = stream.AsInputStream();
ulong totalBytesRead = 0;
while (true)
{
    // Read from the web.
    IBuffer buffer = new Windows.Storage.Streams.Buffer(1024);
    buffer = await inputStream.ReadAsync(
        buffer,
        buffer.Capacity,
        InputStreamOptions.None);

    if (buffer.Length == 0)
    {
        // There is nothing else to read.
        break;
    }

    // Report progress.
    totalBytesRead += buffer.Length;
    System.Diagnostics.Debug.WriteLine("Bytes read: {0}", totalBytesRead);

    // Write to file.
    await fs.WriteAsync(buffer);
}
inputStream.Dispose();
fs.Dispose();
Homs answered 18/12, 2013 at 18:0 Comment(9)
Thanks, this will do, but is there a way to get total amount of bytes i will recieve? To set ProgressBar's maximumChism
I had the filesize coming from the server, so i have managed to use that. Thanks for your working code it helped me a lotChism
Why not use ProgressMessageHandler msdn.microsoft.com/en-us/library/… ?Tauten
@Darrel, have you actually got ProgressMessageHandler working, and if so on what platform? The docs look promising, but I can't find System.Net.Http.Handlers (in WPF 4.5.1).Erbil
@Erbil You will find the handler and associated classes here nuget.org/packages/Microsoft.AspNet.WebApi.Client/5.0.0Tauten
@Homs is it possible to track the progress like this while Uploading file via HttpClient PostAsync methos..? because i saw there is no method of PostAsync available, which takes HttpCompletionOption.ResponseHeadersRead as parameter..! Help me please..Accumbent
You could use PostAsync with a System.Net.Http.StreamContent. You initialize the StreamContent with a stream implemented by you, and you can get an approximate upload progress each time Stream.Read() is called in your stream because you know how much data has been read.Homs
what if you don't have the total bytes coming from the server? is there an accurate way of calculating the total bytes from the response?Stative
You should get the total bytes by looking into the Content-Length header in the response. I.e.: response.Content.Headers.ContentLengthHoms
B
11

The simplest way to implement progress tracking for both uploading and downloading is to use ProgressMessageHandler from the Microsoft.AspNet.WebApi.Client nuget package.

Note: this library was originally named System.Net.Http.Formatting, and was renamed to Microsoft.AspNet.WebApi.Client. However, this library is not related to ASP.Net and can be used by any project looking for official Microsoft extensions to HttpClient. The source code is available here.

Example:

var handler = new HttpClientHandler() { AllowAutoRedirect = true };
var ph = new ProgressMessageHandler(handler);

ph.HttpSendProgress += (_, args) =>
{
    Console.WriteLine($"upload progress: {(double)args.BytesTransferred / args.TotalBytes}");
};

ph.HttpReceiveProgress += (_, args) =>
{
    Console.WriteLine($"download progress: {(double)args.BytesTransferred / args.TotalBytes}");
};

var client = new HttpClient(ph);
await client.SendAsync(...);

Note that this will not report progress if uploading a byte array. The request message content must be a stream.

Broadway answered 2/6, 2022 at 20:15 Comment(4)
But, as you can see, the question didn't ask about ASPChism
This has nothing to do with ASP. This nuget package contains the namespace System.Net.Http.Formatting which is simply content extensions for System.Net.Http. This can (and should) be used by any project looking for official extensions to HttpClient.Broadway
By far the easiest and simplest solution!Portiaportico
this doesnt work using HttpCompletionOption.ResponseHeadersRead, right?Freddie
N
7

The following code shows a minimal example of what must be done against the HttpClient api to get download progress.

HttpClient client = //...

// Must use ResponseHeadersRead to avoid buffering of the content
using (var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)){
    // You must use as stream to have control over buffering and number of bytes read/received
    using (var stream = await response.Content.ReadAsStreamAsync())
    {
        // Read/process bytes from stream as appropriate

        // Calculated by you based on how many bytes you have read.  Likely incremented within a loop.
        long bytesRecieved = //...

        long? totalBytes = response.Content.Headers.ContentLength;
        double? percentComplete = (double)bytesRecieved / totalBytes;

        // Do what you want with `percentComplete`
    }
}

The above does not tell you how to process the stream, how to report the process, or try to provide a direct solution to the code in the original question. However, this answer may be more accessible to future readers who wish to apply having progress to in their code.

Nagano answered 3/4, 2017 at 19:10 Comment(2)
Despite being a little incomplete, this is the only valid answer I can find. Windows.Web.Http is windows only and also it is a UWP library which is basically a nightmare dystopian version of .netFurred
Make sure to use await stream.ReadAsync(...) otherwise it will download everything in one go away way. At least on .NET Framework 4.6.Knotts
T
6

This is my variation on the answer of René Sackers. Main differences:

  • A more functional style.
  • Only one method instead of a whole object.
  • Can cancel the download
        public async static Task Download(
               string downloadUrl,
               string destinationFilePath,
               Func<long?, long, double?, bool> progressChanged)
        {
            using var httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };
            using var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);

            response.EnsureSuccessStatusCode();
            var totalBytes = response.Content.Headers.ContentLength;

            using var contentStream = await response.Content.ReadAsStreamAsync();
            var totalBytesRead = 0L;
            var readCount = 0L;
            var buffer = new byte[8192];
            var isMoreToRead = true;

            static double? calculatePercentage(long? totalDownloadSize, long totalBytesRead) => totalDownloadSize.HasValue ? Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2) : null;

            using var fileStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);

            do
            {
                var bytesRead = await contentStream.ReadAsync(buffer);
                if (bytesRead == 0)
                {
                    isMoreToRead = false;

                    if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
                    {
                        throw new OperationCanceledException();
                    }

                    continue;
                }

                await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));

                totalBytesRead += bytesRead;
                readCount++;

                if (readCount % 100 == 0)
                {
                    if (progressChanged(totalBytes, totalBytesRead, calculatePercentage(totalBytes, totalBytesRead)))
                    {
                        throw new OperationCanceledException();
                    }
                }
            }
            while (isMoreToRead);
        }

It can be called this way:

    // Change this variable to stop the download
    // You can use a global variable or some kind of state management
    var mustStop = false;

    var downloadProgress = (long? _, long __, double? progressPercentage) =>
    {
       if (progressPercentage.HasValue)
          progressBar.Value = progressPercentage.Value;

       // In this example only the variable is checked
       // You could write other code that evaluates other conditions
       return mustStop;
    };

    SomeClass.Download("https://example.com/bigfile.zip", "c:\downloads\file.zip", downloadProgress);
Theine answered 3/11, 2021 at 14:29 Comment(3)
How is this method to be called and update on a progressbar? Thanks!Tiffanietiffanle
Edited the answer to include a use sampleTheine
nice, gotta convert this to DotNetFramework 4.8Brandenburg
M
5

same as @René Sackers solution above, but added the ability to cancel the download

class HttpClientDownloadWithProgress : IDisposable
{
    private readonly string _downloadUrl;
    private readonly string _destinationFilePath;
    private readonly CancellationToken? _cancellationToken;

    private HttpClient _httpClient;

    public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

    public event ProgressChangedHandler ProgressChanged;

    public HttpClientDownloadWithProgress(string downloadUrl, string destinationFilePath, CancellationToken? cancellationToken = null)
    {
        _downloadUrl = downloadUrl;
        _destinationFilePath = destinationFilePath;
        _cancellationToken = cancellationToken;
    }

    public async Task StartDownload()
    {
        _httpClient = new HttpClient { Timeout = TimeSpan.FromDays(1) };

        using (var response = await _httpClient.GetAsync(_downloadUrl, HttpCompletionOption.ResponseHeadersRead))
            await DownloadFileFromHttpResponseMessage(response);
    }

    private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
    {
        response.EnsureSuccessStatusCode();

        var totalBytes = response.Content.Headers.ContentLength;

        using (var contentStream = await response.Content.ReadAsStreamAsync())
            await ProcessContentStream(totalBytes, contentStream);
    }

    private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
    {
        var totalBytesRead = 0L;
        var readCount = 0L;
        var buffer = new byte[8192];
        var isMoreToRead = true;

        using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
        {
            do
            {
                int bytesRead;
                if (_cancellationToken.HasValue)
                {
                    bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, _cancellationToken.Value);
                }
                else
                {
                    bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
                }

                if (bytesRead == 0)
                {
                    isMoreToRead = false;
                    continue;
                }

                await fileStream.WriteAsync(buffer, 0, bytesRead);

                totalBytesRead += bytesRead;
                readCount += 1;

                if (readCount % 10 == 0)
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
            }
            while (isMoreToRead);

        }

        //the last progress trigger should occur after the file handle has been released or you may get file locked error
        TriggerProgressChanged(totalDownloadSize, totalBytesRead);
    }

    private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
    {
        if (ProgressChanged == null)
            return;

        double? progressPercentage = null;
        if (totalDownloadSize.HasValue)
            progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

        ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
    }

    public void Dispose()
    {
        _httpClient?.Dispose();
    }
}
Mapping answered 4/9, 2018 at 17:9 Comment(0)
I
5

René Sackers version is excellent but it could be better. Specifically, it has a subtle race condition caused by TriggerProgressChanged firing before the stream closes. The fix is to fire the event after the stream is explicitly disposed. The version below includes the above change, inherits from HttpClient and adds support for cancellation tokens.

public delegate void ProgressChangedHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

public class HttpClientWithProgress : HttpClient
{
    private readonly string _DownloadUrl;
    private readonly string _DestinationFilePath;

    public event ProgressChangedHandler ProgressChanged;

    public HttpClientWithProgress(string downloadUrl, string destinationFilePath)
    {
        _DownloadUrl = downloadUrl;
        _DestinationFilePath = destinationFilePath;
    }

    public async Task StartDownload()
    {
        using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead))
            await DownloadFileFromHttpResponseMessage(response);
    }

    public async Task StartDownload(CancellationToken cancellationToken)
    {
        using (var response = await GetAsync(_DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
            await DownloadFileFromHttpResponseMessage(response);
    }

    private async Task DownloadFileFromHttpResponseMessage(HttpResponseMessage response)
    {
        response.EnsureSuccessStatusCode();
        long? totalBytes = response.Content.Headers.ContentLength;
        using (var contentStream = await response.Content.ReadAsStreamAsync())
            await ProcessContentStream(totalBytes, contentStream);
    }

    private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
    {
        long totalBytesRead = 0L;
        long readCount = 0L;
        byte[] buffer = new byte[8192];
        bool isMoreToRead = true;

        using (FileStream fileStream = new FileStream(_DestinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
        {
            do
            {
                int bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
                if (bytesRead == 0)
                {
                    isMoreToRead = false;
                    continue;
                }

                await fileStream.WriteAsync(buffer, 0, bytesRead);

                totalBytesRead += bytesRead;
                readCount += 1;

                if (readCount % 10 == 0)
                    TriggerProgressChanged(totalDownloadSize, totalBytesRead);
            }
            while (isMoreToRead);
        }
        TriggerProgressChanged(totalDownloadSize, totalBytesRead);
    }

    private void TriggerProgressChanged(long? totalDownloadSize, long totalBytesRead)
    {
        if (ProgressChanged == null)
            return;

        double? progressPercentage = null;
        if (totalDownloadSize.HasValue)
            progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

        ProgressChanged(totalDownloadSize, totalBytesRead, progressPercentage);
    }
}
Integrate answered 30/3, 2020 at 17:19 Comment(0)
G
0

Hm, you could have another thread check the current size of the stream being written (you'd also pass the expected file size to it) and then update the progress bar accordingly.

Granulation answered 18/12, 2013 at 15:38 Comment(3)
i would be glad if you could provide an exampleChism
Sadly I do not own a Win8 copy, so I can't test your function. Yet, if you want to make things rather simple you could make the filename and filesize global, have a background worker with a loop and a thread sleep checking the fileSize on a regular basis, and updating the progress bar. This is, however, a not very elegant solution.Granulation
I think that won't work. Nothing is written to the stream until ReadAsByteArrayAsync returns.Homs
D
0

This is a modified version of René Sackers answer with the following functional changes:

  • http client not disposed (because it should not be disposed)
  • better progress handling
  • callback to create httpRequest (custom header support)
  • utilizes ArrayPool to reduce memory footprint
  • automatic event subscribe+unsubscribe to prevent memory leaks by event handlers

You can also use this nuget package https://www.nuget.org/packages/Amusoft.Toolkit.Http to gain all benefits. Since it supports net462 and above that is probably the easiest way.

Usage:

await DownloadWithProgress.ExecuteAsync(HttpClients.General, assetUrl, downloadFilePath, progressHandler, () =>
{
    var requestMessage = new HttpRequestMessage(HttpMethod.Get, assetUrl);
    requestMessage.Headers.Accept.TryParseAdd("application/octet-stream");
    return requestMessage;
});

I guess i am not the only one who needs custom headers so i figured i would share this rewrite

Implementation:

public delegate void DownloadProgressHandler(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage);

public static class DownloadWithProgress
{
    public static async Task ExecuteAsync(HttpClient httpClient, string downloadPath, string destinationPath, DownloadProgressHandler progress, Func<HttpRequestMessage> requestMessageBuilder = null)
    {
        requestMessageBuilder ??= GetDefaultRequestBuilder(downloadPath);
        var download = new HttpClientDownloadWithProgress(httpClient, destinationPath, requestMessageBuilder);
        download.ProgressChanged += progress;
        await download.StartDownload();
        download.ProgressChanged -= progress;
    }

    private static Func<HttpRequestMessage> GetDefaultRequestBuilder(string downloadPath)
    {
        return () => new HttpRequestMessage(HttpMethod.Get, downloadPath);
    }
}

internal class HttpClientDownloadWithProgress
{
    private readonly HttpClient _httpClient;
    private readonly string _destinationFilePath;
    private readonly Func<HttpRequestMessage> _requestMessageBuilder;
    private int _bufferSize = 8192;

    public event DownloadProgressHandler ProgressChanged;

    public HttpClientDownloadWithProgress(HttpClient httpClient, string destinationFilePath, Func<HttpRequestMessage> requestMessageBuilder)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _destinationFilePath = destinationFilePath ?? throw new ArgumentNullException(nameof(destinationFilePath));
        _requestMessageBuilder = requestMessageBuilder ?? throw new ArgumentNullException(nameof(requestMessageBuilder));
    }

    public async Task StartDownload()
    {
        using var requestMessage = _requestMessageBuilder.Invoke();
        using var response = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
        await DownloadAsync(response);
    }

    private async Task DownloadAsync(HttpResponseMessage response)
    {
        response.EnsureSuccessStatusCode();

        var totalBytes = response.Content.Headers.ContentLength;

        using (var contentStream = await response.Content.ReadAsStreamAsync())
            await ProcessContentStream(totalBytes, contentStream);
    }

    private async Task ProcessContentStream(long? totalDownloadSize, Stream contentStream)
    {
        var totalBytesRead = 0L;
        var readCount = 0L;
        var buffer = ArrayPool<byte>.Shared.Rent(_bufferSize);
        var isMoreToRead = true;

        using (var fileStream = new FileStream(_destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None, _bufferSize, true))
        {
            do
            {
                var bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length);
                if (bytesRead == 0)
                {
                    isMoreToRead = false;
                    ReportProgress(totalDownloadSize, totalBytesRead);
                    continue;
                }

                await fileStream.WriteAsync(buffer, 0, bytesRead);

                totalBytesRead += bytesRead;
                readCount += 1;

                if (readCount % 100 == 0)
                    ReportProgress(totalDownloadSize, totalBytesRead);
            }
            while (isMoreToRead);
        }

        ArrayPool<byte>.Shared.Return(buffer);
    }

    private void ReportProgress(long? totalDownloadSize, long totalBytesRead)
    {
        double? progressPercentage = null;
        if (totalDownloadSize.HasValue)
            progressPercentage = Math.Round((double)totalBytesRead / totalDownloadSize.Value * 100, 2);

        ProgressChanged?.Invoke(totalDownloadSize, totalBytesRead, progressPercentage);
    }
}
Disunion answered 6/8, 2021 at 17:41 Comment(4)
After 7 years of asked question - this is still actual =)Chism
This is nice. Just need a C# 7.3 version.Brandenburg
where found Https.General? progressHandler is progressbar on windows forms? i'm using this code in net 6.0!!! nice code!!!Cichlid
@Chism yup. Then again seeing how progress is usually a UI desire it does make sense that it is not built in even though it is often a desire with downloads. Oh wellDisunion
T
-1

Im not really sure how to measure how the completion logic, but for now this seems to do it.

public event ProgressChangedHandler ProgressChanged;
public event ProgressCompleteHandler DownloadComplete;

...

TriggerProgressChanged(totalDownloadSize, totalBytesRead);
TriggerDownloadComplete(totalBytesRead == totalDownloadSize);
private void TriggerDownloadComplete(bool status)
{
   DownloadComplete(status);
}
client.DownloadComplete += (status) =>
{
   if (status)
   {
       // success
   }
};
Thurlow answered 18/9, 2022 at 8:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.