WPF MVVM实战:SqlSugar数据层增删改查与DataGrid/ComboBox全攻略

发布时间:2026/9/2 2:49:15
WPF MVVM实战:SqlSugar数据层增删改查与DataGrid/ComboBox全攻略 简介面向刚开始接触WPF、希望把MVVM模式落到实际项目的开发者教程基于SqlSugar ORM与MySql数据库通过一个实际数据库实例演示类库创建、数据功能接口设计以及泛型方法实现。教程从数据访问层编码入手详细说明如何以接口定义增删改查标准将业务逻辑与数据库操作解耦并借助泛型封装通用数据访问方法减少重复代码。配套压缩包共一千六百余个文件包含程序集依赖库、配置文档、C#源码、界面标记文件、NuGet程序包以及示意图等整体大小约124MB目录划分明确便于按模块查找学习。目前已有五百七十余人学习下载。结合示例工程与配套讲解读者能够理解WPF项目中MVVM分层思想学会对接SqlSugar操作MySql数据库掌握仓储模式、泛型仓储及依赖注入等实用技巧为后续开发可维护的桌面应用打好基础。 WPF MVVM这套组合聊的人很多但真正能把数据库接进来跑通增删改查的完整教程其实不多。系列前两篇把MVVM基础、命令绑定和属性通知都理顺了这一篇就直接进入实战用SqlSugar把数据层做扎实在ViewModel里完成列表加载、选中删除、下拉筛选、新增记录这一整套闭环顺手把DataGrid和ComboBox里那些容易踩的坑都排掉。这篇教程适合已经能独立搭出WPF MVVM基础项目、但对“数据层到底怎么接进来”还不太有把握的朋友。如果你正处于“ViewModel里不知道该不该写数据库代码”的迷茫期或者被DataGrid选中行、下拉框空白这类问题卡住过那这篇内容正好可以补上缺口。选SqlSugar而不是EF Core或者Dapper原因是面向中小型桌面工具和内部管理系统时它真的省时间配置少写Lambda表达式跟写SQL一样直觉而且从SQLite切到SQL Server或者MySQL基本只要改连接串和DbType其余代码一动不动。下面这套代码和思路都是我在实际项目里验证过的按开发顺序走一遍不搞教科书式理论。1. 整体思路数据层到底该放哪一层MVVM解决的只是界面和逻辑的分离它并没有规定数据访问代码该写在哪里。如果直接在ViewModel里new一个数据库连接对象短期能跑时间一长就会出问题多个页面都要查同一张表时每个ViewModel都得重复写一套查询换数据库时满项目找连接字符串修改一段公共逻辑要同步改好几个地方。所以我的做法是加一层Service专门封装数据访问。1.1 项目结构怎么划分我习惯在项目里按模块分文件夹即使是一个单项目Demo也建议把代码分层放好Models实体类对应数据库表结构ServicesSqlSugar封装和业务逻辑ViewModels页面对应的ViewModelViewsXAML页面文件这么做最大的好处是依赖方向单向流动View只引用ViewModelViewModel只引用ServiceService只操作Models。反过来如果ViewModel里直接调用SqlSugarClient那后续想加缓存、换数据库、做权限控制都在所有ViewModel里来回改维护成本会直线上升。1.2 SqlSugar在整体结构中负责什么SqlSugar扮演的是ORM里的数据访问层核心工作是把C#对象映射成SQL语句再把查询结果映射回对象。它的底层用表达式树把Lambda表达式翻译成SQL所以你写一行代码它自动帮你做了连接管理、参数化查询、结果映射这些脏活累活var list db.QueryableUser().Where(u u.Age 18).ToList();这行代码执行时会翻译成SELECT * FROM User WHERE Age 18然后返回一个ListUser。对ViewModel来说它只需要Service层返回的结果不需要关心数据是怎么查出来的。这个抽象边界一旦建立起来整个项目的数据流就非常清晰。2. 引入SqlSugar并完成数据库接入为了把数据库实例这件事讲透我用一个典型的用户信息管理场景一张User表字段有Id、Name、Age、Department、CreateTime。围绕这张表实现列表展示、按部门筛选、新增和删除覆盖日常开发中最高频的几个操作。2.1 安装和全局配置在NuGet里安装对应的包。如果是.NET 6以上的项目直接用SqlSugarCore如果是老式的.NET Framework项目用的是SqlSugar包。Install-Package SqlSugarCore装完之后在App.xaml.cs里创建SqlSugarClient实例。我倾向于用依赖注入方式注册成全局单例方便后续替换和测试var config new ConnectionConfig { ConnectionString DataSourceapp.db, DbType DbType.Sqlite, // 也可以是SqlServer/MySql/Oracle/PostgreSQL IsAutoCloseConnection true, // 每次操作完自动关闭连接防连接泄漏 InitKeyType InitKeyType.Attribute // 用特性标注主键和自增列 };这里数据库选择SQLite做演示因为零配置、文件即数据库最容易跑通全流程。等换到SQL Server或MySQL时只需要改ConnectionString和DbType其他代码完全不用动这是SqlSugar的核心优势之一。提示IsAutoCloseConnection建议设为true。不打开的话如果某次操作忘记关闭连接开发期看不出问题上线跑一段时间后表现为连接超时排查起来非常头疼。2.2 实体类设计与自动建表实体类我用特性标注主键和自增列SqlSugar能自动识别省去手工建表脚本的麻烦[SugarTable(UserInfo)] public class User { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public int Id { get; set; } [SugarColumn(Length 50)] public string Name { get; set; } public int Age { get; set; } public string Department { get; set; } public DateTime CreateTime { get; set; } }首次运行的时候调用一行代码自动建表db.CodeFirst.InitTablesUser();这样能保证数据库表一定存在不会出现手工建表字段不一致导致查询时报错。但要注意如果是已有表结构实体类的命名和字段类型必须和表字段严格对齐否则查询结果会全是空值或零。这属于新手最容易忽略的细节。3. 服务层封装让ViewModel不碰数据库连接在MVVM模式下ViewModel应该保持对数据库无感知。我把用户表的所有数据库操作统一封装进UserService对外暴露异步方法界面层只管调用。3.1 服务层基本方法public class UserService { private readonly ISqlSugarClient _db; public UserService(ISqlSugarClient db) { _db db; } public async TaskListUser GetAllUsersAsync() { return await _db.QueryableUser().ToListAsync(); } public async TaskListstring GetAllDepartmentsAsync() { return await _db.QueryableUser() .GroupBy(u u.Department) .Select(u u.Department) .ToListAsync(); } public async Taskbool AddUserAsync(User user) { return await _db.Insertable(user).ExecuteCommandAsync() 0; } public async Taskbool DeleteUserAsync(int id) { return await _db.DeleteableUser().Where(u u.Id id).ExecuteCommandAsync() 0; } }为什么要用异步方法而不是同步因为数据库操作属于IO操作如果直接在UI线程同步执行查询数据量大时界面会假死用户体验很差。用async Task配合await可以让UI线程在等待数据库返回期间继续处理界面消息。3.2 ViewModel里如何加载数据在UserViewModel中我定义了一个ObservableCollection 用来绑定DataGrid构造函数里通过依赖注入拿到UserService然后异步加载数据public class UserViewModel { private readonly UserService _userService; public ObservableCollectionUser Users { get; set; } new(); public UserViewModel(UserService userService) { _userService userService; _ LoadUsersAsync(); } private async Task LoadUsersAsync() { var users await _userService.GetAllUsersAsync(); Users.Clear(); foreach (var user in users) { Users.Add(user); } } }注意这里绑定用的是ObservableCollection而不是List。List绑DataGrid不是不能显示但它没有集合变更通知。如果后续在后台往集合里Add或Remove界面不会自动刷新。而ObservableCollection实现了INotifyCollectionChanged集合一变化界面立刻跟着变。踩坑记录构造函数里调用异步方法时不要用async void LoadUsersAsync()因为async void方法里的异常无法被外部捕获一旦抛错程序直接崩溃。用_ LoadUsersAsync()或者把初始化逻辑放到页面加载事件里是更安全的写法。4. DataGrid绑定与选中行操作DataGrid是WPF里最常用的表格控件但绑定和选中相关的问题特别多。网上被问烂的两个热点就是“wpf datagrid 某一行checkbox选中 点击按键删除”和“wpf datagrid 点单元格选中默认是背景颜色”我放在一起讲。4.1 DataGrid列绑定写法为了让列头显示中文、列宽可控我通常关闭自动生成列手工定义DataGridTextColumnDataGrid ItemsSource{Binding Users} SelectedItem{Binding SelectedUser} AutoGenerateColumnsFalse IsReadOnlyTrue SelectionModeSingle DataGrid.Columns DataGridTextColumn HeaderID Binding{Binding Id} Width80/ DataGridTextColumn Header姓名 Binding{Binding Name} Width150/ DataGridTextColumn Header年龄 Binding{Binding Age} Width100/ DataGridTextColumn Header部门 Binding{Binding Department} Width150/ DataGridTextColumn Header创建时间 Binding{Binding CreateTime} Width180/ /DataGrid.Columns /DataGrid注意IsReadOnlyTrue整张表直接进入只读状态避免用户双击单元格进入编辑态导致误改数据。有的需求要放checkbox列那我就说清楚只读状态下的DataGridCheckBoxColumn里面的checkbox仍然可以切换勾选状态所以如果你要做的只是选中行不要用checkbox列直接依赖DataGrid自带的SelectedItem选中机制最干净。4.2 选中行背景色自定义DataGrid默认选中行的背景是浅蓝色很多项目需要改成与主题一致的强调色。这个需求用CellStyle加触发器就能实现DataGrid.CellStyle Style TargetTypeDataGridCell Setter PropertyBackground ValueTransparent/ Style.Triggers Trigger PropertyIsSelected ValueTrue Setter PropertyBackground Value#FF4F81BD/ Setter PropertyForeground ValueWhite/ /Trigger /Style.Triggers /Style /DataGrid.CellStyle这段样式加到DataGrid.CellStyle里选中某个单元格时就会变成你指定的背景色和白色文字。它触发的是DataGridCell的IsSelected属性不是DataGridRow的所以颜色变化是跟着单元格走的。如果想整行高亮可以在RowStyle里写触发器效果略有差别但二选一即可不要叠加写否则样式可能出现冲突。4.3 点击按钮删除选中行界面底部放一个“删除选中”按钮点击后把当前选中的那行删掉。在MVVM模式下这个动作必须走Command关键问题是怎么把“当前选中行”传给Command。最干净的方案是ViewModel里加一个SelectedUser属性绑到DataGrid.SelectedItemprivate User _selectedUser; public User SelectedUser { get _selectedUser; set { _selectedUser value; OnPropertyChanged(); } } private async Task DeleteSelectedAsync() { if (SelectedUser null) { MessageBox.Show(请先选中一行); return; } bool ok await _userService.DeleteUserAsync(SelectedUser.Id); if (ok) { Users.Remove(SelectedUser); } }按钮的Command绑定到DeleteSelectedCommand点击后先从数据库删除删除成功再同步从ObservableCollection里移除。这里有一个关键细节数据库删除成功后必须同步把集合里的这条记录移除。否则界面一直留着那条数据直到下次刷新才消失容易让人误以为删除没生效。踩坑记录删除确认框不要写在Command里。MVVM虽然强调View不写业务逻辑但弹确认框本质是视图交互的一部分放在View的Click事件里更合理Command里只处理“用户确认删除”之后的业务动作。否则ViewModel就要引用Window类型耦合度立刻上来了。5. 下拉筛选与新增用户功能实现列表和删除只是基础实际项目中下拉筛选和新增也是高频需求。对应搜索热词里的“wpf combobox 下拉框 末尾 空白”和“wpf stackpanel 内的 textblock 允许换行”这两个坑我在项目里都遇到过一并说清楚。5.1 ComboBox绑定部门列表与下拉空白需求是页面上方放一个部门下拉框选择某个部门后下方列表只显示该部门的用户。数据源绑定到一个ObservableCollection ComboBox ItemsSource{Binding Departments} SelectedItem{Binding SelectedDepartment} Width160/ViewModel里public ObservableCollectionstring Departments { get; set; } new(); public string SelectedDepartment { get; set; } public async Task LoadDepartmentsAsync() { var depts await _userService.GetAllDepartmentsAsync(); Departments.Clear(); foreach (var d in depts) Departments.Add(d); }下拉框末尾出现空白项最常见的原因就是SelectedItem绑定到了一个不属于ItemsSource集合的对象。比如页面初始化时给SelectedDepartment赋了一个字符串但Departments集合里当时还没有这个值WPF找不到匹配项就会在下拉框里显示为空白。解决办法是等集合加载完成后再给SelectedItem赋值或者先判断集合是否包含该值再赋值。提示如果下拉框空白是null值造成的可以检查ItemsSource里是否有null项或者DataTable转换后的结果里是不是多了一个空行。用ObservableCollection 时集合元素为null下拉框也会显示成空白。5.2 新增用户的弹窗数据回传新增用户时我的做法是新建一个Window里面放几个TextBox和一个确定按钮通过DialogResult把数据传回ViewModel。为了让ViewModel不直接依赖Window类型窗口类里直接暴露数据属性public partial class AddUserWindow : Window { public string UserName { get; private set; } public int Age { get; private set; } public string Department { get; private set; } private void Ok_Click(object sender, RoutedEventArgs e) { UserName NameBox.Text; Age int.Parse(AgeBox.Text); Department DeptBox.Text; DialogResult true; } }调用处var win new AddUserWindow { Owner Application.Current.MainWindow }; if (win.ShowDialog() true) { var user new User { Name win.UserName, Age win.Age, Department win.Department, CreateTime DateTime.Now }; await _userService.AddUserAsync(user); Users.Add(user); }新增成功后直接把新User对象Add到Users集合里界面立刻可见不需要重新查询数据库。这既省了一次IO又避免了数据延迟的视觉问题。5.3 StackPanel内长文本换行WPF默认的TextBlock如果放在StackPanel里而且没有设置宽度约束它会倾向于一行显示完文字超出后被截断。要让长文本正常换行必须设置TextWrappingWrap同时给它一个宽度约束。我常用的两种写法StackPanel TextBlock Text{Binding Description} TextWrappingWrap Width300/ /StackPanel或者放进DockPanel并横向拉伸StackPanel TextBlock Text{Binding Description} TextWrappingWrap HorizontalAlignmentStretch/ /StackPanel关键点就是别让TextBlock自动计算到无限宽。如果是在DataGrid里展示长文本列可以给DataGridTextColumn设置ElementStyleElementStyle内放一个TextBlock并开启TextWrapping这样长文本就会在单元格里换行而不是把整列撑得奇宽。6. 数据库实例相关配置与常见报错标题里带“数据库实例”我把SqlSugar在不同数据库下的配置差异和几个高频报错集中说明。搜索热词里出现“oracle12数据库怎么创建实例”和“pgsql数据库实例的启动方式有哪些”这类问题说明很多人把数据库实例和ORM连接搞混了。这里先把概念理清SqlSugar连接的是数据库服务中的某个数据库而“实例”是整个数据库服务本身比如Oracle实例、PostgreSQL实例它们负责监听端口、管理数据文件。6.1 不同数据库连接配置对比数据库类型DbType枚举ConnectionString示例SQLiteDbType.SqliteData Sourceapp.dbSQL ServerDbType.SqlServerServerlocalhost;DatabaseTestDb;User Idsa;Password123456;TrustServerCertificateTrueMySQLDbType.MySqlServerlocalhost;DatabaseTestDb;Uidroot;Pwd123456;Allow User VariablesTruePostgreSQLDbType.PostgreSQLHostlocalhost;Port5432;DatabaseTestDb;Usernamepostgres;Password123456OracleDbType.OracleData Sourcelocalhost:1521/orcl;User Idsystem;Password123456连接字符串写对后代码层面唯一要改的是DbType枚举值查询、插入的写法完全通用。不过不同数据库的分页语法不一样Oracle和SQL Server的写法完全不同SqlSugar内部会自动适配作为上层开发者基本不用关心。6.2 “数据库实例未找到”类报错出现这类报错绝大多数不是SqlSugar的问题而是数据库服务本身没有启动。比如本机装了PostgreSQL但服务一直是停止状态连接就会报“could not connect to server”。解决方法是去系统服务里启动对应的服务Windows下按WinR输入services.msc找到postgresql-x64-15或OracleServiceXXX右键启动Linux下用systemctl start postgresql或者systemctl start oracle如果服务已经启动仍然连不上再检查端口。Oracle默认1521PostgreSQL默认5432SQL Server默认1433端口被占用或防火墙拦截都会导致连接超时。6.3 表名和实体类名不一致报“表不存在”SqlSugar默认把类名当作表名如果数据库里的表叫user_info而类名是UserInfo查询就报表不存在。解决办法是加SugarTable特性指定表名字段级不一致则在字段上加SugarColumn特性指定列名。这个坑从Dapper转过来的同事特别容易踩因为Dapper要求自己写SQL对表名不敏感而SqlSugar是约定优先必须主动告诉它映射关系。7. 绕不开的坑和优化建议功能层面跑通后再说几个我在实际项目里被坑过、但搜索引擎不太容易找到的点。7.1 ObservableCollection的线程问题如果Service层用了后台线程或Task.Run去查数据回来之后往ObservableCollection里Add一旦不在UI线程上WPF会直接抛“调用线程无法访问此对象”异常。最简单的规避方式不用Task.Run直接用async方法因为await异步返回后代码默认会回到UI线程上下文。如果确实要手动开线程更新集合前用Dispatcher.Invoke切回UI线程。7.2 全局异常兜底SqlSugar查询失败时会抛异常如果在ViewModel里不catch程序会直接闪退。我建议在每个Service方法调用处加try-catch明确提示用户操作失败原因同时在App.xaml.cs里挂DispatcherUnhandledException事件兜底防止未知异常导致程序默默消失。开发阶段可能无所谓但用户环境网络一抖动数据库偶发超时就直接崩溃反馈会非常差。7.3 大数据量列表要分页如果列表数据量可能上万不要一上来就全表查询加载到ObservableCollection里。SqlSugar自带分页方法ToPageListAsync返回当前页数据并输出总数配合DataGrid的分页或滚动懒加载能解决大部分性能问题var result await _db.QueryableUser() .OrderBy(u u.Id, OrderByType.Desc) .ToPageListAsync(pageIndex, pageSize, ref totalCount);7.4 依赖注入的正确姿势如果用了CommunityToolkit.Mvvm或者Prism这类MVVM框架建议直接在App里用Microsoft.Extensions.DependencyInjection注册SqlSugarClient和UserService然后解析ViewModel。没接触过DI的朋友别急没有DI一样能跑通上面的代码等你发现多个ViewModel需要共享同一个数据服务时自然会明白DI是在帮你减少重复new的负担。8. 后续可以往哪个方向扩展如果已经跟着把增删改查跑通说明WPF MVVM SqlSugar这套组合的基本闭环已经建立了。接下来想深入我个人觉得最有价值的是下面几个方向。第一引入CommunityToolkit.Mvvm改造ViewModel。它的SourceGenerator会自动生成属性通知和Command代码手写的OnPropertyChanged可以大幅减少而且能避免手写属性名时出现的拼写低级错误。上面的示例代码保持手写是为了把底层运行机制讲清楚等你理解了它到底在做什么再换框架就非常顺滑。第二给Service层增加真实业务能力比如用户唯一性校验、软删除、批量导入导出。这些才是实际业务里花时间的地方。SqlSugar对这些场景都有比较成熟的API比如UpdateColumns指定更新字段、Storageable做批量差异更新值得翻一翻官方文档。第三把数据库从SQLite切换到真正的生产库。切换成本低是SqlSugar的核心卖点但不同数据库对字段类型的支持有差异比如SQLite的DateTime和Oracle的Date精度就不同上线前一定要在目标数据库上完整回归测试一遍。这个技术组合特别适合公司内部管理系统、工控上位机、中小型工具类桌面软件。比起WinForm它在界面表现力和数据绑定上的优势很明显比起Web技术栈它又保留了桌面原生操作系统的资源调度能力。整套代码跑通之后接下来真正影响项目质量的就是分层是否清晰、命名是否统一、SQL语句是否真的能用索引这些就需要在实际项目里慢慢积累了。本文还有配套的精品资源点击获取