UI 前沿技术

Windows Phone 7 中的页面和弹出框

Charles Petzold

下载代码示例

Charles Petzold
用户与计算机应用程序之间的交互通常会继续,而不会中断。我们可以在文字处理程序中键入文本,在电子表格中输入数字和公式,并在电子书阅读器中翻动页面。但有时候,应用程序需要用户提供其他信息,或者需要用户启动某个专用操作来向该应用程序提供其他信息。

在传统的 Windows 应用程序中,我们都知道接下来会出现什么情况:此时将出现一个对话框。请填写该对话框并单击“确定”;如果您改变主意,则请单击“取消”。

但是,在 Windows Phone 7 应用程序中会出现什么情况,我们并不十分清楚。Silverlight for the Web 和 Silverlight for Windows Phone 中均没有所谓的“对话框”。虽然我仍然喜欢使用这个术语来描述应用程序针对用户所采用的信息询问机制,但显然 Silverlight 对话框与传统对话框略有不同。

在本专栏的前面几个部分,我一直在尝试着构建一个基于纯文本书籍文件(可从 Project Gutenberg 下载)且适用于 Windows Phone 7 的电子书阅读器。现在到了用一整套对话框来增强该程序的时候了。

两种方法

要在适用于 Windows Phone 7 的 Silverlight 应用程序中实现对话框,您有两种方法可以选择。

也许最通用的方法,就是将对话框定义为单独的页面。首先从 PhoneApplicationPage 派生一个新类,并用一系列按钮、文本框以及诸如此类的项目填充该类。然后,通过某个页面的 NavigationService 对象导航到此对话框进行调用。使用“后退”按钮(或其他按钮)可以终止该对话框并返回到曾经调用该对话框的页面。

我将第二种方法称为“弹出框”方法。您可能认为这种方法要用到 Popup 类,但实际上并不需要。(我随后会讨论二者之间的不同。)通常,这种对话框派生自 UserControl 并在调用它的页面顶部显示,然后在用户完成与它的交互后消失。整个过程不涉及任何导航。

以可导航页面实现对话框是固有的模式。(模式对话框会禁止与调用它的窗口或页面进行交互,直到对话框关闭。)使用“弹出框”方法时,对话框可以为模式对话框或无模式对话框。实现模式弹出框要求程序员确保,在弹出框处于活动状态时,用户无法与基础页面交互。从这个意义上讲,实现无模式弹出框可能要略微简单一些,但通常而言,处理来自无模式对话框和基础页面的并行输入可能是个棘手的问题。

从我在七月份的专栏(msdn.microsoft.com/magazine/hh288085)中介绍的 MiddlemarchReader 程序开始,我的电子书阅读器新增了一个选项,用于显示 ListBox 中的章节列表。因此,您可以通过选择其中一个章节标题,跳转到该章节的开头。这在功能上相当于一个对话框,我选择以弹出框的形式来实现它,虽然其中的逻辑不太好理解:调用弹出框的 ApplicationBar 按钮也用于关闭弹出框,这就要求该按钮使用略有区别的图像。

现在想来,我本应以单独页面的形式来实现此对话框,就像我在 HorrorReader 程序中所做的那样,这一点我随后会在本文中进行介绍。为庆祝十月万圣节,HorrorReader 将向您提供以下四部可供阅读的经典恐怖作品:“Frankenstein”、“Dracula”、“Dr.Jekyll and Mr.Hyde”和“The Turn of the Screw”,如图 1 所示。(下个月,该电子书阅读器会将书库最终扩展到约 35,000 本图书,我保证!)

The MainPage Display of HorrorReader
图 1 HorrorReader 的 MainPage 显示

在将章节 ListBox 从弹出框更改为可导航页面的过程中,我一直在询问自己,为何要以弹出框的形式来实现 Windows Phone 7 对话框。我发现了自己暗藏在内心的令人吃惊的动机:恐惧。

对导航的恐惧

如果您做过 Windows Phone 7 编程工作,您可能会对逻辑删除有所了解:在某些情况下,正在运行的程序可能会被终止并从内存中删除。用户按下电话上的“开始”按钮以查看启动屏幕时、电话在一段时间内没有收到任何输入的情况下关闭其显示屏进入锁定状态时,或者用户手动关闭屏幕时,都会出现这种情况。

当用户解除对屏幕的锁定,或者按下“后退”按钮导航回相应的程序时,应用程序将重新启动。Windows Phone 7 操作系统将显示应用程序中的最后一个活动页面,但还原任何其他信息则完全是程序员的责任。通常,页面使用 PhoneApplicationPage 的 State 字典来保存与页面关联的瞬时信息,App 对象使用 PhoneApplicationService 的 State 字典来存储瞬时应用程序数据,而永久数据则需进行独立存储。

