从零搭建半导体设备上位机(二):Prism + DryIoc,五个工程与区域导航怎么串起来

发布时间:2026/10/26 10:21:17
从零搭建半导体设备上位机(二):Prism + DryIoc,五个工程与区域导航怎么串起来 Prism DryIoc五个工程与区域导航怎么串起来这是「从零搭建半导体设备上位机」系列第2篇。上一篇定了三层架构这一篇把骨架落地五个工程怎么划边界、App 启动时到底按什么顺序跑、区域导航怎么组织以及一个 526 行的App.xaml.cs里藏着的三个坑。一个方法写了两百行项目第一次交给我 review 的时候我打开App.xaml.cs滚了十几屏全是RegisterForNavigation。数了一下六十多行注册中间夹着二十多行被注释掉的注册还有几行是同一个页面注册了两遍。当时的直觉是这文件该拆。但真去拆的时候发现拆不动——不是代码写坏了是注册这件事天然就该集中在一个地方。Prism 的RegisterTypes是容器唯一的配置入口把它拆到各个模块里你就再也说不清某个 View 到底注册没注册。所以后来没拆而是给它定了三条规矩注册必须分组、必须有注释说明、注释掉的必须删。这一篇讲的就是这三条规矩背后的东西——五个工程怎么分、导航怎么走、什么该单例什么该瞬时。一、五个工程怎么划边界先看引用关系这是硬约束写代码时绕不过去工程引用谁职责RoboCell.Core无MVVM 基类、通用命令、可复用的 ViewModel 基类RoboCell.SharedCore领域模型、枚举、DTO、扩展方法RoboCell.DbManageSharedDbContext、数据访问RoboCell.UICore、Shared、DbManageApp、View、ViewModel、用户控件RoboCell.UnitTestsCore、Shared、UI测试引用是一条单向链Core ← Shared ← DbManage ← UI。没有循环也没有跨层。为什么 Shared 引用 Core而不是反过来因为 Core 里放的是MVVM 基类它必须对领域模型一无所知。一开始我把BaseContainer带INotifyPropertyChanged的基类放在了 Shared 里结果 Core 里想写一个通用的ViewModelBase就得反过来引用 Shared立刻成环。挪到 Core 之后干净了Core 只依赖CommunityToolkit.Mvvm和Prism.Wpf不知道什么叫腔体、什么叫配方。Shared 才认识这些词。这条规则的判据很简单如果这个类型出现在业务名词表里它就不该在 Core。DbManage 为什么只依赖 Shared数据访问层只认识模型不认识界面。这条守住了后面做单元测试才有可能——测试可以直接 new 一个 DbContext不用拉起整个 WPF 应用。UnitTests 为什么敢引用 UI这是唯一看起来不合规的引用但它是故意的。工控上位机里大量逻辑长在 ViewModel 上上一篇提过业务层塌陷了。要测这些逻辑测试工程必须能拿到 ViewModel 类型。引用 UI 工程是最省事的办法。代价是测试会连带编译整个 UI——219 个 XAML 一起编慢。但比起为了可测性重构一遍这个代价当时是划算的。如果重来一次我会先划出 Service 层再写测试。这一点在第 21-23 篇讲业务核心时会正面处理。二、启动顺序App 里的五个重写点Prism 应用的启动不是new 一个 Window 就完事它有一套固定顺序。搞不清这个顺序会出现容器里没注册Region 还没创建就去导航这类玄学问题。顺序是这样的1.OnStartup—— 单实例锁IMutexnewMutex(true,RoboCell_Run_App);if(IMutex.WaitOne(0,false)){base.OnStartup(e);}工控上位机必须有这个。双击两次图标开出两个实例两个实例同时连 PLC写入会打架——一个在写开阀另一个在写关阀PLC 那边收到的顺序完全不可控。Mutex用命名互斥量名字带应用标识跨会话也能拦住。第二个实例直接退出不给任何提示——现场操作员不需要知道为什么点不开他只需要知道点一次能开。2.CreateShell—— 决定哪个窗口当壳protectedoverrideWindowCreateShell(){returnContainer.ResolveMainWindow();}这个方法当时被当成调试开关用了。上面压着五行注释//return Container.ResolveDeviceTestView(); // 用户控件测试//return Container.ResolveChamberTest(); // 单腔体测试//return Container.ResolveWaferInfoDisplay(); // 晶圆信息展示//return Container.ResolveMainTestWindow(); // 开发测试容器returnContainer.ResolveMainWindow();// 正式入口改一行、F5就能单独调某个界面不用每次都走完整登录和初始化流程。这个习惯一直保留到最后——它是这套代码里性价比最高的调试手段。代价是这五行注释谁也不敢删。后来约定正式发布前统一检查只留最后一行。3.RegisterTypes—— 容器配置这是那个两百行的方法。分三段导航注册RegisterForNavigationView, ViewModel()决定哪些页面可以被导航到。单例注册RegisterSingletonT()决定哪些对象全局只有一份。工厂注册RegisterSingletonT(() instance)用于已经在外面 new 好了只是让容器认识它。4.OnInitialized—— 壳创建之后这时候 Shell 已经显示Region 已经就位可以安全地做首次导航。把首次导航写在RegisterTypes里是错的——那时 Region 还不存在。5.OnExit—— 释放关 Mutex、关 ADS 连接、flush 日志。顺序不能反先断连接再关日志否则最后一批日志写不进去。三、区域导航8 个 Region 怎么组织Prism 的 Region 是可以往里塞视图的占位符。这个项目用了 8 个Region 常量用途MainViewRegionName主内容区几乎所有页面都导航到这里CalibrationViewRegionName标定页内的子区域LogRecordViewRegionName日志页内的子区域ParameterSettingViewRegionName参数设置页内的子区域RecipeOrSequenceViewRegionName配方/流程页内的子区域RecipeViewRegionName配方管理页内的子区域DeviceContentRegion设备测试页的内容区唯一一个硬编码的用法是嵌套的主窗口里放一个MainViewRegionName导航到参数设置页参数设置页自己内部又有一个ParameterSettingViewRegionName再往里导航腔体A参数“腔体B参数”。这样每个页面都能独立演进主窗口不用知道下面有几层。常量类是个好东西但只做了一半Region 名放在PrismManager静态类里用{x:Static}在 XAML 里引用ContentControlprism:RegionManager.RegionName{x:Static Extensions:PrismManager.MainViewRegionName}/好处是改名只改一处、编译期就能发现拼错。问题是八个区域里有七个用了常量剩下那个硬编码ContentControlprism:RegionManager.RegionNameDeviceContentRegion/后果很实在后来想统一给区域名加前缀全局搜索RegionName的时候漏了这一个导致那个页面导航进去一片空白查了半小时。规则要么全用要么别用。一半一半比全硬编码更危险因为你以为有保护。四、注册策略什么时候单例什么时候瞬时Prism 默认的RegisterForNavigation是每次导航新建实例。这对大部分页面是对的——每次进参数页都想要一份干净的初始状态。但主流程那个 ViewModel 是例外它被显式注册成了单例containerRegistry.RegisterSingletonProcessViewModel();containerRegistry.RegisterForNavigationProcessView,ProcessViewModel();两行一起写效果是导航到ProcessView时不会新建 ViewModel而是复用同一个。为什么因为这个 ViewModel 持有设备当前状态——当前在哪个腔、走到第几步、报警列表是什么。如果每次导航都新建切走再切回来状态全丢界面显示空闲而设备实际正在跑工艺。这在现场是要出事的。代价是它的生命周期和进程一样长里面任何一个订阅没退订就会一直挂着。这就是第 27 篇要讲的资源释放问题的来源之一。工厂注册让容器认识已经存在的对象ADS 连接和配方名集合是典型的进程级单例但必须在启动时才创建containerRegistry.RegisterSingletonAdsHelper(()gAdsHelper);containerRegistry.RegisterSingletonRecipeNames(()gRecipeNames);gAdsHelper在OnStartup里根据配置文件创建创建完再让容器登记。这样 ViewModel 构造函数的参数里可以直接写AdsHelper ads容器能注入而对象本身的生命周期仍由启动流程控制。别在 ViewModel 里new AdsHelper()。一旦这么写容器就管不住它了测试也没法替换。踩坑记录坑一重复注册不报错最后一次生效RegisterTypes里有两个页面各注册了两遍containerRegistry.RegisterForNavigationPageCalibration,PageCalibrationViewModel();// ... 十几行之后containerRegistry.RegisterForNavigationPageCalibration,PageCalibrationViewModel();Prism DryIoc 对重复注册不抛异常后一次覆盖前一次。表面看没影响直到有一次我在中间那段改了 ViewModel 的构造参数改的是一处生效的是另一处调试器里断点死活不进。排查方式很土把RegisterTypes里所有注册行提取出来排序肉眼找重复。后来写了个小脚本做这件事——注册超过五十行之后去重必须自动化。坑二单例 ViewModel 导航状态会跨页面残留主 ViewModel 是单例于是出现过一个诡异现象从参数设置页返回主页面参数页上勾选的某个调试开关还在生效。原因不是参数页没清是主 ViewModel 单例里缓存了一份参数页的状态。导航回来时它没重新初始化。修法不是改成瞬时那样会丢设备状态而是在OnNavigatedTo里显式重置界面相关字段。单例对象必须自己管好哪些字段是设备状态保留、哪些是界面状态重置。这条经验后来写进了团队的 Code Review 清单看到RegisterSingletonXxxViewModel就问一句它的字段分不分设备态和界面态。坑三注释掉的注册堆成山RegisterTypes里有二十多行被注释的注册横跨两年。危险不在于它们占地方在于它们会骗人。有一次我以为某个页面没注册加了一行结果那行注释其实是暂时不用加回去之后导航到了旧界面功能对不上白查半天。后来定规矩注释掉的注册必须带原因和日期超过一个迭代没恢复就删。删掉之后文件短了一百多行可读性立刻不一样。本篇小结五个工程的引用是一条单向链Core 不认识业务名词DbManage 不认识界面。UnitTests 引用 UI 是妥协代价是编译慢。启动顺序是硬约束OnStartup→CreateShell→RegisterTypes→OnInitialized。首次导航只能写在最后一步之后。Region 要么全用常量要么别用一半一半比全硬编码更容易出事。单例 ViewModel 只给持有设备状态的那个并且必须自己区分设备态和界面态。下一篇解决一个上一篇留下的悬念Prism 和 CommunityToolkit.Mvvm 两套 MVVM 框架在同一个项目里混用到底哪些用源生成器、哪些手写边界在哪。下期预告第3篇CommunityToolkit.Mvvm 与 Prism 混用哪些用源生成器哪些手写

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询