简介本资源是一套面向WPF桌面开发初学者的MVVM架构实战教程配套代码聚焦SqlSugar ORM与MySql数据库的集成应用解决新手在WPF项目中构建可维护数据访问层时常见的类库设计、接口抽象与泛型复用难题。压缩包共1612个文件含549个运行依赖dll、450个配置与文档xml、191个临时或元数据文件、104个说明txt及52个核心业务cs源码另有csproj、xaml、targets等工程支撑文件整体123.95MB结构完整覆盖数据层、ViewModel层与基础UI资源。已有576人学习下载资源直接提供可编译运行的完整项目骨架包含MySql连接封装、泛型仓储接口IRepository 、基础CURD实现、SqlSugar上下文配置及典型Model定义省去环境搭建与模板重复编码助开发者快速掌握WPFMVVMSqlSugar三层协作模式。1. WPFMVVM项目入门完整教程-基于SqlSugar的数据库实例三为什么前两章跑通了界面和命令第三章才真正开始“连上数据”很多刚学WPF的开发者卡在这样一个节点XAML写得漂亮按钮点击能弹窗INotifyPropertyChanged也手敲了三遍ViewModel里属性改了UI真能动——但一到“从数据库读用户列表”整个项目突然哑火。不是报错是查不到数据、刷新不触发、新增后界面上没影儿甚至SqlSugar的Insertable 执行完返回true数据库里却空空如也。这不是代码写错了而是MVVM的数据流边界、SqlSugar的上下文生命周期、WPF的绑定更新机制三者没对齐。本篇聚焦“第三章”这个临界点它不讲基础语法专攻“让SqlSugar真正活在MVVM里”的实操闭环——包括DbContext如何注入、异步查询怎么不卡UI、实体变更如何自动同步到DataGrid、以及最常被忽略的“事务提交时机”。适合已写完基础界面、正准备接入真实数据的WPF中级开发者。如果你的项目现在还停留在“MockList new() { … }”那这一章就是你从Demo走向可交付产品的分水岭。2. 搭建SqlSugarClient与MVVM服务层的最小可行集成WPF项目里直接new SqlSugarClient(new ConnectionConfig())可以跑但会埋下三颗雷一是全局单例导致并发查询互相干扰二是无法配合依赖注入做单元测试三是ViewModel销毁后连接未释放内存缓慢上涨。常见做法是把SqlSugarClient包装成一个可注入的服务并限定其作用域为“每次页面/模块级操作”。2.1 创建IDbContextService接口与SqlSugar实现类我们不直接暴露SqlSugarClient而是定义抽象层。这样后续可切换Dapper或EF CoreViewModel代码完全不动。// Interfaces/IDbContextService.cs public interface IDbContextService : IDisposable { SqlSugarClient Client { get; } Taskbool EnsureTableCreatedAsyncT() where T : class; }// Services/SqlSugarDbContextService.cs public class SqlSugarDbContextService : IDbContextService { private readonly SqlSugarClient _client; public SqlSugarDbContextService(string connectionString) { _client new SqlSugarClient(new ConnectionConfig() { ConnectionString connectionString, DbType DbType.SqlServer, // 根据实际数据库调整 IsAutoCloseConnection true, // 关键避免手动Close InitKeyType InitKeyType.Attribute // 使用特性映射 }); // 全局配置启用AOP日志仅开发期 _client.Ado.UseTran true; // 启用事务支持 _client.Ado.IsEnableLogEvent true; _client.Ado.LogEventStarting (sql, pars) { Debug.WriteLine($[SQL] {sql}); foreach (var p in pars) Debug.WriteLine($ {p.ParameterName} {p.Value}); }; } public SqlSugarClient Client _client; public async Taskbool EnsureTableCreatedAsyncT() where T : class { return await Task.Run(() _client.CodeFirst.InitTablesT()); } public void Dispose() { _client?.Dispose(); } }提示IsAutoCloseConnection true是关键开关。它让SqlSugar在每次Execute后自动关闭连接避免WPF长生命周期中连接泄漏。若设为false必须手动调用_client.Ado.UseConnection()并确保Close()极易翻车。2.2 在App.xaml.cs中注册服务并初始化数据库WPF的App启动时机是注入全局服务的最佳位置。注意不要在MainWindow构造函数里new DbContextService否则每次打开新窗口都新建连接池。// App.xaml.cs public partial class App : Application { private IServiceProvider _serviceProvider; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var services new ServiceCollection(); // 注册SqlSugar服务Scoped每个请求/操作独立实例 services.AddScopedIDbContextService(sp { var connStr Data Source.;Initial CatalogWpfMvvmDemo;Integrated SecurityTrue;; return new SqlSugarDbContextService(connStr); }); // 注册ViewModel必须Scoped否则INPC无法响应 services.AddScopedMainViewModel(); _serviceProvider services.BuildServiceProvider(); var mainWindow new MainWindow(); mainWindow.DataContext _serviceProvider.GetRequiredServiceMainViewModel(); mainWindow.Show(); } }注意AddScoped而非AddSingleton。SqlSugarClient虽轻量但内部维护连接池、缓存、AOP拦截器等状态。多线程并发时Singleton会导致SqlSugarException: Connection is already open。Scoped保证每个ViewModel实例拥有独立Client实例且随ViewModel销毁而释放。2.3 ViewModel中安全调用数据库查询ViewModel不能直接await数据库操作会阻塞UI线程也不能在构造函数里查数据WPF绑定尚未建立。正确姿势用IAsyncRelayCommand封装异步逻辑并在OnLoaded事件或首次访问时触发。// ViewModels/MainViewModel.cs public class MainViewModel : INotifyPropertyChanged { private readonly IDbContextService _dbContext; private ObservableCollectionUser _users; private string _statusMessage; public ObservableCollectionUser Users { get _users; set SetProperty(ref _users, value); } public string StatusMessage { get _statusMessage; set SetProperty(ref _statusMessage, value); } public IAsyncRelayCommand LoadUsersCommand { get; } public MainViewModel(IDbContextService dbContext) { _dbContext dbContext; LoadUsersCommand new AsyncRelayCommand(LoadUsersAsync); } private async Task LoadUsersAsync() { try { StatusMessage 正在加载用户...; var users await Task.Run(() { return _dbContext.Client.QueryableUser().ToList(); }); Users new ObservableCollectionUser(users); StatusMessage $共加载 {Users.Count} 条用户数据; } catch (Exception ex) { StatusMessage $加载失败{ex.Message}; Debug.WriteLine(ex); } } // INotifyPropertyChanged 实现略使用CommunityToolkit.Mvvm的ObservableObject基类更佳 }逻辑说明Task.Run将查询移出UI线程避免界面冻结ObservableCollection确保UI自动响应增删改IAsyncRelayCommand由CommunityToolkit.Mvvm提供比手写ICommandTask.Run更健壮。参数说明LoadUsersAsync无入参符合“加载全部”场景若需带条件查询如按姓名模糊搜索可扩展为IAsyncRelayCommandstring并在Execute中接收搜索关键词。3. 实体类设计与SqlSugar特性映射避开字段名、主键、时间戳三大坑SqlSugar通过特性Attribute描述实体与数据库表的关系。新手常因三个细节导致查询为空、插入失败或时间字段乱码① 实体类名与表名不一致② 主键未标注或类型不匹配③ DateTime字段未指定精度或默认值。本节给出生产环境验证过的最小映射模板。3.1 User实体类的标准写法含注释说明// Models/User.cs [SugarTable(Users)] // 显式指定表名避免SqlSugar按类名User生成t_User public class User : IEntity { [SugarColumn(IsPrimaryKey true, IsIdentity true)] // 主键自增 public int Id { get; set; } [SugarColumn(ColumnName UserName, Length 50, IsNullable false)] public string Name { get; set; } [SugarColumn(ColumnName Email, Length 100, IsNullable true)] public string Email { get; set; } [SugarColumn(ColumnName CreatedAt, IsNullable false)] public DateTime CreatedAt { get; set; } DateTime.Now; // C#端默认值非数据库默认 [SugarColumn(ColumnName UpdatedAt, IsNullable false)] public DateTime UpdatedAt { get; set; } DateTime.Now; // 非数据库字段用于UI显示如“已创建3小时” [SugarColumn(IsIgnore true)] public string DisplayTime CreatedAt.ToString(yyyy-MM-dd HH:mm); }参数说明IsIdentity true仅对SQL Server/PostgreSQL自增列有效MySQL需用IsPrimaryKey trueIsIdentity false 数据库侧设AUTO_INCREMENTLength 50必须与数据库varchar(50)一致否则插入超长字符串时静默截断IsIgnore true标记非数据库字段SqlSugar序列化时跳过避免Unknown column DisplayTime错误。3.2 初始化表结构CodeFirst模式下的安全创建流程SqlSugar的CodeFirst不是“一键建库”而是“按需建表字段校验”。必须在首次查询前执行InitTablesT否则QueryableT会抛Table Users doesnt exist。// 在MainViewModel的LoadUsersAsync中加入初始化逻辑 private async Task LoadUsersAsync() { try { // 步骤1确保表存在仅首次运行 await _dbContext.EnsureTableCreatedAsyncUser(); // 步骤2查询数据 var users await Task.Run(() { return _dbContext.Client.QueryableUser() .OrderBy(x x.Id) // 避免无序结果 .ToList(); }); Users new ObservableCollectionUser(users); } catch (Exception ex) { StatusMessage $初始化或查询失败{ex.Message}; } }注意EnsureTableCreatedAsync是同步方法但包装为Task.Run以避免阻塞UI。SqlSugar的InitTables内部会检查表是否存在、字段是否缺失、类型是否匹配缺失则自动ADD COLUMN但不会修改已有字段类型或删除字段防数据丢失。生产环境建议改为手动执行SQL建表脚本CodeFirst仅用于开发期快速迭代。3.3 时间字段的精度陷阱与解决方案SQL Server的datetime类型精度为3.33ms而C#DateTime.Now精度为100ns。直接插入会导致CreatedAt值被数据库四舍五入如2024-05-20 14:22:33.1234567存为2024-05-20 14:22:33.123再查询时比较失败。解决方案统一使用datetime2(7)并配置SqlSugar。// 在SqlSugarDbContextService构造函数中添加 _client.EntityMaintenance.AddEntityInfo(new EntityInfo() { EntityName typeof(User).Name, Columns new ListEntityColumnInfo() { new EntityColumnInfo() { ColumnName CreatedAt, DataType datetime2, DecimalDigits 7 }, new EntityColumnInfo() { ColumnName UpdatedAt, DataType datetime2, DecimalDigits 7 } } });血泪经验某次上线后发现“最近1小时用户”筛选不准排查3小时才发现是datetime精度丢失。加datetime2(7)后问题消失。此配置必须在InitTables之前执行否则无效。4. 常见问题排查五个让WPFSqlSugar组合“静默失败”的典型场景现象、原因、解决一条一条列清楚。这些不是理论假设是某开发者在模拟项目X中连续踩过的坑复现率极高。4.1 现象Client.QueryableUser().ToList()返回空集合但数据库里明明有数据原因实体类的[SugarTable]表名与数据库实际表名不一致如数据库表是T_Users实体标了[SugarTable(Users)]或字段名映射错误如数据库列是user_name实体属性叫UserName但没加[SugarColumn(ColumnNameuser_name)]。解决开启SqlSugar日志见2.1节观察生成的SQL是否为SELECT * FROM Users若SQL表名错误在实体类上修正[SugarTable(T_Users)]若字段名不匹配逐个检查[SugarColumn]的ColumnName值。4.2 现象新增用户后Client.Insertable(user).ExecuteCommand()返回1但数据库无记录原因IsAutoCloseConnection true开启后SqlSugar在执行完Insert后自动关闭连接但事务未提交。常见于忘记调用Client.Ado.UseTran()开启事务或开启后未调用Client.Ado.CommitTran()。解决插入操作必须显式事务包裹await Task.Run(() { _dbContext.Client.Ado.UseTran(); // 开启事务 try { _dbContext.Client.Insertable(newUser).ExecuteCommand(); _dbContext.Client.Ado.CommitTran(); // 提交 } catch { _dbContext.Client.Ado.RollbackTran(); // 回滚 throw; } });4.3 现象修改User.Name后调用Client.Updateable(user).ExecuteCommand()数据库更新成功但DataGrid里Name列仍是旧值原因ObservableCollection只监听集合的“增删”不监听对象内部属性变化。User类未实现INotifyPropertyChanged或实现了但Name属性的setter里没调用OnPropertyChanged()。解决User类继承ObservableObjectCommunityToolkit.Mvvm或手动实现INPCpublic class User : ObservableObject // 替换原IEntity { private string _name; public string Name { get _name; set SetProperty(ref _name, value); // 自动触发UI更新 } }4.4 现象程序退出时抛ObjectDisposedException: Cannot access a disposed object原因IDbContextService被注册为Scoped但ViewModel生命周期长于Service Scope如MainWindow未关闭但Service已被释放。根本原因是App中BuildServiceProvider()后未保存引用导致GC提前回收。解决在App类中持有IServiceProvider强引用private IServiceProvider _serviceProvider; // 已声明 // 在OnStartup中赋值后不释放 protected override void OnExit(ExitEventArgs e) { _serviceProvider?.Dispose(); // 显式释放 base.OnExit(e); }4.5 现象LoadUsersCommand执行后StatusMessage更新但Users集合始终为空原因ObservableCollectionUser赋值时new ObservableCollectionUser(users)创建了新集合但WPF绑定仍指向旧集合set方法未触发PropertyChanged事件。解决确保SetProperty方法正确触发通知private ObservableCollectionUser _users; public ObservableCollectionUser Users { get _users; private set SetProperty(ref _users, value); // 注意setter必须private避免外部直接赋值绕过通知 } // 赋值时用 Users new ObservableCollectionUser(users); // 此行会触发SetProperty进而触发INPC5. 进阶技巧用SqlSugar的AOP拦截实现自动审计日志与软删除当项目从CRUD Demo迈向真实业务系统两个需求必然浮现① 记录谁在什么时候修改了哪条数据② 删除操作不物理删除而是标记IsDeleted1。SqlSugar的AOP面向切面编程能力可零侵入实现无需在每个Update/Delete方法里手写日志和Where(x!x.IsDeleted)。5.1 定义审计字段基类与软删除接口// Models/IAuditable.cs public interface IAuditable { string CreatedBy { get; set; } DateTime CreatedAt { get; set; } string UpdatedBy { get; set; } DateTime UpdatedAt { get; set; } } // Models/ISoftDelete.cs public interface ISoftDelete { bool IsDeleted { get; set; } }// Models/User.cs改造后 [SugarTable(Users)] public class User : IAuditable, ISoftDelete { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public int Id { get; set; } [SugarColumn(ColumnName UserName, Length 50)] public string Name { get; set; } [SugarColumn(ColumnName Email, Length 100)] public string Email { get; set; } // IAuditable成员 [SugarColumn(ColumnName CreatedBy, Length 50)] public string CreatedBy { get; set; } System; [SugarColumn(ColumnName CreatedAt)] public DateTime CreatedAt { get; set; } DateTime.Now; [SugarColumn(ColumnName UpdatedBy, Length 50)] public string UpdatedBy { get; set; } System; [SugarColumn(ColumnName UpdatedAt)] public DateTime UpdatedAt { get; set; } DateTime.Now; // ISoftDelete成员 [SugarColumn(ColumnName IsDeleted)] public bool IsDeleted { get; set; } false; }5.2 编写AOP拦截器自动填充审计字段与过滤软删除SqlSugar的Ado.UseCommand可拦截所有SQL执行。我们在SqlSugarDbContextService构造函数中注入拦截逻辑// Services/SqlSugarDbContextService.cs追加 public SqlSugarDbContextService(string connectionString) { // ... 前置初始化代码 // AOP拦截自动填充审计字段 _client.Ado.UseCommand (sql, pars) { // 对INSERT语句自动添加CreatedBy/CreatedAt if (sql.StartsWith(INSERT INTO, StringComparison.OrdinalIgnoreCase)) { var tableName ExtractTableName(sql); if (typeof(IAuditable).IsAssignableFrom(GetEntityType(tableName))) { // 找到VALUES (...)部分插入审计字段值 sql InjectAuditValues(sql, CreatedBy, System, CreatedAt, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)); } } // 对UPDATE语句自动更新UpdatedAt/UpdatedBy if (sql.StartsWith(UPDATE, StringComparison.OrdinalIgnoreCase)) { var tableName ExtractTableName(sql); if (typeof(IAuditable).IsAssignableFrom(GetEntityType(tableName))) { sql InjectUpdateTime(sql, UpdatedBy, System, UpdatedAt, DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss)); } } // 对所有SELECT/UPDATE/DELETE自动添加IsDeleted0条件 if (sql.StartsWith(SELECT, StringComparison.OrdinalIgnoreCase) || sql.StartsWith(UPDATE, StringComparison.OrdinalIgnoreCase) || sql.StartsWith(DELETE, StringComparison.OrdinalIgnoreCase)) { var tableName ExtractTableName(sql); if (typeof(ISoftDelete).IsAssignableFrom(GetEntityType(tableName))) { sql AppendSoftDeleteCondition(sql, tableName); } } return sql; }; }逻辑说明UseCommand是SqlSugar最强大的钩子它在SQL发送到数据库前最后一步修改SQL。InjectAuditValues和AppendSoftDeleteCondition是自定义方法代码略核心是字符串替换确保所有INSERT自动带System, 2024-05-20 14:22:33所有查询自动加AND IsDeleted 0。参数说明ExtractTableName从INSERT INTO Users (...)中提取UsersGetEntityType根据表名反射获取对应实体类型如Users→User类。5.3 ViewModel中无感使用删除即软删查询自动过滤现在ViewModel里的任何数据库操作都不需要关心审计和软删除// ViewModel中删除用户物理删除不是软删 public async Task DeleteUserAsync(User user) { await Task.Run(() { // 一行代码自动1. 设置IsDeletedtrue2. 更新UpdatedAt3. 仅更新该字段 _dbContext.Client.Updateable(user) .UpdateColumns(it new User() { IsDeleted true, UpdatedAt DateTime.Now }) .ExecuteCommand(); }); } // 查询用户自动过滤IsDeleted0 public async Task LoadActiveUsersAsync() { await Task.Run(() { var activeUsers _dbContext.Client.QueryableUser() .Where(x x.Name.Contains(admin)) // 条件任意 .ToList(); // 实际执行的SQL是WHERE Name LIKE %admin% AND IsDeleted 0 Users new ObservableCollectionUser(activeUsers); }); }这就是AOP的价值把横切关注点审计、软删从业务代码中剥离。某跨平台系统用此方案上线后审计日志100%覆盖运维同事再也不用问“这条数据谁删的”。我一般会在项目初期就集成AOP拦截因为后期补成本高、易遗漏。希望帮到你。本文还有配套的精品资源点击获取