在为逻辑删除编写代码时,程序员会通过覆盖页面中的 OnNavigatedFrom 方法来保存页面信息,以及通过覆盖 OnNavigatedTo 方法来还原这些信息。当程序正在进行逻辑删除或在逻辑删除后进行恢复时,这并没有问题。但是,如果页面正在导航到另一页面,或正从该导航返回,则此时并不需要保存和还原页面信息。根据所涉及的信息量,此类额外活动可能会显著降低页面导航的速度。

要避免此类额外活动,一种方法是将应用程序限制为一个页面,并用弹出框来实现对话框!我在旧版电子书阅读器中就是这样做的。我之所以避免使用页面导航,是因为我害怕使用逻辑删除代码会降低它的速度。

但这样做很愚蠢。如果逻辑删除以我现在认为的“笨方法”来实现,这才会造成问题!页面不应保存大量状态信息,除非系统确实在对应用程序进行逻辑删除,而且页面也不应尝试还原这些信息,除非它正从逻辑删除状态恢复。Windows Phone 7.1 将为导航覆盖提供其他信息,从而以更智能的方式实现这些方法。同时,您可以将所有繁重的工作委托给 App 类来完成,App 类主要负责处理由 PhoneApplicationService 实现的事件。这些事件具体指示应用程序是正在进行逻辑删除,还是正在进行恢复。

通常,这样做还有利于加快 OnNavigatedTo 覆盖。这其中的逻辑其实相当简单:如果特定字段为空,则需要重新生成该字段;如果该字段不为空,则此对象与导航前相同,因为应用程序并未进行逻辑删除。

HorrorReader 的结构

HorrorReader 中的 App 文件具有两个公共属性,这两个属性可用于引用存储在独立存储中的对象。第一个属性 AppSettings 用于存储适用于所有书籍的应用程序设置。这些设置包括字体系列、字体大小和页面过渡样式。第二个属性为 App 的 CurrentBook 属性,它是一个 BookInfo 类型的对象,该对象具有所有与书籍相关的单个属性,包括具体书籍的文件名、当前章节和页面、存储分页数据的 ChapterInfo 对象集合,以及书签与批注的集合,它们都是这个版本的新增属性。图书库中的四部著作都有其各自的 BookInfo 对象(存储在独立存储中),但系统直到您初次阅读书籍时才会创建此 BookInfo 对象。

HorrorReader 具有六个派生自 PhoneApplicationPage 的类。这些类全部属于 HorrorReader 项目,并且可通过名称中的“Page”一词轻松识别出来。除 App 外,所有其他类都位于 Petzold.Phone.EBookReader 动态链接库中。

图 1 所示,MainPage 包含四个按钮,您可以选择所提供的四部著作中的其中一部。(该程序下月推出的版本将更换此页面。)用户单击这其中的一个按钮时,MainPage 将导航到 BookViewerPage,后者主要负责托管 BookViewer 控件并实现数个弹出框。

BookViewerPage 具有四个 ApplicationBar 按钮。前三个按钮用于导航到其他页面:ChaptersPage、BookmarksPage 和 AnnotationsPage。书签只是通过用户输入的标签对书籍中特定页面的引用。批注是与可选注释一起显示的文本选择。这三个页面均包含一个 ListBox,用于使 BookViewerPage 跳转到书籍中的新页面。

BookViewerPage 中的 ApplicationBar 菜单包含三个菜单项:“smaller text”、“larger text”和“settings”。前两个菜单项用于以 10% 的增量更改字体大小,“settings”用于导航到 SettingsPage,后者可用于实现一个选择字体和所需页面过渡用的 Pivot 控件,如图 2 所示。该页底部的文本是所选字体的预览。

The Settings Pivot for Font Selection
图 2 用于选择字体的 Settings Pivot

在该页面上进行设计时,我发现无法避免与 Windows Phone 工具包中的 Slider 和 GestureListener 类的明显冲突。我必须实现自己的 Slider,并在实现过程中使之仅以 10% 的增量进行跳转。

对于将 PhoneApplicationPage 派生类用作对话框,我不是很满意。我宁愿采用一种更加结构化的方法向对话框传递信息,并接收对话框返回的信息。在导航结构中,这种数据传输方式通常有些笨拙。例如,我会选择使用 BookViewerPage 向 ChaptersPage 传递对当前书籍的引用,使用 ChaptersPage 将用户选择的章节返回给 BookViewerPage。换言之,我希望 ChaptersPage 的效果与没有副作用的函数的效果类似。可能有一天我会为 PhoneApplicationPage 设计一个包装来执行上述操作。而对于 HorrorReader,则可以通过引用 App 类中的同一个 CurrentBook 属性,在所有页面间共享数据。

