ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WPF数据库操作实战:从UI线程安全到MVVM生产级封装

WPF数据库操作实战:从UI线程安全到MVVM生产级封装 简介本资源是一份面向C#初学者的WPF数据库开发实战教程聚焦SQL Server环境下增删改查CRUD核心操作帮助开发者快速掌握WPF与数据库交互的关键技能。资源包含56个文件主体为21个C#业务逻辑文件、6个XAML界面定义文件及6个缓存/编译中间文件辅以Sln解决方案、Csproj项目配置、exe可执行程序等完整呈现一个可运行的学生信息管理系统工程压缩包仅95KB轻量易解压学习。已有2212人下载学习适合刚接触WPF或数据库编程的入门者系统实践。读者可直接运行项目查看UI与数据联动效果深入理解SqlConnection连接管理、SqlCommand参数化查询、DataTable数据绑定、MVVM基础结构及事务与异常处理等关键实现细节代码规范、注释清晰是构建桌面数据库应用的优质参考范例。1. WPF里做数据库增删改查不是写个SqlConnection就完事而是让UI不卡、数据能回显、异常有兜底的完整链路你在WPF里写了个SqlConnection.Open()点了按钮弹出“操作成功”但列表没刷新、编辑框清空后下一条数据加载错位、连续点两次新增直接报“连接已关闭”——这不是代码没跑通是WPFSQL这条链路根本没串起来。WPF不是WinForms它靠数据绑定驱动UI而SQL操作是阻塞式IO硬把ExecuteNonQuery()塞进按钮Click事件里等于在UI线程上挖坑。真正落地的WPF数据库操作必须同时解决三件事怎么安全建连接、怎么让查询结果自动填进DataGrid、怎么保证增删改后界面实时响应且不崩线程。这文不讲ADO.NET基础语法只拆解一个真实生产级WPF应用里从连接字符串配置到MVVM层封装、从DataTable直绑到Entity Framework Core轻量接入的全路径。适合正在用WPF写内部工具、设备上位机、工业看板的C#开发者——你不需要高并发但需要每次点击都稳、每次刷新都准、每次报错都看得懂。2. 用WPF原生方式连SQL Server从连接字符串到DataTable绑定的最小可行闭环WPF做数据库操作最易上手也最容易翻车的路径就是绕过MVVM直接用Code-Behind操作DataSet。这条路对快速验证逻辑有效但必须守住三个底线连接不能裸写、UI更新不能跨线程、异常不能吞掉。下面这个方案是我给产线扫码系统做的原型验证版5分钟就能跑通且后续可平滑升级到MVVM。2.1 连接字符串必须外部化且带超时与连接池控制硬编码连接字符串是WPF项目上线后第一个被运维砍掉的点。正确做法是把连接信息抽到App.config.NET Framework或appsettings.json.NET Core/.NET 5并强制启用连接池和超时!-- App.config -- configuration connectionStrings add nameProductionDB connectionStringServer192.168.1.100;DatabaseLineControl;User Idwpf_app;PasswordSecurePass123!;Connection Timeout15;Poolingtrue;Max Pool Size100; providerNameSystem.Data.SqlClient / /connectionStrings /configuration注意Poolingtrue是默认值但必须显式写出——有些老旧设备上的.NET Framework版本会因策略组策略禁用连接池Max Pool Size100不是越大越好WPF桌面应用通常并发连接数10设太高反而占内存Connection Timeout15比默认的15秒更明确避免用户点按钮后干等30秒才报错。2.2 查询结果用DataTable绑定DataGrid零ViewModel也能保UI响应WPF的DataGrid天生支持DataTable这是最快实现“查出来就显示”的方式。关键在于查询必须走独立线程绑定必须回到UI线程。以下代码直接放在Window.xaml.cs里即可运行private async void btnLoadData_Click(object sender, RoutedEventArgs e) { try { // 1. 启动异步查询不阻塞UI var dt await Task.Run(() { using (var conn new SqlConnection(ConfigurationManager.ConnectionStrings[ProductionDB].ConnectionString)) { conn.Open(); using (var cmd new SqlCommand(SELECT ID, ProductCode, Status, LastUpdate FROM Products ORDER BY LastUpdate DESC, conn)) using (var adapter new SqlDataAdapter(cmd)) { var table new DataTable(); adapter.Fill(table); // 填充DataTable return table; } } }); // 2. 切回UI线程绑定Dispatcher.BeginInvoke是WPF线程安全核心 this.Dispatcher.BeginInvoke(new Action(() { dataGridProducts.ItemsSource dt.DefaultView; // 绑定DefaultView而非DataTable本身 })); } catch (SqlException ex) when (ex.Number -2) // 连接超时 { MessageBox.Show($数据库连接超时请检查网络或服务状态。\n错误码{ex.Number}, 连接失败, MessageBoxButton.OK, MessageBoxImage.Error); } catch (Exception ex) { MessageBox.Show($加载数据失败{ex.Message}, 系统错误, MessageBoxButton.OK, MessageBoxImage.Warning); } }Task.Run把耗时的数据库IO移出UI线程避免界面冻结DataTable.DefaultView是绑定必需——直接绑DataTable会导致列顺序错乱、编辑失效SqlException按错误码分类处理-2是连接超时53是服务器不可达18456是登录失败比笼统ex.Message有用十倍。2.3 新增/修改/删除操作必须用事务包裹且反馈要具体到行WPF用户点一次按钮就要知道“哪条数据变了”。用SqlCommand执行CRUD时必须返回影响行数并据此更新UIprivate async void btnAddProduct_Click(object sender, RoutedEventArgs e) { string code txtProductCode.Text.Trim(); if (string.IsNullOrEmpty(code)) return; try { int rowsAffected await Task.Run(() { using (var conn new SqlConnection(ConfigurationManager.ConnectionStrings[ProductionDB].ConnectionString)) { conn.Open(); using (var trans conn.BeginTransaction()) // 显式事务 { try { using (var cmd new SqlCommand(INSERT INTO Products(ProductCode, Status) VALUES (code, Active), conn, trans)) { cmd.Parameters.AddWithValue(code, code); int result cmd.ExecuteNonQuery(); trans.Commit(); // 提交事务 return result; } } catch { trans.Rollback(); // 出错回滚 throw; } } } }); if (rowsAffected 0) { MessageBox.Show($成功添加1条产品记录{code}, 操作成功, MessageBoxButton.OK, MessageBoxImage.Information); await LoadDataAsync(); // 刷新列表复用上面的查询逻辑 } } catch (SqlException ex) when (ex.Number 2627 || ex.Number 2601) // 唯一约束冲突 { MessageBox.Show($产品编码[{code}]已存在请检查后重试。, 重复录入, MessageBoxButton.OK, MessageBoxImage.Exclamation); } }BeginTransaction不是可选项——即使单条INSERT也要包事务否则网络抖动时可能部分写入Parameters.AddWithValue必须用杜绝SQL注入热词里反复出现的sql注入问题根源就在拼接字符串错误码2627主键冲突、2601唯一索引冲突单独捕获比弹“操作失败”更有业务价值。3. 迈向生产级用MVVM模式封装数据库操作让命令可测试、绑定可追溯Code-Behind写法撑不过3个页面。当你的WPF应用要支持多窗口、多数据源、离线缓存时必须切到MVVM。这里不堆框架Prism/MahApps只用原生INotifyPropertyChangedICommand封装一个可复用的ProductDataService。3.1 定义实体类与数据服务接口隔离SQL细节先定义干净的业务实体不依赖System.Data// Models/Product.cs public class Product : INotifyPropertyChanged { private int _id; public int ID { get _id; set { _id value; OnPropertyChanged(); } } private string _productCode; public string ProductCode { get _productCode; set { _productCode value; OnPropertyChanged(); } } private string _status; public string Status { get _status; set { _status value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }再定义数据服务契约所有SQL操作收口于此// Services/IProductService.cs public interface IProductService { TaskIEnumerableProduct GetAllAsync(); TaskProduct GetByIdAsync(int id); Taskint AddAsync(Product product); Taskint UpdateAsync(Product product); Taskint DeleteAsync(int id); }3.2 实现SQL Server服务用SqlClient Dapper简化代码Dapper是WPF场景下的黄金搭档比EF Core轻量无迁移、无上下文开销比原生SqlClient简洁免写DataReader循环。NuGet安装Dapper后实现如下// Services/SqlProductService.cs public class SqlProductService : IProductService { private readonly string _connectionString; public SqlProductService(string connectionString) { _connectionString connectionString; } public async TaskIEnumerableProduct GetAllAsync() { using var conn new SqlConnection(_connectionString); return await conn.QueryAsyncProduct( SELECT ID, ProductCode, Status FROM Products ORDER BY LastUpdate DESC); } public async TaskProduct GetByIdAsync(int id) { using var conn new SqlConnection(_connectionString); return await conn.QueryFirstOrDefaultAsyncProduct( SELECT ID, ProductCode, Status FROM Products WHERE ID Id, new { Id id }); } public async Taskint AddAsync(Product product) { using var conn new SqlConnection(_connectionString); const string sql INSERT INTO Products(ProductCode, Status) VALUES (ProductCode, Status); SELECT CAST(SCOPE_IDENTITY() as int); return await conn.QuerySingleAsyncint(sql, product); } public async Taskint UpdateAsync(Product product) { using var conn new SqlConnection(_connectionString); const string sql UPDATE Products SET ProductCode ProductCode, Status Status WHERE ID ID; return await conn.ExecuteAsync(sql, product); } public async Taskint DeleteAsync(int id) { using var conn new SqlConnection(_connectionString); return await conn.ExecuteAsync(DELETE FROM Products WHERE ID Id, new { Id id }); } }QueryAsyncT自动映射字段名到属性名大小写不敏感ProductCode↔productcodeSCOPE_IDENTITY()获取自增ID比IDENTITY安全不受触发器影响所有方法返回Task天然支持async/awaitUI层调用无阻塞。3.3 ViewModel中集成服务用ObservableCollection实现双向绑定ViewModel是WPF数据流的中枢必须用ObservableCollectionT而非ListT否则UI不会响应增删// ViewModels/MainViewModel.cs public class MainViewModel : INotifyPropertyChanged { private readonly IProductService _productService; private ObservableCollectionProduct _products; private Product _selectedProduct; public ObservableCollectionProduct Products { get _products; private set { _products value; OnPropertyChanged(); } } public Product SelectedProduct { get _selectedProduct; set { _selectedProduct value; OnPropertyChanged(); // 选中时自动加载详情可扩展 } } public ICommand LoadCommand { get; } public ICommand AddCommand { get; } public ICommand SaveCommand { get; } public ICommand DeleteCommand { get; } public MainViewModel(IProductService productService) { _productService productService; Products new ObservableCollectionProduct(); LoadCommand new RelayCommand(async () await LoadProducts()); AddCommand new RelayCommand(AddNewProduct); SaveCommand new RelayCommand(async () await SaveProduct()); DeleteCommand new RelayCommand(async () await DeleteProduct()); } private async Task LoadProducts() { try { var list await _productService.GetAllAsync(); Products.Clear(); foreach (var p in list) Products.Add(p); // ObservableCollection.Add触发UI刷新 } catch (Exception ex) { MessageBox.Show($加载失败{ex.Message}); } } private void AddNewProduct() { var newProduct new Product { Status Active }; Products.Add(newProduct); SelectedProduct newProduct; } private async Task SaveProduct() { if (SelectedProduct null) return; try { if (SelectedProduct.ID 0) await _productService.AddAsync(SelectedProduct); else await _productService.UpdateAsync(SelectedProduct); MessageBox.Show(保存成功); } catch (Exception ex) { MessageBox.Show($保存失败{ex.Message}); } } private async Task DeleteProduct() { if (SelectedProduct null || MessageBox.Show(确定删除, 确认, MessageBoxButton.YesNo) ! MessageBoxResult.Yes) return; try { await _productService.DeleteAsync(SelectedProduct.ID); Products.Remove(SelectedProduct); MessageBox.Show(删除成功); } catch (Exception ex) { MessageBox.Show($删除失败{ex.Message}); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }ObservableCollection.Add()会触发CollectionChanged事件DataGrid自动追加行RelayCommand是经典MVVM命令封装可用CommunityToolkit.Mvvm替代但此处手写更透明SaveCommand根据ID0判断新增/修改这是WPF数据绑定的常见模式。4. 避坑指南WPFSQL开发中90%的人踩过的5个深坑WPF数据库操作的坑不在语法而在WPF线程模型与SQL IO特性的碰撞。这些是我在三个工业上位机项目里血泪总结的真问题不是教科书理论。4.1 现象DataGrid双击编辑后修改内容不保存到数据库原因WPFDataGrid默认启用CanUserEditRowstrue但绑定的DataTable或ObservableCollection没有实现IEditableObject导致编辑结束时变更未提交到源集合。解决若用DataTable设置dataGrid.AutoGenerateColumns true后必须手动为每列设置Binding.UpdateSourceTriggerPropertyChanged若用ObservableCollectionProduct确保Product类的每个属性set访问器中调用OnPropertyChanged()且DataGrid的ItemsSource绑定到ObservableCollection而非其AsEnumerable()最稳妥方案关闭DataGrid内置编辑用TextBoxButton组合控件由ViewModel控制保存时机。4.2 现象连续快速点击“新增”按钮出现“连接池已满”异常SqlException 10928原因SqlConnection对象未被及时释放using块虽写了但Task.Run中创建的连接在异常时可能跳过Dispose。解决所有SqlConnection必须包裹在using中且using块内不抛出未捕获异常在catch块中显式调用conn?.Dispose()双重保险生产环境必须监控连接池SQL Server Profiler抓取SQL:BatchCompleted事件筛选TextData LIKE %sp_who2%查看活动连接。4.3 现象修改某行数据后DataGrid滚动条自动跳到顶部原因ObservableCollection.Clear()重置集合时DataGrid失去焦点并重置滚动位置。解决不要用Clear()AddRange()改用foreach遍历旧集合RemoveAt(0)直到空再Add新项或更优用ICollectionView缓存当前位置var view CollectionViewSource.GetDefaultView(Products); var scrollOffset dataGrid.VerticalOffset; view.Refresh(); dataGrid.ScrollIntoView(view.CurrentItem);4.4 现象部署到客户现场后“找不到指定的数据库”错误但本地一切正常原因连接字符串中的Server指向localhost或.客户SQL Server实例名为SERVERNAME\SQLEXPRESS且未启用TCP/IP协议。解决连接字符串必须用实际IP端口Server192.168.1.100,1433;要求客户开启SQL Server配置管理器 → SQL Server网络配置 → TCP/IP → 启用并重启服务在安装包中附带telnet 192.168.1.100 1433测试脚本比弹窗报错更早暴露网络问题。4.5 现象使用SqlBulkCopy导入大批量数据时WPF界面完全卡死原因SqlBulkCopy.WriteToServer()是同步阻塞调用即使放在Task.Run里若数据量超10万行仍会因GC压力导致UI线程饥饿。解决分批导入每5000行一个批次批次间await Task.Delay(10)让UI线程喘息启用SqlBulkCopy.BatchSize 5000并设置BulkCopyTimeout 300关键导入前调用GC.Collect()强制回收避免大对象堆LOH碎片化拖慢后续操作。5. 进阶技巧用Entity Framework Core替代手写SQL兼顾开发效率与调试可控性Dapper够用但当你需要复杂关联查询、变更追踪、或团队里有新手时EF Core是更稳的选择。WPF项目用EF Core有个隐藏优势设计时生成的.edmx或DbContext可直接拖拽到DataGrid绑定字段名零出错。下面给出最小化接入方案不碰MigrationsWPF桌面应用极少改表结构。5.1 创建轻量DbContext只包含必要DbSet新建Models/LineControlContext.cs禁用所有EF Core高级特性public class LineControlContext : DbContext { public DbSetProduct Products { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 直接读取配置不走依赖注入WPF简单场景够用 var connectionString ConfigurationManager.ConnectionStrings[ProductionDB].ConnectionString; optionsBuilder.UseSqlServer(connectionString, opt opt.EnableRetryOnFailure(3)); // 自动重试3次防瞬时网络抖动 } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityProduct() .ToTable(Products) // 显式指定表名避免约定推导错误 .HasKey(p p.ID); } }5.2 ViewModel中注入DbContext用AsNoTracking提升查询性能WPF只读场景多AsNoTracking()能省下50%内存占用private async Task LoadProducts() { try { using var context new LineControlContext(); var list await context.Products .AsNoTracking() // 关键避免变更追踪开销 .OrderByDescending(p p.LastUpdate) .ToListAsync(); Products.Clear(); foreach (var p in list) Products.Add(p); } catch (Exception ex) { // 日志记录用户提示 Log.Error(ex, LoadProducts failed); MessageBox.Show(数据加载异常请重试); } }5.3 调试SQL语句EF Core日志输出到Debug窗口不依赖第三方工具EF Core生成的SQL常有N1查询陷阱。开启日志只需两行protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var connectionString ConfigurationManager.ConnectionStrings[ProductionDB].ConnectionString; // 开启SQL日志仅Debug模式 #if DEBUG optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information); // 输出到控制台 // 或输出到WPF TextBox需用SynchronizationContext optionsBuilder.LogTo(msg Debug.WriteLine(msg)); #endif optionsBuilder.UseSqlServer(connectionString); }玄学经验EF Core日志里看到Executed DbCommand后跟一堆p0xxx参数说明参数化正确若看到WHERE Name xxx字符串拼接立刻检查是否用了string.Format或$——这就是sql注入温床。5.4 处理并发冲突乐观锁实战避免用户A覆盖用户B的修改WPF多用户编辑同一张表时DbUpdateConcurrencyException是高频问题。EF Core用RowVersion字段解决// Product.cs 中添加 [Timestamp] public byte[] RowVersion { get; set; } // 自动生成timestamp列 // DbContext中配置 modelBuilder.EntityProduct() .Property(p p.RowVersion) .IsRowVersion();更新时捕获并发异常public async Taskbool UpdateWithConcurrencyCheck(Product product) { try { using var context new LineControlContext(); context.Products.Attach(product); context.Entry(product).State EntityState.Modified; await context.SaveChangesAsync(); return true; } catch (DbUpdateConcurrencyException ex) { // 获取数据库当前值 var entry ex.Entries.Single(); var databaseValues await entry.GetDatabaseValuesAsync(); if (databaseValues null) { MessageBox.Show(该记录已被删除); return false; } // 提示用户数据库值 vs 当前值 var currentValues entry.CurrentValues; MessageBox.Show($数据已被他人修改\n $数据库值{databaseValues[Status]}\n $您的值{currentValues[Status]}); return false; } }我习惯在所有Update操作里加这个保护哪怕当前只有单用户。因为产线系统未来可能接入MES提前埋好并发处理比上线后半夜改代码强十倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表