Barra de progreso con HttpClient

tengo una función de descarga de archivos:

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(); 

¿Cómo puedo obtener la funcionalidad de barra de progreso? Tal vez puedo obtener los escritores bytes escritos hasta el momento? ¿O algo?

PD: No puedo usar DownloadOperation (Transferencia en segundo plano) porque los datos del servidor solicitan el certificado, y esta funcionalidad no existe en DownloadOperations.

La mejor manera de hacerlo es usar Windows.Web.Http.HttpClient lugar de System.Net.Http.HttpClient . El primero apoya el progreso.

Pero si por algún motivo desea seguir con System.Net uno, deberá implementar su propio progreso.

Quite el DataWriter , elimine InMemoryRandomAccessStream y agregue HttpCompletionOption.ResponseHeadersRead a la llamada GetAsync para que vuelva tan pronto como se reciban los encabezados, no cuando se reciba toda la respuesta. Es decir:

 // 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(); 

Aquí hay una clase independiente que realizará la descarga e informará el porcentaje de progreso, según el código de TheBlueSky en esta respuesta SO , y eriksendc en este comentario de GitHub .

 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(); } } 

Uso:

 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(); } 

Resultado:

 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) 

De .Net 4.5 en adelante: use IProgress

Desde .Net 4.5 puede manejar el informe de progreso asincrónico con la IProgress . Puede escribir un método de extensión para descargar archivos utilizando el HttpClient que se puede llamar así, donde el progress es la implementación de IProgress para su barra de progreso u otras cosas de UI:

 // 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); } } 

Implementación

El código para este método de extensión se ve así. Tenga en cuenta que esta extensión depende de otra extensión para manejar la copia de flujo asíncrona con informes de progreso.

 public static class HttpClientExtensions { public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress 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()) { // 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(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); } } } } 

Con extensión de flujo para el informe de progreso real:

 public static class StreamExtensions { public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress 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); } } } 

El siguiente código muestra un ejemplo mínimo de lo que se debe hacer contra la API de HttpClient para obtener el progreso de la descarga.

 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` } } 

Lo anterior no le dice cómo procesar la transmisión, cómo informar el proceso o intentar proporcionar una solución directa al código en la pregunta original. Sin embargo, esta respuesta puede ser más accesible para los futuros lectores que deseen solicitar el progreso en su código.

Hm, podrías tener otro hilo que verifique el tamaño actual de la secuencia que se está escribiendo (también le pasarías el tamaño de archivo esperado) y luego actualizar la barra de progreso en consecuencia.

igual que la solución @ René Sackers anterior, pero agregó la capacidad de cancelar la descarga

 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(); } }