弹出框还是弹出窗口?

虽然我将 PhoneApplicationPage 派生类用于处理章节列表、书签列表、批注列表和设置,但我仍然需要几个弹出框。

在 Silverlight for Windows Phone 中,通常有两种方法可用于实现弹出框。一种方法,是直接将一个控件(或者,更加常见地,一个 UserControl 类)放入页面的可视树中。该控件位于其他所有项目的顶端,但其 Visibility 属性已初始化为 Collapsed。在需要弹出弹出框时,只需将 Visibility 属性设置为 Visible 即可。

对于模式弹出框,您还希望禁用页面上的所有其他项目。您可以通过将基础控件的 IsEnabled 属性设置为 false,或将 IsHitTestVisible 属性设置为 false,来实现这一目的。这两个属性的作用类似,但 IsEnabled 仅限于控件,而 IsHitTestEnabled 还可用于 FrameworkElement 派生类(如面板)。IsEnabled 属性还可用于使某些控件淡出。另一个方法,是通过禁止触摸输入的半透明背景使弹出框全屏显示。无论您使用哪一种方法,您可能还需要禁用 ApplicationBar。(我稍后将详细说明这个小问题。)

此外,您还可以使用 Popup 元素。Popup 元素具有一个 Child 属性,您可以将该属性设置为一个 UserControl 派生类。(我希望我可以从 Popup 派生,但它是密封的。)默认情况下,Popup 的 IsOpen 属性为 false,您可以将其设置为 true,使 Popup 的子元素可见。Popup 还提供方便用户使用的 HorizontalOffset 和 VerticalOffset 属性,用于确定子元素的位置。

Popup 有意思的地方在于它不需要父元素。换句话说,不一定要将它设置为可视树的一部分。您只需在代码中创建一个 Popup 对象,将 Child 属性和 IsOpen 属性设置为 true,该对象就会在其他项目的顶部显示。

但是请注意:如果 Popup 没有父元素,则 HorizontalOffset 和 VerticalOffset 属性是相对于 PhoneApplicationFrame 对象的左上角而定义的,这在概念上构成了所有 PhoneApplicationPage 对象的基础。该框架包括屏幕顶部的系统托盘,但弹出框不会在系统托盘的顶部显示。如果系统托盘可见,则会在顶部截断 Popup 的子元素。只有覆盖 PhoneApplicationPage 的那一部分可见。您希望将 VerticalOffset 属性设置为非零值,使之与系统托盘相适应。

没有父元素的 Popup 的优点在于,您可以轻松地将其设置为模式元素。您只需将 PhoneApplicationPage 派生类的 IsEnabled 属性设置为 false,即可一次性地有效禁用页面上的所有内容。

噢,那不是很好吗?但事实并非如此,因为 ApplicationBar 不是页面可视树的一部分。您还需要单独禁用 ApplicationBar。它有一个方便的 IsMenuEnabled 属性,但没有一个单独的属性可用于禁用这些按钮。您必须单独处理这些按钮。

至于我需要的那两个弹出框,我希望将它们移动到页面的不同位置。如果对话框是 Popup 的子元素,则可以轻松做到这一点。否则,您将需要在对话框控件上使用 TranslateTransform。为简化逻辑删除,我决定在设计时保持一致,将 Popup 元素用于所有弹出框。我还决定在页面的可视树中定义每个 Popup 元素和子元素。因此,HorizontalOffset 和 VerticalOffset 属性是相对于页面而不是框架的。

其中的一个弹出框是在 BookViewerPage.xaml 中定义的名为“textSelectionMenu”的 ListBox。这是您在进行文本选择时显示的菜单,如我在九月份的专栏(msdn.microsoft.com/magazine/hh394142)中所述。其中一个选项为“note”,用于调用另一个弹出框,该弹出框由名为 AnnotationDialog 的 UserControl 派生类定义。这便于用户键入注释,如图 3 所示。

The AnnotationDialog Pop-Up
图 3 AnnotationDialog 弹出框

使用手指快速上下翻动页面即可定义新书签。您可以使用 BookmarkDialog 输入标签。

BookmarkDialog 和 AnnotationDialog 还可以在 BookmarksPage 和 AnnotationsPage 上分别显示,用于编辑现有项目。您还可以删除这些页面上的书签或注释,此时会调用另一个称为 OkCancelDialog 的弹出框。该弹出框与 Windows Phone 7 中使用的标准 MessageBox 类似,只是在显示时不会发出任何声音。

