
结合代码整理一下模块化思想也是对WPF客户端的 Prism 模块化实现流程和核心思路进行一遍完整梳理。核心可以概括为一句话每个业务模块是一个独立的 WPF 类库 DLL编译后被拷贝到 Modules 目录主程序启动时用 DirectoryModuleCatalog 扫描加载并通过 [SystemMenu] 特性把模块里的 View 自动注册为导航目标最终通过 RegionManager 把 View 显示到主窗体的 ContentRegion 中。具体设计我们从代码开发的角度详细讲一讲1. 应用根PrismApplication 与启动入口1.1 App.xaml 继承 PrismApplication主程序不是普通的 Application 而是 PrismApplication 。App.xaml处理:prism:PrismApplication x:ClassFKH_DigitalWeighing.App xmlns:prismhttp://prismlibrary.com/ Application.Resources !-- 全局资源 -- /Application.Resources /prism:PrismApplication1.2 App.xaml.cs 里完成三大核心配置① ModuleCatalog从 Modules 目录加载模块protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog { ModulePath .\\Modules }; }这就是“目录式模块发现”。Prism 启动时会扫描 Modules 目录下所有 DLL找到实现了 IModule 的类并加载。② 注册类型扫描模块 DLL把带 [SystemMenu] 的 View 注册为导航目标protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigationLoginContentView(); RegisterViewNavigation(containerRegistry); // 核心扫描模块 View RegisterAssemblyTypes(containerRegistry); // 自动注册 BLL 服务 containerRegistry.RegisterSingletonTcpClient(); containerRegistry.RegisterSingletonHttpClientHelper(); containerRegistry.RegisterSingletonBackgroundTask(); }RegisterViewNavigation 是项目自己写的“菜单/视图发现”逻辑private void RegisterViewNavigation(IContainerRegistry containerRegistry) { string[] files Directory.GetFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Modules)); foreach (string path in files) { Type[] types Assembly.LoadFile(path).GetTypes(); foreach (Type type in types) { if (Attribute.GetCustomAttribute(type, typeof(SystemMenuAttribute)) is SystemMenuAttribute systemMenuAttribute) { // 注册为导航目标导航名就是 View 类型名 containerRegistry.RegisterForNavigation(type, type.Name); // 同时生成系统菜单 MenuList.SystemMenuList.Add(new SystemMenu { MenuName systemMenuAttribute.MenuName, Order systemMenuAttribute.MenuOrder, ViewName systemMenuAttribute.ViewName, MenuImageUri ... ParentMenuName systemMenuAttribute.ParentMenuName }); } } } }关键思路 菜单不是写死在主程序里的而是由每个模块通过 [SystemMenu] 特性“自描述”出来的。③ 自定义 ViewModel 定位约定protected override void ConfigureViewModelLocator() { base.ConfigureViewModelLocator(); ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(viewType { string fullName viewType.FullName; fullName fullName.Replace(.Views., .ViewModels.); string arg fullName.EndsWith(View) ? Model : ViewModel; string vmName ${fullName}{arg}; return viewType.Assembly.GetType(vmName, throwOnError: true); }); }例如 WeighManageModule.Views.WeighManageView 会被解析到 WeighManageModule.ViewModels.WeighManageViewModel 。④ 自定义 RegionAdapter为 TabControl 准备的扩展protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) { base.ConfigureRegionAdapterMappings(regionAdapterMappings); regionAdapterMappings.RegisterMapping(typeof(TabControl), Container.ResolveMenuTabControlRegionAdapter()); }这个 MenuTabControlRegionAdapter 后面会讲到它是把 TabControl 当作 Region 时的自定义适配器。不过当前主窗体用的是 ContentControl 所以它更像是一个预留扩展。2. 模块的物理组织一个模块 一个 DLL以 WeighManageModule 为例。2.1 项目文件WeighManageModule.csprojProject SdkMicrosoft.NET.Sdk PropertyGroup TargetFrameworknetcoreapp3.1/TargetFramework UseWPFtrue/UseWPF UseWindowsFormstrue/UseWindowsForms /PropertyGroup Target NamePostBuild AfterTargetsPostBuildEvent Exec Commandxcopy quot;$(TargetPath)quot; quot;$(SolutionDir)$(SolutionName)\$(OutDir)Modules\quot; /Y /S / /Target /Project编译后自动把 DLL 拷贝到主程序的 Modules 目录保证 DirectoryModuleCatalog 能发现它。2.2 模块入口实现 IModuleWeighManageModule.cs反编译public class WeighManageModule : IModule { public void RegisterTypes(IContainerRegistry containerRegistry) { } public void OnInitialized(IContainerProvider containerProvider) { } }这个类当前是空的但它有两个作用1. 告诉 Prism 这是一个模块 DirectoryModuleCatalog 会加载该程序集。2. 预留扩展点 如果模块需要注册自己的服务或做初始化可以写在这里。 项目中的服务注册和菜单发现实际上是由主程序 App.cs 统一扫描完成的所以模块入口可以保持简洁。3. 视图与菜单的自描述[SystemMenu] 特性每个模块的 View 类上直接打 [SystemMenu] 特性描述它在左侧菜单中应该怎么显示。SystemMenuAttribute.cs反编译public class SystemMenuAttribute : System.Attribute { public string MenuName { get; set; } public int MenuOrder { get; set; } public string ViewName { get; set; } public string MenuImageUri { get; set; } public string ParentMenuName { get; set; } }WeighManageView.cs反编译[SystemMenu( MenuName 称重管理, MenuOrder 1, ViewName WeighManageView, MenuImageUri pack://application:,,,/Client.Controls;component/Images/导航-磅单.svg)] public class WeighManageView : UserControl, IComponentConnector { ... }这样做的好处 - 新增一个业务模块时只需要在模块里新建一个 View 并打上特性编译后放到 Modules 目录即可。- 主程序不需要引用模块项目也不需要修改主程序代码。- 菜单文本、顺序、图标都由模块自己声明。4. 主窗体与 Region 导航4.1 MainWindow.xaml定义 RegionMainWindow.xamlBorder x:NameRegionBorder Grid.Row1 Grid.RowSpan2 Grid.Column1 ContentControl prism:RegionManager.RegionName{x:Static static:RegionNames.ContentRegion} / /BorderRegionNames.cs反编译public class RegionNames { public const string ContentRegion ContentRegion; public const string SystemManageRegion SystemManageRegion; public const string LoginRegion LoginRegion; public const string IntelligentCardRegion IntelligentCardRegion; public const string PoundManagerRegion PoundManagerRegion; }注意 MainWindow.xaml 里有一段被注释掉的 TabControl Region说明项目早期或未来可能用 TabControl 作为 Region因此保留了 MenuTabControlRegionAdapter 。4.2 菜单绑定与导航命令左侧菜单是一个 ItemsControl 绑定到 MainWindowViewModel.MenuCollection ItemsControl ItemsSource{Binding MenuCollection} ItemsControl.ItemTemplate DataTemplate RadioButton Content{Binding MenuName} IsChecked{Binding IsSelected} Tag{Binding ViewName} i:Interaction.Triggers i:EventTrigger EventNameChecked prism:InvokeCommandAction Command{Binding DataContext.MenuSelectedCommand, RelativeSource{RelativeSource ModeFindAncestor, AncestorTypeItemsControl}} CommandParameter{Binding} / /i:EventTrigger /i:Interaction.Triggers /RadioButton /DataTemplate /ItemsControl.ItemTemplate /ItemsControl4.3 MainWindowViewModel真正的导航逻辑MainWindowViewModel.cs反编译初始化时导航到第一个菜单private async void ExecuteLoadCommand(object obj) { _mainWindow obj as MainWindow; _regionManager.RequestNavigate(ContentRegion, MenuCollection.FirstOrDefault()?.ViewName); ... }菜单选中时导航private void ExecuteMenuSelectedCommand(object parameter) { if (parameter is SystemMenu systemMenu) { _regionManager.RequestNavigate(ContentRegion, systemMenu.ViewName); } }整个导航链路 左侧菜单点击 → MenuSelectedCommand → _regionManager.RequestNavigate(ContentRegion, WeighManageView) ↓ 因为之前 App.RegisterViewNavigation 已经执行过 containerRegistry.RegisterForNavigationWeighManageView(WeighManageView) ↓ Prism 自动创建 WeighManageView 并把它注入到 ContentRegion5. MVVM 与 ViewModel 自动绑定5.1 View 上开启自动绑定WeighManageView.xamlUserControl x:ClassWeighManageModule.Views.WeighManageView xmlns:prismhttp://prismlibrary.com/ prism:ViewModelLocator.AutoWireViewModelTrue ... /UserControl5.2 ViewModel 定位结合 App.cs 里的自定义约定Prism 会自动把 WeighManageView 解析到 WeighManageModule.ViewModels.WeighManageViewModel 。5.3 ViewModel 的依赖注入与生命周期WeighManageViewModel.cs反编译public class WeighManageViewModel : BindableBase, IRegionMemberLifetime, INavigationAware { public bool KeepAlive true; public WeighManageViewModel(IEventAggregator ea, IContainerExtension container, IMapper mapper) { _ea ea; _container container; _mapper mapper; // 订阅车牌识别、称重、自动称重等事件 ... } public void OnNavigatedTo(NavigationContext navigationContext) { } public bool IsNavigationTarget(NavigationContext navigationContext) true; public void OnNavigatedFrom(NavigationContext navigationContext) { } }- 构造函数里注入 IEventAggregator 、 IContainerExtension 、 IMapper 说明模块内部大量使用了 事件聚合器 和 容器服务 。- 实现 IRegionMemberLifetime 并设置 KeepAlive true 表示导航离开后 View/ViewModel 不被销毁适合称重这种需要保持状态的业务场景。- 实现 INavigationAware 可以接收导航参数当前未使用但留了扩展点。6. 自定义 RegionAdapterMenuTabControlRegionAdapter项目里自己实现了一个 TabControl 的 RegionAdapterMenuTabControlRegionAdapter.cspublic class MenuTabControlRegionAdapter : RegionAdapterBaseTabControl { public MenuTabControlRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory) { } protected override void Adapt(IRegion region, TabControl regionTarget) { region.Views.CollectionChanged (sender, e) OnViewsCollectionChanged(sender, e, region, regionTarget); } private void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, IRegion region, TabControl regionTarget) { if (e.Action ! NotifyCollectionChangedAction.Add) return; var menu MenuList.SystemMenuList .FirstOrDefault(m m.ViewName e.NewItems[0]?.GetType().Name); if (menu ! null) { var tabItem regionTarget.Items.CastTabItem() .FirstOrDefault(i i.Content null i.Header.ToString() menu.MenuName); if (tabItem ! null) tabItem.Content e.NewItems[0]; } } protected override IRegion CreateRegion() new AllActiveRegion(); }设计意图 - 当 Prism 把 View 注入到 TabControl Region 时不是直接生成新的 TabItem 而是根据 SystemMenuList 找到已经存在的菜单项 TabItem 把 View 填充到它的 Content 里。- CreateRegion 返回 AllActiveRegion 表示所有视图都保持活动状态。虽然当前 MainWindow.xaml 用的是 ContentControl Region一次只显示一个 View但这个 Adapter 展示了项目对 菜单驱动 Tab 页 的扩展思路。7. 完整流程图┌─────────────────────────────────────────────────────────────┐ │ FKH_DigitalWeighing.App (PrismApplication) │ │ 1. CreateModuleCatalog() → DirectoryModuleCatalog(./Modules) │ │ 2. RegisterTypes() │ │ ├── RegisterViewNavigation() 扫描 Modules/*.dll │ │ │ └── 找到 [SystemMenu] 的 View │ │ │ ├── containerRegistry.RegisterForNavigation(type, type.Name) │ │ │ └── MenuList.SystemMenuList.Add(...) │ │ └── RegisterAssemblyTypes() 自动注册 BLL 服务 │ │ 3. ConfigureViewModelLocator() 自定义 View→ViewModel 约定 │ │ 4. ConfigureRegionAdapterMappings() 注册 TabControl Adapter │ └──────────────────────┬───────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 主窗体 MainWindow / MainWindowViewModel │ │ 1. Loaded 后 RequestNavigate(ContentRegion, 首个菜单) │ │ 2. 左侧菜单 ItemsControl 绑定 MenuCollection │ │ 3. 点击菜单 → MenuSelectedCommand → │ │ _regionManager.RequestNavigate(ContentRegion, ViewName)│ └──────────────────────┬───────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ ContentRegion (ContentControl) │ │ Prism 根据注册创建对应 View并通过 AutoWireViewModel 绑定 │ │ ViewModel最终显示到主界面右侧内容区。 │ └─────────────────────────────────────────────────────────────┘8. 关键设计思想总结这套架构非常适合业务功能多、需要按需部署、硬件耦合重 的桌面系统每个模块独立开发、独立编译主程序只负责把它们组装起来。总结下面用流程图把刚才梳理的 Prism 模块化加载链路 串起来方便你一眼看清“从程序启动到页面显示”的完整路径。flowchart TD A[启动 FKH_DigitalWeighing.exe] -- B[App.OnStartup] B -- C[创建 Mutex 与授权/更新检测] C -- D[base.OnStartup 进入 Prism 初始化] D -- E[CreateModuleCatalog] E -- F[DirectoryModuleCatalogbr/扫描 .\\Modules 目录] F -- G[加载所有实现了 IModule 的 DLL] G -- H[WeighManageModule.dll 等模块程序集被加载] D -- I[RegisterTypes] I -- J[RegisterViewNavigation()br/遍历 Modules/*.dll] J -- K{类型是否带有br/[SystemMenu]?} K -- 是 -- L[containerRegistry.RegisterForNavigation(View, View.Name)] L -- M[MenuList.SystemMenuList.Add(菜单信息)] K -- 否 -- N[跳过] I -- O[RegisterAssemblyTypes()br/自动扫描 BLL.IService / BLL.ServiceImpl] O -- P[按命名约定注册 Service 接口与实现] D -- Q[ConfigureViewModelLocator] Q -- R[自定义约定Views.*View → ViewModels.*ViewModel] D -- S[CreateShell] S -- T[根据权限/用户状态创建 LoginWindow / MainWindow] T -- U[MainWindow 初始化] U -- V[RegionManager.SetRegionManagerbr/把 ContentRegion 赋给主窗体 ContentControl] U -- W[MainWindowViewModel.LoadMenu()br/从 SystemMenuList 生成 MenuCollection] W -- X[MainWindow Loaded] X -- Y[_regionManager.RequestNavigate(ContentRegion, 首个菜单.ViewName)] Y -- Z[Prism 根据导航名创建对应 View] Z -- AA[ViewModelLocator.AutoWireViewModelTruebr/自动绑定 ViewModel] AA -- AB[显示到 ContentRegion 中] W -- AC[用户点击左侧菜单] AC -- AD[MenuSelectedCommand 执行] AD -- AE[_regionManager.RequestNavigate(ContentRegion, systemMenu.ViewName)] AE -- Z关键节点对应代码一句话概括 启动时 Prism 把 Modules 目录下所有 DLL 当作模块加载主程序扫描 DLL 里的 [SystemMenu] 视图完成“菜单生成 导航注册”用户点击菜单后Prism 按约定自动创建 View 和 ViewModel 并注入 ContentRegion 。