资讯动态

C#/WPF中CSV读写与DataGrid绑定:解析、编码与增删改查实战指南

发布时间:2026/10/13 17:07:19 来源:尧图企业网站定制
简介这是一份面向C#/WPF开发者的CSV文件操作资源覆盖读取、写入、增加、删除、修改与查询等完整数据管理场景适合需要实现轻量级数据交换或与Excel、文本文件兼容操作的初中级开发者。资源包共46个文件以13个C#源码文件为核心包含CSVHelper.cs、MainWindow.xaml等WPF界面与逻辑实现另有配置文件、资源文件及可直接运行的exe示例压缩包整体约330KB结构简洁便于快速对照学习。已有4325人学习下载。内容围绕CSV基础结构、TextFieldParser与CsvHelper等主流库的用法展开并给出WPF中DataGrid绑定、文件对话框、异步读写等实践方案同时包含增删改查的具体实现思路及大文件处理、编码设置、异常处理等注意事项。通过源码与示例工程读者可以掌握在WPF项目中集成CSV读写与界面展示的完整流程直接复用或改造相关代码提升日常数据操作效率。1. C#/WPF 里的 CSV 操作远不止“读文本按逗号切”某次给一个 WPF 桌面程序加数据导入功能某开发者第一版用 File.ReadAllLines 加 string.Split(,) 把 CSV 读进 DataGrid数据里一个“北京,海淀”就把整行列错位了后面换了解析库写出来的文件在 Excel 里又全是乱码。那个下午我们都在和一个“简单”的文本格式较劲。C#/WPF 的 Csv 读写与增删改查实际包含三件事选对解析方案、把数据和 DataGrid 绑在一起、在写回文件时守住编码和转义规则。这篇文章按这几条线说最后会给出一个适合直接复用的封装。适合正在做导入导出、数据维护功能的 WPF 开发者也适合被 CSV 编码和引号坑过的人对照排雷。2. 读写分家CSV 解析方案怎么选才不踩数据错乱的坑CSV 不是严格标准格式但 RFC 4180 基本被所有工具默认遵守核心规则只有三条记录按行分隔字段默认用逗号分隔字段里如果出现逗号、双引号或换行必须用双引号把这个字段包起来字段内部的双引号写成两个连续双引号。大多数自己写的解析代码翻车都是只实现了第二条忽略了第三条。2.1 手动 split 解析 CSV 为什么必然翻车引号与逗号的嵌套关系一行原本只有两个字段“姓名”和“评语”评语内容是他说不错,继续。按 CSV 规则写成文件是张三,他说不错,继续用 string.Split(,) 去切这一行会变成三段张三、他说不错、继续。问题就出在逗号出现在引号内部时它属于字段内容不是分隔符。要正确解析必须记住“当前是否在引号内”这个状态。下面是一个只处理单行的简化版状态机public static Liststring ParseCsvLine(string line) { var fields new Liststring(); var current new StringBuilder(); bool inQuotes false; for (int i 0; i line.Length; i) { char ch line[i]; if (inQuotes) { if (ch ) { // 两个连续引号表示一个转义的引号 if (i 1 line.Length line[i 1] ) { current.Append(); i; } else { inQuotes false; } } else { current.Append(ch); } } else { if (ch current.Length 0) { inQuotes true; // 字段以引号开头进入引号模式 } else if (ch ,) { fields.Add(current.ToString()); current.Clear(); } else { current.Append(ch); } } } fields.Add(current.ToString()); return fields; }这个版本只覆盖了最简单字段结构条件current.Length 0假设字段值前面没有空格也不处理字段内换行。CSV 字段允许包含\r\n一旦出现按物理行读取就会把一条记录切断。所以它只能用于你完全控制格式的内部小工具。我一般不建议在业务代码里维护这种解析器除非确认整个文件不会有引号字段。2.2 内置 TextFieldParser不引第三方依赖的兜底方案如果项目不想引入第三方库.NET 里有一个被很多人忽略的内置解析器TextFieldParser位于 Microsoft.VisualBasic.FileIO 命名空间。它能正确识别引号内逗号、转义引号并按记录而不是按物理行读取。using Microsoft.VisualBasic.FileIO; public static ListListstring ReadCsvWithTextFieldParser(string filePath) { var rows new ListListstring(); using (var parser new TextFieldParser(filePath, Encoding.UTF8)) { parser.TextFieldType FieldType.Delimited; parser.SetDelimiters(,); parser.HasFieldsEnclosedInQuotes true; while (!parser.EndOfData) { string[] fields parser.ReadFields(); rows.Add(new Liststring(fields)); } } return rows; }逻辑说明TextFieldParser 打开文件后按分隔符逐条读取ReadFields()返回当前记录的字段数组遇到引号内逗号不会误切。HasFieldsEnclosedInQuotes必须设为 true否则带引号的字段原样带引号返回。参数说明TextFieldType.Delimited表示按分隔符解析SetDelimiters(,)可以改成其他分隔符比如制表符或分号构造参数里Encoding.UTF8决定文件按什么编码读。它的问题在于返回的是二维字符串数组列名映射、字段类型转换仍然要自己写。2.3 CsvHelper 的延迟读取与类型映射主力方案这么选CsvHelper 是目前 C# 生态里处理 CSV 最接近“省心”的库。选它主要因为三点字段到 C# 属性的强类型映射由库完成RFC 4180 的引号、转义、换行规则全部内置基于流式读写既能处理大文件也提供异步接口。最基础的一段读取代码using CsvHelper; using System.Globalization; public static ListStudent ReadStudents(string filePath) { ListStudent data; using (var reader new StreamReader(filePath, Encoding.UTF8)) using (var csv new CsvReader(reader, CultureInfo.InvariantCulture)) { data csv.GetRecordsStudent().ToList(); } return data; }逻辑说明GetRecordsStudent()返回的是延迟求值的IEnumerableStudent真正解析发生在这条链被迭代时。所以必须在using块内调用ToList()把数据完整取出来。常见翻车写法是把这个IEnumerable直接返回给 ViewModel等 DataGrid 绑定去遍历时StreamReader已经关闭抛ObjectDisposedException。参数说明CultureInfo.InvariantCulture而不是当前文化区域主要是避免数值字段解析受区域设置影响例如某些地区的系统把逗号当小数点导致decimal转换出错。3. 把 CSV 接进 DataGrid模型映射、集合绑定与增删改查落地读完数据只是第一步WPF 场景的核心是把 CSV 内容变成界面能编辑的结构。这里用“学生成绩”做示例CSV 文件表头是中文C# 模型属性是英文。3.1 模型与 ClassMap先让 CSV 列名和 C# 属性对齐CSV 要对应上强类型先定义模型类。推荐加一个 ClassMap它负责把 CSV 表头和 C# 属性对应起来public class Student { public int Id { get; set; } public string Name { get; set; } public decimal Score { get; set; } } public sealed class StudentMap : ClassMapStudent { public StudentMap() { Map(m m.Id).Name(学号); Map(m m.Name).Name(姓名); Map(m m.Score).Name(成绩); } }逻辑说明ClassMap 配置对读写双向生效。读 CSV 时CsvHelper 按Name(学号)找到对应列写 CSV 时表头按同样规则生成。这样就不用在业务代码里写“第 0 列是学号”这类索引式判断。参数说明Map(m m.Score).Name(成绩)表示默认按属性名匹配只有列名不一致时才需要显式指定。我会把 ClassMap 写在模型类旁边保持一个人一个文件后期列名变了只改一处。3.2 读取到 ObservableCollectionDataGrid 绑定与增删操作WPF 的 DataGrid 绑定集合用的标准结构是 ObservableCollection它能通知 UI 集合新增或删除。先看绑定DataGrid ItemsSource{Binding Students} SelectedItem{Binding SelectedStudent} AutoGenerateColumnsFalse CanUserAddRowsFalse DataGrid.Columns DataGridTextColumn Header学号 Binding{Binding Id} / DataGridTextColumn Header姓名 Binding{Binding Name} / DataGridTextColumn Header成绩 Binding{Binding Score} / /DataGrid.Columns /DataGrid然后是加载方法public ObservableCollectionStudent Students { get; } new(); public Student SelectedStudent { get; set; } public void LoadFile(string filePath) { ListStudent data; using (var reader new StreamReader(filePath, Encoding.UTF8)) using (var csv new CsvReader(reader, CultureInfo.InvariantCulture)) { csv.Context.RegisterClassMapStudentMap(); data csv.GetRecordsStudent().ToList(); } Students.Clear(); foreach (var item in data) { Students.Add(item); } }逻辑说明先在局部变量里把数据完整读出来再逐条加入 ObservableCollection。如果直接在foreach里Add每加一条都会触发 UI 集合通知数据量大时界面刷新开销很大。这里先Clear()再添加为的是重复加载同一份文件不会残留旧数据。参数说明RegisterClassMapStudentMap()是让当前 CsvReader 使用上一步定义的列映射如果模型属性名和表头完全一致也可以不注册。增删操作只需操作集合public void AddStudent() { int nextId Students.Count 0 ? Students.Max(s s.Id) 1 : 1; Students.Add(new Student { Id nextId }); } public void DeleteSelected() { if (SelectedStudent is null) return; Students.Remove(SelectedStudent); }要注意一点ObservableCollection 只通知集合级增删。DataGrid 里编辑单元格内容时数据是会写回实体属性的但如果程序代码里修改某个对象的属性希望 UI 同步刷新就需要给 Student 补 INotifyPropertyChanged。项目里建议直接引入 CommunityToolkit.Mvvm 的 ObservableObject省掉重复代码。3.3 保存策略全量重写还是增量追加以及写回时的三个注意点增删改查做完后把集合整体写回文件。最可靠的做法是全量重写因为 CSV 没有行定位能力修改中间某一行靠追加是做不到的public void SaveAll(string filePath) { using (var writer new StreamWriter(filePath, false, new UTF8Encoding(true))) using (var csv new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.Context.RegisterClassMapStudentMap(); csv.WriteRecords(Students); } }逻辑说明WriteRecords(Students)一次性遍历集合写出表头顺序由 ClassMap 决定。数据量在几万行以内时这种方式最简单出错也容易排查。三个注意点第一编码必须用new UTF8Encoding(true)。Encoding.UTF8默认不带 BOM写出的文件被 Excel 打开时中文大概率乱码后面的避坑章节会细说。第二追加写只适合日志场景。如果当前集合是从文件读出来改了中间某行再File.AppendAllText把整份数据追加到文件末尾会得到两份内容和错乱的行。增删改场景一律全量重写。第三覆盖前先备份。写死后才意识到列映射配错是每个写过 CSV 导出的人都经历过的后悔药时刻。我的习惯是在 Save 方法里先复制一份带时间戳的备份文件成本极低关键时刻能救命。4. 格式雷区与排查编码、转义、换行和 Excel 兼容性这个章节是全文最容易翻车的部分。很多问题不是代码逻辑错而是对 CSV 文件本身的兼容规则没吃透。4.1 中文乱码文件没坏是编码和 BOM 的锅现象程序用Encoding.UTF8写出 CSV记事本打开内容正常Excel 双击打开中文全是乱码。原因Excel 判断 CSV 编码时如果没有检测到 BOM就按本机 ANSI 代码页解读中文系统是 GBK。UTF-8 的字节序列被 ANSI 解码后自然乱码。这不是解析问题是文件头缺少 BOM 标记。解决写文件时显式使用带 BOM 的 UTF-8using (var writer new StreamWriter(filePath, false, new UTF8Encoding(true)))参数true表示输出 BOM。如果是读别人生成的文件不确定有没有 BOM用detectEncodingFromByteOrderMarks让 StreamReader 自动探测using (var reader new StreamReader(filePath, Encoding.UTF8, true))如果源文件是 GBK/GB2312 编码就不能按 UTF-8 读需要显式指定Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); using (var reader new StreamReader(filePath, Encoding.GetEncoding(GB2312)))RegisterProvider那行在 .NET Core / .NET 5 环境必须加否则Encoding.GetEncoding(GB2312)会直接抛NotSupportedException。建议把这句放在程序启动时执行一次。4.2 字段内的逗号和引号导出后列错位现象某个字段值是“海淀,北京”导出的 CSV 在 Excel 里这个字段被拆成两列后面的列全部右移。原因手动拼行时只用了string.Join(,, fields)没有给含特殊字符的字段加引号包裹。解决优先交给 CsvWriter 处理它会在需要时自动转义。如果因为某些原因必须手动拼接按这个规则写public static string EscapeField(string value) { if (value.Contains(,) || value.Contains() || value.Contains(\n) || value.Contains(\r)) { return \ value.Replace(\, \\) \; } return value; }逻辑说明先判断字段是否包含分隔符、引号或换行只要命中任一就加引号包裹包进去的引号全部翻倍。顺序不能反必须先 Replace 再包裹否则新加的包裹引号也会被翻倍结果完全错乱。这条规则适用于任何手写 CSV 导出的场景不只是 WPF 项目。4.3 字段内换行行数对不上别按行读现象数据一共 10 条记录导出后用File.ReadAllLines数出来 15 行重新读进来时记录错位。原因某个字段值是带换行的多行文本。CSV 允许引号内出现换行符一条记录占据多个物理行。任何按物理行切分的读取方式都会误判。解决读取必须用按记录解析的库CsvReader 和 TextFieldParser 都处理了引号状态。要记住一个原则在 CSV 场景里ReadLine不等于“读一条记录”。手工状态机要处理这种情况得扫描引号是否闭合直到闭合才认为记录结束复杂度比 2.1 那个版本高不少。4.4 Excel 占用导致保存失败IOException 的常见来源现象文件正在 Excel 中打开程序执行写回时抛IOException: 文件正由另一进程使用因此进程无法访问此文件。原因Excel 打开 CSV 时对该文件持有读锁程序按覆盖模式写入被系统拒绝。解决捕获 IOException 给出明确提示别让用户看到裸异常窗try { service.SaveAll(filePath); } catch (IOException ex) { MessageBox.Show(CSV 文件可能正被 Excel 打开请关闭后重试。, 保存失败); }更稳妥的做法是写临时文件再替换。先写到同目录的临时文件写成功后再File.Replace覆盖原文件能缩短文件被占用的时间窗口。但File.Replace要求目标文件存在且在同一卷上用前先确认这两个条件。4.5 字段数不一致时 CsvHelper 的异常先配置再上线现象读一个手工编辑过的 CSV某行少了一列CsvHelper 抛MissingFieldException某行比表头多出内容抛BadDataException。原因CsvHelper 默认严格校验任何列数不匹配都会中断读取。这是保护机制不是 Bug。解决如果业务允许跳过脏行可以配置宽松行为var config new CsvConfiguration(CultureInfo.InvariantCulture) { MissingFieldFound null, BadDataFound ctx { // 这里记录原始行到日志便于事后排查 Debug.WriteLine(ctx.RawRecord); } }; using (var reader new StreamReader(filePath, Encoding.UTF8)) using (var csv new CsvReader(reader, config)) { // 继续读取 }逻辑说明MissingFieldFound null表示缺失字段按默认值处理不再抛异常BadDataFound委托让你在跳过坏行前留一份日志。我不建议把这两个都直接设为 null 却不记录脏数据被无声跳过用户以为导入成功实际少了几条后面核对数据会对不上。5. 大数据量 CSV 在 WPF 中的异步读写与进度反馈当 CSV 到达几十万行时前面几章的直接读写方式会把 UI 线程卡死。界面假死、鼠标转圈用户的第一反应是“程序崩了”。5.1 界面卡死的两个根因同步 IO 与逐条插入第一个根因是文件 IO 和解析全在 UI 线程执行。第二个是逐条ObservableCollection.Add会反复触发 DataGrid 的布局计算。十万行数据分十万次插入每次都走一次集合通知UI 自然扛不住。解决方向有三个IO 放到后台线程先在后台线程组装成ListT读完后一次性填充集合DataGrid 保持默认的虚拟化面板不要把 DataGrid 包在外部 ScrollViewer 里否则虚拟化失效界面仍然卡。5.2 用 Task.Run 配合 IProgress 做异步导入进度条是异步导入的关键体验。实现思路是用 Task.Run 把解析工作丢到线程池用 IProgress 回报进度public async Task LoadLargeFileAsync(string filePath, IProgressdouble progress) { int total CountLines(filePath); ListStudent list await Task.Run(() { var data new ListStudent(); int current 0; using (var reader new StreamReader(filePath, Encoding.UTF8)) using (var csv new CsvReader(reader, CultureInfo.InvariantCulture)) { csv.Context.RegisterClassMapStudentMap(); foreach (var record in csv.GetRecordsStudent()) { data.Add(record); current; progress?.Report(current * 100.0 / total); } } return data; }); Students.Clear(); foreach (var item in list) { Students.Add(item); } }逻辑说明Task.Run里做文件读取和实体转换期间 UI 线程保持响应Progressdouble如果在 UI 线程创建它的Report回调会自动封送回 UI 线程所以进度条更新是安全的。读到的是完整ListStudent最后一次性填充 ObservableCollection避免逐条刷新。参数说明CountLines预扫描文件总行数用于进度计算。如果 CSV 字段里有换行预估行数会偏多进度可能提前到 100%这是允许的。不想预扫行数也可以改用文件长度做比例误差更大但实现更简单。5.3 写入侧的大文件策略分批写与控制缓冲区写入大文件时避免一次性把所有记录转成一个大字符串再写。CsvHelper 本身就是流式写入配好缓冲区即可public void SaveLargeFile(string filePath) { var config new CsvConfiguration(CultureInfo.InvariantCulture) { BufferSize 65536 }; using (var writer new StreamWriter(filePath, false, new UTF8Encoding(true), 8192)) using (var csv new CsvWriter(writer, config)) { csv.Context.RegisterClassMapStudentMap(); csv.WriteRecords(Students); writer.Flush(); } }参数说明BufferSize是 CsvWriter 使用的缓冲字节数适当调大能减少小片 IO 次数StreamWriter构造里的8192是写缓冲大小。保存完成后Flush()确保缓冲落盘。写入侧一般不需要太精细的进度条显示“保存中”比实时进度更有性价比也避免每写一条回调 UI 带来的额外开销。6. 把 CSV 读写封装成泛型服务列映射与备份技巧项目里一旦有多个页面、多种实体都要做 CSV 导入导出读写逻辑就会重复。抽一个泛型服务类可以收敛所有公共逻辑。6.1 泛型 CsvService 的最小实现public class CsvServiceT where T : class { private readonly string _filePath; private readonly Type _mapType; public CsvService(string filePath, Type mapType) { _filePath filePath; _mapType mapType; } public ListT Load() { if (!File.Exists(_filePath)) return new ListT(); ListT data; using (var reader new StreamReader(_filePath, Encoding.UTF8)) using (var csv new CsvReader(reader, CultureInfo.InvariantCulture)) { csv.Context.RegisterClassMap(_mapType); data csv.GetRecordsT().ToList(); } return data; } public void Save(IEnumerableT records) { Backup(); using (var writer new StreamWriter(_filePath, false, new UTF8Encoding(true))) using (var csv new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.Context.RegisterClassMap(_mapType); csv.WriteRecords(records); } } private void Backup() { if (!File.Exists(_filePath)) return; string backupPath _filePath . DateTime.Now.ToString(yyyyMMddHHmmss) .bak; File.Copy(_filePath, backupPath, true); } }使用方式var service new CsvServiceStudent(students.csv, typeof(StudentMap)); ListStudent students service.Load(); service.Save(students);逻辑说明RegisterClassMap(_mapType)接收 ClassMap 的 TypeCsvHelper 内部会实例化并注册映射。每个业务模型配自己的 ClassMap服务类本身不用改。Load在文件不存在时返回空集合避免调用方每次判断File.Exists。参数说明备份文件名用时间戳后缀同一秒内多次保存不会互相覆盖。File.Copy的第三个参数true表示目标存在时覆盖配合时间戳实际很少触发覆盖。6.2 覆盖写之前先备份这个习惯第一次做 CSV 写回时我也觉得备份多余直到某次在模拟项目X里把列映射写反保存后整个配置表全部错位。幸好当时目录里残留了一份前一天的手动备份才把数据捞回来。从那以后凡是覆盖写文件的方法第一行一定是备份逻辑这个习惯一直保留到现在。如果你正在给 WPF 项目接 CSV 读写建议按这个顺序拆任务先用 CsvHelper 跑通读写再做 ClassMap 和 ObservableCollection 绑定最后把编码和备份写进公共方法。中间遇到乱码、列错位回来对照第四章排查。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