简介面向C#/WPF开发者的CSV文件操作资源覆盖读写与增删改查全流程适合需要在桌面应用中处理轻量数据交换的初中级开发人员。包内包含可直接运行的WPF示例工程涉及TextFieldParser、CsvHelper等库的用法以及DataGrid数据绑定、文件对话框、异步读写等常见场景并附带异常处理与编码注意事项的参考实现。资源包共46个文件以C#源码13个cs、XAML界面文件2个xaml及项目配置为主辅以可执行exe便于直接查看效果另有少量缓存与资源文件整体约330KB结构精简。已有4325人学习下载适合通过完整示例快速掌握CSV在WPF中的落地方法能直接参考其读写流程、增删改查逻辑与界面交互写法。1. C#/WPF 里的 CSV 读写文件与增删改查别把表格数据写死在代码里桌面工具开发里最绕不开的一类需求就是用 C#/WPF 做 CSV 的读写文件与增删改查业务数据不重犯不上为导入导出专门上数据库可又确实需要把界面表格存下来、改回去。这份资源把 CSV 读、写、增删改查整理成一套可复用的封装顺手解决了编码、引号转义、DataGrid 绑定这些配套问题。适合还在用 string.Split 拼数据的开发者也适合想把 CSV 层从业务代码里剥干净的熟手。先丑话说在前面CSV 看着简单真正让人翻车的全在编码和引号处理上后面逐个拆。2. 先选数据容器DataTable 还是 List 决定 CSV 映射的复杂度CSV 本身没有类型信息读进来全是字符串。真正决定“列从哪来”的是你选 DataTable 还是 List DataTable 的列是运行期动态创建的表头行可以直接变列名List 的列在编译期就写死了映射要自己维护。两者都能做增删改查但一个顺了另一个就拧巴所以先定容器再写解析。2.1 DataTable列名即表头适合结构不固定的动态导入如果 CSV 的列会变或者你要“用户导入什么界面上就显示什么”DataTable 是最省事的。WPF 的 DataGrid 把 AutoGenerateColumns 打开后直接把 DataTable.DefaultView 丢给 ItemsSource列就自动生成了不用手写一列一列的映射。DataTable dt new DataTable(); dt.Columns.Add(Id, typeof(string)); dt.Columns.Add(Name, typeof(string)); dt.Rows.Add(A001, 测试数据A); dataGrid.ItemsSource dt.DefaultView; // 直接绑定显示逻辑说明先依据表头或固定结构建列再逐行用 Rows.Add 填充。DefaultView 返回的 DataView 自带筛选和排序能力后续做查询不用自己写循环。参数说明给列指定 typeof(int) 后 DataGrid 会按数值排序否则“10”排在“2”前面但 CSV 读进来的都是字符串粘贴到 DataTable 前最好先做一次转换别指望 DataTable 自动帮你转类型。动态导入的常见做法是先把第一行拆成列名再对剩下每一行执行 Rows.Add(object[])。这里有个细节CSV 里两列同名时 DataTable 会自动改成 Column1、Column2你后续按原名取值就拿到空。我一般会在建列前先做一次重名检测直接抛错让用户看到原始文件的问题而不是让数据莫名其妙少一列。DataView view new DataView(dt); view.RowFilter Id A000; // 字符串列记得加单引号 view.Sort Name ASC;逻辑说明RowFilter 做筛选时字符串列的值要包单引号数字列不用Sort 语法和 SQL 的 ORDER BY 基本一致。参数说明RowFilter 一旦写错不会被语法检查拦下而是在运行期抛异常建议做成方法参数而不是直接拼字符串。2.2 List 类型安全但表头映射得自己维护业务表单固定、字段有明确类型和校验时List 更舒服。CSV 的行读成 List 的元素属性名就是列名可以顺手做必填校验和格式校验。缺点是要把“表头名 ↔ 属性名”的对应关系写清楚。public class Record { public string Id { get; set; } public string Name { get; set; } } Dictionarystring, string headerMap new Dictionarystring, string { { 编号, nameof(Record.Id) }, { 姓名, nameof(Record.Name) } };逻辑说明headerMap 左边是 CSV 表头右边是 C# 属性名用 nameof 保证属性改名时编译期能报错。参数说明为什么要显式字典而不是按位置映射因为 CSV 表头顺序一变位置映射就全错按名字映射后列顺序随便调整都不影响解析。对比一下两者的取舍对比维度DataTable DataViewList ObservableCollection列定义时机运行时动态编译期固定表头来源首行或手动建列字典映射DataGrid 绑定DefaultView 直接绑定需包集合还得实现属性通知类型安全弱值都是 object强编译期检查增删改查Rows.Add / Delete / 列赋值集合操作 INotifyPropertyChanged适合场景结构会变的导入文件固定业务表单选型我一般遵循一个原则CSV 文件的列在导入前不可控就用 DataTable列结构在整个项目生命周期内都不会变才用 List 。很多人一上来就反射映射结果 CSV 表头变了两个字就崩纯属给自己加戏。另外不管哪种容器表头行要不要跳过最好在界面上留个选项用户拿来的文件千奇百怪有的第一行直接就是数据。3. 读写文件实现编码、引号转义与文件对话框一个都不能少读和写是这套资源的地基增删改查做得再多落盘不对全白搭。这里给的是可以直接抄进 Demo 的两段方法读重在编码处理和引号状态判断写重在统一带 BOM 的 UTF-8 和字段转义。3.1 读取StreamReader 按行读别用 File.ReadAllLines 一把梭新手最顺手的是 File.ReadAllLines(path)但它背后默认编码在 .NET Core 里是 UTF-8在 .NET Framework 里是系统 ANSI同一个文件换个环境行为就变。我一般用 StreamReader 显式指定编码逐行 ReadLine这样 BOM 和编码都能控制。public static DataTable ReadCsv(string path, Encoding encoding, bool hasHeader true) { DataTable dt new DataTable(); using (StreamReader reader new StreamReader(path, encoding, true)) { string line; bool firstRow true; while ((line reader.ReadLine()) ! null) { if (string.IsNullOrWhiteSpace(line)) continue; // 跳过空行和末尾多余换行 string[] fields line.Split(,); if (firstRow hasHeader) { for (int i 0; i fields.Length; i) dt.Columns.Add(string.IsNullOrWhiteSpace(fields[i]) ? $Col{i} : fields[i]); firstRow false; } else { dt.Rows.Add(fields); } } } return dt; }逻辑说明StreamReader 的第三个参数 detectEncodingFromByteOrderMarks 为 true遇到带 BOM 的文件会自动切换编码空行跳过是为了防止文件末尾多余换行产生空行记录。这里用 Split(,) 是简化版只适合字段里没有逗号和换行的数据真正生产代码要接 3.2 的引号状态机逐字符判断当前是否在一对双引号内。参数说明hasHeader 为 false 时列名生成 Col0、Col1适合第一行就是数据、没有表头的文件。读取前需要先处理编码注册否则遇到 GB2312 文件会直接报“不支持”Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding gbk Encoding.GetEncoding(GB2312); DataTable dt ReadCsv(path, gbk, true);逻辑说明.NET Core 默认不带 GB2312/GBK 编码表启动时必须注册 CodePagesEncodingProvider。参数说明GetEncoding 的字符串参数写成 “GB2312” 和 “GBK” 都能拿到同一个编码对象别纠结大小写。文件对话框的代码也要配套WPF 里的 OpenFileDialog 用起来很简单OpenFileDialog dlg new OpenFileDialog { Filter CSV 文件 (*.csv)|*.csv }; if (dlg.ShowDialog() true) dataGrid.ItemsSource ReadCsv(dlg.FileName, Encoding.UTF8, true).DefaultView;说明ShowDialog() 在 WPF 里返回 bool?必须和 true 比较这是和 WinForms 最不一样的地方。3.2 写入UTF-8 带 BOM逗号、引号、换行一律转义写文件比读文件更容易留下暗坑。常见做法是 StreamWriter 包 using表头和每一行都经 EscapeCsvField 转义最后统一存成 UTF-8 带 BOM保证 Excel 直接打开不乱码。public static void WriteCsv(DataTable dt, string path, bool writeHeader true) { var utf8Bom new UTF8Encoding(true); using (StreamWriter writer new StreamWriter(path, false, utf8Bom)) { if (writeHeader) { writer.WriteLine(string.Join(,, dt.Columns.CastDataColumn() .Select(c EscapeCsvField(c.ColumnName)))); } foreach (DataRow row in dt.Rows) { string[] values dt.Columns.CastDataColumn() .Select(c EscapeCsvField(row[c.ColumnName]?.ToString() ?? )) .ToArray(); writer.WriteLine(string.Join(,, values)); } } } public static string EscapeCsvField(string field) { if (field.Contains(,) || field.Contains(\) || field.Contains(\r) || field.Contains(\n)) return \ field.Replace(\, \\) \; return field; }逻辑说明EscapeCsvField 是整篇资源里最关键的函数。字段含逗号、引号、回车换行时用双引号把整个字段包起来字段内部的引号再翻倍这是 CSV 规范里最标准的一套规则。参数说明StreamWriter 构造函数的第二个参数 append 必须传 false否则重复保存会追加到旧文件后面产生两份表头换行统一用 \r\nExcel 和记事本都认。保存对话框同理用 SaveFileDialog重点是默认扩展名SaveFileDialog dlg new SaveFileDialog { Filter CSV 文件 (*.csv)|*.csv|所有文件 (*.*)|*.*, DefaultExt csv, AddExtension true }; if (dlg.ShowDialog() true) WriteCsv((DataTable)((DataView)dataGrid.ItemsSource).Table, dlg.FileName, true);说明从 ItemsSource 拿 DataTable 时先转 DataView 再取 .Table直接强转成 DataTable 会拿到视图而不是表。参数说明AddExtension 为 true 时用户漏写后缀也会自动补 .csv。提示Excel 打开 UTF-8 无 BOM 的文件会乱码所以写入时用 new UTF8Encoding(true)这个“true”就是 BOM 开关别省。4. 增删改查落库DataGrid 绑定与保存策略怎么对齐CSV 读进来只是第一步。真正的增删改查其实发生在 DataTable 上文件操作只在最后一步全量落盘。先讲内存里的四个动作再讲 DataGrid 怎么把编辑同步回 DataTable最后讲保存策略。4.1 新增、删除、修改记住 Delete 不会立刻减少行数// 新增 DataRow newRow dt.NewRow(); newRow[Id] A002; newRow[Name] 测试数据B; dt.Rows.Add(newRow); // 修改 dt.Rows[0][Name] 改名后的数据; // 删除 dt.Rows[0].Delete(); dt.AcceptChanges();逻辑说明Rows.Add 追加到末尾修改直接按列名给值Delete 只是把行标记为 DeletedRows.Count 在 AcceptChanges 之前不会变这是新手最容易懵的点。参数说明如果删除后马上要用 Rows.Count 判断是否为空先调 AcceptChanges如果只是想在界面上暂时隐藏可以用 RowFilter 过滤 RowState。新增前最好做一次主键去重CSV 没有数据库约束重复行会被直接写进文件DataRow[] dup dt.Select(Id A002); if (dup.Length 0) { // 提示用户编号重复不允许新增 }逻辑说明DataTable.Select 返回满足条件的行数组查重比遍历 Rows 方便。参数说明Select 的过滤表达式里字符串值同样要包单引号和 DataView.RowFilter 是同一套语法。4.2 DataGrid 绑定编辑单元格后必须让 DataRow 收到提交WPF 的 DataGrid 在用户编辑完单元格后默认不会立刻写进 DataRow要等到整行失焦或显式提交。DataGrid ItemsSource{Binding} DataGrid.Columns DataGridTextColumn Binding{Binding Id} Header编号 / DataGridTextColumn Binding{Binding Name} Header姓名 / /DataGrid.Columns /DataGridDataView view dt.DefaultView; dataGrid.ItemsSource view; dataGrid.CellEditEnding (s, e) { dataGrid.CommitEdit(DataGridEditingUnit.Row, true); };逻辑说明绑定用 DataView 而不是 DataTable 本身DataGrid 才能识别到新增虚拟行。CellEditEnding 里调 CommitEdit 是常见做法它把当前编辑行一次性提交回 DataRow。参数说明CommitEdit 的第二个参数 exitEditingMode 传 true强制结束编辑状态否则按 Esc 取消时界面和 DataTable 会出现不一致。这里有个坑DataGrid 底部的“新增行”是虚拟行直接往里面打字不会自动调 dt.Rows.Add要先 CommitEdit 让 DataGrid 提交虚拟行DataTable 才会真正多出一行。如果发现新增的行保存后消失先检查这个事件的写法。4.3 保存策略小文件全量重写大文件才考虑增量合并CSV 没有数据库那样的行级更新接口最稳妥的做法是保存时把 DataTable 全量写回文件。文件几百 KB 时毫秒级完成完全不用纠结性能。只有上了几十万行、保存明显卡顿才考虑“按主键匹配增量写”。private void SaveToFile(string path) { DataView view dataGrid.ItemsSource as DataView; if (view null) return; DataTable dt view.ToTable(); // 取当前视图快照 dt.AcceptChanges(); WriteCsv(dt, path, true); }逻辑说明ToTable() 会复制一份当前视图的快照避免保存过程中 DataGrid 还在编辑同一张表导致的行状态冲突。参数说明AcceptChanges 放在保存前把 Deleted 行真正移除否则 WriteCsv 遍历 Rows 时已标记删除的行会带着旧数据一起写进文件。增量方案的思路是读旧文件建一个以主键为 key 的字典新 DataTable 里存在的行更新不存在的追加主键在文件里但不在新表里的行跳过不写写的时候先写临时文件再用 File.Replace 覆盖原文件防止写到一半程序崩溃把原文件弄坏。这个方案实现成本不低数据量没到十万行级别之前全量重写就是最好的方案。5. 避坑清单乱码、字段拆分、文件占用五个高频翻车点下面五条都是实际用这套资源时最容易踩的坑前两条是 CSV 规范问题后三条是 WPF/DataGrid 环境问题。每条按“现象 → 原因 → 解决”写排查时对着症状找。5.1 Excel 打开 CSV 全乱码或显示成问号现象用 Excel 双击导出的 CSV中文变成“锟斤拷”或一堆问号。 原因Excel 默认按系统 ANSI 编码打开写入端如果存成 UTF-8 无 BOM字节流就被强行按别的编码解析反过来如果读取端没注册 CodePagesEncodingProviderGB2312 文件也会读成乱码。 解决写入用 new UTF8Encoding(true) 带 BOM读取时对老系统导出的文件显式用 Encoding.GetEncoding(GB2312)并在 App 启动处注册 CodePagesEncodingProvider。BOM 是给 Excel 看的身份标识三个字节换不来乱码值得。5.2 字段里有逗号或换行读出来被拆成多列现象某一行 CSV 从第 3 列开始整体右移DataGrid 里列数变多后面的字段全部错位。 原因解析用了 line.Split(,)没处理双引号包裹的字段。CSV 规范里含逗号、引号、换行的字段必须用引号包起来引号内部的内容不能参与分割。 解决读取也实现状态机遇双引号进入引号模式引号模式里逗号和换行都算字段内容写入时所有字段统一过 EscapeCsvField。3.1 里的简化版 Split 只适合内部格式受控的导出用户手填的 CSV 一定要换状态机版本。5.3 保存时报“文件正由另一进程使用”现象第一次读取后修改数据再点保存抛 IOException。 原因读取用的 StreamReader 或 FileStream 没有释放文件句柄一直占着或者 DataGrid 绑定的视图还持有文件引用。 解决读和写都用 using 包住流读完之后如果立刻要覆盖写读取时可以指定 FileShare.ReadWrite 让其他句柄也能打开。StreamReader 用完立刻 Dispose别等 GC 回收这是血泪经验。5.4 删除行后 DataGrid 行没消失保存后旧行又回来了现象Delete 之后界面行还在重新打开文件数据也没少。 原因DataRow.Delete() 只是标记行状态DataGrid 默认显示的是行状态为 Unchanged 的行没调 AcceptChanges 或没重新绑定视图界面和表就不同步。 解决删除后调用 dt.AcceptChanges()再让 dataGrid.ItemsSource 重新指向 dt.DefaultView 刷新一下视图。注意顺序先 AcceptChanges再重新赋值 ItemsSource反了会触发一遍 SelectionChanged 的重复加载。5.5 十万行 CSV 读入时界面卡死几秒现象大文件打开时窗口白屏、拖动无响应像死了一样。 原因ReadCsv 整个循环在 UI 线程同步执行DataTable 逐行 Add 把 UI 线程占满了。 解决用 async/await 把解析放进 Task.Run结束后再回 UI 线程绑定如果绑定也卡可以先插入前一万行让界面有内容剩下的分批用 Dispatcher.BeginInvoke 追加。超过 50 万行就别硬扛 CSV 了换 Sqlite 更现实。6. 进阶技巧把 DataGrid 当前内容一键导回 CSV保存按钮绑定的方法处理的是整个 DataTable但用户经常在 DataGrid 里筛选出一些行然后想“只导出当前看到的这些”。这个需求很常见做法不是遍历 DataTable而是遍历 DataGrid.Columns 拿表头再遍历 grid.Items 拿每行显示的 DataRowView6.1 导出当前视图的通用方法public static void ExportDataGridToCsv(DataGrid grid, string path) { var lines new Liststring(); string[] headers grid.Columns .Select(c EscapeCsvField(c.Header?.ToString() ?? )) .ToArray(); lines.Add(string.Join(,, headers)); foreach (var item in grid.Items) { if (item is DataRowView rowView) { string[] fields grid.Columns .Select(c EscapeCsvField( rowView[c.Header?.ToString() ?? ]?.ToString() ?? )) .ToArray(); lines.Add(string.Join(,, fields)); } } File.WriteAllLines(path, lines, new UTF8Encoding(true)); }逻辑说明grid.Columns 的顺序就是导出顺序自动生成列时 Header 就是列名DataRowView 按列名取值和 DataTable 里的取值一致。参数说明这个函数对 DataGrid 的筛选结果同样有效导出的就是当前显示的数据不是整表对“临时筛出几条发给别人”的场景很实用。注意 DataGrid 的列可能是别的类型比如 DataGridComboBoxColumnHeader 仍然有效但值要自己去绑定属性里取不要死等 DataRowView。通用性再强一点的话可以再包一层“枚举对象转 CSV”的泛型方法那是下一个版本的事。从那以后我每次做导入导出都会强制走一遍检查清单编码声明写了没、转义函数过没、流释放了没、AcceptChanges 调了没、导出的视图是不是用户正在看的。CSV 是最不像技术难点的技术难点翻车永远在细节。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?