WPF MVVM实战系列教程(七、Prism模块化)

🧭 WPF MVVM入门系列教程


🍠 WPF MVVM进阶系列教程

模块化基础

首先我们了解一下插件这个概念

插件(add-in,也称为plug-in)是应用程序能够动态发现、加载和使用的单独编译过的组件。

通常,应将应用程序设计成能使用插件,从而可在将来进行增强,而不必进行任何修改、重新编译以及重新测试。

插件还为针对特殊的市场或客户单独定制应用程序实例提供了灵活性。但使用插件模型最常见的原因是,允许第三方开发人员扩展应用程序的功能。

例如,AdobePhotoshop中的插件提供了大量图片处理效果,类似Camera Raw。Google Chrome中的插件提供了增强的Web 冲浪特性以及全新功能。对于这两种情况,插件都是由第三方开发人员创建的。

.NET提供了MAF和MEF两种框架可以实现插件化。

MAF:https://learn.microsoft.com/en-us/dotnet/desktop/wpf/app-development/wpf-add-ins-overview

MEF:https://learn.microsoft.com/zh-cn/dotnet/framework/mef/

 

Prism的模块化原理就类似插件模型。它也能够动态地发现、加载和使用单独编译的组件。

在前面介绍Prism框架时,提到模块化的作用。模块化拆分、隔离与复用,核心目标都是降低应用耦合度,提升可维护性。

 

模块化以后

1、每个模块负责特定功能(如用户管理、订单管理),实现 “高内聚、低耦合”,便于团队协作、维护和迭代。

2、模块之间可以做到低依赖(减少直接引用,通过依赖注入实现复用),单个模块可独立编译、测试,甚至在多个应用中复用。

3、最终都能将多个独立模块整合到同一个主应用中,形成完整的功能体系,而非零散的组件。

 

image

就拿医疗行业的软件来说,它可能会划分如上的模块。不同的模块可以单独开发,测试。最后再将它们一起”组装“到”外壳“上。

对于一些基础模块,通常会定义成公用模块。需要使用的子模块可以直接引用这些基础模块。

注意:这里我们的目的是做到模块间低依赖,而不是完全没有依赖。

 

但是Prism的模块化和插件化会有稍许区别

Prism模块化:

模块化是 “核心架构手段”,最终目的是 “大型应用的结构化拆分与管理”。

Prism 希望在应用开发初期,就将应用拆分为多个模块,每个模块遵循统一的规范(IModule),主应用通过明确配置加载模块,便于团队分工、版本控制和后期维护。

简单来说:Prism 的模块化是为了 “事前规划”。

 

插件化:

最终目的是 “可扩展性”(插件化)。

当主应用发布后,无需修改主程序代码、无需重新编译,只需将新的模块(插件)放入指定目录,主应用就能自动识别并加载该模块,实现功能扩展。

简单说:插件化是为了 “事后扩展”。

 

模块化原则 

有些小伙伴可能对这个模块的划分还存在一些困惑。

这里介绍 一下模块划分的原则 

1、高内聚:一个模块只负责完成单一且明确的业务功能,不包含无关逻辑。

2、低耦合:模块之间尽量减少直接依赖,禁止跨模块直接引用View、ViewModel 或内部业务类。

3、创建独立的基础设施类库(CommonInfrastructure 项目),该项目仅包含公共接口、枚举、实体(DTO/POCO,无业务逻辑)。

这个基础设备类库是模块间交互的核心

 

模块加载基础原理

在Prism中涉及两种模块化的加载,一种是配置式,一种是扫描式。

配置式是显式配置,也就是主动指定要加载的模块。扫描式是隐式配置,也就是对目录进行扫描,然后自动添加符合要求的模块。

模块加载这里主要用到的是.NET的反射机制。

假设我定义了一个接口(仅演示用,所以未定义具体的接口函数)

1 public interface IModule
2 {
3 
4  
5 }

 

然后我定义了一个类库LibA,增加一个类ModuleAModule继承自IModule接口

1    public class ModuleAModule : IModule
2    {
3       
4    }

 

再定义一个类库LibB,增加一个类ModuleBModule

1  public class ModuleBModule
2 {
3   
4 }

 

image

当我们去扫描这两个dll,通过判断是否实现了IModule接口,就可以判断是否是我们定义的模块。

简易验证过程如下:

  • 第一步:通过Type.GetInterface(typeof(IModule).FullName)验证该类型是否实现了IModule
  • 第二步:验证该类型是否为class、是否非抽象(type.IsClass && !type.IsAbstract)。

 

示例代码如下:

 1 public List<Type> FindAllIModuleTypes(string moduleDirectory)
 2 {
 3     var moduleTypes = new List<Type>();
 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 }

 

IModule接口

首先我们了解一下IModule接口,在Prism中,可以被加载的模块都需要实现IModule接口。

IModule接口定义如下:

IModule提供了两个接口函数:RegisterTypes和OnInitialized,可以把它理解为模块的Bootstrapper。

 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 }

 

如何创建Prism模块

这里我们以一个简易的信息登记功能为例,增加一个Register模块,一个ViewList模块,一个CommonModule公共模块。

Register模块:登记Employee信息

ViewList模块:查看登记的Employee列表

CommonModule公共模块:定义Employee及通信事件

当在Register模块登记一个Employee后,通过ViewModel通信,将Employee添加到ViewList模块的列表中。

整体框架如下:

image

 

1、创建类库工程Module.Register

