使用HttpClient下载文件时,如何添加下载完成事件?
实现文件下载的进度与完成事件处理
核心思路
通过自定义事件和事件参数类,在单个文件下载完成(无论成功/失败)时触发完成事件,同时手动读取流来实现进度跟踪。
步骤1:定义事件参数类
用来传递下载过程中的关键信息:
public class FileDownloadCompletedEventArgs : EventArgs { public string FileUrl { get; } public string LocalFilePath { get; } public bool IsSuccess { get; } public Exception? Exception { get; } public FileDownloadCompletedEventArgs(string fileUrl, string localFilePath, bool isSuccess, Exception? exception = null) { FileUrl = fileUrl; LocalFilePath = localFilePath; IsSuccess = isSuccess; Exception = exception; } } public class FileDownloadProgressEventArgs : EventArgs { public string FileUrl { get; } public string LocalFilePath { get; } public long BytesDownloaded { get; } public long TotalBytes { get; } public double ProgressPercentage => TotalBytes > 0 ? (double)BytesDownloaded / TotalBytes * 100 : 0; public FileDownloadProgressEventArgs(string fileUrl, string localFilePath, long bytesDownloaded, long totalBytes) { FileUrl = fileUrl; LocalFilePath = localFilePath; BytesDownloaded = bytesDownloaded; TotalBytes = totalBytes; } }
步骤2:在窗体类中声明事件
public event EventHandler<FileDownloadCompletedEventArgs>? FileDownloadCompleted; public event EventHandler<FileDownloadProgressEventArgs>? FileDownloadProgressChanged;
步骤3:修改下载方法,触发事件
替换原有的Download方法,添加进度跟踪和完成事件触发逻辑:
private async Task Download() { FilesDownoads downloads = new FilesDownloads(@"d:\testfiles"); downloads.PrepareLinks(); using var client = new HttpClient(); for (int i = 0; i < files.Count; i++) { string fileUrl = files[i]; string localPath = @"e:\files1\file" + i.ToString() + ".txt"; bool isSuccess = false; Exception? ex = null; try { // 获取响应头先拿到文件总大小 using var response = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); long totalBytes = response.Content.Headers.ContentLength ?? -1; using var s = await response.Content.ReadAsStreamAsync(); using var fs = new FileStream(localPath, FileMode.OpenOrCreate); byte[] buffer = new byte[8192]; long bytesDownloaded = 0; int bytesRead; // 手动读写流,实时计算进度 while ((bytesRead = await s.ReadAsync(buffer)) > 0) { await fs.WriteAsync(buffer.AsMemory(0, bytesRead)); bytesDownloaded += bytesRead; // 触发进度更新事件 FileDownloadProgressChanged?.Invoke(this, new FileDownloadProgressEventArgs(fileUrl, localPath, bytesDownloaded, totalBytes)); } isSuccess = true; } catch (Exception exception) { ex = exception; } finally { // 无论成功失败,都触发完成事件 FileDownloadCompleted?.Invoke(this, new FileDownloadCompletedEventArgs(fileUrl, localPath, isSuccess, ex)); } } }
步骤4:订阅并处理事件
在窗体初始化时订阅事件,编写完成后的处理逻辑(注意UI操作需切换到主线程):
public YourFormName() { InitializeComponent(); FileDownloadCompleted += OnFileDownloadCompleted; FileDownloadProgressChanged += OnFileDownloadProgressChanged; } private void OnFileDownloadCompleted(object? sender, FileDownloadCompletedEventArgs e) { // UI操作必须回到主线程 if (InvokeRequired) { Invoke(new Action(() => OnFileDownloadCompleted(sender, e))); return; } if (e.IsSuccess) { // 这里写文件下载成功后的操作,比如校验、解析、记录日志 MessageBox.Show($"文件 {Path.GetFileName(e.LocalFilePath)} 下载完成"); } else { MessageBox.Show($"下载失败:{e.FileUrl}\n错误信息:{e.Exception?.Message}"); } } private void OnFileDownloadProgressChanged(object? sender, FileDownloadProgressEventArgs e) { if (InvokeRequired) { Invoke(new Action(() => OnFileDownloadProgressChanged(sender, e))); return; } // 更新进度显示,比如进度条或标签 labelProgress.Text = $"当前文件:{Path.GetFileName(e.LocalFilePath)}\n进度:{e.ProgressPercentage:F1}%"; }
关键说明
- 完成事件:在
finally块中触发,确保每个文件下载结束后都会通知,不管成功还是异常。 - 进度跟踪:通过手动读写流的方式统计已下载字节数,实时触发进度事件;如果服务器返回的响应头没有
Content-Length,进度百分比会显示0。 - 线程安全:因为下载操作在后台线程执行,UI操作必须通过
Invoke切换到主线程,避免跨线程操作异常。
内容的提问来源于stack exchange,提问作者Daniel Lip
相关产品推荐
相关产品推荐

