接口1. 抽象功能子类实现 —— 语法层的「契约约束」这是接口最基础的语法作用核心是定规矩、统一规范。本质接口只描述「必须具备什么功能」完全不写「功能具体怎么实现」所有继承接口的子类都必须按接口约定的方法名、参数、返回值来写实现否则编译不通过。项目实例定义IAxisController接口约定必须有MoveToAsync、EmergencyStop、GetPosition三个方法真实硬件类EtherCatAxis实现接口内部调用EtherCAT驱动下发运动指令模拟调试类SimulatedAxis实现同一个接口内部用软件模拟走位、返回假坐标。核心价值强制同类型组件写法完全统一避免多人协作时出现一人一个方法名的混乱同时为多态打下基础——同一个接口可以有任意多个实现类对外表现一致。这是接口的“地基”没有这层契约约束后面的解耦、依赖注入都无从谈起。2. 接口对象 解耦 —— 设计层的「上下隔离」这是接口最核心的设计价值也是工业上位机必须用接口的根本原因。本质上层业务代码只用接口类型声明变量、调用方法完全不依赖、不感知具体的实现类把「业务逻辑怎么用」和「底层功能怎么做」彻底拆成两层中间靠接口做“隔离墙”。项目实例扫描业务里只写IAxisController _axis永远调用_axis.MoveToAsync()业务层完全不知道底层是EtherCAT真实轴、还是模拟轴也不需要知道——只要接口约定的方法能用就行。之前学的DbRepositoryFactory.Create()返回IDetectionRepository接口对象就是最典型的落地工厂负责选实现业务只管拿接口用。核心价值改底层不碰业务换硬件、换数据库只改实现类工厂/注册核心检测逻辑零改动风险极低无硬件也能开发调试开发阶段用模拟实现类不用接真实X光机、电机就能跑通完整检测流程职责清晰业务层只关心“做什么”底层实现才关心“怎么做”代码分层一目了然。这是接口的“灵魂”不用接口也能写代码但只有用接口才能真正做到上下层解耦支撑工业软件长期迭代维护。3. 面向接口编程 依赖注入 —— 工程层的「自动托管」这是接口解耦思想的工业化落地把“手动创建对象”升级为“框架自动托管”是中大型项目的标准做法。本质配合.NET DI依赖注入容器提前注册「接口→实现类」的映射关系比如AddTransientIAxisController, EtherCatAxis()上层业务只需要在构造函数里声明接口类型容器就会自动创建对应的实现类实例并注入进去全程不用手动new。项目实例// 注册映射App.xaml.csservices.AddTransientIDetectionRepository,SqliteDetectionRepository();// ViewModel 直接注入使用完全不用手动new仓储publicScanViewModel(IDetectionRepositoryrepo){_reporepo;}核心价值依赖自动传递如果实现类还依赖其他服务比如日志、配置容器会递归自动注入不用层层手动传参生命周期统一管控Transient/Singleton/Scoped 三种生命周期在注册时一键配置对象创建、销毁全由容器管理避免手动管理的内存泄漏、重复实例问题切换实现零侵入从真实版切换到模拟版只需要修改注册的那一行代码所有业务调用处完全不用动比手动写工厂类更简洁、更统一。这是接口的“放大器”DI容器把接口的解耦价值从“设计层面”落地成了“工程层面”的自动化管理是大型项目的标配。三者的递进关系第1点是语法基础没有契约约束就没有统一的调用规范第2点是核心思想接口的本质价值就是解耦这也是工业软件架构的核心诉求第3点是工程落地依赖注入把接口解耦从“手动写工厂”升级成了框架级的自动化方案。也正因为如此才有了之前的结论ViewModel、纯数据类通常不需要接口——它们没有多种实现、不需要替换既用不到第1点的多态契约也发挥不了第2点的解耦价值强行写接口属于过度设计。ILogger接口ILogger接口完整讲解适配X光工控上位机结合DI容器、分层架构一、核心定位ILogger是.NET 官方内置的日志抽象接口命名空间Microsoft.Extensions.Logging和你学的IDetectionRepository、ITemperatureController完全是同一套设计思想接口只约定「能写日志」的标准方法底层可以自由切换实现控制台日志、文件日志、数据库日志、第三方日志框架业务代码只依赖ILogger接口完全不感知日志具体写到哪里、用什么组件写彻底解耦业务与日志实现。工业项目标配作用记录设备运行状态、硬件故障、操作记录、异常堆栈是现场排查设备问题的核心依据。三、6个标准日志级别工控项目选型指南日志级别从低到高生产环境可配置过滤级别避免日志文件过大占满磁盘级别用途工控场景示例生产环境是否开启Trace最详细调试信息原始报文、逐行流程Modbus原始寄存器报文、EtherCAT周期数据❌ 关闭仅本地调试Debug调试细节开发排障用仿真模式参数、算法中间计算值❌ 生产默认关闭Information正常运行记录关键业务节点扫描开始/完成、用户登录、配方切换✅ 必开Warning警告不影响运行但需关注Modbus单次通讯超时、温度接近阈值✅ 必开Error错误功能异常业务中断PLC心跳断线、数据库写入失败、电机急停✅ 必开Critical致命错误程序崩溃、设备危险高压异常、运动轴撞限位、软件闪退✅ 必开四、标准用法DI注入 业务代码示例1. 推荐写法泛型ILoggerT工业项目统一用泛型版本自动携带「所属服务类」的分类标签排查问题时可直接筛选出扫描服务、PLC心跳各自的日志publicclassScanService{privatereadonlyILoggerScanService_logger;privatereadonlyITemperatureController_tempCtrl;// 构造函数注入DI容器自动传入日志实例publicScanService(ILoggerScanServicelogger,ITemperatureControllertempCtrl){_loggerlogger;_tempCtrltempCtrl;}publicasyncTaskStartScanAsync(){// 记录正常运行信息_logger.LogInformation(开始执行IGBT扫描检测);try{doubletempawait_tempCtrl.GetCurrentTempAsync();if(temp30){// 记录警告_logger.LogWarning(水温超限当前{Temp}℃禁止扫描启动,temp);thrownewException(水温过高);}}catch(Exceptionex){// 记录错误 异常堆栈_logger.LogError(ex,扫描启动失败水温检测异常);throw;}}}2. 常用方法速记// 信息日志支持占位符参数_logger.LogInformation(批次{BatchNo}检测完成空洞率{Rate:F2}%,batchNo,voidRate);// 警告日志_logger.LogWarning(Modbus第{Count}次重试通讯,retryCount);// 错误日志附带异常对象自动记录堆栈_logger.LogError(ex,PLC心跳连续超时设备急停);// 致命错误_logger.LogCritical(ex,X光高压异常程序强制退出);五、DI容器注册方式配合你的 App.xaml.cs.NET 提供官方扩展方法一行完成注册默认自带控制台、调试输出实现接入第三方框架Serilog/NLog后注册代码几乎不变varservicesnewServiceCollection();// 注册日志服务全局单例services.AddLogging(builder{// 开发环境输出到控制台、调试窗口builder.AddConsole();builder.AddDebug();// 生产环境接入Serilog写本地文件只改这里业务代码不动// builder.AddSerilog();// 配置生产环境默认只显示 Information 及以上级别builder.SetMinimumLevel(LogLevel.Information);});// 注册你的业务、硬件、仓储服务services.AddSingletonITemperatureController,ModbusTemperatureController();services.AddTransientScanService();一句话总结ILogger是.NET标准日志接口依托DI容器注入使用实现业务与日志实现解耦工业项目用来记录设备运行、故障、操作痕迹是现场问题排查的核心工具。(s,e)一、完整语法拆分完整代码模板控件.事件(s,e){// 事件触发后执行的业务逻辑};事件订阅运算符只有事件event能使用绑定处理方法对应之前学的public event Action(s, e)Lambda 表达式的形参列表仅用于.NET原生EventHandler类型事件按钮点击、窗口加载等控件路由事件s sender事件发送者对象比如点击的ToggleButton、触发事件的控件e EventArgs事件附带的参数鼠标位置、触发状态等系统内置数据Lambda箭头后面是事件触发时要运行的代码块。二、两种事件参数格式严格区分1. WPF控件原生事件Click/Loaded/MouseEnter→ 固定(s,e)这类事件底层是标准EventHandler签名强制要求2个参数必须写(s,e)。实战示例ToggleButton点击切换仿真模式// 按钮点击事件标准 (s,e) 写法SimToggle.Click(s,e){// s 就是触发点击的按钮控件ToggleButtonbtnsasToggleButton;// 切换仿真开关状态_viewModel.IsSimModebtn.IsChecked.Value;};2. 你项目自定义硬件事件event ActionPLC心跳/急停→不能写 (s,e)根据Action有无参数分两种写法① 无参自定义事件public event Action PlcDisconnectAlarm;没有任何参数直接写()_plcHeart.PlcDisconnectAlarm(){// PLC断线执行急停_axisCtrl.EmergencyStop();};② 单参数自定义事件public event Actionstring DeviceStatus;只有1个参数直接写(msg)_plc.DeviceStatusChanged(msg){Logger.WriteLog($设备状态{msg});};核心误区(s,e)只属于系统控件事件自己写的event Action强行写(s,e)会直接编译报错。三、两种订阅方式对比命名方法 vs 匿名(s,e)Lambda方式1命名方法推荐ViewModel长期订阅无内存泄漏可以用-在窗口关闭时取消订阅释放资源// 订阅_simBtn.ClickOnSimToggleClick;// 独立处理方法privatevoidOnSimToggleClick(objects,EventArgse){varbtnsasToggleButton;_vm.IsSimModebtn.IsChecked.Value;}// 窗口销毁时取消订阅GC可回收ViewModel_simBtn.Click-OnSimToggleClick;方式2匿名(s,e)Lambda临时简易逻辑致命缺陷匿名方法没有名称无法使用-取消订阅_simBtn.Click(s,e){_vm.IsSimMode((ToggleButton)s).IsChecked.Value;};⚠️ 工控软件重大隐患瞬时ViewModel、窗口关闭后事件依然持有ViewModel引用GC永远无法回收对象长期运行内存持续泄漏、软件卡顿。四、工控项目高频场景完整示例示例1DataGrid单元格鼠标悬浮原生MouseEnter事件(s,e)// 表格行悬浮高亮dataGrid.MouseEnter(s,e){// s 是DataGrid控件};示例2PLC心跳后台事件自定义无参Action不用s,e// 心跳丢失告警_plcHeart.PlcDisconnectAlarm(){// 后台线程必须切UI主线程才能弹窗Application.Current.Dispatcher.Invoke((){MessageBox.Show(PLC通讯断开设备已急停);});};一句话总结 (s,e){}是WPF控件原生点击/悬浮事件的匿名订阅写法s是触发控件、e是系统事件参数自定义Action硬件事件不能使用该格式且匿名Lambda无法取消订阅极易造成工控软件内存泄漏。Transient 瞬时生命周期一、核心定义1. Transient瞬时生命周期规则在.NET依赖注入(DI)容器中每次从容器获取/注入该类型时都会创建一个全新独立的实例容器不会缓存、复用旧对象对象使用完毕后无强引用则由GC回收。DI三大生命周期对比区分关键生命周期创建规则适用对象Transient 瞬时每次请求都 new 全新实例窗口ViewModel、临时弹窗页面Scoped 作用域同一个作用域内只创建1次跨作用域新建DbContext、仓储RepositorySingleton 单例全局仅创建1个程序全程复用硬件服务、日志、全局工厂DbRepositoryFactory2. 注册语句含义// App.xaml.cs DI服务注册代码services.AddTransientDetectionRecordViewModel();services.AddTransientLoginViewModel();services.AddTransientParameterEditViewModel();AddTransientT代表将视图模型注册为瞬时模式每次打开对应窗口都生成干净无残留的全新ViewModel。二、为什么ViewModel必须用Transient工业软件刚需反面风险注册为Singleton单例的致命问题如果把检测记录ViewModel注册成全局单例第一次打开检测窗口加载100条IGBT检测记录到DataGrid关闭窗口ViewModel实例仍被容器保留第二次打开窗口DataGrid直接显示上次残留的100条旧数据筛选条件、分页、空洞率参数全部保留多窗口同时打开时一个窗口修改参数会同步污染所有页面造成检测数据混淆、质量追溯出错。Transient 瞬时模式的核心优势天然数据隔离每打开一次窗口 全新ViewModel列表、输入框、状态标记全部重置无需手动写Clear()清空集合/属性从根源避免旧数据残留。多窗口互不干扰同时打开2份IGBT检测对比窗口两个ViewModel完全独立修改其中一个的筛选条件不会影响另一个页面。生命周期与视图同步窗口关闭后ViewModel无强引用自动被GC回收长期运行软件不会堆积内存。依赖自动隔离ViewModel构造注入的仓储IDetectionRepository、硬件IAxisController每次新建ViewModel都会注入独立作用域资源数据库、硬件状态互不串扰。三、完整实操流程DI容器Transient ViewModel步骤1App.xaml.cs 全局注册服务protectedoverridevoidOnStartup(StartupEventArgse){base.OnStartup(e);varserviceCollectionnewServiceCollection();// 1. 瞬时注册所有页面ViewModel核心serviceCollection.AddTransientDetectionRecordViewModel();serviceCollection.AddTransientLoginViewModel();serviceCollection.AddTransientScanParamViewModel();// 2. 仓储Scoped作用域生命周期适配EF DbContextserviceCollection.AddScopedIDetectionRepository,SqliteDetectionRepository();serviceCollection.AddScopedIPermissionRepository,SqlitePermissionRepo();// 3. 全局单例硬件、工厂serviceCollection.AddSingletonDbRepositoryFactory();serviceCollection.AddSingletonIAxisController,EtherCatAxis();_serviceProviderserviceCollection.BuildServiceProvider();// 启动主窗口瞬时VMvarmainVm_serviceProvider.GetRequiredServiceMainViewModel();newMainWindow(mainVm).Show();}步骤2窗口构造函数注入ViewModelpublicpartialclassDetectionRecordWindow:Window{// DI自动传入全新Transient实例publicDetectionRecordWindow(DetectionRecordViewModelvm){InitializeComponent();this.DataContextvm;}}步骤3业务代码打开窗口每次获取全新VM// 每次调用GetRequiredService都会新建一个ViewModelvarrecordVm_serviceProvider.GetRequiredServiceDetectionRecordViewModel();varrecordWinnewDetectionRecordWindow(recordVm);recordWin.Show();每次弹窗VM都是空白初始化状态不会携带上一次的检测记录、筛选参数。四、适用/不适用场景✅ 适合注册Transient的ViewModel临时弹窗登录窗口、配方编辑弹窗、单条IGBT复检详情页独立子窗口检测记录查询窗口、参数配置面板一次性操作视图单次扫描参数临时面板关闭后无需保留状态。❌ 不适合Transient的对象改用Scoped/SingletonIDetectionRepository、DbContext频繁瞬时创建会反复打开/关闭SQLite连接损耗磁盘IO统一用ScopedIAxisController运动轴、Modbus温控硬件服务硬件总线全局唯一注册SingletonDbRepositoryFactory数据库工厂、全局日志服务全局单例无需重复实例化。五、Transient ViewModel 配套注意事项避坑1. 事件订阅必须手动取消防内存泄漏ViewModel中订阅硬件event Action、全局异常事件时窗口关闭必须-取消订阅// ViewModel内部订阅急停事件_device.EmergencyStopEventOnEmergencyStop;// 窗口关闭时执行释放publicvoidDispose(){_device.EmergencyStopEvent-OnEmergencyStop;}Transient对象仅在无事件订阅、无UI强引用时才能被GC正常回收。2. 共享全局状态不要依赖ViewModel若多窗口需要共用设备状态如设备就绪、当前扫描批次不要共用ViewModel单独创建Singleton全局状态服务注入各个瞬时VM。3. 构造函数轻量化ViewModel构造不要执行重型初始化批量加载上万条检测记录把数据加载放到窗口加载完成异步方法中减少瞬时创建耗时。4. 异步操作强制使用await瞬时VM内部数据库查询repo.SaveAsync()、硬件移动axis.MoveToAsync()必须加await避免产生未观察Task异常单个窗口异步报错不会污染其他页面。六、串联你之前学过的知识点DAL仓储Scoped注入每个瞬时ViewModel都会注入独立仓储上下文数据库读写隔离避免实体追踪冲突接口对象解耦Transient VM构造仅依赖IDetectionRepository、IAxisController抽象接口真实/模拟实现可一键切换异步未观察异常每个瞬时VM的异步任务独立一个窗口数据库读写失败不会导致全软件闪退event Action广播事件每个VM独立订阅硬件告警事件窗口销毁释放订阅减少内存堆积INotifyPropertyChanged每个瞬时VM拥有独立属性变更通知多窗口界面刷新互不干扰。一句话总结AddTransientViewModel()就是让每个窗口都拿到全新干净、互不干扰的视图模型从生命周期层面杜绝界面数据残留、多窗口状态串扰是WPF工业上位机MVVM标准注册方案。