1. 为什么我们需要模块化?从“一锅炖”到“乐高积木”
大家好,我是老张,一个在WPF和智能硬件领域摸爬滚打了十多年的老码农。今天想和大家聊聊一个让很多WPF开发者又爱又恨的话题:如何构建一个真正“能长大”的应用程序。
回想我早期做项目,一个典型的WPF应用是什么样子?一个庞大的解决方案,里面塞满了各种窗口、用户控件、业务逻辑类,所有的代码都挤在一起。主窗口的代码文件动辄几千行,想改个按钮功能,得在一堆事件处理程序里翻半天。更头疼的是,当业务需求变化,想加个新功能,或者把某个功能独立出来给另一个项目用,那简直是牵一发而动全身,改得心惊胆战。这种“一锅炖”式的架构,我们称之为“单体应用”。它在项目初期确实简单直接,但随着功能迭代,维护成本会指数级上升,最终变成一个没人敢动的“屎山”。
那么,理想的架构应该是什么样的呢?我觉得可以把它想象成玩乐高积木。每个独立的功能模块,就像一块块标准接口的乐高积木。主程序只是一个“底板”和“说明书”,负责把这些积木拼装起来。今天你想拼个汽车,就把轮子、车身的积木装上;明天想改成飞船,就把轮子模块卸了,装上推进器和机翼模块。整个过程清晰、灵活,而且不会因为改动一个部件而把整个模型搞垮。
WPF+Prism这套组合拳,就是帮我们实现“乐高式”开发的利器。Prism是微软官方推出的一个用于构建松耦合、可维护、可测试的XAML应用程序的框架。它不是一个全新的UI框架,而是建立在WPF(或WinUI、Xamarin.Forms等)之上的“架构指南”和“工具箱”。它的核心思想就是模块化和依赖注入。通过Prism,我们可以把应用拆分成多个独立的、可以动态加载的模块,每个模块只关心自己的那部分功能,模块之间通过定义好的接口进行通信,而不是直接互相引用。这样一来,应用的扩展性、可维护性和团队协作效率都会得到质的提升。
2. 迈出第一步:创建你的第一个Prism项目
光说不练假把式,咱们直接上手。我会带你从零开始,一步步搭建一个具备模块化能力的Prism项目骨架。这个过程就像盖房子先打地基,地基打好了,后面往上垒砖(加模块)就轻松了。
2.1 项目初始化与结构规划
首先,打开Visual Studio(我用的2022,其他版本也大同小异),新建一个WPF应用项目。这里有个小选择:用传统的.NET Framework还是新的.NET 6/7/8?如果你的项目有遗留依赖或者特定要求,用.NET Framework没问题。但我更推荐使用**.NET 6或更高版本**的WPF应用模板,它能享受到跨平台潜力、更好的性能和更现代的依赖注入支持。我们给项目起个名,比如PrismModularAppDemo。
项目创建好后,你会看到一个经典的MainWindow.xaml和App.xaml。先别急着写代码,我们要对项目结构做一个重要的规划。在解决方案资源管理器里,右键点击项目,新建几个文件夹:
- Views:存放所有的视图(View),也就是XAML文件,比如窗口、用户控件。
- ViewModels:存放所有的视图模型(ViewModel),这是Prism MVVM模式的核心,负责视图的展示逻辑。
- Models:存放业务模型和数据实体类。
- Services:存放各种服务类,比如数据访问、网络通信、日志等。
- Modules:这个文件夹先建好,后面我们会把各个功能模块放在这里,或者作为模块项目的引用目录。
现在,把自动生成的MainWindow.xaml和MainWindow.xaml.cs一起,拖到Views文件夹里。移动后,VS会提示你是否要更新所有引用,点“是”。但为了保险起见,我们手动检查两个地方:
- 打开
App.xaml文件,找到StartupUri属性,把它从StartupUri="MainWindow.xaml"修改为StartupUri="Views/MainWindow.xaml"。 - 打开
Views/MainWindow.xaml文件,找到根标签里的x:Class属性,把它从x:Class="PrismModularAppDemo.MainWindow"修改为x:Class="PrismModularAppDemo.Views.MainWindow"。
这一步操作看似简单,却是关注点分离的良好开端。视图和视图模型分开放,结构一目了然。
2.2 引入Prism框架:安装与基础配置
接下来,就是请出我们的主角——Prism框架。我们通过NuGet包管理器来安装。右键点击项目,选择“管理NuGet程序包”。在浏览标签页中,搜索“Prism.Unity”。
注意:Prism支持多种依赖注入容器,比如Unity、DryIoc、Autofac等。
Prism.Unity包就包含了Prism核心库和Unity容器的集成。对于新手,我推荐从Unity开始,它的概念相对直观。如果你熟悉其他容器,也可以选择对应的包,如Prism.DryIoc。
找到Prism.Unity包(作者是Brian Lagunas),点击安装。这会自动安装一系列依赖包,包括Prism的核心库Prism.Core和Unity容器。
安装完成后,我们首先来体验一下Prism带来的第一个便利:自动的ViewModel绑定。传统WPF MVVM中,我们需要在View的后台代码或XAML里手动设置DataContext。Prism简化了这个过程。
在ViewModels文件夹中,新建一个类,命名为MainWindowViewModel。让它继承Prism.Mvvm.BindableBase这个类,它提供了属性变更通知(INotifyPropertyChanged)的快捷实现。
using Prism.Mvvm; namespace PrismModularAppDemo.ViewModels { public class MainWindowViewModel : BindableBase { private string _title = "我的第一个Prism模块化应用"; public string Title { get { return _title; } set { SetProperty(ref _title, value); } } public MainWindowViewModel() { // 这里可以初始化一些数据 } } }然后,打开Views/MainWindow.xaml文件。在Window标签里,添加Prism的命名空间引用,并设置一个特殊的附加属性来实现自动绑定。
<Window x:Class="PrismModularAppDemo.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="True" Title="{Binding Title}" Height="450" Width="800"> <Grid> <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="24"/> </Grid> </Window>关键就在prism:ViewModelLocator.AutoWireViewModel="True"这一行。Prism的视图模型定位器(ViewModelLocator)会遵循一个命名约定:自动去寻找与当前View(MainWindow)对应的ViewModel(MainWindowViewModel),并且把它设置为View的DataContext。这个约定就是为什么我们之前要把View和ViewModel分开放到对应文件夹的原因。
现在直接运行程序,你会看到窗口标题和中间的文本都显示为“我的第一个Prism模块化应用”。这说明ViewModel已经成功绑定并工作了!我们并没有在任何地方写this.DataContext = new MainWindowViewModel();,这就是约定优于配置的魅力,也是依赖注入开始起作用的地方。
3. 核心改造:让应用“模块化”起来
前面我们只是用上了Prism的MVVM绑定,这已经很方便了,但还没触及模块化的核心。接下来,我们要对启动流程进行“大手术”,把普通的WPF应用彻底改造成一个支持动态加载模块的Prism应用。
3.1 重构应用启动类:继承PrismApplication
这是最关键的一步。默认的WPF应用入口是App.xaml和它的后台代码App.xaml.cs,它继承自Application类。Prism要求我们使用它提供的PrismApplication类作为启动基类。
首先,打开App.xaml,把Application改成prism:PrismApplication,并引入Prism命名空间。
<prism:PrismApplication x:Class="PrismModularAppDemo.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/"> <Application.Resources> </Application.Resources> </prism:PrismApplication>然后,打开App.xaml.cs文件。把类的继承从Application改为PrismApplication。
using Prism.Unity; using System.Windows; namespace PrismModularAppDemo { public partial class App : PrismApplication // 继承PrismApplication { // 原有的StartupUri配置已经不需要了,可以删除App.xaml中的StartupUri } }一保存,你会发现代码报错了。因为PrismApplication是一个抽象类,它要求我们实现几个核心的方法。点击类名App下的红色波浪线,按Alt+Enter(或点击小灯泡),选择“实现抽象类”。VS会自动生成三个方法:CreateShell()、RegisterTypes()和ConfigureModuleCatalog()。
3.2 实现抽象方法:搭建模块化骨架
我们来逐一填充这些方法。
1. CreateShell() - 创建应用的主壳(Shell)
Shell可以理解为应用程序的主容器窗口,它定义了应用的总体布局结构,比如顶部菜单、底部状态栏、左侧导航栏和中间的内容区域。在这里,我们直接使用之前创建的MainWindow作为Shell。
protected override Window CreateShell() { // 使用容器来解析MainWindow,而不是直接new。 // 这样MainWindow及其依赖(如它的ViewModel)都会被依赖注入容器管理。 return Container.Resolve<Views.MainWindow>(); }注意,这里用的是Container.Resolve<Views.MainWindow>(),而不是new Views.MainWindow()。这是依赖注入的核心:对象不由我们直接创建,而是向“容器”索取。容器负责创建对象,并自动注入它所依赖的其他对象(比如MainWindowViewModel)。
2. RegisterTypes() - 注册类型到依赖注入容器
这个方法是我们向IoC(控制反转)容器注册各种类型的地方。哪些类型需要注册呢?主要是那些接口和它们的实现类,以及一些需要容器管理的单例或共享实例。
在我们这个初始阶段,可能还没有很多服务,但我们可以先把视图模型注册一下,虽然Prism的AutoWireViewModel通常能搞定View和ViewModel的绑定,但显式注册也是一个好习惯,特别是当ViewModel有构造函数依赖时。
protected override void RegisterTypes(IContainerRegistry containerRegistry) { // 注册MainWindowViewModel,当需要MainWindowViewModel实例时,容器会提供它。 // 使用PerRequest或Transient取决于你的需求,对于View的ViewModel,通常Transient即可。 containerRegistry.Register<ViewModels.MainWindowViewModel>(); // 未来我们会在这里注册更多服务,例如: // containerRegistry.Register<IDataService, DataService>(); // containerRegistry.RegisterSingleton<ILogger, FileLogger>(); }3. ConfigureModuleCatalog() - 配置模块目录
这是模块化的心脏。模块目录(IModuleCatalog)管理着应用中所有模块的信息:模块有哪些、它们在哪里、应该按什么顺序初始化。在这个方法里,我们告诉Prism去哪里发现和加载我们的模块。
初期,我们可以先添加一个内联的、直接引用程序集的模块。后面我们会升级为从目录动态加载。
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { base.ConfigureModuleCatalog(moduleCatalog); // 方式一:直接添加一个模块类型(要求该模块所在程序集已被主程序引用) // moduleCatalog.AddModule<ModuleA.ModuleAModule>(); // 方式二:通过类型名和所在程序集名称添加(更灵活) // moduleCatalog.AddModule(typeof(ModuleA.ModuleAModule).Name, typeof(ModuleA.ModuleAModule).Assembly.FullName); // 我们现在还没有创建模块,所以先注释掉。下一章我们会创建第一个模块并在这里添加。 }完成以上步骤后,记得删除App.xaml中之前设置的StartupUri="Views/MainWindow.xaml"属性,因为现在主窗口是由CreateShell()方法创建并显示的。
现在再次运行程序,应用应该能正常启动,并显示MainWindow。看起来和之前没什么不同,但内在的引擎已经彻底更换了。我们的应用已经从一辆“焊接死的汽车”,变成了一个拥有标准接口的“底盘”,随时可以按上各种功能“模块”。
4. 创建你的第一个功能模块
地基和骨架都有了,现在我们来造第一块“乐高积木”——一个独立的功能模块。假设我们要为一个客户管理系统添加一个“客户管理”模块。
4.1 模块项目的创建与结构
在解决方案中,我们不再把所有代码都塞进启动项目。而是为“客户管理”功能单独创建一个项目。
- 在解决方案上右键 -> 添加 -> 新建项目。
- 选择“类库(.NET Framework)”或“类库(.NET Standard/.NET Core)”。我强烈建议选择.NET Standard 2.0类库。.NET Standard是一个API规范,能被.NET Framework、.NET Core/.NET 5+等多种.NET实现引用,兼容性最好,是模块化开发的理想选择。给项目起名为
CustomerModule。 - 在这个新类库项目中,同样引用
Prism.Unity的NuGet包。因为模块也需要使用Prism的模块化接口和依赖注入。 - 在
CustomerModule项目中,创建类似的文件夹结构:Views,ViewModels,Services等。
一个Prism模块的核心,是一个实现了IModule接口的类。这个类通常以Module结尾,例如CustomerModule。
4.2 实现IModule接口:模块的入口点
在CustomerModule项目的根目录(或者单独建一个Infrastructure文件夹),创建一个类CustomerModule.cs。
using Prism.Ioc; using Prism.Modularity; using Prism.Regions; using CustomerModule.Views; using System.Windows.Controls; namespace CustomerModule { public class CustomerModule : IModule { private readonly IRegionManager _regionManager; // 通过构造函数注入IRegionManager,用于向Shell中的区域注册视图 public CustomerModule(IRegionManager regionManager) { _regionManager = regionManager; } public void OnInitialized(IContainerProvider containerProvider) { // 模块初始化时,将客户列表视图注册到Shell中名为“MainContentRegion”的区域。 // 假设你的Shell(MainWindow)里有一个ContentControl,其RegionName="MainContentRegion" _regionManager.RegisterViewWithRegion("MainContentRegion", typeof(CustomerListView)); } public void RegisterTypes(IContainerRegistry containerRegistry) { // 在这里注册本模块特有的类型到容器中 // 例如,注册本模块的视图、服务等 containerRegistry.RegisterForNavigation<CustomerListView>(); // 注册视图用于导航 containerRegistry.Register<ICustomerService, CustomerService>(); // 注册服务 } } }这个CustomerModule类就是模块的“大脑”。OnInitialized方法在模块被加载后调用,这里我们通常做视图注册到区域(Region)的操作。RegisterTypes方法和主程序中的类似,用于向容器注册本模块提供的服务。
4.3 定义模块视图与区域(Region)
区域(Region)是Prism中一个极其强大的概念。你可以把它理解为Shell(主窗口)中预留的、可以被动态替换内容的“占位符”。比如,主窗口左边是导航栏区域(NavigationRegion),中间是主内容区域(MainContentRegion)。
首先,我们需要修改主程序Shell(MainWindow.xaml),定义区域。我们使用Prism的附加属性来标记一个控件为区域。
<Window ...> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 顶部菜单栏 --> <Menu Grid.Row="0"> <MenuItem Header="模块"> <MenuItem Header="加载客户模块" Command="{Binding LoadCustomerModuleCommand}"/> </MenuItem> </Menu> <!-- 主内容区域,这里被定义为一个Region --> <ContentControl Grid.Row="1" prism:RegionManager.RegionName="MainContentRegion"/> </Grid> </Window>在CustomerModule项目中,我们创建一个简单的视图CustomerListView.xaml(放在Views文件夹)和对应的CustomerListViewModel。
<!-- CustomerListView.xaml --> <UserControl x:Class="CustomerModule.Views.CustomerListView" ...> <StackPanel> <TextBlock Text="客户列表" FontSize="18" Margin="10"/> <ListBox ItemsSource="{Binding Customers}" DisplayMemberPath="Name" Height="200"/> <Button Content="添加测试客户" Command="{Binding AddTestCustomerCommand}" Margin="10"/> </StackPanel> </UserControl>// CustomerListViewModel.cs using Prism.Commands; using Prism.Mvvm; using System.Collections.ObjectModel; namespace CustomerModule.ViewModels { public class CustomerListViewModel : BindableBase { public ObservableCollection<Customer> Customers { get; set; } = new ObservableCollection<Customer>(); public DelegateCommand AddTestCustomerCommand { get; private set; } public CustomerListViewModel() { AddTestCustomerCommand = new DelegateCommand(AddTestCustomer); // 初始化一些测试数据 Customers.Add(new Customer { Id = 1, Name = "张三" }); Customers.Add(new Customer { Id = 2, Name = "李四" }); } private void AddTestCustomer() { Customers.Add(new Customer { Id = Customers.Count + 1, Name = $"测试客户{Customers.Count + 1}" }); } } public class Customer { public int Id { get; set; } public string Name { get; set; } } }4.4 在主程序中加载模块
最后一步,回到主程序(PrismModularAppDemo),我们需要告诉它去哪里找这个CustomerModule。
首先,在主项目中添加对CustomerModule类库项目的项目引用。这是最简单直接的模块加载方式(模块与主程序一起编译)。
然后,修改主程序App.xaml.cs中的ConfigureModuleCatalog方法:
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { base.ConfigureModuleCatalog(moduleCatalog); // 添加CustomerModule,并指定其初始化模式为WhenAvailable(默认,主程序启动时加载) // 也可以设置为OnDemand,在需要时手动加载 moduleCatalog.AddModule<CustomerModule.CustomerModule>(); }现在,运行主程序。由于我们在模块的OnInitialized中,将CustomerListView注册到了MainContentRegion,所以程序一启动,主内容区域就会自动显示客户列表视图。点击“添加测试客户”按钮,列表会增加新项目。
至此,你已经成功创建并加载了第一个Prism模块!这个模块完全独立于主程序,它有自己的视图、视图模型、模型甚至服务。主程序除了通过项目引用知道它的存在,在代码层面几乎没有耦合。你可以独立地开发、测试、甚至替换这个模块,只要它遵守约定的接口(比如注册到哪个区域)。
5. 进阶实战:动态加载与模块通信
直接引用项目的方式适合紧密集成的模块。但更酷的模式是动态加载:主程序在运行时,从一个指定的目录(如Modules)去发现和加载模块的DLL文件。这样,你甚至可以在不重新发布主程序的情况下,通过增删DLL文件来扩展或移除功能。
5.1 配置目录发现模块
修改主程序的ConfigureModuleCatalog方法,改用目录扫描方式:
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { base.ConfigureModuleCatalog(moduleCatalog); // 获取模块目录的路径(假设在应用程序根目录下的Modules文件夹) string modulePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules"); // 使用DirectoryModuleCatalog来扫描指定目录下的所有模块 if (Directory.Exists(modulePath)) { var directoryCatalog = new DirectoryModuleCatalog { ModulePath = modulePath }; // 这里通常需要将directoryCatalog中的模块合并到主catalog中, // 但更常见的做法是直接使用DirectoryModuleCatalog作为主catalog。 // 我们可以重写CreateModuleCatalog方法来返回一个DirectoryModuleCatalog。 } }更常见的做法是重写CreateModuleCatalog()方法,直接返回一个配置好的目录模块目录。
protected override IModuleCatalog CreateModuleCatalog() { // 返回一个配置了模块路径的DirectoryModuleCatalog string modulePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules"); return new DirectoryModuleCatalog() { ModulePath = modulePath }; }使用这种方式,你需要将编译好的CustomerModule.dll及其依赖(如Prism相关DLL)复制到主程序运行目录下的Modules文件夹中。主程序启动时会自动扫描并加载该目录下的所有有效模块。
5.2 模块间通信:解耦的艺术
模块之间不应该直接互相引用,否则就失去了模块化的意义。Prism提供了几种优雅的通信机制:
事件聚合器(EventAggregator):这是最常用、最解耦的方式。一个模块发布(Publish)一个事件,其他模块订阅(Subscribe)这个事件。发布者和订阅者彼此不知道对方的存在。
// 在模块A中定义事件 public class CustomerAddedEvent : PubSubEvent<Customer> { } // 模块A发布事件 _eventAggregator.GetEvent<CustomerAddedEvent>().Publish(newCustomer); // 模块B订阅事件 _eventAggregator.GetEvent<CustomerAddedEvent>().Subscribe(c => { // 处理新客户添加的逻辑,比如刷新自己的列表 RefreshCustomerList(); }, ThreadOption.UIThread); // 指定在UI线程执行共享服务(Shared Services):将公共功能抽象成接口,放在一个公共的类库(如
Infrastructure)中。所有模块都引用这个公共库,并通过依赖注入获取服务的实现。主程序负责注册这个服务的具体实现。区域上下文(Region Context):当多个视图被注入到同一个区域时,可以通过区域上下文来传递数据。
在实际项目中,我强烈推荐使用事件聚合器来处理跨模块的业务通知。它最大限度地降低了模块间的耦合度。
5.3 导航与视图切换
Prism提供了强大的导航服务,可以让你在不同的视图之间切换,并传递参数。这比直接操作Region的Views集合更高级、更规范。
在ViewModel中,你可以注入IRegionManager或INavigationService来进行导航。
// 使用IRegionManager进行区域内的视图导航(更底层) _regionManager.RequestNavigate("MainContentRegion", "CustomerDetailView", parameters); // 使用INavigationService进行导航(更高级,与平台无关性更好) _navigationService.NavigateAsync("CustomerDetailView", parameters);导航的目标“CustomerDetailView”是一个字符串标识,它需要在模块的RegisterTypes中通过RegisterForNavigation注册。
containerRegistry.RegisterForNavigation<CustomerDetailView, CustomerDetailViewModel>("CustomerDetailView");踩过的坑:导航时一定要确保目标视图和视图模型已经正确注册到容器中,并且导航参数的类型是简单的、可序列化的类型,复杂对象最好传递一个ID,然后在目标视图的初始化过程中再去查询完整数据。
6. 项目组织与最佳实践建议
经过前面的实战,一个模块化应用的雏形已经出来了。但要把它用到真正的企业级项目里,还需要一些工程化的考量。
6.1 解决方案与项目结构
一个中等规模的Prism模块化应用,解决方案结构可能如下:
MyEnterpriseApp.sln ├── MyEnterpriseApp.Shell (WPF应用,启动项目) │ ├── Views (Shell的视图,如MainWindow) │ ├── ViewModels │ ├── App.xaml/.cs (PrismApplication入口) │ └── Modules 文件夹 (存放动态加载的模块DLL) ├── MyEnterpriseApp.Infrastructure (.NET Standard 类库) │ ├── Interfaces (公共接口,如 ILogger, IDataService) │ ├── Models (公共数据模型) │ ├── Events (公共事件,使用EventAggregator) │ └── Constants (公共常量,如RegionNames) ├── ModuleA (.NET Standard 类库) │ ├── ModuleAModule.cs (实现IModule) │ ├── Views │ ├── ViewModels │ └── Services ├── ModuleB (.NET Standard 类库) └── (其他模块...)关键点:
- Shell项目尽可能“薄”:它只负责启动、加载模块、提供主界面框架。具体的业务功能全部放在模块里。
- 基础设施(Infrastructure)项目:这是所有模块和Shell的“公约数”。里面放大家都要用的接口、事件、模型、工具类。任何模块都不应该直接引用另一个模块,但都可以引用Infrastructure。
- 模块项目独立:每个模块都是一个完整的、可以独立编译和测试的功能单元。
6.2 依赖注入与生命周期管理
Prism的依赖注入容器是你应用的核心“管家”。要善用它:
- 接口与实现分离:始终针对接口进行注册和注入。
containerRegistry.Register<IMyService, MyService>()。 - 生命周期:
Transient:每次请求都创建一个新实例。这是默认的,适用于无状态的轻量级服务。Singleton:整个应用生命周期内只有一个实例。适用于配置服务、缓存服务等。Scoped:在某些容器中支持(如DryIoc),可以创建某个范围内的单例,在WPF中通常用RegisterScoped或特定区域内的单例。
- 构造函数注入:这是首选的方式。确保你的类(尤其是ViewModel)的依赖都通过构造函数传入,容器会自动解析。
6.3 调试与部署技巧
- 模块加载失败:最常见的错误是模块DLL或其依赖项(如特定版本的Prism)没有放在正确的位置。使用
DirectoryModuleCatalog时,打开Prism的日志输出(Prism.Logging)可以帮助诊断加载问题。 - 设计时支持:在XAML设计器中,如果ViewModel是通过容器解析的,设计时可能看不到数据。可以在ViewModel的构造函数中,通过
IsInDesignMode属性来提供设计时数据。 - 部署:对于动态加载,你需要一个清晰的部署脚本,将主程序(Shell)、基础设施库、各个模块的DLL以及它们各自的依赖,正确地复制到输出目录的相应位置(如主目录和Modules子目录)。可以使用构建后事件或专门的打包工具(如Inno Setup, WiX)来处理。
从我个人的经验来看,第一次搭建Prism模块化架构会花一些时间,可能会觉得比直接写“一锅炖”的代码更繁琐。但一旦项目规模超过3个主要功能点,或者有超过2个开发者参与,模块化带来的优势就会迅速体现出来:并行开发互不干扰、功能复用简单、定位bug范围小、测试可以针对模块进行。它迫使你思考代码的边界和职责,这本身就是对代码质量的一次巨大提升。