
简介面向 WPF 初学者的数据库开发实战资源聚焦 MVVM 模式与 SqlSugar ORM 在 MySql 场景下的落地。资源以数据库类库创建、数据功能接口、泛型方法实现为主线配套完整示例项目与分层代码结构适合想打通桌面 UI 与数据操作的开发者。压缩包共 1612 个文件大小约 123.95MB包含 52 个 C# 源码、6 个 XAML 界面、549 个 DLL 依赖库、450 个 XML 配置文档、47 个 NuGet 包及解决方案文件便于按模块查阅复现。已有 573 人学习下载。学习后可系统掌握 SqlSugar 安装配置、数据库模型与表结构设计、数据访问层与业务逻辑层分离以及在 ViewModel 中处理数据绑定和命令的完整思路借助可运行的泛型方法示例能减少重复的数据访问代码提升 WPF 桌面应用的开发效率和可维护性。 这个系列写到第三篇终于要把数据库接进来了。前两篇我们把WPF项目的壳子搭了起来MVVM的框架也理顺了界面能用命令也能点但总感觉缺了点什么——没错就是数据。没有数据库的桌面项目就像没有货架的商店界面再好看也空荡荡的。这一篇我准备用SqlSugar这个ORM在一个WPFMVVM项目里落地一套完整的数据库实例从连接配置、实体映射到增删改查一路写到DataGrid上的实际交互。这篇文章适合已经掌握前两篇内容、能跑起基础MVVM框架的读者也适合想从零开始用SqlSugar做桌面端数据管理的朋友。我会用一个小型的员工信息管理模块作为贯穿全文的实例数据库就用SQLite文件库不依赖外部服务拷贝就能跑。你跟着走一遍基本就知道这类项目的数据层应该怎么搭了。1. 项目准备与整体设计思路1.1 为什么选SqlSugar而不是其他ORM很多新手问过我同一个问题既然微软官方的EF Core那么好用为什么还要用SqlSugar我最早也用过EF Core写WPF写起来确实不慢但一旦涉及稍微复杂一点的查询各种导航配置、Lambda表达式嵌套马上就来新手很容易被劝退。SqlSugar最大的优点就是“直白”写起来像SQL又不用拼字符串中文文档也全遇到问题能快速找到答案。另外一个很实际的原因是SqlSugar对数据库的切换很友好。同一个DbContext改一行配置就能从SQLite切到SQL Server再改成MySQL也行。桌面项目经常要面对客户不同的服务器环境这个特性在交付部署时会省很多事。1.2 MVVM分层与数据访问边界MVVM的核心是“界面和逻辑分离”但很多人把数据访问也直接堆到ViewModel里这是我要提醒的第一个坑。ViewModel应该是纯粹的“状态和行为协调者”它只关心界面需要什么数据、用户点了什么命令不应该知道ConnectionString长什么样。我的习惯是在项目里加一层Service专门做数据访问。ViewModel调用ServiceService内部才用SqlSugar。这样做的优势很直接以后换数据库、加缓存、写单元测试都不需要动界面代码。项目结构大概是这样的Demo/ ├─ App.xaml ├─ Models/ │ └─ Employee.cs ├─ Services/ │ ├─ DatabaseService.cs │ └─ EmployeeService.cs ├─ ViewModels/ │ ├─ ViewModelBase.cs │ └─ MainViewModel.cs ├─ Views/ │ └─ MainWindow.xamlModels放实体类Services放DbContext和业务数据操作ViewModels放绑定属性与命令Views放界面。这个分层没有多复杂但能把责任划得很清楚。1.3 实例模块与功能范围为了让你看到完整的闭环我用“员工信息管理”这个小模块来演示。功能不贪多覆盖四件事列表展示、新增/编辑、按关键字搜索、勾选后删除。为什么选这四件事因为它们是WPFMVVMSqlSugar最常见的业务组合绑定集合、命令参数、确认弹窗、数据刷新。把这四条走通其他业务模块基本是同样的套路。2. SqlSugar核心配置与数据库连接2.1 安装SqlSugarCore我使用的是.NET 6以上的WPF项目所以选的包是SqlSugarCore。在Visual Studio的NuGet管理器里搜“SqlSugarCore”装最新稳定版就行。用命令行也方便dotnet add package SqlSugarCore如果你还在维护.NET Framework 4.6.1的老项目那要装的是SqlSugar而不是SqlSugarCoreAPI大体一致少部分泛型写法会有差异。装完包之后第一步不是写代码而是想清楚连接配置放哪里。2.2 数据库连接与初始化我在入门项目里习惯先做一个DatabaseService把SqlSugarClient的创建集中在这一个地方。这样连接串统一管理万一要加日志、加拦截器也不会散落得到处都是。public class DatabaseService { public static SqlSugarClient GetInstance() { var db new SqlSugarClient(new ConnectionConfig() { ConnectionString Data Sourcedemo.db, DbType DbType.Sqlite, IsAutoCloseConnection true, InitKeyType InitKeyType.Attribute }); return db; } }几个关键参数说一下。IsAutoCloseConnection true表示每次操作完自动关闭连接防止句柄泄漏。InitKeyType.Attribute告诉SqlSugar主键、自增这些信息从实体的特性去读而不是靠数据库的元数据推断。这样写的好处是CodeFirst建表时能保证字段和实体完全对齐。2.3 使用CodeFirst自动建表SqlSugar的CodeFirst对小项目来说非常友好不需要手动创建数据库文件。只要在程序入口调用一次var db DatabaseService.GetInstance(); db.CodeFirst.InitTablesEmployee();第一次运行会生成demo.db文件并自动建表后面再次启动时表已存在不会重复创建也不会偷偷改你手工加的索引。不过这里要注意如果实体类改了字段SqlSugar默认不会自动加列不要指望它像数据库迁移工具那样智能。入门阶段问题不大正式项目建议用专门的数据库迁移方案。2.4 实体类与表映射实体类我不喜欢用SqlSugar自带的那种“类名即表名属性即字段”的方式总感觉太隐晦。我习惯把映射关系写清楚后期维护起来一眼就能看懂[SugarTable(Employee)] public class Employee { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public int Id { get; set; } [SugarColumn(ColumnName Name, Length 50)] public string Name { get; set; } [SugarColumn(ColumnName Department, Length 50)] public string Department { get; set; } [SugarColumn(ColumnName Salary, DecimalDigits 2)] public decimal Salary { get; set; } public DateTime HireDate { get; set; } [SugarColumn(IsIgnore true)] public bool IsSelected { get; set; } }这里有一个非常容易踩的坑IsSelected是我为了DataGrid里的CheckBox绑定加的但它根本不需要存到数据库所以必须标注IsIgnore true否则SqlSugar建表时会给你加一列后面查询也可能会出问题。这个坑我见过太多次了。2.5 简单的Service封装实体类建好之后写一个数据操作层public class EmployeeService { public ListEmployee GetAll() { using var db DatabaseService.GetInstance(); return db.QueryableEmployee().ToList(); } public bool Insert(Employee employee) { using var db DatabaseService.GetInstance(); return db.Insertable(employee).ExecuteCommand() 0; } public bool Update(Employee employee) { using var db DatabaseService.GetInstance(); return db.Updateable(employee).ExecuteCommand() 0; } public bool Delete(int id) { using var db DatabaseService.GetInstance(); return db.DeleteableEmployee(id).ExecuteCommand() 0; } }using var db是C# 8以后的写法方法结束会自动释放。这里也要说清楚IsAutoCloseConnection负责的是数据库连接的关闭using负责的是SqlSugarClient这个对象的释放。两者不冲突。不过SqlSugar官方其实更推荐在DI容器里注册单例SqlSugarScope入门阶段先用这种最简单的方式等做大型项目再考虑性能优化。3. 基于MVVM的增删改查实战3.1 MainViewModel的属性与命令设计进了MVVM的环节很多人的第一反应是“把属性写出来就完事”其实不然。设计属性要围绕界面来。我的MainViewModel里会这样安排public class MainViewModel : ViewModelBase { private readonly EmployeeService _service new EmployeeService(); public ObservableCollectionEmployee Employees { get; set; } new ObservableCollectionEmployee(); private Employee _selectedEmployee; public Employee SelectedEmployee { get _selectedEmployee; set { _selectedEmployee value; OnPropertyChanged(); } } private string _keyword; public string Keyword { get _keyword; set { _keyword value; OnPropertyChanged(); } } public RelayCommand LoadCommand { get; } public RelayCommand DeleteCommand { get; } // 新增、保存等命令同理 }为什么集合用ObservableCollection而不是List因为ObservableCollection会在集合元素增加或移除时自动通知界面刷新。如果你直接给ItemsSource赋值一个List绑定也能显示数据但清空重填时就要手动把它设置为null再设置一次非常容易漏掉通知。命令我用的RelayCommand是前两篇已经实现的ICommand封装。这里不重复贴代码只需要记住所有按钮点击都应该走命令不要在Code-Behind里写Click事件。3.2 DataGrid绑定与CheckBox选中删除热搜词里有一条很典型“DataGrid某一行CheckBox选中点击按键删除”。这个需求在WPF里太常见了。我的做法是在DataGrid里加一列CheckBox绑定实体类的IsSelectedDataGrid ItemsSource{Binding Employees} SelectedItem{Binding SelectedEmployee} AutoGenerateColumnsFalse SelectionUnitFullRow CanUserAddRowsFalse DataGrid.Columns DataGridCheckBoxColumn Header选择 Binding{Binding IsSelected} Width60 / DataGridTextColumn Header姓名 Binding{Binding Name} / DataGridTextColumn Header部门 Binding{Binding Department} / DataGridTextColumn Header薪资 Binding{Binding Salary} / /DataGrid.Columns /DataGrid这里有两个细节。第一CanUserAddRowsFalse必须设置否则DataGrid底部会出现一空行新手经常被坑。第二SelectedItem绑定的是整行员工对象不是CheckBox那一列所以删除命令的参数可以直接传SelectedEmployee。按钮这样写Button Content删除选中 Command{Binding DeleteCommand} CommandParameter{Binding SelectedEmployee} /对应的删除命令逻辑private void OnDelete(object parameter) { if (parameter is Employee employee) { _service.Delete(employee.Id); Employees.Remove(employee); } }你可能会问如果用户没勾选CheckBox只是点了这一行也会被删除吗是的因为这个方案里SelectedItem只认当前行CheckBox并不参与判断。如果你希望严格“勾选后才删”那就改成遍历集合中IsSelected true的元素再删除。3.3 新增、编辑与保存流程新增和编辑我一般共用同一个表单区域放在窗口右侧或者底部绑定一个CurrentEmployee属性private Employee _currentEmployee; public Employee CurrentEmployee { get _currentEmployee; set { _currentEmployee value; OnPropertyChanged(); } } private void OnAdd() { CurrentEmployee new Employee(); } private void OnSave() { if (CurrentEmployee null) return; if (CurrentEmployee.Id 0) _service.Insert(CurrentEmployee); else _service.Update(CurrentEmployee); LoadData(); }为什么根据Id 0判断新增还是修改因为自增主键在新增时默认是0一旦保存成功就变成数据里的真实Id。这是一种简单可靠的区分方式。如果你用GUID主键那就改成判断Guid.Empty。LoadData方法就是清空集合重新查询public void LoadData() { Employees.Clear(); var list _service.GetAll(); foreach (var item in list) { Employees.Add(item); } }这里没有用Employees new ObservableCollectionEmployee(list)这种写法因为重新赋值属性需要再触发一次OnPropertyChanged而Clear加Add能保证界面直接跟着集合走少一次通知。数据量小的时候完全够用如果一次查出来几万条再考虑分批加载或者虚拟化。3.4 搜索功能与WhereIF搜索看着复杂其实SqlSugar提供了一个特别方便的方法WhereIF。这个方法的英文已经说明了用法——如果条件成立才加这个过滤条件。public ListEmployee Search(string keyword) { using var db DatabaseService.GetInstance(); return db.QueryableEmployee() .WhereIF(!string.IsNullOrWhiteSpace(keyword), e e.Name.Contains(keyword) || e.Department.Contains(keyword)) .ToList(); }我一开始很不习惯这种写法总想着先拼SQL再执行后来发现WhereIF才是ORM该有的样子。关键字的筛选逻辑全在Service层页面只管把Keyword传进来ViewModel一行代码都不需要变。3.5 事务处理的基本写法如果一次操作要更新多张表比如新增员工的同时要写一条操作日志那就必须用事务。SqlSugar的事务写法比较简洁public bool AddEmployeeWithLog(Employee employee, string logMessage) { using var db DatabaseService.GetInstance(); var result db.Ado.UseTran(() { db.Insertable(employee).ExecuteCommand(); db.Insertable(new OperationLog { Message logMessage }) .ExecuteCommand(); }); return result.IsSuccess; }UseTran会在Lambda内部包一层事务如果中间任何一步抛出异常会自动回滚。返回结果里的IsSuccess和ErrorMsg足够我们判断成功与否。入门阶段不用背太多API记住这一个就够了。4. 常见问题与排查技巧实录4.1 界面不刷新绑定失效WPFMVVM新手最常见的问题没有之一。现象是数据明明已经加载了界面上就是一片空白或者点了按钮之后列表没有反应。排查思路很简单集合属性有没有实现INotifyPropertyChanged集合类型是不是ObservableCollectionTViewModel有没有设置给DataContext我有一段时间总是忘记在属性setter里调OnPropertyChanged结果改SelectedEmployee之后界面一直用旧值。后来我把ViewModelBase的SetProperty封装好尽量不在手写属性时遗漏通知。4.2 DataGrid某一行CheckBox选中后删除执行命令没反应原因通常是命令的CanExecute没有通知刷新。RelayCommand的CanExecute一般会判断SelectedEmployee ! null但用户选中行后界面上并没有地方通知命令去重新查询CanExecute。解决办法是在SelectedEmployee的setter里调用CommandManager.InvalidateRequerySuggested()或者让RelayCommand支持RaiseCanExecuteChanged。另外删除前最好弹个确认框if (MessageBox.Show(确定删除这条记录吗, 提示, MessageBoxButton.YesNo, MessageBoxImage.Question) ! MessageBoxResult.Yes) { return; }4.3 ComboBox下拉框末尾出现空白行如果你在项目里用了ComboBox很可能会碰到末尾多了一个空白下拉项。这个问题的排查顺序我建议先看数据源里有没有null。比如你绑定的是部门列表但集合里某个元素的DepartmentName为空ComboBox就会显示一个空白行。第二要看SelectedValue对应的值在数据源里存不存在。如果SelectedValue绑定了一个不存在的值ComboBox会显示成空白非常迷惑。4.4 DataGrid点单元格选中默认是背景颜色怎么改成整行高亮WPF默认的DataGrid是点哪个单元格哪个单元格变蓝。很多做桌面软件的客户不习惯这种交互更希望点一下整行高亮。最简单的方法是把SelectionUnit改成FullRowDataGrid SelectionUnitFullRow SelectionModeSingle /DataGrid如果你还想自定义选中颜色那就要写CellStyleStyle TargetTypeDataGridCell Setter PropertyBackground ValueTransparent / Style.Triggers Trigger PropertyIsSelected ValueTrue Setter PropertyBackground Value#E0F0FF / Setter PropertyForeground ValueBlack / /Trigger /Style.Triggers /Style这个Style会覆盖默认的蓝色背景同时不会影响DataGridRow本身的选中逻辑。注意优先级问题如果你同时在RowStyle里设置了背景最终显示效果可能是两个Style叠加的结果调试时要先分清生效的是哪一层。4.5 需要动态创建大量Button时怎么办热搜词里有“WPF需要创建大量Button”这也是MVVM新手比较迷茫的地方。如果界面上一排按钮是固定的那直接在XAML里写就行。但如果运行时按钮数量不确定千万不要在后台用for循环new Button再往容器里Add。正确思路是用ItemsControl配合DataTemplateItemsControl ItemsSource{Binding ActionItems} ItemsControl.ItemTemplate DataTemplate Button Content{Binding DisplayName} Command{Binding DataContext.ExecuteAction, RelativeSource{RelativeSource AncestorTypeItemsControl}} CommandParameter{Binding} / /DataTemplate /ItemsControl.ItemTemplate /ItemsControl这样按钮数量和布局都由数据驱动界面逻辑不会和一坨控件创建代码纠缠在一起。这也是MVVM被设计出来的初衷。5. 一点个人经验与扩展建议最后说点个人的习惯。我现在做WPF项目时会把数据库连接串放在appsettings.json里用ConfigurationBuilder来读而不是像前面示例那样写死在DatabaseService里。原因很简单客户换电脑、换数据库我只需要发一个新配置文件过去不用重新编译整个程序。另外等你的项目到了需要依赖注入和单元测试的阶段可以把SqlSugar注册成单例SqlSugarScope各方面体验会比每次new好很多。SqlSugar本身还自带代码生成功能能从已有数据库反向生成实体类非常适合从老WinForm项目迁移到新WPF项目时省力气。不过在这个入门教程里我故意让你手写实体类就是想让你把映射关系彻底吃透工具可以后面再学原理不能跳过。希望第三篇能帮你把数据这块跑通后面再遇到什么坑欢迎一起交流。本文还有配套的精品资源点击获取