image

 

2、增加一个模块类RegisterModule实现IModule接口

注意:模块类在命名时,尽量以Module结尾,而且放在项目的根目录下,这样方便后期区分和查找

image

 

RegisterModule.cs

 1  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识别的模块

接下来我们对这个模块进行完善 

3、添加View和ViewModel

注意:类库工程在添加View时,会找不到WPF项。

image

 需要修改项目工程文件,添加UseWPF

image

 

添加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 Content="Id" HorizontalAlignment="Right" Margin="0,0,30,0" VerticalAlignment="Center"></Label>
15      <TextBox Grid.Row="0" Grid.Column="1" Margin="40,0" VerticalAlignment="Center"  Text="{Binding EmployeeViewModel.Id}"></TextBox>
16 
17      <Label Content="Name" Grid.Row="1" HorizontalAlignment="Right" Margin="0,0,30,0" VerticalAlignment="Center"></Label>
18      <TextBox Grid.Row="1" Grid.Column="1" Margin="40,0"  VerticalAlignment="Center"  Text="{Binding EmployeeViewModel.Name}"></TextBox>
19 
20      <Label Content="Phone" Grid.Row="2" HorizontalAlignment="Right" Margin="0,0,30,0" VerticalAlignment="Center"></Label>
21      <TextBox Grid.Row="2" Grid.Column="1" Margin="40,0"  VerticalAlignment="Center" Text="{Binding EmployeeViewModel.Phone}"></TextBox>
22 
23      <Button Content="登记" Width="88" Height="28" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="3" Grid.ColumnSpan="2" Command="{Binding RegisterCommand}"></Button>
24  </Grid>

 

添加ViewModels\RegisterViewModel.cs

 1   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.GetEvent<SelectEmployeeEvent>().Publish(this.EmployeeViewModel);
27       }
28   }

 

5、注册模块里的视图

此时我们再回到RegisterModule类,在RegisterTypes中注册View

 1  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.RegisterForNavigation<RegisterView, RegisterViewModel>();
12 
13          //也可以只注册View
14          //然后通过ViewModelLocator.AutoWireViewModel附加属性来自动绑定ViewModel
15          //containerRegistry.RegisterForNavigation<RegisterView>();
16      }
17  }

这样我们就拥有了一个完整的登记模块

然后我们按照这个逻辑,再添加ViewList模块

ViewListModule.cs

 1 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.RegisterForNavigation<EmployeeListView, EmployeeListViewModel>();
12     }
13 }

 

Views\EmployeeListView.xaml

 1 <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}" HorizontalAlignment="Left" Margin="10,0,0,0" FontWeight="Bold" FontSize="13" Foreground="Blue"></TextBlock>
12                     <DockPanel Grid.Row="1" Margin="0,5,0,0">
13                         <TextBlock Text="Id:" Margin="10,0,5,0"></TextBlock>
14                         <TextBlock Text="{Binding Id}" Margin="5,0,10,0"></TextBlock>
15                         <TextBlock Text="Phone:" Margin="10,0,5,0"></TextBlock>
16                         <TextBlock Text="{Binding Phone}" FontStyle="Italic"></TextBlock>
17                     </DockPanel>
18                 </Grid>
19             </DataTemplate>
20         </ListBox.ItemTemplate>
21     </ListBox>
22 </Grid>

 

ViewModels\EmployeeListViewModel.cs

 1 public class EmployeeListViewModel : BindableBase
 2 {
 3     //列表
 4     private ObservableCollection<IEmployeeViewModel> employeeList = new ObservableCollection<IEmployeeViewModel>();
 5 
 6     public ObservableCollection<IEmployeeViewModel> EmployeeList
 7     {
 8         get => this.employeeList;
 9         set => this.SetProperty(ref employeeList, value);
10     }
11 
12     public EmployeeListViewModel(IEventAggregator eventAggregator)
13     {
14         //创建登记界面发送数据订阅
15         eventAggregator.GetEvent<SelectEmployeeEvent>().Subscribe(OnAddEmployee);
16     }
17 
18      //接收从登记界面发送过来的数据
19     private void OnAddEmployee(IEmployeeViewModel model)
20     {
21         this.EmployeeList.Add(model);
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工程上添加模块项目引用

image

 

引用我们需要使用的Prism模块项目

image

 

2、在Shell(MainWindow)的Bootstrapper中重写ConfigureModuleCatalog函数,配置要加载的模块

1  protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
2  {
3      //添加模块引用
4      moduleCatalog.AddModule<RegisterModule>();
5      moduleCatalog.AddModule<ViewListModule>();
6  }

 

3、在Shell(MainWindow)的Bootstrapper中加载View

1  protected override void OnInitialized()
2  {
3      base.OnInitialized();
4 
5      var regionManager = this.Container.Resolve<IRegionManager>();
6 
7      regionManager.RegisterViewWithRegion("EditArea", typeof(RegisterView));
8      regionManager.RegisterViewWithRegion("ListArea", typeof(EmployeeListView));
9  }

 

在前面我介绍介绍过,RegisterViewWithRegion函数可以绑定默认视图。

现在就等于将RegisterView绑定到EditArea区域,将EmployeeListView绑定到ListArea区域。

而RegisterView和EmployeeListView都分别来自于不同的模块。

 

程序运行效果如下:

image

 

posted @ 2026-01-27 11:41  zhaotianff  阅读(18)  评论(0)    收藏  举报