☰
WPF连接SQL Server增删改查实战:DataGrid数据绑定与参数化查询详解
2026/10/9 10:43:55 网站建设 项目流程

简介:面向C#初级开发者的WPF与SQL Server数据库交互实践资源,演示Windows桌面应用中完成增删改查(CRUD)的完整流程,主要涵盖SqlConnection连接字符串配置、编写SQL语句、SqlCommand参数化执行、DataGrid数据绑定以及异常处理等核心技术知识点。压缩包共56个文件,以21个cs源文件和6个xaml界面文件为核心,同时包含了编译生成的exe、dll、pdb以及项目工程sln、csproj等,其中cs实现业务逻辑与数据库操作,xaml定义窗口与控件布局,exe可直接双击运行体验效果,整体体积仅95KB,非常轻量易用,便于快速部署。资源提供完整的StudentManagement示例工程,通过学生信息管理场景展示INSERT、DELETE、UPDATE、SELECT四种操作,同时配有UI界面与数据绑定代码,代码结构清晰,注释易懂,适合作为学习模板。已有2212人学习浏览,适合希望快速上手WPF数据操作的初学者,可作为很好的课堂练习或项目起步参考。

1. WPF 连 SQL 做增删改查:数据网格不刷新才是第一个坎

做 WPF 上位机或内部工具时,最常被问到的一件事就是「怎么把数据库里的表显示出来、还能改回去」。网上教程一搜一大把,但很多一上来就引 Entity Framework,反而把最简单的增删改查讲复杂了。我自己的经验是:WPF 里连 SQL Server 做增删改查,核心难点根本不在 SQL 语句,而在于 UI 线程和 DataTable 的联动、连接字符串的坑、以及参数化查询怎么写得顺手。这篇文章就按我实际搭过的一套最小方案来写,用到的就是 C# + ADO.NET + SQL Server,适合做小型管理系统、上位机数据看板、内部工具这类场景。你不用装额外 ORM,跟着把增删改查四件事跑通,再回头处理边界问题,会顺很多。

2. 前置准备:连接字符串与项目结构先定下来

2.1 App.config 里定死连接字符串的写法,为什么我说要留个开关

WPF 项目默认有一个 App.config,很多新手会忽略它,直接把连接字符串写在每个窗口的构造函数里。这种做法一旦数据库服务器地址变了,就得改代码重新编译,非常被动。我一般会在 App.config 里用 connectionStrings 节点把连接串集中管理,并且额外留一个配置项用来切换「本地测试库」和「正式库」。

<connectionStrings> <add name="LocalDB" connectionString="Data Source=.;Initial Catalog=WpfDemo;Integrated Security=True;TrustServerCertificate=True;" providerName="System.Data.SqlClient" /> <add name="RemoteDB" connectionString="Data Source=192.168.1.10;Initial Catalog=WpfDemo;User Id=sa;Password=your_password;TrustServerCertificate=True;" providerName="System.Data.SqlClient" /> </connectionStrings>

这里有个容易被忽略的点:Integrated Security=True表示用 Windows 身份验证,适合本地开发;User Id=sa这种是 SQL Server 身份验证,适合部署到内网服务器。我习惯把这两种都预设好,然后在使用时通过一个 bool 变量或直接改 App.config 的 value 来切换,这样调试时不会因为本地没开 SQL 服务而浪费时间。TrustServerCertificate=True是给本地自签名证书场景用的,不加这个,某些 SQL Server 版本会报证书链验证失败,属于典型的 WPF 连数据库踩坑点。

2.2 一个最基本的 SqlHelper:为什么我不用 DbHelper 那种大而全的封装

很多博客喜欢贴一个几百行的 DbHelper,支持一堆重载,看着很专业,实际用起来反而难排查问题。我倾向写一个精简的 SqlHelper,只封装三件事:执行非查询语句(增删改)、返回 DataTable(查询)、返回单个值(比如 Count)。这个类同时也能自然接住 SQL 注入的防护问题,后面会展开。

public class SqlHelper { private static readonly string ConnStr = ConfigurationManager.ConnectionStrings["LocalDB"].ConnectionString; public static DataTable ExecuteQuery(string sql, SqlParameter[] parameters = null) { using (SqlConnection conn = new SqlConnection(ConnStr)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); SqlDataAdapter adapter = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } } public static int ExecuteNonQuery(string sql, SqlParameter[] parameters = null) { using (SqlConnection conn = new SqlConnection(ConnStr)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } } }

