应使用 HttpClient 配合流式读取与 IProgress 主动推送实现可靠下载进度更新,避免 WebClient 及其已移除的 DownloadProgressChanged 事件;需用临时文件、CancellationToken 和线程安全调度确保健壮性。

用 HttpClient 下载文件并实时更新进度条
直接用 WebClient 无法可靠获取下载进度,尤其在 .NET Core/.NET 5+ 中已不推荐;HttpClient 配合流式读取才是可控方案。关键不是“能不能”,而是“怎么让 HttpContent.ReadAsStreamAsync() 和 UI 线程安全协作”。
常见错误是把整个响应体一次性 await client.GetByteArrayAsync(url),这会阻塞进度反馈,且大文件容易 OOM。
- 必须用
HttpResponseMessage.Content.ReadAsStreamAsync()获取响应流,再分块读取 - UI 更新(如进度条)必须通过
Dispatcher.Invoke(WPF)或BeginInvoke(WinForms)回主线程,不能在后台线程直接改控件属性 - 记得设置
HttpClient.Timeout,否则慢网或中断时会卡死几十秒 - 示例中用
progress.Report((int)(100 * totalRead / contentLength)),但注意contentLength可能为-1(服务器未返回Content-Length),此时只能靠已读字节数做相对估算
DownloadProgressChanged 事件根本不会触发?
这是 WebClient 的典型幻觉——它只在部分 HTTP 场景下报告进度,比如 POST 上传或重定向后重试时行为不一致;更严重的是,.NET Core 起已移除该事件支持,项目若从 .NET Framework 迁移过来,编译能过但运行时静默失效。
别挣扎了,删掉所有 webClient.DownloadProgressChanged += ...,换成基于 IProgress<int></int> 的主动推送模型。
-
WebClient的进度事件依赖底层 WinINet 或系统代理,跨平台(Linux/macOS)完全不可用 - 即使在 Windows 上,启用
webClient.Credentials = CredentialCache.DefaultCredentials后,进度事件也可能丢失 - 如果你还在用
BackgroundWorker+ReportProgress,那和WebClient是同一套过时机制,一并淘汰
如何避免下载中途取消导致文件损坏
用户点“取消”按钮时,不是简单停掉 Stream.CopyToAsync 就完事。没写完的临时文件如果直接保留,下次续传逻辑又没做,就会变成垃圾残留。
- 务必用唯一临时名(如
path + ".tmp")写入,下载成功后再File.Move替换原文件 - 取消操作要传递
CancellationToken到所有异步调用:包括GetAsync、ReadAsStreamAsync、CopyToAsync - 捕获
OperationCanceledException后立即删除临时文件,否则磁盘悄悄吃满 - 不要在
catch块里吞掉异常还继续往下走——比如finally里又去Move一个不完整的 .tmp 文件
WPF 进度条卡顿或跳变?检查绑定和调度频率
进度值频繁更新(比如每 KB 就 Report 一次)会导致 UI 线程被大量小任务挤占,尤其在低配机器上进度条反而“卡住不动”,或者突然从 10% 跳到 95%。
- 限制上报频率:用
Stopwatch控制最小间隔(如 ≥50ms),避免每读 1KB 就刷一次 UI - 绑定属性别用
NotifyPropertyChanged暴力刷新,WPF 的ProgressBar.Value本身支持异步更新,但前提是你的INotifyPropertyChanged实现没锁主线程 - 如果用了
async void处理按钮点击,取消后可能引发NullReferenceException(因为 UI 元素已被释放),一律改成async Task并await - 调试时加个
Debug.WriteLine($"Progress: {value}"),确认是不是后台确实没推进,还是只是 UI 没跟上
最麻烦的其实是服务器不支持 Range 请求,也没发 Content-Length ——这种情况下你连“还剩多少”都算不出来,只能显示“正在下载…”配合旋转动画,硬加百分比反而误导用户。


















