ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Prism导航框架从入门到实战:核心概念与常见坑

Prism导航框架从入门到实战:核心概念与常见坑 忘了说导航框架可能是Prism最容易让人劝退的部分大概两年前我第一次在项目里引入Prism看完官方文档里关于导航的那一章整个人是懵的。Regions、NavigationService、NavigationParameters、INavigationAware一堆概念砸过来和之前用Code-Behind写new Window()、frame.Navigate()完全是两套思维。当时我甚至怀疑是不是我打开错了文档——顺手搜了一下Prism结果出来一堆GraphPad Prism统计绘图软件的教程什么我想建立一个prism让有2个纵坐标如何设置生成看得我哭笑不得。GraphPad Prism是画科研图表的工具而我要找的是.NET生态里那个著名的MVVM导航框架同名不同命这事儿估计不少人都踩过。这篇文章就围绕Prism导航框架本身来聊。你大概率已经知道Prism在WPF、Xamarin.Forms里很出名但导航这块始终是拦路虎它和你过去习惯的页面跳转方式完全不一样得先理解它的设计意图才能用得顺手。我会从框架到底解决了什么问题讲起把NavigationService、页面注册、参数传递、生命周期回调这些核心概念拆开揉碎再带一个完整可跑的双页面示例最后附上我在实际项目里排查各种导航问题的经验。想系统学会Prism导航的看这一篇就够了。1. Prism导航框架到底解决了什么问题1.1 MVVM框架为什么会需要一个导航框架先说个最朴素的问题WPF自带NavigationServiceXamarin.Forms也内置了导航API为什么还要用Prism的导航关键在于MVVM。Prism是个MVVM框架它的分工原则很明确视图模型里不能出现Window、Frame、NavigationPage这类UI类型不然没法做单元测试也没法跨平台复用逻辑。但导航偏偏是个和UI强耦合的动作——你要打开一个新页面总得有地方知道这个页面长什么样。原生导航API全是面向UI控件的ViewModel里直接调用它们等于把视图和业务逻辑又绑回去了。Prism的解决办法是把导航动作抽象成服务调用。ViewModel里不写new MyWindow().Show()而是叫_navigationService.NavigateAsync(MyPage)。到底这个MyPage是一个Window还是一个Page由框架在背后解析ViewModel完全不关心。这样ViewModel变得干干净净全是可测试的业务逻辑和导航意图。这个抽象还有个额外的好处如果你从WPF迁移到Xamarin.Forms甚至后来的MAUIViewModel里的导航代码几乎不用改因为Prism在不同平台上提供了接口一致的导航服务。我做过的项目里业务层的一组ViewModel直接拖到了新平台的UI项目里只改了注册代码导航意图一点没动。1.2 Prism导航的设计哲学谁负责导航理解Prism导航前先记住一句话导航是视图模型的职责不是视图的职责。这里要打破一个惯性思维。传统做法里导航是页面主动发起的按钮的Click事件里写this.NavigationService.Navigate...。但在Prism里按钮通过Command绑定到ViewModelViewModel拿到INavigationService接口实例执行导航。整个链路是用户点击 - Command触发 - ViewModel调用NavigationService - Prism找到目标页面 - 实例化并导航过去。所以你代码里根本拿不到Page对象也不需要拿。你只需要知道目标页面的注册名或者注册时的类型。这个按名字导航的设计还让页面之间的耦合降到了最低——页面A完全不知道页面B的构造函数长啥样只知道它叫DetailPage。后面加个新页面不影响已有代码。1.3 Prism导航和原生导航的直观区别放一张对比表看得更清楚维度原生导航Prism导航调用方视图Code-Behind视图模型页面引用直接操作Page/Window对象按注册名或类型解析参数传递构造函数、全局静态、属性NavigationParameters对象页面生命周期感知重写视图方法ViewModel实现INavigationAware接口脱离UI控件很难完全分离可单元测试更关键的是生命周期。原生导航时要在页面出现或消失时做些事情比如刷新数据、释放资源得去重写视图的OnAppearing、OnNavigatedTo这些方法。Prism里这些逻辑都下沉到了ViewModel你实现INavigationAware接口就行UI层保持沉默。这样等于把页面行为从视图层整个搬到了逻辑层MVVM才算贯彻到底。2. 骨架搭建导航框架的四个核心概念2.1 NavigationService导航的总调度INavigationService是整个导航框架的中枢。每个ViewModel被创建时Prism容器会自动注入这个服务实例不需要手动new也不需要静态类。这也是依赖注入的好处你要做的只是在构造函数里声明参数。Prism的导航服务设计得像一个会话操作员你告诉它我要去哪它会自己查路由表其实就是注册的页面映射找到对应的页面并完成实例化、显示、数据传递。整个过程中你拿不到页面实例但可以通过回调拿到这次导航成功了还是失败了。拿WPF举个例子导航到一个叫DetailPage的页面代码就是一行_navigationService.RequestNavigateAsync(DetailPage);如果是Xamarin.Forms的MasterDetailPage或者TabbedPage导航语义会有些差异但调用方式基本一致。关键认知是这个服务对象是按页面会话的不是全局单例。每个页面ViewModel注入的NavigationService知道当前页面的导航栈上下文所以你在A页面用GoBackAsync()它能正确回到A的上一页而不是乱跳。2.2 页面注册没有注册就没有导航Prism导航的前提是告诉框架有哪些页面。这个动作叫页面注册通常在容器初始化时统一完成。注册的本质是把一个字符串键映射到一个页面类型相当于给导航框架建了一张路由表。protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigationMainPage, MainPageViewModel(); containerRegistry.RegisterForNavigationDetailPage, DetailPageViewModel(); }注意这里注册的是泛型对页面类型 视图模型类型。Prism的RegisterForNavigation不只是注册页面还会顺带把视图模型绑定好并且自动建立二者的关联。注册时如果不指定名字默认就是用类型名去掉Page后缀作为导航键。上面的代码注册后的导航名分别是Main和Detail——这一点特别容易坑人你以为注册名是MainPage实际是Main。提示命名规则带来一个常见坑——如果你想让导航名清晰一点注册时手动指定名称RegisterForNavigationMainPage, MainPageViewModel(MainPage)这样名字和类型对得上排查问题也省心。如果没有注册就发起导航Prism会抛异常或者导航失败报错信息类似于找不到目标页面类型这块我在第5节会展开讲。2.3 NavigationParameters状态传递的快递员页面之间怎么传数据框架给的标准答案是NavigationParameters。它是一个键值对集合但跟字典不同的是它区分了全局参数和页面参数并且会自动伴随导航过程中各层级页面的生命周期回调。var parameters new NavigationParameters { { id, 1001 }, { name, Prism教程 } }; _navigationService.NavigateAsync(DetailPage, parameters);接收方在INavigationAware的OnNavigatedTo回调里取参数public void OnNavigatedTo(INavigationParameters parameters) { var id parameters.GetValueint(id); var name parameters.GetValuestring(name); }为什么不用公有的全局静态变量或者构造函数的参数因为那两个方案在简单的双页面场景下好像能用但应用复杂后就会失控页面B可能在栈里被多次创建也可能被别的页面复用静态全局变量会串数据构造函数传参会把页面创建和业务逻辑耦合住。NavigationParameters是跟着导航走的数据载体无论页面从哪个入口进来都能拿到那条导航路径对应的数据逻辑干净排查也方便。2.4 视图模型的生命周期回调Prism导航和原生导航最大的体验差异就是这个接口INavigationAware。你的ViewModel实现它就可以感知导航事件public class DetailPageViewModel : BindableBase, INavigationAware { public void OnNavigatedTo(INavigationParameters parameters) { // 页面被导航到进入当前页时触发 // 适合加载数据、刷新界面状态 } public void OnNavigatedFrom(INavigationParameters parameters) { // 页面被导航离开从当前页跳走时触发 // 适合保存草稿、暂停任务 } }这个接口的设计思路是把页面生命周期从视图层拉到视图模型层。原生环境里你要在Page的OnNavigatedTo里调用ViewModel的方法或者用行为去触发Prism直接把回调注入到ViewModel省了一道工序。等一下还有一个经常被忽略的细节OnNavigatedFrom在什么情况下会触发不只是你按返回键离开时还包括你从页面A导航到页面B时页面A的OnNavigatedFrom会先执行然后页面B的OnNavigatedTo才执行。这个先后顺序在跨页面传数据时可太重要了比如A页面在离开前要写日志B页面进入时要读日志顺序错了数据就丢了。3. 实操过程从零实现一个双页面导航示例3.1 明确需求与工程结构光说不练假把式。我做了一个最小但完整的示例有一个主页面输入一段文本点击按钮跳转到详情页详情页显示这段文本并可以返回。这个示例覆盖了Prism导航最核心的路径注册页面、发起导航、传递参数、返回可以说麻雀虽小五脏俱全。我建议你也照着搭一个几分钟就能跑通以后在这个例子上加逻辑理解更快。工程类型可以是WPF或者Xamarin.FormsPrism在这俩平台上的导航API已经高度统一了。我用WPF演示但代码思路在Xamarin.Forms里一样成立。3.2 初始化Prism应用与注册导航页面先建工程NuGet装Prism.DryIoc或者Prism.Unity看你偏好哪个容器。改App代码让Prism接管启动public partial class App : PrismApplication { protected override Window CreateShell() { return Container.ResolveMainWindow(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterForNavigationMainPage, MainPageViewModel(); containerRegistry.RegisterForNavigationDetailPage, DetailPageViewModel(); } }注意这里MainWindow是一般的宿主窗口里面的实际内容是MainPage。MainPage在XAML里就是一个ContentControl或者ContentPagePrism的Region机制会把它加载进去。注册时注册的是内容页不是宿主窗口。看起来简单实际上背后发生了三件事容器被初始化、页面和ViewModel被注册到路由表、Shell窗口被解析出来。任何一步出错应用都起不来所以如果你照着写跑不通先检查是不是漏了PrismApplication继承或者没装Prism容器包。3.3 主页面里发起带参数的导航主页面ViewModel里注入INavigationService然后执行导航public class MainPageViewModel : BindableBase { private string _message; public string Message { get _message; set SetProperty(ref _message, value); } private readonly INavigationService _navigationService; public MainPageViewModel(INavigationService navigationService) { _navigationService navigationService; GoDetailCommand new DelegateCommand(GoDetail); } public DelegateCommand GoDetailCommand { get; } private async void GoDetail() { var parameters new NavigationParameters { { message, Message } }; await _navigationService.NavigateAsync(DetailPage, parameters); } }这段代码有几个地方值得细说第一INavigationService是构造函数注入的。这叫依赖注入Prism容器帮你把这个服务实例化并传进来你不用管它的生命周期。第二按钮的点击是通过DelegateCommand绑定到GoDetailCommand的不在Code-Behind里写事件。第三导航方法是异步的用await等待它完成。为什么强调异步因为Prism在导航过程中可能加载模块、实例化页面、解析依赖这些都是耗时操作如果你不等待后续处理会依赖一个未必完成的导航结果。3.4 详情页接收参数与返回逻辑详情页ViewModel实现INavigationAware在OnNavigatedTo里取数据public class DetailPageViewModel : BindableBase, INavigationAware { private string _receivedMessage; public string ReceivedMessage { get _receivedMessage; set SetProperty(ref _receivedMessage, value); } private readonly INavigationService _navigationService; public DetailPageViewModel(INavigationService navigationService) { _navigationService navigationService; GoBackCommand new DelegateCommand(GoBack); } public DelegateCommand GoBackCommand { get; } public void OnNavigatedTo(INavigationParameters parameters) { _receivedMessage parameters.GetValuestring(message); } public void OnNavigatedFrom(INavigationParameters parameters) { // 离开前的清理工作 } private async void GoBack() { await _navigationService.GoBackAsync(); } }取参数用GetValueT()注意泛型类型要和传参时对应。传的时候是字符串取的时候也用GetValuestring。如果你传的是对象记得对象要可序列化或者进程内可传递跨进程导航如果以后用Prism Modules做进程隔离会有额外的要求。返回时调用GoBackAsync()这个简单到难以置信但它确实就是Prism里标准的后退操作。它会把当前页面弹栈回到上一个页面。而且妙的是上一个页面的OnNavigatedTo会再次触发——这就是为什么前面说返回也能感知。整个流程跑通后你会发现主页面没有直接引用DetailPage类型也没有实例化它传递数据时没有用静态变量。所有页面交互都走Prism的统一通道替你把代码整理得清清爽爽。4. 核心细节解析与进阶玩法4.1 导航拦截不只是跳转还能拦下来普通的跳转只能选择跳或不跳但Prism给了一个更细粒度的控制导航拦截。这怎么理解打个比方导航就像送快递快递员从A站点出发去B站点。默认情况下货物顺利送达。但有时候你要在货物出发前检查一下是不是违禁品是不是收件人拒收Prism的INavigationAware和IConfirmNavigationRequest接口就是干这事的。INavigationAware能让你感知导航前面已经实现而IConfirmNavigationRequest能让你审批导航public class EditPageViewModel : BindableBase, IConfirmNavigationRequest { public void ConfirmNavigationRequest(NavigationContext navigationContext, Actionbool continuationCallback) { bool canNavigate !HasUnsavedChanges; continuationCallback(canNavigate); } public bool HasUnsavedChanges { get; set; } }这个场景太实用了。我做过一个表单编辑页用户填了一半数据误触返回如果直接退回去填的内容全没了。有了拦截我可以先弹一个确认框有未保存的内容确定离开吗用户点了确定才真正执行导航点了取消就留在原页面。注意continuationCallback是异步回调模式不是直接返回bool。这意味着你可以在确认框弹出等待用户点击后再回调框架等收到回调结果才决定导航是否继续。理解了这层再复杂的前置条件比如等待网络请求结果都能做。4.2 深链导航与多级回退实际项目中导航不会是简单的主页面 - 详情页 - 返回两级结构。更常见的是三级、四级页面而且用户可能横跨不同模块跳转比如从首页深链到一个三层之外的详情页。深链在Prism里怎么实现用绝对路径导航。先看概念相对路径和绝对路径的区别就像文件系统里相对路径和绝对路径的区别。// 相对路径从当前页面继续往下 await _navigationService.NavigateAsync(DetailPage); // 绝对路径从根页面开始重新构建导航路径 await _navigationService.NavigateAsync(/MainPage/ListPage/DetailPage);绝对路径开头的斜杠是个关键信号表示导航将从根重新计算。这在你做从通知栏点击通知直接跳到三层详情页这类需求时尤其有用——你不关心用户当前在哪一页你只关心最终要展示什么页面。Prism会帮你销毁当前堆栈按斜杠路径新建一串页面。但绝对路径有个隐藏的注意事项路径里的每一个页面都必须是已注册的页面而且路径顺序代表堆栈顺序。/MainPage/ListPage/DetailPage意味着MainPage在最底DetailPage在顶返回时依次经历ListPage、MainPage最后退出应用。如果你的MainPage是个NavigationPage或者承载其他Region的宿主页路径写法还要更细致一点。4.3 多Region协作导航Tab页和子页面的导航关系WPF里ContentControl加Region可以创建多个内容区Xamarin.Forms里TabbedPage、MasterDetailPage天然就是Region容器。这就带来一个常见困惑Tab页之间切换算不算导航实际上在Prism里页面静态结构内的切换比如Tab从一个切到另一个通常不靠导航框架完成而是靠Region内容切换。导航框架负责的是页面级的跳转。但如果你要在Tab页的某个子内容区域里做深层次跳转就需要用区域化导航Region Navigation了。用法是这样的先定义RegionContentControl prism:RegionManager.RegionNameMainRegion /然后向Region里导航_regionManager.RequestNavigate(MainRegion, DetailPage);这会将该Region显示的内容切换成DetailPage不影响其他Region。这在仪表盘类应用里极有用页面划分成多个区域某块区域操作只更新自己区域的内容其他区域保持不动。配合RegionMemberLifetime接口还能控制页面离开Region时是否保持ViewModel状态、是否销毁。这个进阶特性解决的最常见问题是一个页面承载了多种子页面状态但你又不想用UserControl堆一堆代码。Region Navigation把子区域也变成页面各大区块独立维护自己的ViewModel和生命周期代码组织和单独页面一样清爽。4.4 传参技巧不只是基础类型还能传对象和回调第3节示例传的是字符串但实际场景里你很可能需要传一个复杂对象甚至把一个回调委托传过去比如详情页编辑完要把结果回传给调用页。传对象没问题直接放进NavigationParametersvar item new Product { Id 1001, Name 键盘, Price 299 }; var parameters new NavigationParameters { { product, item } }; // 接收方 var product parameters.GetValueProduct(product);但要提醒你尽量保证这个对象可序列化。虽然Prism在同一个进程内传递对象不强制要求序列化但一旦你以后把模块拆成独立进程Prism Modules的进程隔离模式不可序列化的对象会导致运行时错误。尽早养成用POCO传参的习惯风险最低。再说传回调。有些场景下页面B要通知页面A某个操作结束了最优雅的方式不是广播也不是全局事件而是把Action当作参数传进去var parameters new NavigationParameters { { callback, new Actionobject(OnResultFromDetail) } }; await _navigationService.NavigateAsync(DetailPage, parameters);接收方拿到委托再调用public void OnNavigatedTo(INavigationParameters parameters) { var callback parameters.GetValueActionobject(callback); callback?.Invoke(resultData); }这样页面之间的数据回传变成了直接调用而且不用知道对方是谁——解耦程度又上了一个台阶。不过这个技巧也别滥用嵌套太深会造成调试困难一两个层次的回调足够覆盖绝大多数场景。4.5 导航事件拦截与会话保持的灵感来源也许你会好奇这一套导航拦截、回调、生命周期的设计是从哪来的。回头看看微软的Prism Library它的导航模型借鉴了浏览器和Web框架的理念URL代表页面位置导航参数类似QueryString拦截器类似路由器中间件。理解了这个血缘关系你就能把Web里路由守卫、全局拦截器的概念迁移过来用。Prism在WPF时代就默默奠定了导航应该是框架级的职责这一理念后来的MAUI生态也延续了类似的思路。虽然现在各种新技术层出不穷但导航与UI解耦这件事Prism做得早也做得很彻底值得学习。5. 常见问题与排查技巧实录5.1 页面找不到或导航静默失败这是我在Prism交流群里看到最多的提问我NaviagteAsync之后页面没反应也没有报错。排查步骤几乎可以固化下来按顺序走一遍基本能找到问题确认页面已注册。RegisterForNavigation里是否写了这个页面注意注册名和调用名是否一致。如果你注册时没指定名字框架会自动用去掉Page后缀的类型名所以调用NavigateAsync(DetailPage)而注册的是DetailPage类型但默认名是Detail这就会失败。确认ViewModel的构造函数可解析。Prism容器创建页面时会顺带解析ViewModel的构造函数参数。如果ViewModel构造函数里有一个没有注册的服务容器解析失败导航也会异常。检查依赖注入注册是否齐全。确认Shell窗口里的Region名称正确。RegionManager找不到对应Region时导航会静默失败。你可以在Shell XAML和导航代码里分别打个日志确认Region名一致。查看输出窗口的异常信息。Prism的异常不一定弹窗但会写到调试输出里。如果真的完全没有异常试着在NavigateAsync调用后检查返回的NavigationResultvar result await _navigationService.NavigateAsync(DetailPage); if (!result.Success) { // 打印 result.Exception Debug.WriteLine(result.Exception); }这个习惯强烈建议养成。很多不明显的导航失败结果里都藏着异常详情一眼就能定位。5.2 参数传了但接收不到传参没接住的场景也挺常见。先查接收方是否实现了INavigationAware并且OnNavigatedTo方法里取了参数。接口没实现参数当然接不到。再看一个容易忽略的细节如果你在返回时也传参数比如从B返回A时带个结果数据B里要这样写var parameters new NavigationParameters { { result, edit done } }; await _navigationService.GoBackAsync(parameters);A页面再拿参数。但注意GoBackAsync的参数传递A页面接收的地方也是OnNavigatedTo因为返回的时候A确实被导航到了。在这个回调里你要分清当前是首次进入还是从子页面返回依据就是参数里有没有预期的键。没有就用默认值兜底别想当然认为必定有值。还有个典型的坑ViewModel的构造函数里接收参数。虽然Prism的构造函数注入支持带参数的注册但导航参数不是通过构造函数传的千万别试图在ViewModel构造函数里取页面参数。统一走INavigationAware回调这是框架的约定。5.3 导航后页面UI不刷新页面导航成功了但显示的数据是旧的。这种问题多半和实例缓存有关。Prism默认每次导航都会创建新的页面实例这通常是符合预期的。但如果你用了RegionMemberLifetime或者自己缓存了ViewModel实例那页面复用时就可能带着上次的状态。排查思路很简单确认OnNavigatedTo是否每次都执行并在这个回调里重新赋值界面数据。另外检查ViewModel是否实现了INavigationAware且正确绑定到了View的DataContext。有时候Object被复用属性变更通知INotifyPropertyChanged没触发界面看起来就没刷新。用SetProperty设置属性值是Prism最常见做法它内部会触发PropertyChanged只要你的XAML绑定了这个属性UI就跟着更新。如果你手写属性然后忘了通知绑定界面死也不动——这种问题编译器不提只能靠调试器看属性值到底有没有变化。5.4 关于GraphPad Prism双纵坐标的说明既然标题带了热词搜索记录我单独澄清一句如果想要的是统计绘图软件GraphPad Prism比如网上常搜到的两个纵坐标如何设置那不是本文讨论的对象。那些教程一般在软件的Graph设置里添加右Y轴和.NET的导航框架完全是两个世界。搜索时认准Prism LibraryPrism WPF这些关键词就行别在统计数据软件和无头导航框架之间白绕路。5.5 性能问题导航越来越慢或者卡顿页面多了之后导航可能变慢。大多数人第一反应是页面本身复杂但其实导航框架也有可优化空间。Prism每次导航都要做解析目标类型 - 在容器中实例化页面和ViewModel - 执行依赖注入 - 触发生命周期回调。如果这些步骤里任何一个环节有耗时操作比如ViewModel构造函数里加载数据库数据、调用远程接口导航就卡住了。优化建议构造函数只做轻量初始化数据加载放到OnNavigatedTo异步处理。构造函数里做重活的坏处是容器解析时就得等待而OnNavigatedTo里加载数据可以配合加载动画用户体感好得多。还有个经常被忽视的性能点页面注册时如果用了模块化架构Prism需要加载模块再解析页面。这时候可以考虑按需注册模块而不是启动时就ALL IN。我见过一个项目启动时加载了30多个模块光模块初始化就卡了3秒。改成路由按需后启动速度明显提升。6. 实操心得与扩展建议6.1 我个人的使用体会踩过几次坑之后我对Prism导航的评价是学习曲线确实陡但用顺了就是生产力。最大的体会是它逼着你把代码按MVVM规范组织界面上不再有散落的导航逻辑ViewModel之间通过导航参数通信测试和排查都清晰很多。如果是小工具型项目只有两三个窗口老实说用原生导航就够了引入Prism是杀鸡用牛刀。但项目一旦上了规模页面层级深、模块多、需要跨页面传参和生命周期管理Prism导航的优势就完全体现出来了。6.2 最后分享一个实用小技巧调试导航时经常要看导航栈的状态但Prism没有可视化的栈查看器。我自己养成了一个习惯在导航基类ViewModel里加一个统一的日志输出每次OnNavigatedTo就把当前导航栈打出来。关键是取当前页面栈信息Prism的INavigationService没有直接暴露栈但你可以通过PageDialogService或者自己注入IRegionManager去读Region的Views把它们逐个输出。这样导航异常时日志里能看到完整路径定位问题快了不是一点半点。还有一个偷懒技巧把NavigateAsync封装成通用扩展方法统一处理成功失败日志。业务ViewModel只传页面名和参数不考虑异常处理异常统一在扩展方法里捕获记录。这样既精简了代码又保证了每个导航动作都有迹可循。Prism导航框架的可讲内容远不止这些比如模块化导航、ViewModelLocator配合导航参数解析、深层链路的参数包装都是可以继续挖的方向。但核心思路就那么多导航归ViewModel管、参数跟着导航走、生命周期可以拦截。抓住这三条主线后面再接触任何Prism导航的高级玩法你就都不会觉得晕了。
返回列表