这段代码的思路是:每次都新建连接、用完即关,配合 using 保证连接不会泄漏。ExecuteQuery返回 DataTable,直接就能作为 WPF DataGrid 的 ItemsSource。ExecuteNonQuery返回受影响行数,用来判断增删改是否成功。参数数组是可选的,这样简单查询时可以少写几行,但涉及用户输入的地方必须传参,不能拼接字符串。

注意我用了ConfigurationManager.ConnectionStrings,这需要项目里引用 System.Configuration.dll,并且 App.config 里确实有对应节点。新手经常漏掉引用,然后报错说找不到类型,这不是代码问题,是工程引用问题。

2.3 主窗口布局:DataGrid 和按钮别堆在一个 Grid 里

WPF 的界面布局直接决定你写增删改查时顺手不顺手。我的做法是两个区域:上半部分放查询条件和操作按钮,下半部分放 DataGrid。DataGrid 要能显示选中行、能编辑、能删除,这些依赖几个属性设置,不能照搬 WinForms 的习惯。

<Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10"> <TextBox x:Name="txtKeyword" Width="200" Margin="0,0,10,0" ToolTip="支持模糊查询,按回车触发"/> <Button x:Name="btnQuery" Content="查询" Width="80" Click="btnQuery_Click"/> <Button x:Name="btnAdd" Content="新增" Width="80" Margin="10,0,0,0" Click="btnAdd_Click"/> <Button x:Name="btnUpdate" Content="修改" Width="80" Margin="10,0,0,0" Click="btnUpdate_Click"/> <Button x:Name="btnDelete" Content="删除" Width="80" Margin="10,0,0,0" Click="btnDelete_Click"/> </StackPanel> <DataGrid x:Name="dgvList" Grid.Row="1" AutoGenerateColumns="True" IsReadOnly="False" SelectionMode="Single" SelectionUnit="FullRow" CanUserAddRows="False" CanUserDeleteRows="False"/> </Grid>

这里我特意把CanUserAddRows和CanUserDeleteRows设为 False,因为在 WPF DataGrid 上直接增加行会生成一个空白行,很多人不知道这个默认行为,导致数据莫名其妙多了一行空的、保存时报错。增删改统一走按钮事件,界面逻辑更清晰。SelectionUnit="FullRow"是为了后面取选中行数据时方便,鼠标点单元格也能选中整行,减少误操作。

3. 查询先跑通:DataTable 和 DataGrid 绑定后,翻车点就来了

3.1 SELECT 转成 DataTable:ExecuteReader 与 SqlDataAdapter 的取舍

查询是增删改查的起点。常见写法有两种:一种是用ExecuteReader逐行读,另一种是用SqlDataAdapter.Fill(DataTable)。我几乎只用后者,因为 DataTable 本身就是断开式数据,Fill 完关连接,数据仍然在内存里,直接给 WPF 绑定用最合适。而ExecuteReader是流式读取,DataReader 挂着连接不放手,WPF 绑定后稍微拖一下界面就容易报「连接尚未关闭」的错。

private void btnQuery_Click(object sender, RoutedEventArgs e) { string sql = "SELECT Id, Name, Age, CreateTime FROM Users WHERE 1=1"; List<SqlParameter> parameters = new List<SqlParameter>(); if (!string.IsNullOrWhiteSpace(txtKeyword.Text)) { sql += " AND Name LIKE @keyword"; parameters.Add(new SqlParameter("@keyword", $"%{txtKeyword.Text.Trim()}%")); } sql += " ORDER BY CreateTime DESC"; DataTable dt = SqlHelper.ExecuteQuery(sql, parameters.ToArray()); dgvList.ItemsSource = dt; }

这段代码里WHERE 1=1是我刻意写的,它不是为了炫技,而是为了让后续条件拼接时不用判断「第一个条件前面要不要加 AND」。这个技巧在做多条件筛选时非常省事,尤其当查询条件不固定时,避免了一大段 if else 拼字符串的冗余。参数化查询在这里体现得很自然:@keyword是占位符,值通过 SqlParameter 传入,用户输入永远到不了 SQL 语句本身。

3.2 DataTable 绑定 DataGrid 后,为什么修改单元格没反应

