ARTICLE DETAIL

资讯详情

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

WPF+MVVM中SqlSugar仓储抽象与命令生命周期管理

WPF+MVVM中SqlSugar仓储抽象与命令生命周期管理 简介本资源是一套面向WPF桌面开发初学者的MVVM架构实战教程配套代码聚焦数据库层设计与SqlSugar ORM集成解决新手在WPF项目中构建可维护数据访问层的核心难点。资源包含1612个文件主体为549个运行依赖DLL、450个NuGet包元数据XML、191个临时或隐藏文件如_开头配置、104个说明类TXT及52个C#业务逻辑源码另有CSProj工程文件、XAML界面定义、BAML编译资源及NUPKG离线包等完整复现从MySql建模、泛型仓储封装到ViewModel数据绑定的全流程压缩包大小123.95MB。已有576人学习下载提供开箱即用的分层项目结构、标准化接口定义、通用CRUD泛型实现及配套调试资源助开发者快速掌握WPFMVVMSqlSugar技术栈的协同开发范式。1. WPFMVVM项目入门完整教程-基于SqlSugar的数据库实例三为什么这一节必须讲清楚“仓储层抽象”和“命令生命周期管理”这是整个系列里最常被跳过的章节也是上线后翻车率最高的环节——你用SqlSugar写好了增删改查ViewModel里堆满了ICommand界面能动、数据能存但一加多线程操作就报ObjectDisposedException一做批量导入就卡死UI一换数据库比如从SQL Server切到SQLite就得重写半个项目。根本原因不是SqlSugar不会用而是没把“仓储契约”和“命令执行边界”真正立住。本节不讲WPF基础控件怎么拖不重复绑定语法专攻MVVM落地中最容易被当成“玄学”的两个硬骨头如何让SqlSugar的DbContext在MVVM架构里安全复用如何让ICommand真正响应业务语义而非UI点击事件适合已能跑通WPF窗口、写过简单绑定、但一碰真实业务逻辑就掉链子的C#开发者。如果你正卡在“ViewModel里new了SqlSugarClient却不敢传参”“Command执行完数据没刷新”“改个字段就要改三层代码”这节就是你的后悔药。2. 用SqlSugarClient封装仓储层为什么不能直接在ViewModel里new一个实例MVVM的核心矛盾之一是View和ViewModel之间必须解耦而数据访问层DAL如果直接暴露给ViewModel就会把数据库连接、事务控制、连接池管理这些基础设施细节污染进业务逻辑。常见错误做法是在ViewModel构造函数里直接new SqlSugarClient(...)然后所有方法都调它。这会导致三个致命问题连接泄漏、事务无法跨方法传播、单元测试完全不可行。我们得用接口抽象出仓储契约再用依赖注入控制生命周期。2.1 定义泛型仓储接口IRepository 的最小必要成员public interface IRepositoryT where T : class, new() { /// summary /// 获取实体列表支持分页、条件、排序 /// /summary TaskListT GetListAsync(ExpressionFuncT, bool whereExpression null, FuncIQueryableT, IQueryableT includeFunc null, int pageIndex 1, int pageSize 20); /// summary /// 根据主键获取单个实体 /// /summary TaskT GetByIdAsync(object id); /// summary /// 插入单个实体返回自增ID /// /summary Taskint InsertAsync(T entity); /// summary /// 批量插入注意SqlSugar原生支持InsertRange但需处理主键策略 /// /summary Taskint InsertRangeAsync(ListT entities); /// summary /// 更新实体按主键匹配非空字段才更新 /// /summary Taskbool UpdateAsync(T entity); /// summary /// 删除支持软删除标记如IsDeleted字段 /// /summary Taskbool DeleteAsync(object id); }提示这里没写SaveChangesAsync()——因为SqlSugar没有EF那种ChangeTracker每次操作都是立即执行。所以UpdateAsync内部直接调Ado.UseTran().Update(...)而不是攒一堆变更最后提交。这点和EF Core完全不同必须扭转思维。2.2 实现SqlSugar仓储ISqlSugarClient的线程安全复用策略SqlSugarClient本身不是线程安全的但官方推荐每个请求/操作创建新实例。然而在WPF桌面应用中“请求”概念模糊我们采用“操作粒度复用”即一个业务操作如保存订单内共享同一个SqlSugarClient实例操作结束立即释放。关键在于不能跨异步方法传递同一实例否则可能遇到InvalidOperationException: Connection is already in use。public class SqlSugarRepositoryT : IRepositoryT where T : class, new() { private readonly ISqlSugarClient _client; private readonly IConnectionConfig _config; public SqlSugarRepository(IConnectionConfig config) { _config config; // 每次new仓储时创建新Client但复用同一连接字符串配置 _client new SqlSugarClient(_config); // 全局配置启用AOP日志、设置主键策略、开启自增列识别 _client.Ado.IsEnableLogEvent true; _client.Ado.LogEvent (msg, type) Debug.WriteLine($[{DateTime.Now:HH:mm:ss}] {type}: {msg}); _client.EntityMaintenance.AddTableDescription(typeof(T).Name, 业务实体表); } public async TaskListT GetListAsync(ExpressionFuncT, bool whereExpression null, FuncIQueryableT, IQueryableT includeFunc null, int pageIndex 1, int pageSize 20) { // 注意SqlSugar的Queryable不支持LINQ to Entities的复杂嵌套此处用Ado.RawSql更可控 var queryable _client.QueryableT(); if (whereExpression ! null) queryable queryable.Where(whereExpression); // 分页必须用SqlSugar原生方法避免ToLis()后内存分页 return await queryable.ToPageListAsync(pageIndex, pageSize); } public async TaskT GetByIdAsync(object id) { // 主键类型自动推断int/string/Guid return await _client.GetByIdAsyncT(id); } public async Taskint InsertAsync(T entity) { // SqlSugar会自动识别自增主键并返回值 return await _client.InsertAsync(entity); } public async Taskint InsertRangeAsync(ListT entities) { // 批量插入性能比循环Insert高10倍以上但需注意内存占用 return await _client.InsertRangeAsync(entities); } public async Taskbool UpdateAsync(T entity) { // 默认只更新非空字段除主键外避免覆盖null值 var result await _client.UpdateAsync(entity); return result 0; } public async Taskbool DeleteAsync(object id) { // 支持软删除检查实体是否有IsDeleted属性有则Update否则Delete var entityType typeof(T); var deletedProp entityType.GetProperty(IsDeleted); if (deletedProp ! null deletedProp.PropertyType typeof(bool)) { var updateObj new { IsDeleted true }; var result await _client.Ado.UseTran().Update(updateObj, new { Id id }, entityType.Name); return result 0; } else { return await _client.DeleteAsyncT(id) 0; } } }参数说明IConnectionConfig是SqlSugar的连接配置接口包含连接字符串、数据库类型、是否启用AOP等。我们把它注入进来而不是硬编码为后续切换数据库如SQLite留出扩展点。_client.Ado.UseTran()是SqlSugar手动开启事务的方式比BeginTran更轻量且支持异步。ToPageListAsync是SqlSugar分页核心方法底层生成ROW_NUMBER() OVER或LIMIT OFFSET避免Skip/Take导致的全表扫描。2.3 在ViewModel中通过构造函数注入仓储彻底告别new SqlSugarClient()WPF默认不带DI容器我们选用轻量级Microsoft.Extensions.DependencyInjectionNuGet包Microsoft.Extensions.DependencyInjection在App.xaml.cs中初始化// App.xaml.cs public partial class App : Application { private IServiceProvider _serviceProvider; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var serviceCollection new ServiceCollection(); ConfigureServices(serviceCollection); _serviceProvider serviceCollection.BuildServiceProvider(); var mainWindow _serviceProvider.GetRequiredServiceMainWindow(); mainWindow.Show(); } private void ConfigureServices(IServiceCollection services) { // 注册连接配置可从app.config读取 var connectionString ConfigurationManager.ConnectionStrings[Default].ConnectionString; var config new ConnectionConfig { ConnectionString connectionString, DbType DbType.SqlServer, IsAutoCloseConnection true, // 关键设为true才能自动释放连接 InitKeyType InitKeyType.Attribute // 使用特性定义主键/忽略字段 }; services.AddSingletonIConnectionConfig(sp config); // 注册仓储Scoped生命周期每次Resolve新建但同一次请求内复用 services.AddScoped(typeof(IRepository), typeof(SqlSugarRepository)); // 注册ViewModel必须Scoped否则Command绑定失效 services.AddScopedMainViewModel(); // 注册ViewWPF窗口需手动Resolve不能用XAML直接绑定 services.AddTransientMainWindow(); } }注意IsAutoCloseConnection true是SqlSugar桌面应用的关键开关。设为false时连接不会自动关闭极易导致连接池耗尽。WPF无HTTP上下文必须靠此参数兜底。3. ViewModel中的ICommand重构从“按钮点击”到“业务动作”的语义升级很多初学者把ICommand当成Button.Click的代理写成SaveCommand new RelayCommand(SaveExecute)然后SaveExecute里直接调SqlSugar。这看似能跑但埋下三大隐患无法取消长耗时操作、无法显示执行状态如按钮变灰、无法组合多个操作如“保存发送邮件更新日志”。真正的MVVM命令必须承载可取消性、状态反馈、错误隔离三要素。3.1 使用CommunityToolkit.Mvvm的AsyncRelayCommand一行代码解决异步命令痛点安装NuGet包CommunityToolkit.Mvvmv8.2它内置AsyncRelayCommandT支持CanExecuteChanged自动触发、Task返回、CancellationToken注入public partial class MainViewModel : ObservableObject { private readonly IRepositoryOrder _orderRepository; private readonly ILoggerMainViewModel _logger; // 状态属性控制UI是否禁用 [ObservableProperty] private bool _isSaving; // 命令属性使用生成器语法自动实现INotifyPropertyChanged [ObservableProperty] private AsyncRelayCommand _saveOrderCommand; public MainViewModel(IRepositoryOrder orderRepository, ILoggerMainViewModel logger) { _orderRepository orderRepository; _logger logger; SaveOrderCommand new AsyncRelayCommand(SaveOrderAsync, CanSaveOrder); } private bool CanSaveOrder() !IsSaving !string.IsNullOrEmpty(CurrentOrder?.CustomerName); private async Task SaveOrderAsync(CancellationToken token) { try { IsSaving true; // UI自动响应 await _orderRepository.InsertAsync(CurrentOrder); _logger.LogInformation(订单{OrderId}保存成功, CurrentOrder.Id); } catch (Exception ex) when (!token.IsCancellationRequested) { _logger.LogError(ex, 保存订单失败); throw; // 让上层捕获ViewModel不处理具体错误 } finally { IsSaving false; } } }关键点解析AsyncRelayCommand构造函数第二个参数CanSaveOrder是启用条件当CurrentOrder.CustomerName为空时按钮自动禁用无需手动调RaiseCanExecuteChanged()。CancellationToken token由WPF框架自动注入若用户点击“取消”按钮需额外绑定可中断正在执行的InsertAsync。IsSaving属性用[ObservableProperty]标记编译时自动生成OnPropertyChanged绑定到Button的IsEnabled{Binding IsSaving, Converter{StaticResource InverseBooleanConverter}}即可。3.2 复合命令设计一个按钮触发多阶段操作保存通知日志真实业务中“保存订单”往往不是原子操作。我们需要串行执行① 保存订单 → ② 发送邮件通知 → ③ 写入操作日志。若用传统RelayCommand得手写状态机用AsyncRelayCommand只需组合Task.WhenAll或顺序awaitprivate async Task SaveOrderWithNotifyAsync(CancellationToken token) { try { IsSaving true; // 阶段1保存订单 var orderId await _orderRepository.InsertAsync(CurrentOrder); // 阶段2发送邮件假设用IMailService await _mailService.SendOrderConfirmedEmailAsync(CurrentOrder.CustomerEmail, orderId); // 阶段3写入审计日志 await _auditLogService.LogAsync(new AuditLog { Action CreateOrder, UserId CurrentUser.Id, TargetId orderId.ToString(), Timestamp DateTime.Now }); _logger.LogInformation(订单{OrderId}全流程执行完毕, orderId); } catch (OperationCanceledException) { _logger.LogWarning(订单保存被用户取消); } catch (Exception ex) { _logger.LogError(ex, 订单全流程执行失败); // 此处可弹窗提示但不要吞异常 throw; } finally { IsSaving false; } }血泪经验不要在复合命令里用try-catch吞掉所有异常MVVM要求错误向上抛由View层如MainWindow统一处理。ViewModel只负责记录日志和清理状态。3.3 命令参数化支持不同实体类型的通用保存命令为避免为每个实体写重复的SaveXxxCommand我们提取通用保存逻辑。关键在于命令参数类型必须与仓储泛型一致且WPF绑定时需显式指定CommandParameter!-- XAML中 -- Button Content保存客户 Command{Binding SaveEntityCommand} CommandParameter{Binding SelectedCustomer} / Button Content保存产品 Command{Binding SaveEntityCommand} CommandParameter{Binding SelectedProduct} /// ViewModel中 [ObservableProperty] private AsyncRelayCommandobject _saveEntityCommand; public MainViewModel() { SaveEntityCommand new AsyncRelayCommandobject(SaveEntityAsync); } private async Task SaveEntityAsync(object entity, CancellationToken token) { if (entity null) return; // 反射判断实体类型动态获取对应仓储 var entityType entity.GetType(); var repositoryType typeof(IRepository).MakeGenericType(entityType); var repository _serviceProvider.GetService(repositoryType) as dynamic; if (repository ! null) { try { IsSaving true; // 动态调用InsertAsync var result await repository.InsertAsync(entity); _logger.LogInformation({EntityType}保存成功ID{Result}, entityType.Name, result); } finally { IsSaving false; } } }参数说明AsyncRelayCommandobject接受任意对象但实际调用时需确保该对象类型已注册对应仓储如IRepositoryCustomer。dynamic调用避免泛型反射的复杂性性能损失可忽略WPF非高频场景。此方案牺牲了编译时类型检查换来高度复用适合中大型项目。4. 避坑WPFMVVMSqlSugar组合开发的5个高频翻车点WPF桌面应用的调试环境比Web复杂得多很多问题在Debug模式下不显在Release或客户机器上突然爆发。以下是我在3个量产项目中踩过的坑按发生频率排序4.1 现象程序运行几分钟后报“Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool”原因SqlSugar连接池耗尽。根本原因是IsAutoCloseConnection false默认值且未手动调用Ado.Close()。WPF无请求生命周期连接一旦打开就一直占着。解决在ConnectionConfig中强制设IsAutoCloseConnection true并确保所有仓储方法都用async/await——SqlSugar的异步方法内部会自动关闭连接。禁止在同步方法里混用Ado.UseConnection()。4.2 现象DataGrid绑定List 后修改集合元素属性UI不刷新原因ListT实现的是INotifyCollectionChanged但不实现INotifyPropertyChanged。当集合内对象属性变化时DataGrid无法感知。解决让实体类继承ObservableObjectCommunityToolkit.Mvvm提供或手动实现INotifyPropertyChanged。例如public partial class Order : ObservableObject { [ObservableProperty] private string _customerName; [ObservableProperty] private decimal _amount; }4.3 现象ViewModel中调用await _repository.GetListAsync()后UI线程卡死1秒原因SqlSugar的Ado.UseTran().Ado.UseConnection()在某些驱动下如SQL Server Native Client会阻塞UI线程尤其在首次连接时。解决在App启动时预热连接池。在App.OnStartup中添加// 预热连接执行一条空查询 await Task.Run(async () { using var client new SqlSugarClient(config); await client.Ado.UseConnection().Ado.GetDataTableAsync(SELECT 1); });4.4 现象切换数据库如从SQL Server改为SQLite后InsertAsync报“no such table”原因SqlSugar默认不自动建表。SQLite需要手动调用CodeFirst.InitTables且表名映射规则不同SQLite默认小写SQL Server保留大小写。解决在仓储构造函数中加入建表逻辑并统一表名策略_client.CodeFirst.InitTablesT(); // 自动建表 _client.EntityMaintenance.SetTableName(typeof(T), typeof(T).Name); // 强制用类名作表名4.5 现象AsyncRelayCommand执行时CanExecute不自动刷新按钮始终禁用原因CanExecute方法依赖的属性如CurrentOrder.CustomerName未正确触发PropertyChanged。常见于① 属性用string.Empty初始化但未调用OnPropertyChanged②CurrentOrder本身不是ObservableObject子类。解决用[ObservableProperty]生成器替代手写属性或确保所有绑定属性都调用OnPropertyChanged()。调试时在CanExecute方法里打日志确认是否被调用。5. 进阶技巧用SqlSugar的AOP日志Serilog实现可追溯的操作审计生产环境中光有Console日志远远不够。我们需要把每一次数据库操作谁、何时、执行了什么SQL、耗时多久、影响几行结构化记录便于排查问题和满足合规审计。SqlSugar内置AOP日志但输出格式简陋Serilog支持结构化日志和多种输出目标文件、Elasticsearch、Seq。二者结合能低成本实现企业级审计能力。5.1 配置SqlSugar AOP日志为Serilog事件源SqlSugar的LogEvent委托只提供原始SQL字符串和类型我们需要提取参数、计算耗时、关联用户上下文。关键技巧用Stopwatch包装SQL执行用LogContext.PushProperty注入业务上下文public class SqlSugarLogger { private static readonly ILogger _logger Log.Logger.ForContextSqlSugarLogger(); public static void ConfigureSqlSugarLogging(ISqlSugarClient client, string userId unknown) { client.Ado.IsEnableLogEvent true; client.Ado.LogEvent (sql, type) { var stopwatch Stopwatch.StartNew(); // 执行前推送上下文 using (LogContext.PushProperty(UserId, userId)) using (LogContext.PushProperty(SqlType, type.ToString())) { try { // 注意此处不能await因为LogEvent是同步回调 // 我们只记录日志不干预SQL执行 _logger.Information(SQL执行开始: {Sql}, sql); } catch (Exception ex) { _logger.Error(ex, 日志记录失败); } } }; // 替换Ado.UseTran()为带日志的包装器 var originalAdo client.Ado; client.Ado new CustomAdo(originalAdo, userId); } } // 自定义Ado包装器拦截所有执行方法 public class CustomAdo : IAdo { private readonly IAdo _innerAdo; private readonly string _userId; public CustomAdo(IAdo innerAdo, string userId) { _innerAdo innerAdo; _userId userId; } public async TaskDataTable GetDataTableAsync(string sql, object parameters null) { var stopwatch Stopwatch.StartNew(); try { var result await _innerAdo.GetDataTableAsync(sql, parameters); LogExecution(sql, stopwatch.Elapsed, result.Rows.Count); return result; } catch (Exception ex) { LogExecution(sql, stopwatch.Elapsed, -1, ex); throw; } } private void LogExecution(string sql, TimeSpan duration, int rowsAffected, Exception ex null) { var logLevel ex null ? LogEventLevel.Information : LogEventLevel.Error; var logMessage ex null ? $SQL执行完成: {sql.Substring(0, Math.Min(100, sql.Length))}... 耗时{duration.TotalMilliseconds:F2}ms 影响{rowsAffected}行 : $SQL执行失败: {sql.Substring(0, Math.Min(100, sql.Length))}... 耗时{duration.TotalMilliseconds:F2}ms 错误:{ex.Message}; using (LogContext.PushProperty(UserId, _userId)) using (LogContext.PushProperty(DurationMs, duration.TotalMilliseconds)) using (LogContext.PushProperty(RowsAffected, rowsAffected)) { Log.Logger.Write(logLevel, logMessage); } } }5.2 在ViewModel中注入当前用户ID实现操作溯源WPF无HttpContext用户ID需在登录后存入全局状态。我们用IOptionsMonitor管理// 登录成功后 var userOptions _serviceProvider.GetRequiredServiceIOptionsMonitorUserOptions(); userOptions.CurrentValue.UserId loginResult.UserId; // ViewModel中获取 public class MainViewModel : ObservableObject { private readonly UserOptions _userOptions; public MainViewModel(IOptionsMonitorUserOptions userOptionsMonitor) { _userOptions userOptionsMonitor.CurrentValue; // 初始化SqlSugar时传入_userOptions.UserId SqlSugarLogger.ConfigureSqlSugarLogging(_client, _userOptions.UserId); } }5.3 Serilog配置输出到JSON文件控制台支持按日志级别过滤// Program.cs 或 App.xaml.cs Log.Logger new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console(outputTemplate: [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}) .WriteTo.File( path: logs/log-.json, rollingInterval: RollingInterval.Day, outputTemplate: {Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:j}{NewLine}{Exception}, retainedFileCountLimit: 30) .CreateLogger();效果示例JSON日志片段{ Timestamp: 2024-06-15T14:22:33.12308:00, Level: Information, Message: SQL执行完成: INSERT INTO [Order] ([CustomerName],[Amount]) VALUES (p0,p1)... 耗时12.34ms 影响1行, UserId: U1001, DurationMs: 12.34, RowsAffected: 1 }我的习惯上线前必开AOP日志但只记录Information及以上日常开发用Debug级别看参数绑定审计时导出JSON用Kibana分析慢SQL。这套组合拳让我在客户现场3分钟定位90%的数据问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表