最后一个弹出框为 FindDialog,它由第四个 ApplicationBar 按钮调用,如图 4 所示。您可以使用这个弹出框通过熟悉的选项在书籍中搜索文本。找到每一个匹配项后,匹配的文本将突出显示,FindDialog 会跳转到相关页面的顶部或底部,以使突出显示的文本可见。

The FindDialog Pop-Up
图 4 FindDialog 弹出框

对弹出框进行逻辑删除

是否应允许对弹出框进行逻辑删除?以下是我设想的情形:假设我正使用如图 4 所示的弹出框在一本著作中搜索某些文本。然后我将电话放在桌上。几分钟后,我拿起电话,此时它的屏幕处于锁定状态。我按下“开机”按钮并关闭墙纸。FindDialog 在显示时是否仍会保持当初使用时的状态?

我的答案是(虽然这不是我想要的):当然会。我必须承认,这是用户期待的情况,并且是应该出现的情况。这意味着需要对弹出框进行逻辑删除。

因此,我在任意托管 Popup 的页面中定义了一个名为 activePopup 的 Popup 类型字段,只要弹出框可见,就设置该字段。如果此字段是在调用 OnNavigationFrom 的过程中设置的,只能说明系统正在对程序进行逻辑删除,因为在弹出框可见时,所有其他项目均被禁用。在这种情况下,页面会在其 State 字典中保存该 Popup 的名称。它还会检查 Popup 的子元素是否实现了 ITombstonable 接口,该接口我是通过下面的这两个方法定义的:SaveState 和 RestoreState。AnnotationDialog、BookmarkDialog、OkCancelDialog 和 FindDialog 弹出式对话框就是这样在逻辑删除过程中保存和还原其当前状态的。

“后退”按钮覆盖

关于 Windows Phone 7 编程,我撰写了整整一本书,但其中却连一个说明如何覆盖由 PhoneApplicationPage 定义的 OnBackKeyPress 方法的示例都没有。我真笨。此方法中提到的“后退”按键是电话上三个硬件按钮中最左侧的按钮。默认情况下,使用“后退”按钮可从一个页面导航回调用它的页面。如果应用程序位于其主页上,则“后退”按钮将终止该程序。

在我以弹出框的形式实现各种对话框时,很明显,我需要相当频繁地覆盖“后退”按钮的行为。大致规则如下:任何时候,如果认为用户在按下“后退”按钮时不希望离开页面或终止应用程序,则应覆盖 OnBackKeyPress。

任何能够托管弹出框的页面(在我的示例中为 BookViewerPage、AnnotationsPage 和 BookmarksPage)均应覆盖“后退”按键,使之能够关闭弹出框,就像在传统对话框中单击窗口“关闭”按钮一样。OnBackKeyPress 方法的参数是 CancelEventArgs 的实例;将 Cancel 属性设置为 true 可禁止“后退”按键执行正常的导航功能。

如果弹出框处于活动状态且弹出框上的 TextBox 具有输入焦点,则会显示屏幕键盘。此时按下“后退”按键将自动关闭该键盘。再次按下“后退”按键将关闭弹出框。再次按下“后退”按键将导航回前一页面。在任何情况下,显著的视觉变化将为用户提供良好的反馈,说明按下“后退”按键的具体效果。

在覆盖 OnBackKeyPress 时,请谨慎行事。绝不能让用户陷入这样的循环:连续按下“后退”按钮也无法终止应用程序!如果出现这种情况,则绝不能向 Windows Phone 7 市场推出该程序。

为前端做好准备

在编程过程中,支持某个实体的一个实例与支持多个实例通常是有很大区别的。除 HorrorReader 中的各种页面和弹出框外,该程序已对我之前的电子书阅读器进行了大幅改进,因此我可以阅读四部,而不仅仅是一部著作。每部著作都自带存储在独立内存中的 BookInfo 对象,因此均独立于其他著作。

现在,您只需要用一个新的、能够从 Project Gutenberg 站点下载书籍的前端来替换 MainPage,而不需要进行其他更改。这应该很简单,对吧?

Charles Petzold《MSDN 杂志》 *的长期特约编辑。*他的新书《Programming Windows Phone 7》(Microsoft Press,2010 年)可从 bit.ly/cpebookpdf 免费下载获得。这个月正值 Petzold 加入《MSDN 杂志》及其前身 Microsoft Systems Journal 25 周年。

衷心感谢以下技术专家审阅本文:Richard Bailey