WPF 的 DataGrid 绑定 DataTable 后,界面能显示数据,但你直接改单元格、再把 DataTable 写回数据库,会发现写不进去。原因是 DataTable 默认的 RowState 没有被正确识别。你用adapter.Fill(dt)拿到的数据,所有行的 RowState 是 Unchanged;在 DataGrid 上改单元格,如果没有走双向绑定,DataTable 里的值根本没变。

这就要在绑定源头做手脚。我不直接绑定原始 DataTable,而是先调用dt.DefaultView或给 DataGrid 设置EnableRowVirtualization之类的属性,但这都是外力。真正稳妥的做法是:把 DataGrid 的SelectedItem转成DataRowView,通过它取当前行的原始值和新值,供修改时用。查询方法只负责展示,修改行为在修改按钮里单独处理,这样才能清晰知道改的是哪一行、改成了什么。

private DataRowView GetSelectedRow() { if (dgvList.SelectedItem is DataRowView rowView) return rowView; return null; }

这个辅助方法几乎是我所有 WPF 增删改查示例里的标配。DataGrid 绑定了 DataTable,它的 SelectedItem 类型就是 DataRowView,通过它可以拿到整行的列值,也可以判断用户有没有选中行。这一行代码解决了「我点了 DataGrid 里的某一格,但程序不知道是哪行」的问题。

4. 增删改与参数化:Regenerate 一套模板,覆盖 80% 的表

4.1 INSERT 的写法:别把新增按钮做成弹窗,先做简单的固定表单

新增数据的交互我建议分两步:第一版用固定表单(几个 TextBox + 确定按钮),跑通后再考虑弹窗或独立窗口。固定表单的好处是逻辑简单,你不用处理窗口关闭时的数据回传问题。

private void btnAdd_Click(object sender, RoutedEventArgs e) { string name = txtName.Text.Trim(); int age = 0; if (!int.TryParse(txtAge.Text.Trim(), out age)) { MessageBox.Show("年龄必须是数字"); return; } string sql = "INSERT INTO Users (Name, Age, CreateTime) VALUES (@name, @age, @createTime)"; SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("@name", name), new SqlParameter("@age", age), new SqlParameter("@createTime", DateTime.Now) }; int rows = SqlHelper.ExecuteNonQuery(sql, parameters); if (rows > 0) { MessageBox.Show("新增成功"); btnQuery_Click(null, null); // 刷新列表 } else { MessageBox.Show("新增失败"); } }

注意我在参数里用了@name、@age、@createTime,这些名字和 SQL 语句里的占位符一一对应。SqlParameter 的顺序其实无所谓,因为它是按名字匹配的,这比在 SQL 字符串里拼变量安全得多。另外一个细节:DateTime.Now是应用程序所在机器的当前时间,如果你和数据库服务器不在同一个时区,可能出现时间偏差,内网工具一般无所谓,但如果要严谨,可以用GETDATE()写在 SQL 语句里,让数据库自己取时间。

4.2 UPDATE 的雷区:靠主键定位,而不是靠选中行序号

修改操作的教训最多。很多人把 DataGrid 的行号 index 拿来当定位条件,结果排序一变、数据刷新一下,改的就不是原来那一行了。正确做法是:从选中的 DataRowView 里取主键字段的值,作为 UPDATE 语句的条件。

private void btnUpdate_Click(object sender, RoutedEventArgs e) { DataRowView row = GetSelectedRow(); if (row == null) { MessageBox.Show("请先选择一行数据"); return; } int id = Convert.ToInt32(row["Id"]); string name = txtName.Text.Trim(); int age = Convert.ToInt32(txtAge.Text.Trim()); string sql = "UPDATE Users SET Name = @name, Age = @age WHERE Id = @id"; SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("@name", name), new SqlParameter("@age", age), new SqlParameter("@id", id) }; int rows = SqlHelper.ExecuteNonQuery(sql, parameters); if (rows > 0) { MessageBox.Show("修改成功"); btnQuery_Click(null, null); } else { MessageBox.Show("修改失败,可能该行已被删除"); } }

