wpf开发系列
开发笔记
nuget工具篇
nuget包工具命令
//删除包
dotnet nuget delete -s https://nuget.lingyanspace.com/v3/index.json LingYanAutoUpdate 1.0.0 -k nugetlingyanspace --non-interactive
课纲目录
模块Ⅰ:WPF高阶技术精讲
深入掌握自定义控件
主要是附加属性与控件加载部分
深入掌握控件模板与数据模板
理解控件模板当中包含数据模板
数据模板当中又可以包含控件模板
来回组合学习
最终以模板选择器来弥补遗漏的部分
深入掌握资源样式与动态主题
资源字典如何加载
静态资源与动态资源区别
主题动态切换核心理念
以及后续写项目的规范
埋坑
此处本来是想带大家完完全全手写一个wpf控件库
但是由于很多技术涉及到c#代码,所以先讲后续,高阶通信模块讲完之后再返回这儿
然后还有MAUI部分公开课要把讲xaml基础控件正好中间多学点儿做个过渡。
但是主要的xaml技术就是这三节内容和B站wpf公开课那一套视频看完
模块Ⅱ:高阶通信与高阶模式加并发控制
基础知识
1、进程与线程 :
2、进程与线程的区别。
3、线程生命周期(启动、运行、终止)。
4、同步与异步编程 :
5、同步与异步的基本概念。
6、异步编程的优势与挑战。
7、C# 中的多线程基础 :
8、Thread类和Task类。
9、如何创建和管理线程。
10、WPF 中的线程模型 :
11、UI 线程与后台线程的关系。
12、使用Dispatcher更新 UI。
协议解析与错误处理模块
1、Span<T> 优化二进制协议解析,使用Span<T>提高内存操作效率。
2、错误处理模式 :异常传播与CancellationToken的结合。
3、WPF 中的应用 :在 WPF 应用中处理网络通信错误,实现用户友好的错误提示机制。
虚拟通信模拟模块
1、TcpListener 模拟网络设备 :
创建一个简单的 TCP 服务器。
处理客户端连接和数据传输。
2、SignalR 的内存模拟 :
不依赖真实服务端的情况下,使用 SignalR 模拟实时通信。
3、WPF 中的应用 :
在 WPF 应用中集成虚拟通信模拟工具。
实现一个简单的聊天窗口或状态监控界面。
并发控制与任务调度模块
1、线程同步机制 :MonitorMutex,Semaphore等同步原语,Dispatcher与BackgroundWorker的协作。
2、异步编程与任务调度 :async/await的深入讲解,使用TaskScheduler实现优先级队列。
3、TPL Dataflow 数据流水线 :构建高效的生产者-消费者模型
4、WPF 中的应用 :在 WPF 中实现多线程任务调度,避免 UI 冻结问题。
高效数据处理模块
1、TPL 数据流 :构建数据流管道,实现并行数据处理。
2、内存管理 :使用Span<T>和Memory<T>减少内存分配。
3、WPF 中的应用 :在 WPF 中实现高性能的数据流处理,示例:实时处理传感器数据并在 UI 上显示。
高级通信技术模块
1、WebSocket 通信 :使用System.Net.WebSockets实现 WebSocket 客户端和服务端。
2、SignalR 实时通信 :构建基于 SignalR 的实时应用
3、跨平台通信 :使用 gRPC 或 RESTful API 实现跨平台通信。
4、WPF 中的应用 :在 WPF 中集成 WebSocket 或 SignalR,实现一个实时更新的仪表盘或聊天界面。
性能优化与调试模块
1、常见问题与解决方案 :死锁与竞争条件。内存泄漏与资源耗尽。
2、WPF 中的性能优化 :减少 UI 线程负担。使用虚拟化技术优化列表显示。
实践巩固
实际项目实践模块
目标:
通过实际项目巩固所学知识。
项目主题 :
实现一个简单的聊天应用。
构建一个实时监控系统。
功能要求 :
支持多线程和异步通信。
使用协议解析和错误处理机制。
集成 SignalR 或 WebSocket。
WPF 界面开发 :
设计一个用户友好的界面。
实现动态更新和多线程交互。
聊天应用 :
使用 SignalR 实现实时消息传递。
使用Dispatcher更新聊天记录。
监控系统 :
使用 TPL Dataflow 处理传感器数据。
使用TaskScheduler优化任务调度。
模块Ⅲ:高阶项目实战(全栈)
3.1 综合项目案例
3.1.1 虚拟监控系统
- 使用ICollectionView实现动态数据过滤
- 基于VisualStateManager的报警状态可视化
3.1.2 日志与权限管理
- 使用NLog实现日志分级(Debug/Info/Error)
- 基于角色的权限系统(RBAC)
3.2 精细化案例
3.2.1 数据可视化
- 使用OxyPlot实现动态波形图
- 基于WriteableBitmap的实时图像处理
模块Ⅳ:前沿技术与扩展
4.1 跨平台开发
4.1.1 .NET MAUI深度集成
- 共享业务逻辑层与UI分离设计
- 使用SkiaSharp实现跨平台绘图
4.1.2 WPF与Web技术结合
- 嵌入WebView2实现混合开发
- 使用WebAssembly与Blazor交互
4.2 人工智能集成
4.2.1 机器学习模型集成
- 使用ML.NET实现本地预测
- 基于ONNX的图像识别
4.2.2 数据分析与可视化
- 使用LiveCharts实现动态仪表盘
- 基于Parallel.For的并行数据处理
打包篇
inno
中文环境【ChineseSimplified.isl】
isl环境中文
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
完整的通信链路
OSI 七层模型与协议对照表
层次 | 功能 | 常见协议/标准 | 工业协议 |
---|---|---|---|
物理层 | 定义硬件接口、电气特性、信号传输方式。 | RS-232、RS-485、USB、Ethernet、CAN、Wi-Fi、蓝牙、ZigBee、LoRa、NFC | Modbus RTU(基于RS-485)、CANopen(基于CAN) |
数据链路层 | 提供可靠的数据帧传输,处理错误检测和纠正。 | Ethernet(IEEE 802.3)、Wi-Fi(IEEE 802.11)、PPP、HDLC | Profinet、EtherCAT |
网络层 | 负责路由选择和逻辑地址分配,确保数据包跨网络传输。 | IPv4、IPv6、ICMP、ARP | Modbus TCP(基于IP)、CIP(Common Industrial Protocol) |
传输层 | 提供端到端的通信服务,负责数据分段、重组和流量控制。 | TCP、UDP | MQTT(基于TCP)、CoAP(基于UDP) |
会话层 | 管理会话(连接的建立、维护和终止)。 | SMB、RPC | OPC UA |
表示层 | 数据格式转换、加密解密、数据压缩。 | TLS/SSL、JSON、XML | 无特定工业协议,但加密和数据格式在工业通信中广泛使用。 |
应用层 | 为用户提供直接的网络服务,定义应用程序之间的通信规则。 | HTTP/HTTPS、FTP/SFTP、SMTP/IMAP/POP3、WebSocket、gRPC | Modbus TCP(基于TCP)、OPC UA、BACnet(楼宇自动化控制网络) |
IPC(进程间通信)方式与层次对照表
通信方式 | 功能 | 适用层次 | 应用场景 | 优点 | 缺点 |
匿名管道 | 单向或双向通信,适用于父子进程之间的数据传递。 | 操作系统层(不属于OSI模型,但常用于应用层之上)。 | 本地进程间通信,适用于简单的数据传递。 | 简单易用,操作系统原生支持。 | 仅限本地通信,数据量较小,父子进程关系限制。 |
命名管道 | 支持无亲缘关系的进程间通信,支持本地或网络通信。 | 操作系统层(不属于OSI模型,但常用于应用层之上)。 | 本地或网络进程间通信,适用于跨进程的数据传递。 | 支持跨进程和跨网络通信,灵活性高。 | 实现复杂度较高,性能受限于操作系统。 |
消息队列 | 通过消息队列传递数据,支持异步通信。 | 操作系统层(不属于OSI模型,但常用于应用层之上)。 | 本地或分布式系统中的任务调度和消息传递。 | 支持异步通信,适合任务队列和事件驱动模型。 | 消息队列可能存在阻塞,消息大小有限制。 |
共享内存 | 多个进程共享一块内存区域,速度快。 | 操作系统层(不属于OSI模型,但常用于应用层之上)。 | 实时性要求高的本地进程间通信。 | 速度快,适合大数据量传输。 | 需要进程同步机制(如信号量)避免竞争,开发复杂度高。 |
信号量 | 用于进程同步,避免资源竞争。 | 操作系统层(不属于OSI模型,但常用于应用层之上)。 | 进程间同步和资源管理。 | 简单高效,适合资源锁定和同步。 | 仅用于同步,不适合数据传输。 |
套接字(Socket) | 支持本地和网络通信,基于 TCP/UDP。 | 传输层 | 本地或网络通信,适用于客户端与服务器之间的通信。 | 支持本地和远程通信,灵活性高,适合分布式系统。 | 需要手动管理连接和协议,开发复杂度较高。 |
信号(Signal) | 用于进程间的简单通知机制。 | 操作系统层(不属于OSI模型)。 | 进程间的简单事件通知(如终止、暂停)。 | 简单高效,适合轻量级通知。 | 仅支持简单的信号传递,不适合复杂数据通信。 |
文件映射(Memory-Mapped Files) | 通过文件共享内存区域,支持进程间通信。 | 操作系统层(不属于OSI模型,但常用于应用层之上)。 | 本地进程间通信,适用于大数据量的共享。 | 速度快,适合大数据量传输,支持持久化。 | 需要同步机制避免竞争,依赖文件系统。 |
gRPC | 基于 HTTP/2 的高性能 RPC 框架。 | 应用层 | 微服务之间的高效通信。 | 高性能,支持流式通信,跨语言支持。 | 不适合浏览器直接使用,消息格式为二进制,调试较复杂。 |
REST API | 基于 HTTP 的轻量级通信方式。 | 应用层 | 请求-响应模式的通信,如数据查询,前端与后端之间的通信,适用于 Web 应用。 | 简单易用,广泛支持,适合标准化的请求-响应模式。 | 不支持实时通信,延迟较高,需频繁轮询实现实时性。 |
WebSocket | 基于 TCP 的全双工通信协议。 | 应用层 | 实时性要求高的前端与后端通信(如聊天应用、实时数据推送)。 | 延迟低,性能高,支持全双工通信,适合高并发场景。 | 需要手动管理连接和消息格式,开发复杂度较高。 |
SignalR | 基于 WebSocket/SSE/Long Polling 的实时通信框架。 | 应用层 | 聊天、通知、仪表盘、多人协作。 | 自动选择最佳协议,开发简单,支持广播和组通信。 | 如果无法使用 WebSocket,性能可能下降(如 Long Polling)。 |
MQTT | 基于 TCP 的轻量级发布/订阅协议。 | 应用层 | 物联网设备的轻量级通信。 | 轻量级,低带宽消耗,支持 QoS(服务质量)等级,适合低功耗设备。 | 需要专门的 MQTT Broker,消息格式简单,不适合复杂数据结构。 |
D-Bus | Linux 系统中用于进程间通信的消息总线。 | 操作系统层(不属于OSI模型,但常用于应用层之上)。 | Linux 系统中的进程间通信(如桌面环境组件之间的通信)。 | 高效,支持广播和点对点通信,适合 Linux 环境。 | 仅适用于 Linux 系统,跨平台支持较差。 |
ZeroMQ | 高性能消息队列库,支持多种通信模式(如发布/订阅、请求/响应)。 | 应用层 | 分布式系统中的高性能通信。 | 高性能,支持多种通信模式,跨平台支持好。 | 需要手动管理消息格式和连接,学习曲线较陡峭。 |
补充说明
关于 Modbus 的位置说明
- Modbus RTU:基于 RS-485 的物理层和数据链路层协议,主要用于工业现场设备之间的通信。
- Modbus TCP:基于 TCP/IP 的应用层协议,适用于工业自动化中设备与服务器之间的通信。
协议的组合应用示例
1. 下位机与上位机
- 场景:工业现场的传感器(下位机)通过 RS-485 与 PLC(上位机)通信。
- 协议组合:
- 物理层:RS-485。
- 数据链路层:Modbus RTU。
- 应用层:Modbus 协议解析传感器数据。
2. 上位机与远程服务器
- 场景:PLC(上位机)通过以太网将数据上传到远程服务器。
- 协议组合:
- 物理层:Ethernet。
- 数据链路层:以太网帧。
- 网络层:IPv4。
- 传输层:TCP。
- 应用层:HTTP 或 MQTT。
3. 远程服务器与前端客户
- 场景:远程服务器通过 Web 服务向前端客户提供实时数据。
- 协议组合:
- 物理层:光纤或 Wi-Fi。
- 数据链路层:以太网或 Wi-Fi。
- 网络层:IPv4/IPv6。
- 传输层:TCP。
- 表示层:TLS(HTTPS)。
- 应用层:REST API 或 WebSocket。
总结
- 清晰的分层职责:每一层都有明确的功能和协议,避免混淆。
- Modbus 的位置:RTU 属于物理层和数据链路层,TCP 属于应用层。
- IPC 的层次划分:大部分 IPC 通信方式位于操作系统层,但也可以延伸到传输层和应用层。
- 协议组合的灵活性:根据场景
需求,协议可以跨层组合使用,形成完整的通信链路。
基础篇
依赖属性propdb
public int MyProperty
{
get { return (int)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));
附加属性propa
public static int GetMyProperty(DependencyObject obj)
{
return (int)obj.GetValue(MyPropertyProperty);
}
public static void SetMyProperty(DependencyObject obj, int value)
{
obj.SetValue(MyPropertyProperty, value);
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.RegisterAttached("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));
xaml资源键类型
定义方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
普通字符串键 | 简单直观,易于使用 | 命名冲突风险,无法跨程序集共享 | 小型项目,局部资源 |
类型键 | 自动应用,无需显式引用 | 灵活性较低 | 全局样式,基础样式复用 |
静态资源键 | 强类型支持,可维护性高 | 定义稍复杂 | 大型项目,组件化开发 |
ComponentResourceKey |
跨程序集支持,语义化标识 | 定义和使用复杂 | 组件库开发,主题或样式库 |
动态资源键 | 动态绑定,灵活性高 | 性能开销 | 主题切换,多语言支持 |
//普通字符串键
<Style x:Key="MyButtonStyle" TargetType="Button" />
//类型键(隐式样式)
<Style TargetType="Button">
<Setter Property="Background" Value="LightBlue" />
</Style>
//静态资源键
public static class ResourceKeys
{
public static readonly string CloseButtonStyle = "CloseButtonStyle";
}
<Style x:Key="{x:Static local:ResourceKeys.CloseButtonStyle}" TargetType="Button" />
//组件资源键ComponentResourceKey
public partial class DataTemplateKeys
{
public static ComponentResourceKey ItemClose => new ComponentResourceKey(typeof(DataTemplateKeys), "S.DataTemplate.Item.Close");
}
<DataTemplate x:Key="{ComponentResourceKey ResourceId=S.DataTemplate.Item.Close, TypeInTargetAssembly={x:Type local:DataTemplateKeys}}">
//静态资源键与组件资源键结合
public static class ResourceKeys
{
public static readonly ComponentResourceKey CloseButtonStyleKey = new ComponentResourceKey(typeof(ResourceKeys), "CloseButtonStyle");
}
<Style x:Key="{x:Static local:ResourceKeys.CloseButtonStyleKey}" TargetType="Button" />
//动态资源键
<Button Style="{DynamicResource MyButtonStyle}" />
编译篇
编译后事件
:: 检查Lib、Dll文件夹路径是否存在
IF NOT EXIST "$(TargetDir)libs" (
MD "$(TargetDir)libs"
)
:: 将指定的dll、xml、pdb、config文件移动到libs文件夹
move "$(TargetDir)*.dll" "$(TargetDir)libs"
move "$(TargetDir)*.xml" "$(TargetDir)libs"
move "$(TargetDir)*.pdb" "$(TargetDir)libs"
move "$(TargetDir)*.config" "$(TargetDir)libs"
:: 将runtimes文件夹移动到libs文件夹
move "$(TargetDir)runtimes" "$(TargetDir)libs"
:: 把主程序的相关文件从libs转移出来
move "$(TargetDir)libs\NLog.config" "$(TargetDir)NLog.config"
move "$(TargetDir)libs\$(ProjectName).exe.config" "$(TargetDir)$(ProjectName).exe.config"
move "$(TargetDir)libs\$(ProjectName).exe.xml" "$(TargetDir)$(ProjectName).exe.xml"
move "$(TargetDir)libs\$(ProjectName).pdb" "$(TargetDir)$(ProjectName).pdb"
加扫描文件夹
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- 添加对libs文件夹的搜索路径 -->
<probing privatePath="libs"/>
</assemblyBinding>
</runtime>
扩展篇
HttpCilent发送文件有进度
try
{
var lcaolSelectTeam = await this.ToGetSelectTeam();
if (lcaolSelectTeam.Code != 20000)
{
throw new Exception(lcaolSelectTeam.Message);
}
var localToken = await this.ToGetUserToken();
if (localToken.Code != 20000)
{
throw new Exception(localToken.Message);
}
var taskworkFloderBody = await this.ToGetTaskworkProxyFloder();
if (taskworkFloderBody.Code != 20000)
{
throw new Exception(taskworkFloderBody.Message);
}
var rootTaskworkFloder = taskworkFloderBody.Data.PathCombine(teamTaskwrokId.ToString());
HttpClientHandler handler = new HttpClientHandler();
ProgressMessageHandler progressMessageHandler = new ProgressMessageHandler(handler);
progressMessageHandler.HttpSendProgress += (sender, e) =>
{
action.Invoke(e.ProgressPercentage);
};
using (HttpClient httpClient = new HttpClient(progressMessageHandler))
{
httpClient.BaseAddress = new Uri("https://lycg.lingyanspace.com/");
httpClient.DefaultRequestHeaders.Add("Authorization", localToken.Data);
using (var multipartFormData = new MultipartFormDataContent())
{
var bom = rootTaskworkFloder.PathCombine("bom").FileCombine("default.json");
if (File.Exists(bom) && needUploadCloudModel.BOM)
{
AddFile(multipartFormData, "bom", bom);
}
var bIfc = rootTaskworkFloder.PathCombine("bifc").FileCombine("default.ifc");
if (File.Exists(bIfc) && needUploadCloudModel.BIFC)
{
AddFile(multipartFormData, "bIfc", bIfc);
}
var nc1Files = Directory.GetFiles(rootTaskworkFloder.PathCombine("nc1"), "*.nc1", SearchOption.TopDirectoryOnly).ToList();
if (nc1Files != null && nc1Files.Count > 0 && needUploadCloudModel.NC1)
{
nc1Files.ForEach(f =>
{
AddFile(multipartFormData, "nc1Files", f);
});
}
var dxfFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("dxf"), "*.dxf", SearchOption.TopDirectoryOnly).ToList();
if (dxfFiles != null && dxfFiles.Count > 0 && needUploadCloudModel.DXF)
{
dxfFiles.ForEach(f =>
{
AddFile(multipartFormData, "dxfFiles", f);
});
}
var aifcFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("aifc"), "*.ifc", SearchOption.TopDirectoryOnly).ToList();
if (aifcFiles != null && aifcFiles.Count > 0 && needUploadCloudModel.AIFC)
{
aifcFiles.ForEach(f =>
{
AddFile(multipartFormData, "aifcFiles", f);
});
}
var pifcFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("pifc"), "*.ifc", SearchOption.TopDirectoryOnly).ToList();
if (pifcFiles != null && pifcFiles.Count > 0 && needUploadCloudModel.PIFC)
{
pifcFiles.ForEach(f =>
{
AddFile(multipartFormData, "pifcFiles", f);
});
}
var drawingFiles = Directory.GetFiles(rootTaskworkFloder.PathCombine("drawing"), "*.pdf", SearchOption.TopDirectoryOnly).
Concat(Directory.GetFiles(rootTaskworkFloder.PathCombine("drawing"), "*.dwg", SearchOption.TopDirectoryOnly)).ToList();
if (drawingFiles != null && drawingFiles.Count > 0 && needUploadCloudModel.Drawing)
{
drawingFiles.ForEach(f =>
{
AddFile(multipartFormData, "drawingFiles", f);
});
}
var response = await httpClient.PutAsync($"/api/Team/UploadTeamTaskworkBatchData?teamId={lcaolSelectTeam.Data.Id}&teamTaskwrokId={teamTaskwrokId}", multipartFormData);
if (response.IsSuccessStatusCode)
{
var data = await response.Content.ReadAsStringAsync();
var jsonBody = JsonConvert.DeserializeObject<ResponceBody<string>>(data);
return jsonBody;
}
else
{
throw new Exception(await response.Content.ReadAsStringAsync());
}
}
}
}
catch (Exception ex)
{
return new ResponceBody<string>(40000, ex.Message, null);
}
编号排序
public class StringSortComparer : IComparer<string>
{
public bool MatchCase { get; }
public StringSortComparer(bool matchCase)
{
MatchCase = matchCase;
}
private int CharCompare(char a, char b, bool matchCase)
{
char _a = char.MinValue, _b = char.MinValue;
if (matchCase) { _a = a; _b = b; }
else { _a = char.ToUpper(a); _b = char.ToUpper(b); }
if (_a > _b) return 1;
if (_a < _b) return -1;
return 0;
}
public int Compare(string x, string y)
{
// 如果 y 为空,则 y 应该排在最后面
if (string.IsNullOrEmpty(y)) return -1;
// 如果 x 为空,而 y 不为空,则 x 应该排在 y 之前
if (string.IsNullOrEmpty(x)) return 1;
int len;
if (x.Length > y.Length) len = x.Length;
else len = y.Length;
string numericx = "";
string numericy = "";
for (int i = 0; i < len; i++)
{
char cx = char.MinValue;
char cy = char.MinValue;
if (i < x.Length) cx = x[i];
if (i < y.Length) cy = y[i];
if (cx >= 48 && cx <= 57) numericx += cx;
if (cy >= 48 && cy <= 57) numericy += cy;
if (i == len - 1)
{
if (numericx.Length > 0 && numericy.Length > 0)
{
if (decimal.Parse(numericx) < decimal.Parse(numericy)) return -1;
if (decimal.Parse(numericx) > decimal.Parse(numericy)) return 1;
}
return CharCompare(cy, cy, MatchCase);
}
if ((cx >= 48 && cx <= 57) && (cy >= 48 && cy <= 57)) continue;
if (numericx.Length > 0 && numericy.Length > 0)
{
if (decimal.Parse(numericx) < decimal.Parse(numericy)) return -1;
if (decimal.Parse(numericx) > decimal.Parse(numericy)) return 1;
}
if (CharCompare(cx, cy, MatchCase) == 0) continue;
return CharCompare(cx, cy, MatchCase);
}
return 0;
}
}
下载文件进度
public class HttpHelper
{
/// <summary>
/// 下载单个文件
/// </summary>
/// <param name="action"></param>
/// <param name="netWrokUrl"></param>
/// <param name="localUrl"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<long> DownloadSingleFile(Action<double> action, string netWrokUrl, string localUrl)
{
long totalBytesReceived = 0;
var progress = new Progress<HttpDownloadProgress>(p =>
{
if (p.TotalBytesToReceive.HasValue)
{
totalBytesReceived = (long)p.BytesReceived;
double percent = (double)p.BytesReceived / p.TotalBytesToReceive.Value * 100.0;
action.Invoke(percent);
}
else
{
LoggerHelper.DefaultLogger($"特殊情况:{netWrokUrl}的TotalBytesToReceive无值");
}
});
var fileBytes = await new HttpClient().GetByteArrayAsync(new Uri(netWrokUrl), progress, CancellationToken.None);
if (File.Exists(localUrl))
{
File.Delete(localUrl);
}
await localUrl.SaveLocalFileAsync(new MemoryStream(fileBytes));
return totalBytesReceived;
}
private static async Task<long> DownloadSingleFile(Action<long, long> progressAction, string networkUrl, string localUrl, long totalBytes)
{
long bytesReceived = 0;
var progress = new Progress<HttpDownloadProgress>(p =>
{
bytesReceived = (long)p.BytesReceived;
progressAction(bytesReceived, totalBytes);
});
using (var httpClient = new HttpClient())
{
var fileBytes = await httpClient.GetByteArrayAsync(new Uri(networkUrl), progress, CancellationToken.None);
if (File.Exists(localUrl))
{
File.Delete(localUrl);
}
using (var fileStream = new FileStream(localUrl, FileMode.CreateNew))
{
await fileStream.WriteAsync(fileBytes, 0, fileBytes.Length);
}
return bytesReceived;
}
}
/// <summary>
/// 下载文件集合
/// </summary>
/// <param name="overallProgressAction"></param>
/// <param name="files"></param>
/// <returns></returns>
public static async Task DownloadMultipleFiles(Action<double> overallProgressAction,Dictionary<string, string> files)
{
var downloadTasks = new List<Task<long>>();
long totalFileSize = 0;
Dictionary<string, long> fileSizes = new Dictionary<string, long>();
// 首先,预估所有文件的大小(可以通过HEAD请求或是其他方式获取)
using (var httpClient = new HttpClient())
{
foreach (var file in files)
{
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, file.Key));
long contentLength = response.Content.Headers.ContentLength ?? 0;
fileSizes[file.Key] = contentLength;
totalFileSize += contentLength;
}
}
// 存储每个文件的已接收字节
Dictionary<string, long> receivedBytes = new Dictionary<string, long>();
foreach (var file in files)
{
string networkUrl = file.Key;
string localUrl = file.Value;
Task<long> downloadTask = DownloadSingleFile(
(bytesReceived, totalBytes) =>
{
receivedBytes[networkUrl] = bytesReceived;
// 计算总体进度
long totalReceived = 0;
foreach (var received in receivedBytes.Values)
{
totalReceived += received;
}
double overallProgress = (double)totalReceived / totalFileSize * 100.0;
overallProgressAction(overallProgress);
},
networkUrl,
localUrl,
fileSizes[networkUrl]
);
downloadTasks.Add(downloadTask);
}
// 等待所有下载任务完成
long[] results = await Task.WhenAll(downloadTasks);
}
}
下载byte
public static class HttpClientExtension
{
private const int BufferSize = 262144;
public static async Task<byte[]> GetByteArrayAsync(this HttpClient client, Uri requestUri, IProgress<HttpDownloadProgress> progress, CancellationToken cancellationToken)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
using (var responseMessage = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
{
responseMessage.EnsureSuccessStatusCode();
var content = responseMessage.Content;
if (content == null)
{
return Array.Empty<byte>();
}
var headers = content.Headers;
var contentLength = headers.ContentLength;
using (var responseStream = await content.ReadAsStreamAsync().ConfigureAwait(false))
{
var buffer = new byte[BufferSize];
int bytesRead;
var bytes = new List<byte>();
var downloadProgress = new HttpDownloadProgress();
if (contentLength.HasValue)
{
downloadProgress.TotalBytesToReceive = (ulong)contentLength.Value;
}
progress?.Report(downloadProgress);
while ((bytesRead = await responseStream.ReadAsync(buffer, 0, BufferSize, cancellationToken).ConfigureAwait(false)) > 0)
{
bytes.AddRange(buffer.Take(bytesRead));
downloadProgress.BytesReceived += (ulong)bytesRead;
progress?.Report(downloadProgress);
}
return bytes.ToArray();
}
}
}
}
public struct HttpDownloadProgress
{
public ulong BytesReceived { get; set; }
public ulong? TotalBytesToReceive { get; set; }
}