
简介这份资源围绕WPF与Prism框架展开面向希望深入掌握桌面应用开发的中高级.NET开发者及WPF初学者。内容系统讲解MVVM设计模式并重点剖析Prism框架的依赖注入、模块化、导航服务、事件总线、命令、ViewModel定位器与Region管理等核心机制帮助读者理解如何构建结构清晰、易于维护和扩展的大型WPF应用。压缩包为rar格式共57个文件约255KB包含21个cs源码、4个xml配置、3个xaml界面文件以及pdb、exe、dll、baml、resources等编译与资源文件另附csproj项目文件与settings配置目录涵盖Models、Views、ViewModels、Services等典型MVVM分层结构便于直接运行调试与二次开发。目前已有919人学习下载适合作为Prism入门与项目实践的参考范例。1. 从一次“模块加载失败”说起WPF 与 Prism 到底在解决什么一个跑了三年的 WPF 客户端主界面代码从 800 行涨到 6000 行按钮事件里塞着数据库查询、日志上报、权限判断改一个登录逻辑要重新编译整个解决方案。这不是段子是我接手过的一个真实维护场景。后来用 Prism 把系统拆成 Shell、Module、View、ViewModel 四层登录模块单独编译成 DLL主程序启动时按需加载改登录不再动主框架。Prism 不是 UI 库它是一套让 WPF 应用“可拆、可换、可测”的架构框架核心解决四件事模块化加载、依赖注入、MVVM 绑定、区域导航。适合谁适合已经写过几个 WPF 窗口、被代码耦合折磨过、想往企业级客户端方向走的开发者。如果你还在用Click事件写业务Prism 的收益会非常明显如果你只做单窗口小工具它反而增加复杂度。下面按“能跑起来 → 能拆开 → 能避坑 → 能进阶”的顺序讲透。2. 用 Prism 搭一个能跑的最小骨架从空项目到模块加载2.1 选型理由为什么是 Prism 而不是手写 MVVMWPF 原生支持数据绑定和命令但没规定模块怎么拆、ViewModel 怎么注入、导航怎么统一。手写一套轻量 MVVM 通常要自己实现ViewModelLocator、ICommand封装、消息聚合项目一大就变成“每个人一套写法”。Prism 把这些约定固化成库BindableBase提供SetPropertyDelegateCommand封装命令IRegionManager管理界面区域IModuleCatalog管理模块目录。常见做法是新建 WPF 项目后通过 NuGet 引入Prism.DryIoc或Prism.Unity前者容器更轻后者生态更熟。我一般选 DryIoc启动速度在中小型客户端上体感更快。2.2 最小可运行骨架App.xaml 与 PrismApplicationPrism 8 之后推荐继承PrismApplication而不是在App.xaml.cs里手动 new 容器。下面是最小启动代码// App.xaml.cs using Prism.DryIoc; using Prism.Ioc; using Prism.Modularity; using System.Windows; namespace DemoShell { public partial class App : PrismApplication { // 创建主窗口Prism 会把它当作 Shell protected override Window CreateShell() { return Container.ResolveMainWindow(); } // 注册全局类型ViewModel 和 View 在这里绑定 protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigationViews.HomeView(); containerRegistry.RegisterSingletonIService, DataService(); } // 配置模块目录指定模块 DLL 的加载方式 protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModuleLoginModule.LoginModule(); } } }逻辑说明CreateShell返回主窗口Prism 自动完成区域注册和导航初始化。RegisterTypes是依赖注入的入口RegisterForNavigation让某个 View 可以被RequestNavigate导航到。ConfigureModuleCatalog决定模块是启动即加载还是按需加载。参数上RegisterSingleton适合无状态服务Register适合每次新建的 ViewModel。注意App.xaml里要去掉StartupUri否则会创建两个主窗口。2.3 模块项目怎么建一个类库加一个 IModule 实现模块化是 Prism 最核心的价值。新建一个 WPF 类库项目引用Prism.Wpf然后写模块入口// LoginModule.cs using Prism.Ioc; using Prism.Modularity; using Prism.Regions; namespace LoginModule { public class LoginModule : IModule { public void RegisterTypes(IContainerRegistry containerRegistry) { // 模块内部的 View 和 Service 在这里注册 containerRegistry.RegisterForNavigationViews.LoginView(); containerRegistry.RegisterILoginService, LoginService(); } public void OnInitialized(IContainerProvider containerProvider) { // 把登录视图注入到 Shell 的 LoginRegion 区域 var regionManager containerProvider.ResolveIRegionManager(); regionManager.RegisterViewWithRegion(LoginRegion, typeof(Views.LoginView)); } } }逻辑说明RegisterTypes在模块加载时调用负责模块内部依赖注册OnInitialized在模块初始化时调用适合做区域注入和事件订阅。参数上RegisterViewWithRegion是“视图优先”的注入方式适合固定区域如果要做动态导航用RequestNavigate。模块 DLL 的输出路径要能被主程序扫描到常见做法是把模块项目的生成目录设到 Shell 的Modules文件夹下。2.4 区域与导航把界面切成可替换的块区域是 Shell 上预留的占位符导航是往区域里塞视图的动作。Shell 里写!-- MainWindow.xaml -- Window x:ClassDemoShell.MainWindow xmlns:prismhttp://prismlibrary.com/ prism:ViewModelLocator.AutoWireViewModelTrue Grid ContentControl prism:RegionManager.RegionNameLoginRegion / ContentControl prism:RegionManager.RegionNameMainRegion / /Grid /Window逻辑说明RegionName是区域的唯一标识AutoWireViewModel让 Prism 自动把MainWindowViewModel绑定到MainWindow。导航代码// 在 ViewModel 中注入 IRegionManager _regionManager.RequestNavigate(MainRegion, HomeView);参数说明第一个参数是区域名第二个是注册过的 View 名称。导航失败最常见的原因是 View 没注册、区域名拼错、或者模块没加载。排查时先看RegionManager.Regions里有没有目标区域再看IRegionNavigationJournal的日志。3. 依赖注入与 MVVM 绑定Prism 里最容易写错的几处3.1 构造函数注入与属性注入的边界Prism 的容器支持构造函数注入ViewModel 里直接写public class LoginViewModel : BindableBase { private readonly ILoginService _loginService; private readonly IRegionManager _regionManager; public LoginViewModel(ILoginService loginService, IRegionManager regionManager) { _loginService loginService; _regionManager regionManager; LoginCommand new DelegateCommand(ExecuteLogin, CanExecuteLogin) .ObservesProperty(() UserName); } private string _userName; public string UserName { get _userName; set SetProperty(ref _userName, value); } public DelegateCommand LoginCommand { get; } private bool CanExecuteLogin() !string.IsNullOrWhiteSpace(UserName); private async void ExecuteLogin() { var ok await _loginService.LoginAsync(UserName); if (ok) _regionManager.RequestNavigate(MainRegion, HomeView); } }逻辑说明SetProperty触发PropertyChangedDelegateCommand的ObservesProperty让命令的可用状态随属性变化。参数上CanExecuteLogin返回 false 时按钮自动置灰这是 WPF 命令系统的原生能力。注意async void只适合事件入口内部逻辑要 try-catch否则异常会直接崩掉进程。3.2 事件聚合器模块之间怎么不互相引用模块 A 要通知模块 B 刷新列表直接引用 B 的类就破坏了模块化。Prism 的IEventAggregator是发布订阅模式// 定义事件 public class UserLoggedInEvent : PubSubEventstring { } // 发布 _eventAggregator.GetEventUserLoggedInEvent().Publish(admin); // 订阅 _eventAggregator.GetEventUserLoggedInEvent().Subscribe(OnUserLoggedIn, ThreadOption.UIThread);逻辑说明PubSubEventT的泛型参数是载荷类型ThreadOption.UIThread保证回调在 UI 线程执行。参数上Subscribe还有keepSubscriberReferenceAlive参数默认 false 使用弱引用避免内存泄漏如果订阅者是短生命周期对象建议保持默认。常见坑是订阅后忘记取消导致重复触发用SubscriptionToken可以在OnNavigatedFrom里统一释放。3.3 导航传参与返回INavigationAware 的三个方法导航不只是切界面还要传参数和拦截。实现INavigationAwarepublic class HomeViewModel : BindableBase, INavigationAware { public void OnNavigatedTo(NavigationContext navigationContext) { var id navigationContext.Parameters[userId] as string; // 根据 id 加载数据 } public bool IsNavigationTarget(NavigationContext navigationContext) true; public void OnNavigatedFrom(NavigationContext navigationContext) { // 清理订阅、保存状态 } }逻辑说明OnNavigatedTo接收参数IsNavigationTarget决定是否复用当前实例返回 false 会新建 ViewModel。参数上NavigationContext.Parameters是字典取值前先判空。常见错误是导航时传了对象但没实现IsNavigationTarget导致每次导航都重建状态丢失。4. 避坑与排查Prism 项目里最常见的 5 个翻车现场4.1 模块加载了但视图不显示现象模块 DLL 已复制到输出目录ConfigureModuleCatalog也加了但区域里空白。原因通常是模块没被扫描到或者RegisterViewWithRegion的区域名和 Shell 里的不一致。解决在OnInitialized里打断点确认模块是否执行检查区域名大小写确认模块项目引用了Prism.Wpf且输出路径正确。血泪经验是模块项目的CopyLocal设置类库默认不复制依赖需要手动把 Prism 相关 DLL 一起输出。4.2 导航报“View not found”现象RequestNavigate返回 false界面不跳转。原因有三类View 没注册、注册名和导航名不一致、模块未加载。解决在RegisterTypes里用RegisterForNavigationViews.HomeView(HomeView)显式指定名称导航前检查regionManager.Regions.ContainsRegionWithName用IRegionNavigationService的NavigationFailed事件捕获异常信息。4.3 ViewModel 构造函数注入失败现象启动时报“无法解析类型”或者 ViewModel 为 null。原因是 ViewModel 没有注册或者构造函数参数里有未注册的接口。解决Prism 默认用ViewModelLocator按命名约定查找 ViewModel如果 ViewModel 在别的程序集需要重写ViewModelLocationProvider的映射规则。参数上AutoWireViewModel为 true 时View 名HomeView对应HomeViewModel命名空间要一致。4.4 事件订阅导致内存泄漏现象页面关闭后事件还在触发内存持续上涨。原因是Subscribe默认弱引用但如果订阅者被其他强引用持有弱引用失效。解决在OnNavigatedFrom里用SubscriptionToken.Dispose()取消订阅或者用keepSubscriberReferenceAlive: false并确保订阅者不被长生命周期对象引用。排查时用内存分析工具看PubSubEvent的订阅列表。4.5 区域适配器不生效现象ItemsControl里注册了区域但导航后不显示。原因是 Prism 默认只为ContentControl、ItemsControl等少数控件提供适配器自定义控件需要自己实现RegionAdapter。解决继承RegionAdapterBaseT重写Adapt和CreateRegion然后在ConfigureRegionAdapterMappings里注册。参数上Adapt里要把视图添加到控件的Items或Content属性。5. 进阶技巧用延迟加载和导航日志把启动速度压下来5.1 按需加载模块OnDemand 的配置与触发大型客户端启动时加载所有模块会拖慢首屏。Prism 支持OnDemand加载// 在 ConfigureModuleCatalog 中 moduleCatalog.AddModule(new ModuleInfo { ModuleName LoginModule, ModuleType LoginModule.LoginModule, LoginModule, InitializationMode InitializationMode.OnDemand }); // 在需要时触发加载 _moduleManager.LoadModule(LoginModule);逻辑说明InitializationMode.OnDemand让模块在首次请求时才加载LoadModule触发RegisterTypes和OnInitialized。参数上ModuleType要写程序集限定名格式是“命名空间.类名, 程序集名”。注意LoadModule是同步的如果模块初始化耗时长放到后台线程再回 UI 线程更新区域。5.2 导航日志实现前进后退IRegionNavigationJournal记录导航历史// 在 ViewModel 中 private readonly IRegionNavigationJournal _journal; public void GoBack() { if (_journal.CanGoBack) _journal.GoBack(); } public void OnNavigatedTo(NavigationContext navigationContext) { _journal navigationContext.NavigationService.Journal; }逻辑说明Journal在每次导航时自动记录CanGoBack和CanGoForward判断边界。参数上GoBack会触发目标 ViewModel 的OnNavigatedTo如果不想重复加载数据在IsNavigationTarget里返回 true 复用实例。常见坑是导航日志在区域切换后丢失因为Journal绑定在NavigationService上区域重建会重置。5.3 用 DelegateCommand 的异步版本处理耗时操作DelegateCommand不支持 asyncPrism 提供了DelegateCommandT和AsyncDelegateCommand部分版本在Prism.Commands里。如果版本没有可以自己封装public class AsyncDelegateCommand : DelegateCommand { private bool _isExecuting; public AsyncDelegateCommand(FuncTask execute, Funcbool canExecute null) : base(async () await ExecuteAsync(execute), canExecute) { } private static async Task ExecuteAsync(FuncTask execute) { await execute(); } }逻辑说明把异步方法包成async void传给基类内部用Task等待。参数上canExecute里要排除_isExecuting避免重复点击。注意异常处理异步命令里抛出的异常不会自动冒泡到 UI 线程需要 try-catch 后弹窗或记日志。5.4 验证方法用单元测试确认模块注册和导航Prism 的模块化让单元测试变得可行。用Prism.TestSupport或手动 mockIContainerRegistry[Test] public void LoginModule_Should_Register_LoginView() { var registry new MockIContainerRegistry(); var module new LoginModule(); module.RegisterTypes(registry.Object); registry.Verify(r r.RegisterForNavigationLoginView(), Times.Once); }逻辑说明验证模块的RegisterTypes是否按预期注册类型。参数上MockIContainerRegistry需要引入 Moq 或 NSubstitute。导航测试可以 mockIRegionManager验证RequestNavigate被调用且参数正确。我一般会在 CI 里跑这些测试防止重构时漏注册。5.5 一个具体技巧用 RegionManager 的 GetRegion 做动态布局有时候需要在运行时动态添加区域比如插件式面板var region _regionManager.Regions[MainRegion]; var view _container.ResolvePluginView(); region.Add(view, PluginView); region.Activate(view);逻辑说明Regions索引器获取区域Add添加视图Activate激活显示。参数上Add的第二个参数是视图名称用于后续Remove或Activate。注意动态添加的视图不会自动触发INavigationAware需要手动调用OnNavigatedTo或改用RequestNavigate。最后说个我自己的习惯每次新建 Prism 项目先把App.xaml.cs里的RegisterTypes和ConfigureModuleCatalog写空跑通 Shell 显示再加第一个模块再加导航再加事件。这样出问题时能快速定位是哪一层没配好。Prism 的学习曲线不在 API 多而在“约定大于配置”的思维转换一旦适应WPF 项目的可维护性会上一个台阶。希望帮到你。本文还有配套的精品资源点击获取