为什么我要特意强调「先选行,再点修改」这个交互顺序?因为很多新手把表单放在另一个窗口,选中行后弹窗让用户改,弹窗里还得传值,增加了复杂度。固定表单方式下,你选中一行,表单里的 TextBox 应该自动填上当前行的值,这样一眼就能看到自己在改什么。这个「选中行自动回填表单」的逻辑,通常放在 DataGrid 的 SelectionChanged 事件里,这也是一种很自然的 WPF 数据绑定用法。

4.3 DELETE 的确认提示与连带坑:外键约束会给你上最现实的一课

删除看起来最简单,其实最容易出事故。首先是用户误点,其次是外键约束。我见过好几次删不掉数据,报「DELETE 语句与 REFERENCE 约束冲突」,这不是代码 bug,是数据库设计上存在关联表。

private void btnDelete_Click(object sender, RoutedEventArgs e) { DataRowView row = GetSelectedRow(); if (row == null) { MessageBox.Show("请先选择一行数据"); return; } int id = Convert.ToInt32(row["Id"]); if (MessageBox.Show($"确定删除 Id 为 {id} 的记录吗?", "删除确认", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) { return; } string sql = "DELETE FROM Users WHERE Id = @id"; SqlParameter[] parameters = new SqlParameter[] { new SqlParameter("@id", id) }; try { int rows = SqlHelper.ExecuteNonQuery(sql, parameters); if (rows > 0) MessageBox.Show("删除成功"); else MessageBox.Show("删除失败,记录可能不存在"); } catch (SqlException ex) { if (ex.Number == 547) MessageBox.Show("该记录存在关联数据,无法直接删除"); else MessageBox.Show($"删除时发生错误:{ex.Message}"); return; } btnQuery_Click(null, null); }

ex.Number == 547是 SQL Server 外键约束冲突的错误号,这可是血泪经验换来的。不捕获这个异常,用户看到的是一串看不懂的英文错误,捕获之后就能给出人话提示。删除操作一定不要省略确认框,这个习惯能救你无数次,尤其是在测试数据库里手滑删了正式数据的时候。

5. WPF 连数据库常见问题:连接超时、界面卡死、数据不刷新都在这

5.1 连接字符串的坑:Data Source=.和localhost差在哪

这是入门阶段最常见的坑,现象是程序一运行就报「在建立与服务器的连接时出错」。原因通常是 Data Source 写错了。.代表本机默认实例,localhost一般也能通,但如果你装的是 SQL Server Express,默认实例名是.\SQLEXPRESS,写错了就连不上。另一个隐藏坑是 TCP/IP 协议没启用,SQL Server 安装时默认可能没开,连接字符串怎么写都白搭。解决方法是打开 SQL Server 配置管理器,把 SQLEXPRESS 的 TCP/IP 启用,然后重启 SQL 服务。这种问题看起来像代码问题,其实是环境问题,排查方向错了会浪费一晚上。

5.2 跨线程访问 UI 与界面卡死:为什么查询大表时窗口假死

WPF 的 UI 线程只有一个,你在按钮事件里同步执行adapter.Fill(dt),数据库响应慢或者数据量大,界面就会卡住。现象是窗口无响应、标题栏显示「未响应」,很多人以为程序崩了,其实是在等数据库返回。解决思路有两种:一种是给查询按钮加「查询中」状态,配合await把耗时操作放到后台线程;另一种是用 BackgroundWorker 或 async/await。

private async void btnQuery_Click(object sender, RoutedEventArgs e) { btnQuery.IsEnabled = false; try { DataTable dt = await Task.Run(() => SqlHelper.ExecuteQuery("SELECT * FROM Users", null)); dgvList.ItemsSource = dt; } catch (Exception ex) { MessageBox.Show($"查询失败:{ex.Message}"); } finally { btnQuery.IsEnabled = true; } }

这段代码是防止界面卡死的最小改动。Task.Run把查询丢到后台线程,通过await回到 UI 线程更新 DataGrid。btnQuery.IsEnabled是防重复点击的,不然用户等不及连点三次,等于开了三个查询在跑,数据库压力瞬间上来。注意async void只用于事件处理器,普通方法别用这个签名,否则异常无法捕获。

5.3 增删改后列表不刷新:ItemsSource 换了 DataTable 还不够

这个坑几乎每个人都会踩一次。现象是:你用 SqlHelper 执行了 INSERT,数据库里确实多了数据,但界面上 DataGrid 没变化。原因是你往ItemsSource里赋的是一个 DataTable 引用,它不是实时感知数据库变化的。数据库变了,DataTable 不会自己重新查询。

解决方式也很直白:增删改成功后重新调用btnQuery_Click或者把查询逻辑提取成独立方法。我一般会写一个RefreshData()方法,内部调用查询代码,然后每个按钮在操作成功后都调它。这不是什么高级技巧,但能保证界面和数据一致性。另一个隐藏路径是:如果你用了 WPF 数据绑定,DataTable作为 ItemsSource 时,DataGrid 的列头默认用列名,改字段名后要重建列绑定才能更新,这个后面进阶部分说。

5.4 SQL 注入的民间说法与正解:为什么网上那种「万能密码」能绕过

在写 WPF 增删改查时总会遇到 SQL 注入的话题,很多新手觉得「我这程序是内部用的,不用防护」。但内网工具一旦连了核心业务库,风险就完全不一样了。网上流传的「万能密码绕过」原理其实就是把输入内容当成了 SQL 语句的一部分,比如在用户名输入框里传' OR 1=1 --,如果程序直接拼接字符串,查询条件就会变成恒真,把整个表都查出来。参数化查询从根上解决了这个问题:用户输入被当成参数值处理,永远不会被解析成 SQL 代码。

也可以考虑用存储过程再包一层,但存储过程内部如果也用拼接,一样有风险,所以参数化才是关键。我强烈建议所有 WPF 连数据库的增删改查操作都统一走参数化,不为别的,就为了少一个被人利用的口子。这是就业余和专业的明显分界线。

6. 进阶:把 CRUD 固化下来,再谈 WPF 数据绑定与 MVVM

到这里,你已经能用 ADO.NET 在 WPF 里跑通基本的增删改查。下一步要做的不是急着换 EF Core,而是把事务处理和绑定逻辑固定下来。比如批量删除,逐条 DELETE 在数据量小的时候没感觉,一旦涉及几十上百条,要么用事务包起来,要么拼成一个 IN 条件。事务的正确用法不是conn.BeginTransaction()就算完,事务对象要赋值给cmd.Transaction,且所有操作共用一个连接,否则事务不生效。

using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction trans = conn.BeginTransaction()) { using (SqlCommand cmd = new SqlCommand("DELETE FROM Users WHERE Id = @id", conn, trans)) { try { foreach (int id in idList) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue("@id", id); cmd.ExecuteNonQuery(); } trans.Commit(); } catch { trans.Rollback(); throw; } } } }

