简介:面向刚开始接触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:实体类,对应数据库表结构
- Services:SqlSugar封装和业务逻辑
- ViewModels:页面对应的ViewModel
- Views:XAML页面文件
这么做最大的好处是依赖方向单向流动:View只引用ViewModel,ViewModel只引用Service,Service只操作Models。反过来如果ViewModel里直接调用SqlSugarClient,那后续想加缓存、换数据库、做权限控制,都在所有ViewModel里来回改,维护成本会直线上升。
1.2 SqlSugar在整体结构中负责什么
SqlSugar扮演的是ORM里的数据访问层,核心工作是把C#对象映射成SQL语句,再把查询结果映射回对象。它的底层用表达式树把Lambda表达式翻译成SQL,所以你写一行代码,它自动帮你做了连接管理、参数化查询、结果映射这些脏活累活:
var list = db.Queryable<User>().Where(u => u.Age > 18).ToList();这行代码执行时会翻译成SELECT * FROM User WHERE Age > 18,然后返回一个List<User>。对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 = "DataSource=app.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.InitTables<User>();这样能保证数据库表一定存在,不会出现手工建表字段不一致导致查询时报错。但要注意,如果是已有表结构,实体类的命名和字段类型必须和表字段严格对齐,否则查询结果会全是空值或零。这属于新手最容易忽略的细节。
3. 服务层封装:让ViewModel不碰数据库连接
在MVVM模式下,ViewModel应该保持对数据库无感知。我把用户表的所有数据库操作统一封装进UserService,对外暴露异步方法,界面层只管调用。
3.1 服务层基本方法
public class UserService { private readonly ISqlSugarClient _db; public UserService(ISqlSugarClient db) { _db = db; } public async Task<List<User>> GetAllUsersAsync() { return await _db.Queryable<User>().ToListAsync(); } public async Task<List<string>> GetAllDepartmentsAsync() { return await _db.Queryable<User>() .GroupBy(u => u.Department) .Select(u => u.Department) .ToListAsync(); } public async Task<bool> AddUserAsync(User user) { return await _db.Insertable(user).ExecuteCommandAsync() > 0; } public async Task<bool> DeleteUserAsync(int id) { return await _db.Deleteable<User>().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 ObservableCollection<User> 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列绑定写法
为了让列头显示中文、列宽可控,我通常关闭自动生成列,手工定义DataGridTextColumn:
<DataGrid ItemsSource="{Binding Users}" SelectedItem="{Binding SelectedUser}" AutoGenerateColumns="False" IsReadOnly="True" SelectionMode="Single"> <DataGrid.Columns> <DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="80"/> <DataGridTextColumn Header="姓名" Binding="{Binding Name}" Width="150"/> <DataGridTextColumn Header="年龄" Binding="{Binding Age}" Width="100"/> <DataGridTextColumn Header="部门" Binding="{Binding Department}" Width="150"/> <DataGridTextColumn Header="创建时间" Binding="{Binding CreateTime}" Width="180"/> </DataGrid.Columns> </DataGrid>注意IsReadOnly="True",整张表直接进入只读状态,避免用户双击单元格进入编辑态导致误改数据。有的需求要放checkbox列,那我就说清楚:只读状态下的DataGridCheckBoxColumn,里面的checkbox仍然可以切换勾选状态,所以如果你要做的只是选中行,不要用checkbox列,直接依赖DataGrid自带的SelectedItem选中机制最干净。
4.2 选中行背景色自定义
DataGrid默认选中行的背景是浅蓝色,很多项目需要改成与主题一致的强调色。这个需求用CellStyle加触发器就能实现:
<DataGrid.CellStyle> <Style TargetType="DataGridCell"> <Setter Property="Background" Value="Transparent"/> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="#FF4F81BD"/> <Setter Property="Foreground" Value="White"/> </Trigger> </Style.Triggers> </Style> </DataGrid.CellStyle>这段样式加到DataGrid.CellStyle里,选中某个单元格时就会变成你指定的背景色和白色文字。它触发的是DataGridCell的IsSelected属性,不是DataGridRow的,所以颜色变化是跟着单元格走的。如果想整行高亮,可以在RowStyle里写触发器,效果略有差别,但二选一即可,不要叠加写否则样式可能出现冲突。
4.3 点击按钮删除选中行
界面底部放一个“删除选中”按钮,点击后把当前选中的那行删掉。在MVVM模式下这个动作必须走Command,关键问题是怎么把“当前选中行”传给Command。
最干净的方案是ViewModel里加一个SelectedUser属性,绑到DataGrid.SelectedItem:
private 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}" Width="160"/>ViewModel里:
public ObservableCollection<string> 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里,而且没有设置宽度约束,它会倾向于一行显示完,文字超出后被截断。要让长文本正常换行,必须设置TextWrapping="Wrap",同时给它一个宽度约束。我常用的两种写法:
<StackPanel> <TextBlock Text="{Binding Description}" TextWrapping="Wrap" Width="300"/> </StackPanel>或者放进DockPanel并横向拉伸:
<StackPanel> <TextBlock Text="{Binding Description}" TextWrapping="Wrap" HorizontalAlignment="Stretch"/> </StackPanel>关键点就是别让TextBlock自动计算到无限宽。如果是在DataGrid里展示长文本列,可以给DataGridTextColumn设置ElementStyle,ElementStyle内放一个TextBlock并开启TextWrapping,这样长文本就会在单元格里换行而不是把整列撑得奇宽。
6. 数据库实例相关配置与常见报错
标题里带“数据库实例”,我把SqlSugar在不同数据库下的配置差异和几个高频报错集中说明。搜索热词里出现“oracle12数据库怎么创建实例”和“pgsql数据库实例的启动方式有哪些”这类问题,说明很多人把数据库实例和ORM连接搞混了。这里先把概念理清:SqlSugar连接的是数据库服务中的某个数据库,而“实例”是整个数据库服务本身,比如Oracle实例、PostgreSQL实例,它们负责监听端口、管理数据文件。
6.1 不同数据库连接配置对比
| 数据库类型 | DbType枚举 | ConnectionString示例 |
|---|---|---|
| SQLite | DbType.Sqlite | Data Source=app.db |
| SQL Server | DbType.SqlServer | Server=localhost;Database=TestDb;User Id=sa;Password=123456;TrustServerCertificate=True |
| MySQL | DbType.MySql | Server=localhost;Database=TestDb;Uid=root;Pwd=123456;Allow User Variables=True |
| PostgreSQL | DbType.PostgreSQL | Host=localhost;Port=5432;Database=TestDb;Username=postgres;Password=123456 |
| Oracle | DbType.Oracle | Data Source=localhost:1521/orcl;User Id=system;Password=123456 |
连接字符串写对后,代码层面唯一要改的是DbType枚举值,查询、插入的写法完全通用。不过不同数据库的分页语法不一样,Oracle和SQL Server的写法完全不同,SqlSugar内部会自动适配,作为上层开发者基本不用关心。
6.2 “数据库实例未找到”类报错
出现这类报错,绝大多数不是SqlSugar的问题,而是数据库服务本身没有启动。比如本机装了PostgreSQL,但服务一直是停止状态,连接就会报“could not connect to server”。解决方法是去系统服务里启动对应的服务:
- Windows下按Win+R输入services.msc,找到postgresql-x64-15或OracleServiceXXX,右键启动
- Linux下用systemctl start postgresql,或者systemctl start oracle
如果服务已经启动仍然连不上,再检查端口。Oracle默认1521,PostgreSQL默认5432,SQL 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.Queryable<User>() .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语句是否真的能用索引,这些就需要在实际项目里慢慢积累了。
本文还有配套的精品资源,点击获取