WPF Prism中的模块化详解
模块化基础首先我们了解一下插件这个概念插件(add-in也称为plug-in)是应用程序能够动态发现、加载和使用的单独编译过的组件。通常应将应用程序设计成能使用插件从而可在将来进行增强而不必进行任何修改、重新编译以及重新测试。插件还为针对特殊的市场或客户单独定制应用程序实例提供了灵活性。但使用插件模型最常见的原因是允许第三方开发人员扩展应用程序的功能。例如AdobePhotoshop中的插件提供了大量图片处理效果类似Camera Raw。Google Chrome中的插件提供了增强的Web 冲浪特性以及全新功能。对于这两种情况插件都是由第三方开发人员创建的。.NET提供了MAF和MEF两种框架可以实现插件化。MAFhttps://learn.microsoft.com/en-us/dotnet/desktop/wpf/app-development/wpf-add-ins-overviewMEFhttps://learn.microsoft.com/zh-cn/dotnet/framework/mef/Prism的模块化原理就类似插件模型。它也能够动态地发现、加载和使用单独编译的组件。在前面介绍Prism框架时提到模块化的作用。模块化拆分、隔离与复用核心目标都是降低应用耦合度提升可维护性。模块化以后1、每个模块负责特定功能如用户管理、订单管理实现 “高内聚、低耦合”便于团队协作、维护和迭代。2、模块之间可以做到低依赖减少直接引用通过依赖注入实现复用单个模块可独立编译、测试甚至在多个应用中复用。3、最终都能将多个独立模块整合到同一个主应用中形成完整的功能体系而非零散的组件。就拿医疗行业的软件来说它可能会划分如上的模块。不同的模块可以单独开发测试。最后再将它们一起”组装“到”外壳“上。对于一些基础模块通常会定义成公用模块。需要使用的子模块可以直接引用这些基础模块。注意这里我们的目的是做到模块间低依赖而不是完全没有依赖。但是Prism的模块化和插件化会有稍许区别Prism模块化模块化是 “核心架构手段”最终目的是 “大型应用的结构化拆分与管理”。Prism 希望在应用开发初期就将应用拆分为多个模块每个模块遵循统一的规范IModule主应用通过明确配置加载模块便于团队分工、版本控制和后期维护。简单来说Prism 的模块化是为了 “事前规划”。插件化最终目的是 “可扩展性”插件化。当主应用发布后无需修改主程序代码、无需重新编译只需将新的模块插件放入指定目录主应用就能自动识别并加载该模块实现功能扩展。简单说插件化是为了 “事后扩展”。模块化原则有些小伙伴可能对这个模块的划分还存在一些困惑。这里介绍 一下模块划分的原则1、高内聚一个模块只负责完成单一且明确的业务功能不包含无关逻辑。2、低耦合模块之间尽量减少直接依赖禁止跨模块直接引用View、ViewModel 或内部业务类。3、创建独立的基础设施类库CommonInfrastructure 项目该项目仅包含公共接口、枚举、实体DTO/POCO无业务逻辑。这个基础设备类库是模块间交互的核心模块加载基础原理在Prism中涉及两种模块化的加载一种是配置式一种是扫描式。配置式是显式配置也就是主动指定要加载的模块。扫描式是隐式配置也就是对目录进行扫描然后自动添加符合要求的模块。模块加载这里主要用到的是.NET的反射机制。假设我定义了一个IModule接口仅演示用所以未定义具体的接口函数1 public interface IModule 2 { 3 4 5 }然后我定义了一个类库LibA增加一个类ModuleAModule继承自IModule接口1 public class ModuleAModule : IModule 2 { 3 4 }再定义一个类库LibB增加一个类ModuleBModule1 public class ModuleBModule 2 { 3 4 }当我们去扫描这两个dll通过判断是否实现了IModule接口就可以判断是否是我们定义的模块。简易验证过程如下第一步通过Type.GetInterface(typeof(IModule).FullName)验证该类型是否实现了IModule。第二步验证该类型是否为class、是否非抽象type.IsClass !type.IsAbstract。示例代码如下1 public ListType FindAllIModuleTypes(string moduleDirectory) 2 { 3 var moduleTypes new ListType(); 4 var assemblyFiles Directory.GetFiles(moduleDirectory, *.dll, SearchOption.TopDirectoryOnly); 5 6 foreach (var assemblyFile in assemblyFiles) 7 { 8 try 9 { 10 // 1. 加载程序集 11 Assembly assembly Assembly.LoadFrom(assemblyFile); 12 13 // 2. 遍历程序集中的所有类型 14 foreach (Type type in assembly.GetTypes()) 15 { 16 // 3. 核心筛选识别 IModule 实现类 17 bool isValidModule typeof(IModule).IsAssignableFrom(type) // 实现 IModule 18 type.IsClass // 是类 19 !type.IsAbstract; // 非抽象 20 21 if (isValidModule) 22 { 23 moduleTypes.Add(type); 24 } 25 } 26 } 27 catch 28 { 29 // 跳过无法加载或无有效模块的程序集 30 continue; 31 } 32 } 33 34 return moduleTypes; 35 }Prism中的IModule接口首先我们了解一下IModule接口在Prism中可以被加载的模块都需要实现Prism.Modularity.IModule接口。IModule接口定义如下1 // 2 // 摘要: 3 // 为Prism应用程序中部署的模块定义契约。 4 public interface IModule 5 { 6 // 7 // 摘要: 8 // 用于向容器注册应用程序将使用的类型。 9 void RegisterTypes(IContainerRegistry containerRegistry); 10 11 // 12 // 摘要: 13 // 通知模块它已被初始化。 14 void OnInitialized(IContainerProvider containerProvider); 15 }IModule提供了两个接口函数RegisterTypes和OnInitialized可以把它理解为模块的Bootstrapper。如何创建Prism模块这里我们以一个简易的信息登记功能为例增加一个Register模块一个ViewList模块一个CommonModule公共模块。Register模块登记Employee信息ViewList模块查看登记的Employee列表CommonModule公共模块定义Employee及通信事件当在Register模块登记一个Employee后通过ViewModel通信将Employee添加到ViewList模块的列表中。整体框架如下1、定义公用模块CommonModule这个模块主要是用于定义Employee及通信事件。SelectEmployeeEvent.cs1 public class SelectEmployeeEvent : PubSubEventIEmployeeViewModel 2 { 3 }IEmployeeViewModel.cs1 public interface IEmployeeViewModel 2 { 3 int Id { get; set; } 4 5 string Name { get; set; } 6 7 string Phone { get; set; } 8 9 void XXX(); 10 }2、创建类库工程Module.Register3、增加一个模块类RegisterModule实现IModule接口注意模块类在命名时尽量以Module结尾而且放在项目的根目录下这样方便后期区分和查找RegisterModule.cs1 public class RegisterModule : Prism.Modularity.IModule 2 { 3 public void OnInitialized(IContainerProvider containerProvider) 4 { 5 6 } 7 8 public void RegisterTypes(IContainerRegistry containerRegistry) 9 { 10 11 } 12 }这样我们就拥有了一个可以被Prism识别的模块接下来我们对这个模块进行完善4、添加View和ViewModel注意类库工程在添加View时会找不到WPF项。需要修改项目工程文件添加UseWPF添加Views\Register.xaml这个页面用于登记1 Grid 2 Grid.RowDefinitions 3 RowDefinition/ 4 RowDefinition/ 5 RowDefinition/ 6 RowDefinition/ 7 /Grid.RowDefinitions 8 9 Grid.ColumnDefinitions 10 ColumnDefinition/ 11 ColumnDefinition/ 12 /Grid.ColumnDefinitions 13 14 Label ContentId HorizontalAlignmentRight Margin0,0,30,0 VerticalAlignmentCenter/Label 15 TextBox Grid.Row0 Grid.Column1 Margin40,0 VerticalAlignmentCenter Text{Binding EmployeeViewModel.Id}/TextBox 16 17 Label ContentName Grid.Row1 HorizontalAlignmentRight Margin0,0,30,0 VerticalAlignmentCenter/Label 18 TextBox Grid.Row1 Grid.Column1 Margin40,0 VerticalAlignmentCenter Text{Binding EmployeeViewModel.Name}/TextBox 19 20 Label ContentPhone Grid.Row2 HorizontalAlignmentRight Margin0,0,30,0 VerticalAlignmentCenter/Label 21 TextBox Grid.Row2 Grid.Column1 Margin40,0 VerticalAlignmentCenter Text{Binding EmployeeViewModel.Phone}/TextBox 22 23 Button Content登记 Width88 Height28 HorizontalAlignmentCenter VerticalAlignmentCenter Grid.Row3 Grid.ColumnSpan2 Command{Binding RegisterCommand}/Button 24 /Grid添加ViewModels\RegisterViewModel.cs1 public class RegisterViewModel :BindableBase 2 { 3 private IEventAggregator eventAggregator; 4 5 public DelegateCommand RegisterCommand { get; private set; } 6 7 //EmployeeViewModel的定义请到文末示例代码中查看 8 private EmployeeViewModel employeeViewModel new EmployeeViewModel(); 9 10 public EmployeeViewModel EmployeeViewModel 11 { 12 get this.employeeViewModel; 13 set this.SetProperty(ref employeeViewModel, value); 14 } 15 16 public RegisterViewModel(IEventAggregator eventAggregator) 17 { 18 RegisterCommand new DelegateCommand(Register); 19 20 this.eventAggregator eventAggregator; 21 } 22 23 private void Register() 24 { 25 //点击登记时发送消息到ViewList模块 26 this.eventAggregator.GetEventSelectEmployeeEvent().Publish(this.EmployeeViewModel); 27 } 28 }5、注册模块里的视图此时我们再回到RegisterModule类在RegisterTypes中注册View1 public class RegisterModule : Prism.Modularity.IModule 2 { 3 public void OnInitialized(IContainerProvider containerProvider) 4 { 5 6 } 7 8 public void RegisterTypes(IContainerRegistry containerRegistry) 9 { 10 //注册View的时候同步绑定ViewModel 11 containerRegistry.RegisterForNavigationRegisterView, RegisterViewModel(); 12 13 //也可以只注册View 14 //然后通过ViewModelLocator.AutoWireViewModel附加属性来自动绑定ViewModel 15 //containerRegistry.RegisterForNavigationRegisterView(); 16 } 17 }这样我们就拥有了一个完整的登记模块然后我们按照这个逻辑再添加ViewList模块ViewListModule.cs1 public class ViewListModule : IModule 2 { 3 public void OnInitialized(IContainerProvider containerProvider) 4 { 5 6 } 7 8 public void RegisterTypes(IContainerRegistry containerRegistry) 9 { 10 //注册视图 11 containerRegistry.RegisterForNavigationEmployeeListView, EmployeeListViewModel(); 12 } 13 }Views\EmployeeListView.xaml1 Grid 2 ListBox ItemsSource{Binding EmployeeList} 3 ListBox.ItemTemplate 4 DataTemplate 5 Grid 6 Grid.RowDefinitions 7 RowDefinition/ 8 RowDefinition/ 9 /Grid.RowDefinitions 10 11 TextBlock Text{Binding Name} HorizontalAlignmentLeft Margin10,0,0,0 FontWeightBold FontSize13 ForegroundBlue/TextBlock 12 DockPanel Grid.Row1 Margin0,5,0,0 13 TextBlock TextId: Margin10,0,5,0/TextBlock 14 TextBlock Text{Binding Id} Margin5,0,10,0/TextBlock 15 TextBlock TextPhone: Margin10,0,5,0/TextBlock 16 TextBlock Text{Binding Phone} FontStyleItalic/TextBlock 17 /DockPanel 18 /Grid 19 /DataTemplate 20 /ListBox.ItemTemplate 21 /ListBox 22 /GridViewModels\EmployeeListViewModel.cs1 public class EmployeeListViewModel : BindableBase 2 { 3 //列表 4 private ObservableCollectionIEmployeeViewModel employeeList new ObservableCollectionIEmployeeViewModel(); 5 6 public ObservableCollectionIEmployeeViewModel EmployeeList 7 { 8 get this.employeeList; 9 set this.SetProperty(ref employeeList, value); 10 } 11 12 public EmployeeListViewModel(IEventAggregator eventAggregator) 13 { 14 //创建登记界面发送数据订阅 15 eventAggregator.GetEventSelectEmployeeEvent().Subscribe(OnAddEmployee); 16 } 17 18 //接收从登记界面发送过来的数据 19 private void OnAddEmployee(IEmployeeViewModel model) 20 { 21 this.EmployeeList.Add(model); 22 } 23 }Prism模块加载流程Prism模块加载流程如下图所示这里我们了解一下IModuleCatalog接口和IModuleManager接口。IModuleCatalog接口IModuleCatalog是模块清单负责 “记录” 应用中有哪些模块、加载规则和依赖关系是静态的元数据存储1 /// summary 2 /// 这是ModuleManager的预期目录定义。ModuleCatalog包含应用程序可使用的模块信息。 3 /// 每个模块都在ModuleInfo类中进行了描述该类记录了模块的名称、类型和位置。 4 /// /summary 5 public interface IModuleCatalog 6 { 7 /// summary 8 /// 获取IModuleCatalog中的所有IModuleInfo类型 9 /// /summary 10 IEnumerableIModuleInfo Modules { get; } 11 12 IEnumerableIModuleInfo GetDependentModules(IModuleInfo moduleInfo); 13 14 IEnumerableIModuleInfo CompleteListWithDependencies(IEnumerableIModuleInfo modules); 15 16 /// summary 17 /// 初始化目录这可能会加载并验证模块。 18 /// /summary 19 void Initialize(); 20 21 /// summary 22 /// 将IModuleInfo添加到IModuleCatalog中 23 /// /summary 24 IModuleCatalog AddModule(IModuleInfo moduleInfo); 25 }它会在Bootstrapper初始化时进行初始化。过程如下所示1 public abstract class PrismApplicationBase : Application 2 { 3 protected override void OnStartup(StartupEventArgs e) 4 { 5 InitializeInternal(); 6 } 7 8 void InitializeInternal() 9 { 10 Initialize(); 11 } 12 13 protected virtual void Initialize() 14 { 15 _moduleCatalog CreateModuleCatalog(); 16 } 17 18 protected virtual IModuleCatalog CreateModuleCatalog() 19 { 20 return new ModuleCatalog(); 21 } 22 }IModuleManagerIModuleManager是模块加载器负责 “执行” 模块的加载和初始化支持立即加载和按需加载是动态的操作组件1 /// summary 2 /// Defines the interface for the service that will retrieve and initialize the applications modules. 3 /// /summary 4 public interface IModuleManager 5 { 6 /// summary 7 /// Gets all the see crefIModuleInfo/ classes that are in the see crefIModuleCatalog/. 8 /// /summary 9 IEnumerableIModuleInfo Modules { get; } 10 11 /// summary 12 /// Initializes the modules marked as see crefInitializationMode.WhenAvailable/ on the see crefIModuleCatalog/. 13 /// /summary 14 void Run(); 15 16 /// summary 17 /// Loads and initializes the module on the see crefIModuleCatalog/ with the name paramref namemoduleName/. 18 /// /summary 19 /// param namemoduleNameName of the module requested for initialization./param 20 void LoadModule(string moduleName); 21 22 /// summary 23 /// Raised repeatedly to provide progress as modules are downloaded. 24 /// /summary 25 event EventHandlerModuleDownloadProgressChangedEventArgs ModuleDownloadProgressChanged; 26 27 /// summary 28 /// Raised when a module is loaded or fails to load. 29 /// /summary 30 event EventHandlerLoadModuleCompletedEventArgs LoadModuleCompleted; 31 }IModuleManager也是在Bootstrapper初始化时进行初始化调用流程如下所示1 public abstract class PrismApplicationBase : Application 2 { 3 protected override void OnStartup(StartupEventArgs e) 4 { 5 InitializeInternal(); 6 } 7 8 void InitializeInternal() 9 { 10 Initialize(); 11 } 12 13 protected virtual void Initialize() 14 { 15 RegisterRequiredTypes(_containerExtension); 16 } 17 18 protected virtual void RegisterRequiredTypes(IContainerRegistry containerRegistry) 19 { 20 //注入IModuleManager单例 21 containerRegistry.RegisterSingletonIModuleManager, ModuleManager(); 22 } 23 }如何加载Prism模块经过上述步骤我们就创建好了两个Prism模块。接下来我们需要做的就是把它们加载到主窗口Shell然后一起工作。Prism提供了5种加载模块的方法1、代码加载2、目录扫描3、手动加载4、Xaml加载5、AppConfig加载这里的话只介绍前面三种方式因为就我目前实际开发情况来看后面两种方法用到的相对较少。感兴趣的可以参考下面的代码自行学习https://github.com/PrismLibrary/Prism-Samples-Wpf/tree/master/07-Modules-AppConfig(AppConfig加载)https://github.com/PrismLibrary/Prism-Samples-Wpf/tree/master/07-Modules-Xaml(Xaml加载)代码加载实现步骤如下1、在Shell工程上添加模块项目引用引用我们需要使用的Prism模块项目2、在Shell(MainWindow)的Bootstrapper中重写ConfigureModuleCatalog函数配置要加载的模块1 protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) 2 { 3 //添加模块引用 4 moduleCatalog.AddModuleRegisterModule(); 5 moduleCatalog.AddModuleViewListModule(); 6 }3、在Shell(MainWindow)的Bootstrapper中加载View1 protected override void OnInitialized() 2 { 3 base.OnInitialized(); 4 5 var regionManager this.Container.ResolveIRegionManager(); 6 7 regionManager.RegisterViewWithRegion(EditArea, typeof(RegisterView)); 8 regionManager.RegisterViewWithRegion(ListArea, typeof(EmployeeListView)); 9 }在前面我介绍介绍过RegisterViewWithRegion函数可以绑定默认视图。现在就等于将RegisterView绑定到EditArea区域将EmployeeListView绑定到ListArea区域。而RegisterView和EmployeeListView都分别来自于不同的模块。程序运行效果如下可以看到这些功能虽然不是在一个模块里定义的但最终可以被组装到一起实现一个完整的功能 。目录扫描目录扫描主要是通过指定一个目录然后Prism框架自动去扫描目录中符合要求的Prism模块。实现步骤如下1、将模块输出到固定位置例如我们将前面的两个模块输出到Modules目录下Modules目录项目输出路径2、配置目录加载1 public partial class App : PrismApplication 2 { 3 protected override Window CreateShell() 4 { 5 return Container.ResolveMainWindow(); 6 } 7 8 protected override void RegisterTypes(IContainerRegistry containerRegistry) 9 { 10 11 } 12 13 protected override IModuleCatalog CreateModuleCatalog() 14 { 15 //指定目录 16 //可以使用相对路径也可以使用绝对路径 17 //推荐相对路径 18 return new DirectoryModuleCatalog() { ModulePath .\Modules }; 19 } 20 }说明在功能模块明确的情况下推荐使用代码加载方式加载模块。有时候我们想动态扩展一些功能可以使用目录加载。例如我有一款设备在出厂时只有A型号后期可能会增加B、C型号或者更多。手动加载手动加载的整体流程和代码加载是一样的都是需要引用项目工程。在前面我们介绍了IModuleManager接口它会在Bootstrapper初始化时自动注入到容器中手动加载模块主要用到IModuleManager接口。实现步骤如下1、在Shell中增加一个按钮用于手动加载模块1 Grid 2 Grid.RowDefinitions 3 RowDefinition Height35/ 4 RowDefinition/ 5 /Grid.RowDefinitions 6 7 Grid Grid.Row0 8 Button Content加载模块 HorizontalAlignmentCenter VerticalAlignmentCenter Width88 Height28 Command{Binding ManualLoadModuleCommand}/ 9 /Grid 10 11 Grid Grid.Row1 12 Grid.ColumnDefinitions 13 ColumnDefinition/ 14 ColumnDefinition/ 15 /Grid.ColumnDefinitions 16 17 ContentControl prism:RegionManager.RegionNameEditArea/ContentControl 18 ContentControl prism:RegionManager.RegionNameListArea Grid.Column1/ContentControl 19 /Grid 20 /Grid2、在Shell工程上添加模块项目引用引用我们需要使用的Prism模块项目3、在Bootstrapper中重写ConfigureModuleCatalog函数添加模块到IModuleCatalog中1 public partial class App : PrismApplication 2 { 3 protected override Window CreateShell() 4 { 5 return Container.ResolveMainWindow(); 6 } 7 8 protected override void RegisterTypes(IContainerRegistry containerRegistry) 9 { 10 11 } 12 13 /// summary 14 /// 配置模块 15 /// /summary 16 /// param namemoduleCatalog/param 17 protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) 18 { 19 var moduleRegisterType typeof(RegisterModule); 20 var moduleViewListType typeof(ViewListModule); 21 moduleCatalog.AddModule(new ModuleInfo() 22 { 23 ModuleName moduleRegisterType.Name, 24 ModuleType moduleRegisterType.AssemblyQualifiedName, 25 InitializationMode InitializationMode.OnDemand //需要时再加载 26 }); 27 moduleCatalog.AddModule(new ModuleInfo() 28 { 29 ModuleName moduleViewListType.Name, 30 ModuleType moduleViewListType.AssemblyQualifiedName, 31 InitializationMode InitializationMode.OnDemand //需要时再加载 32 }); 33 } 34 }4、在需要加载的地方使用IModuleManager接口的LoadModule函数手动执行加载这里我们在ViewModel中进行加载1 /// summary 2 /// 手动加载模块 3 /// /summary 4 private void ManualLoadModule() 5 { 6 this.moduleManager.LoadModule(RegisterModule); 7 this.moduleManager.LoadModule(ViewListModule); 8 }运行效果示例代码https://github.com/zhaotianff/WPF-MVVM-Beginner/tree/main/15_Prism_Module.sln我开通了公众号每日更新Windows开发技术快来关注吧