这段代码里有个关键点:cmd.Parameters.Clear()必须加。因为cmd是同一个对象,循环里反复 AddWithValue 会把参数积累起来,第一次执行只有一个 @id,第二次变成两个 @id、第三次三个,SQL Server 会报参数过多。这个坑藏得很深,我当初排查了半个多小时,最后是倒日志才看到参数数量异常。AddWithValue的另一个问题是会隐式推断数据类型,对长度敏感的字段最好用new SqlParameter("@id", SqlDbType.Int)显式指定类型。

再看 WPF 数据绑定本身。传统方式里DataContext = dt绑定 DataTable,变量名改了就崩;进阶方向是 MVVM,但我不建议新手在没跑通 CRUD 之前就上 MVVM,它会引入大量间接层,排查问题难度倍增。我自己是把这两者结合起来:用 Code-Behind 把连接和 SQL 稳住,界面逻辑一点点迁移到 ObservableCollection 和 INotifyPropertyChanged。你可以先从 DataTable 换成ObservableCollection<实体类>开始,SQL 不变,但 ViewModel 里能精确控制集合的新增和删除,DataGrid 的绑定也更可控。

最后给你一个实用的验证方法:写一个独立的小函数,专门测试 SqlHelper 的三种方法是否能正常连库、执行和返回。我习惯在程序启动时按 F5 跑一个自查,把连接串、表是否存在、权限是否足够一次验证完。另外,慢 SQL 排查用 SQL Server Profiler 或直接查 sys.dm_exec_query_stats 视图,比在 C# 代码里加日志快得多。记住一个朴素的道理:WPF 的 UI 再重要,底层拿不到数据、写不回数据,界面做的再漂亮也是零。带着这套最朴素的 ADO.NET 底子去碰 EF Core、Dapper,你会发现 ORM 只是换了个语法,很多坑反而因为封装消失了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询