
Prism 的官方文档对适用场景有一个克制的界定:面向“需要长期存活、并预期会持续变化”的 WPF 应用。如果你的项目是三个月交付后不再大改的内部工具,引入模块化框架带来的启动配置和概念开销可能不值得。但如果你的应用满足以下任一条件,模块化就是刚需:
多个团队或开发者需要并行开发不同功能区,彼此不能因为对方的代码变动而阻塞;功能模块需要支持运行时加载或热插拔;界面区域需要由不同来源的视图动态填充,且来源在编译期不确定。
一个常见的误判是“项目大了就必须模块化”。实际上,模块化的驱动力是团队边界和变更频率,而非代码行数。一个单人维护但持续迭代多年的 WPF 应用,同样可以从清晰的模块划分中受益;而一个十人团队三个月赶工的项目,强行上 Prism 反而可能因为学习成本拖慢交付。
Prism 的现代版本通过 NuGet 包 Prism.Unity 或 Prism.DryIoc 引入,不再需要手动下载旧的 Composite Application Library。安装后,App.xaml 的根节点从标准的 Application 改为 PrismApplication,并在 code-behind 中实现两个必须的方法。
<prism:PrismApplication
x:Class="Shell.App"
xmlns:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:Shell">
<Application.Resources />
</prism:PrismApplication>public partial class App : PrismApplication
{
protected override Window CreateShell()
=> Container.Resolve<MainWindow>();
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册全局服务,模块内部的服务由模块自己注册
}
}CreateShell 返回应用的主窗口。RegisterTypes 用于注册需要在整个应用范围内可见的服务。这一步的关键判断是:只有跨模块共享的东西才应该放在这里。模块内部的 ViewModel、仓储、辅助类,应该在模块自己的初始化阶段注册,否则启动类会逐渐膨胀成一个知道所有模块内部细节的上帝类。
一个模块的实现则更简单。它只需实现 IModule 接口,在 RegisterTypes 中注册自己的依赖,在 OnInitialized 中把视图注入到指定的 Region:
public class OrderModule : IModule
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<OrderView, OrderViewModel>();
}
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("MainRegion", typeof(OrderView));
}
}"MainRegion" 是 Shell 中某个控件上声明的区域名称。模块不知道 Shell 长什么样,只知道“有一个叫 MainRegion 的位置可以放视图”。这是模块化界面组合的基本契约。
模块拆分之后,第一个现实问题是:订单模块需要通知通知模块“有新订单了”,但订单模块不应该引用通知模块的程序集。EventAggregator 解决的就是这个问题。
定义一个事件类,订阅方和发布方都只依赖这个事件类型,而不依赖彼此:
public class OrderCreatedEvent : PubSubEvent<Order> { }发布方注入 IEventAggregator,调用 Publish:
_eventAggregator.GetEvent<OrderCreatedEvent>().Publish(newOrder);订阅方同样注入 IEventAggregator,调用 Subscribe:
_eventAggregator.GetEvent<OrderCreatedEvent>()
.Subscribe(order => HandleNewOrder(order));这里有一个容易被忽略的细节:订阅的线程模型。Prism 的 Subscribe 方法可以指定 ThreadOption,默认是在发布者的线程上执行回调。如果发布方在后台线程发布事件,而订阅方需要更新 UI,就必须显式指定 ThreadOption.UIThread,否则会触发跨线程访问异常。这不是 Prism 的缺陷,是 WPF 线程模型的必然要求,但模块化之后,发布方和订阅方互相不知道对方的存在,也就无法预判对方的线程需求。所以事件定义本身应该带上线程语义的文档说明,这是模块化开发中容易被跳过的一步。
Prism 支持多种模块加载方式,最基础的分法是静态与动态。
静态加载在 Shell 项目中直接引用模块程序集,启动时由 ModuleEnumerator 枚举并加载。优点是启动速度快、调试方便、部署简单。缺点是 Shell 在编译期就依赖了所有模块,增加一个模块需要重新编译 Shell。动态加载则不在 Shell 中引用模块程序集,而是在运行时从指定目录(或配置、数据库)发现并加载。Shell 与模块彻底解耦,但启动时需要执行发现和加载逻辑,启动性能会受影响,且调试时如果模块加载失败,错误信息可能不够直观。
一个务实的策略是:开发阶段用静态加载降低调试摩擦,发布阶段切换到动态加载实现部署灵活性。Prism 的模块目录(Directory)加载方式支持从指定文件夹读取模块程序集,部署时只需要把编译好的模块 DLL 放进对应目录,Shell 不需要重新编译。这个切换的成本主要在于 Shell 项目需要移除对模块项目的引用,并配置模块目录路径,工作量可控,但对部署流程的改善是实质性的。
第一个坑是把 Region 当成万能容器。Region 的职责是“在某个位置展示某个视图”,仅此而已。如果把 Region 用于传递数据、控制业务流程、管理状态,它很快就会变成一个隐式的全局变量池,模块之间的耦合通过 Region 名称悄悄建立起来,比直接引用更难排查。
第二个坑是模块划分按技术分层而非业务能力。把“所有 View”“所有 ViewModel”“所有 Service”各做成一个模块,表面上模块化了,实际上每个模块都没有独立意义,改一个功能仍然要动三个模块。正确的划分单位是业务能力:订单模块、用户模块、报表模块,每个模块内部包含自己的 View、ViewModel、服务和数据访问逻辑。
第三个坑是过早动态化。动态加载听起来更“架构”,但如果模块之间的依赖关系还没有稳定,动态加载会让启动失败的原因变得难以定位。先跑通静态加载下的完整功能,确认模块边界合理、事件契约稳定,再切换到动态加载。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。