(本文档是WPF的帮助文档,但大部分内容也适用于WP7)
导航服务
虽然 Hyperlink 允许用户发起转向特定 Page 的导航,但是定位和下载该页的工作仍由 NavigationService 类执行。从根本上说,NavigationService 提供了代表客户端代码(如 Hyperlink)处理导航请求的功能。此外,对于跟踪和影响导航请求,NavigationService 实现了更高级别的支持。
单击 Hyperlink 时,WPF 将调用 NavigationService.Navigate 来定位和下载位于指定 pack URI 的 Page。下载的 Page 将转换为对象树,其根对象为下载的 Page 的实例。对根 Page 对象的引用存储在 NavigationService.Content 属性中。导航过的内容的 pack URI 存储在 NavigationService.Source 属性中,而 NavigationService.CurrentSource 存储所导航的上一页的 pack URI。
如果导航是在标记中使用 Hyperlink 以声明方式实现的,则无需了解 NavigationService,这是因为 Hyperlink 代表您使用 NavigationService。这意味着,只要 Hyperlink 的直接或间接父级为导航宿主(请参见导航宿主),Hyperlink 就能够找到和使用导航宿主的导航服务来处理导航请求。
但是,某些情况下,需要直接使用 NavigationService,这些情况包括:
在这些情况下,需要编写代码,以编程方式调用 NavigationService 对象的 Navigate 方法来发起导航。这样就需要获得对 NavigationService 的引用。
由于导航宿主一节中所提及的原因,WPF 应用程序可以有多个 NavigationService。这意味着代码需要一种方法以找到 NavigationService(通常就是导航到当前 Page 的 NavigationService)。 通过调用 staticNavigationService.GetNavigationService 方法可以获取对 NavigationService 的引用。若要获得导航到特定 Page 的 NavigationService,请将对 Page 的引用传递为 GetNavigationService 方法的参数。下面的代码演示如何获取当前 Page 的 NavigationService。
Page 实现了 NavigationService 属性,作为查找 Page 的 NavigationService 的快捷方法。下面的示例对此进行演示。
下面的示例演示如何使用 NavigationService 以编程方式导航到 Page。由于要导航到的 Page 只能使用单个非默认构造函数来实例化,因此需要编程导航。下面的标记和代码演示具有非默认构造函数的 Page。
下面的标记和代码演示导航到具有非默认构造函数的 Page 的 Page。
如果需要以编程方式构造 pack URI(例如,如果只能在运行时确定 pack URI),可以使用 NavigationService.Navigate 方法。下面的示例对此进行演示。
如果 Page 的 pack URI 与存储在 NavigationService.Source 属性中的 pack URI 相同,则不会下载该页。若要强制 WPF 再次下载当前页,可以调用 NavigationService.Refresh 方法,如下例所示。
正如您看到的那样,发起导航的方式有很多。发起导航时,或正在进行导航时,都可以使用 NavigationService 实现的下列事件来跟踪和影响导航:
Navigating.在请求新导航时发生。可用于取消导航。
NavigationProgress.在下载过程中定期发生,以提供导航进度信息。
Navigated.在已定位和下载页后发生。
NavigationStopped.在导航停止(通过调用 StopLoading)时发生,或者在当前导航进行过程中请求新的导航时发生。
NavigationFailed.在导航到所请求内容出错时发生。
LoadCompleted.在导航到的页已加载、分析并已开始呈现时发生。
FragmentNavigation.在开始导航到内容片段时发生,具体情况为:
如果所需片段在当前内容中,则立即发生。
如果所需片段在其他内容中,则在加载源内容之后发生。
导航事件的引发顺序如下图所示。
通常,Page 与这些事件无关。应用程序与这些事件的关系可能更大,因此,Application 类也会引发这些事件:
每当 NavigationService 引发事件时,Application 类就会引发相应的事件。Frame 和 NavigationWindow 提供相同的事件,以检测二者各自范围内的导航。
某些情况下,Page 可能需要这些事件。例如,Page 可能处理 NavigationService.Navigating 事件以确定是否取消从自己发出的导航。下面的示例对此进行演示。
如上例所示,如果针对 Page 中的某个导航事件注册了相应的处理程序,则还必须注销该事件处理程序。否则,对于 WPF 导航使用日记记住 Page 导航来说,可能会产生副作用。
WPF 使用两个堆栈来记住导航过的:一个后退堆栈和一个前进堆栈。从当前 Page 导航到新 Page,或者前进到现有 Page 时,当前 Page 将添加到后退堆栈中。从当前 Page 导航到上一 Page 时,当前 Page 将添加到前进堆栈中。后退堆栈、前进堆栈和用于管理它们的功能统称为日记。后退堆栈和前进堆栈中的每一项都是 JournalEntry 类的实例,称为“日记条目”。
请考虑这样一个 XBAP,它具有多个包含丰富内容的页,这些内容包括图形、动画和媒体。这类页面可能占用大量内存,使用视频和音频媒体时这种现象尤为明显。如果日记“记忆”导航过的页,则此类 XBAP 可能很快就会明显消耗大量内存。
因此,日记的默认行为是在每个日记条目中存储 Page 元数据,而不是存储对 Page 对象的引用。在导航到日记条目时,其 Page 元数据用于创建指定 Page 的新实例。因此,导航过的每一个 Page 都有如下图所示的生存期。
虽然使用默认日记行为可以减少内存消耗,但是可能导致逐页呈现性能降低;重新实例化 Page 可能消耗大量时间,尤其是在有大量内容的情况下。如果需要在日记中保留 Page 实例,可以采用两种技术来实现。第一种是,调用 NavigationService.Navigate 方法,从而以编程方式导航到 Page 对象。
第二,通过将 KeepAlive 属性设置为 true(默认为 false),可以指定 WPF 在日记中保留 Page 的实例。如下例所示,可以在标记中以声明方式设置 KeepAlive。
活动状态的 Page 的生存期与非活动状态的页稍有不同。在首次导航到活动状态的 Page 时,它的实例化方式与非活动状态的 Page 相同。但是,由于 Page 的实例已保留在日记中,因此,只要它还在日记中,就不会再次实例化。因此,如果 Page 具有需要在每次导航到 Page 时都调用的初始化逻辑,就应将该逻辑从构造函数移到 Loaded 事件的处理程序中。如下图所示,每当导航到 Page 和从其导航到别处时,仍然会分别引发 Loaded 和 Unloaded 事件。
如果 Page 不处于活动状态,不应执行以下任何操作:
存储对该页或其任何部分的引用。
不是由该页实现的事件注册事件处理程序。
如果执行上面的任一操作,则会创建强制 Page 保留在内存中的引用,即使该页从日记中移除后,它仍保留在内存中。
通常,应首选默认 Page 行为,即不使 Page 保持为活动状态。不过,这涉及到将在下一节中讨论的状态问题。
如果 Page 不活动,并且它具有用于收集用户数据的控件,则当用户离开再返回 Page 时,数据会发生什么情况?从用户体验的角度讲,用户希望看到自己此前输入的数据。遗憾的是,因为每次导航都会创建 Page 的新实例,所以收集数据的控件将重新实例化,从而丢失数据。
所幸的是,日记为跨 Page 导航记忆数据(包括控件数据)提供了支持。具体地说,每个 Page 的日记条目都用作关联的 Page 状态的临时容器。下面的步骤概述在从 Page 导航到其他位置时是如何使用此支持的:
在使用日记导航回 Page 时,将执行以下步骤:
如果 Page 上使用了以下控件,WPF 会自动使用此支持:
如果 Page 使用这些控件,则在进行跨 Page 导航时,会记住输入这些控件的数据,如下图中的“Favorite Color”(喜好颜色)ListBox 所示。
如果 Page 含有上面列表中未列出的控件,或者状态存储在自定义对象中,则需要编写代码使日记能够记忆跨 Page 导航状态。
如果需要记忆少量跨 Page 导航状态,可以使用通过 FrameworkPropertyMetadata.Journal 元数据标志配置的依赖项属性(请参见 DependencyProperty)。
如果 Page 需要跨导航记住的状态包含较多数据,则将状态封装在单个类中,然后实现 IProvideCustomContentState 接口,可以减少代码量。
如果需要在单个 Page 的各状态之间导航,而又不离开 Page 本身,则可以使用 IProvideCustomContentState 和 NavigationService.AddBackEntry。
如果需要将数据从一个 Page 传到另一页,可以将数据作为参数传给 Page 的非默认构造函数。注意,如果要使用此方法,必须使 Page 保持为活动状态;否则,当下次导航到该 Page 时,WPF 将使用默认构造函数重新实例化该 Page。
另一种办法是,Page 可实现用需要传入的数据设置的属性。但是,当 Page 需要将数据传回导航到它的 Page 时,就比较麻烦。问题是导航本质上不支持可保证在从 Page 离开之后将再返回到该页的机制。导航实质上不支持调用/返回语义。为了解决此问题,WPF 提供了 PageFunction<T> 类,使用该类可确保以可预知的结构化方式返回到 Page。有关更多信息,请参见结构化导航概述。