课程内容
TODO List使得我们能够快速、简单并且有效地管理任务。我们不仅可以用带颜色的五角星和具体的描述来标记的任务,而且也可以用多种方式来进行过滤,比如,按照已经过期的任务、今天需要完成的任务或者带星级的任务来对任务进行过滤。在浏览“已经完成”的任务列表时,我们也可以对任务进行撤销。一般情况下,我们寻找所关心的任务时,会触发过滤器。
相对于本书的其他应用程序而言,TODO List包含的代码更多,主要是由List管理所带来的。List管理包括任务的浏览、任务明细的查看、任务的排序、新建或删除任务以及任务的编辑。该应用程序总共有5页,包括主页面、新建/编辑任务页面、设置页面和帮助页面。该应用程序涉及的任务管理的页面设计和代码可以很容易地移植到其他类似的任务管理应用中。但是,TODO List应用的主要目的是展示pivot控件。Pivot是Windows Phone 7平台引入的两个独具特色的控件之一(另一个就是下一章介绍的panorama控件)。
The Pivot Control
Pivot是一个接受用户点击的控件,我们可以在Pivot上进行水平的滑动,或者通过点击header来切换不同的视图。这种类型的控件已经被内置到Mail、 Calendar和Settings页面,而且它还被很多其他内置的应用所使用,包括Internet Explorer、Maps、Marketplace、Music + Videos、People和Pictures。
Pivot控件可以对同一个数据集显示过滤后的视图(比如Mail应用);对同一个数据集显示不同的视图(比如Calendar应用);或者是为独立的数据集提供简单的、可切换的视图(比如Settings应用程序中,application是一个数据集,而settings就是一个它的视图)。但是,Pivot控件并不会用于任务所包含步骤的顺序显示,比如新建工程的用户向导界面。一般情况下,Pivot控件占据整个页面,除了那种需要应用程序栏或者状态栏的应用。
就像list box和list picker控件一样,Pivot是一个items控件。虽然Pivot类中Items集合可以添加任意的对象,但是其类型只能是PivotItem的对象或者是数据对象。
PivotItem是一个简单的内容控件,它具有Content和Header属性。虽然这两个属性可以设置为任意值,但是一般情况下,Content被设定为panel,就像一个包含了复杂用户界面的grid控件一样;而Header一般被设置为一个字符串。
在“Windows Phone Application”类型的项目中,默认不包含对pivot 和 panorama类型控件的引用!
虽然pivot 和panorama类型控件的命名空间(Microsoft.Phone.Controls)和PhoneApplicationPage等其他常用的控件类似,但是它们是定义在Microsoft.Phone.Controls的二进制集中(而PhoneApplicationPage是定义在Microsoft.Phone二进制集中)。
在使用pivot 和panorama控件时,需要添加对Microsoft.Phone.Controls.dll的引用。如果我们在Visual Studio中新建工程时,以“Windows Phone Pivot Application” 或 “Windows Phone Panorama Application”类型为模板,那么工程中就默认添加了对Microsoft.Phone.Controls.dll的引用。同样,如果我们在Visual Studio的Add New Item中,选择了“Windows Phone Pivot Application” 或 “Windows Phone Panorama Application”,那么工程就会自动添加对Microsoft.Phone.Controls.dll的引用。
以下是设计应用程序时,pivot控件需遵循的三条设计指导原则:
➔ 除特有的名称之外,Header中的文本应该小写。
➔ 正如前文所述,不要试图使用pivot控件来设计连续的用户必须完成任务。
➔ 在单个pivot控件中,不要使用超过7个页面。
A Pivot without PivotItems
在没有PivotItes的情况下,Pivot是不可用的。Pivot利用该控件来存放每个记录的头和内容。因此,如果我们尝试使用其他不同的UI元素时,应用程序会抛出“Element is already the child of another element”的异常。但这不是问题,因为没有理由不使用PivotItems,它可以包含任何对象,所以我们可以将需要的内容嵌入其中。我们还可以将非可视化的数据对象添加到Pivot中,使用ItemTemplate和HeaderTemplate属性来设置合适的格式。
The Main Page
TODO List的主页面使用了Pivot控件。它包含了5个pivot items,图26.1显示了第一次运行应用程序时,状态为空的情况。
图26.1 五个初始化状态的Pivot item页面
➔ 由于Pivot需要唯一的命名空间,因此需要使用一个独立的XML命名空间。常用的XML命名空间的前缀为:controls。
➔ 使用Pivot意味着应用程序处于全屏状态,所以它包含了一个Title属性,就像普通的page header一样,我们可以使用它来显示应用程序的名称。该控件在显示应用的标题方面模仿的很不错,但需要注意的是位置和字体大小有些不一样。幸好,我们可以对Title的外观进行自定义。Title是一个类型对象,所以我们可以将它设置为UI中的任意元素,而不只是一个简单的字符串。或者,我们可以使用TitleTemplate属性来自定义它的外观。图26.2显示了TitleTemplate对于标题字符串的作用效果。在Windows Phone以后的发布中,如果Silverlight支持本地文字间距排版的话,可能会处理好这个问题。但是从目前来看,应用自定义模板是无法实现的。
图26.2 用户自定义标题对默认的Pivot标题外观进行了细微的改变
➔ Pivot同时也提供HeaderTemplate属性来自定义每个pivot item的标题。但是,默认的标题与系统内置的应用是相吻合的,所以大多数应用程序一般不会使用该属性。如果我们想做标题的自定义,比如在每个标题中放入文本和图片,那么这个属性就有用武之地了。
➔ 每个pivot item包含一个text block控件(在显示列表为空时显示)和一个嵌入list box的grid控件。在list box中的每条记录内嵌了图片,或者对文本进行了修饰。
➔ 前四个list box拥有相同的item模板,该模板被称为DataTemplate。但是,“done” pivot页面的list box使用了它自身的模板,如图26.3所示,该模板加入了检查标记和删除线的效果。
图26.3 “done” list box中的item模板加入了检查标记和删除线效果。
➔ 两种模板利用Silverlight for Windows Phone Toolkit中的ContextMenu元素,在每个item中加入了上下文菜单。在使用上下文菜单时,我们只需要将ContextMenuService.ContextMenu属性设置为接收用户touch-and-hold手势的元素。在用户保持touch-and-hold手势一秒钟以后,上下文菜单就显示我们添加在菜单中的内容。详见图26.4。
图26.4 “Do the dishes”的上下文菜单展示了三种不同的任务选项。
在Windows桌面平台上,上下文菜单通常包含了对默认的item的单击处理,而且还可以加粗显示。在Windows Phone平台上,上下文菜单不应该包含默认的单击处理。相反,上下文菜单应该保留给那些无法通过页面方法触发的行为。比如,本例中的上下文菜单并没有显示“view details”,因为对于每条记录的单击行为就已经完成了这个功能。遵循这条设计原则,不仅使得我们应用程序的上下文菜单与系统内置的应用程序相一致,而且也节约了宝贵的屏幕显示资源。
尽管TODO List应用中没有用到下面的Pivot控件事件,但是它们对于动态Pivot页面非常有用:
➔ SelectionChanged:当前屏幕上的Pivot页面切换时触发。
➔ LoadingPivotItem:一个Pivot页面第一次显示前触发。
➔ LoadedPivotItem:一个Pivot页面第一次显示后触发。
➔ UnloadingPivotItem:将一个Pivot页面从Pivot页面集合中删除前触发。
➔ UnloadedPivotItem:将一个Pivot页面从Pivot页面集合中删除后触发。
Pivot控件的页面延时加载机制提高了程序启动的性能,但在很多流行的应用中,都使用以上这些事件来提高程序性能,甚至是它们自身的pivotitem虚拟化机制。
MainPage.xaml.cs
➔ Pivot控件具有SelectedItem和SelectedIndex属性,它表示了目前哪个Pivot页面占据了屏幕。TODO List应用将存储当前的页面,但它只是存储元素的名称,而非它的索引。我们可以通过这种方法来实现,那是因为本应用程序的设置页面允许用户隐藏除第一页以外的任何Pivot页面,这种隐藏其实就是将Pivot页面从Pivot集合中移除。
在Loading事件之前设置Pivot的SelectedItem或者SelectedIndex属性会导致操作失败!
在OnNavigatedTo事件中设置Pivot的SelectedItem或者SelectedIndex属性,这看上去很自然。但这是一个系统的Bug,到目前为止还没有解决。在这个问题解决以后,使用Loading事件来设置选择的页面。
设置Pivot的SelectedItem或者SelectedIndex属性可以改变当前的页面选择!
当我猜测这两个属性的使用方法时特别恼火。比如,当应用程序被激活,我们想要Pivot恢复之前的状态时(假设应用程序一直在运行),希望它能够立即显示之前选择的页面。一个变通的做法是,物理上改变Pivot页面的顺序,使得之前选择的页面永远是第0个页面,并且,不要再索引的基础上写代码。
设置Pivot页面的可见性不会起到效果!
暂时隐藏Pivot页面的操作比较简单,我们只要将它的Visibility属性设置为Collapsed就可以了。但是,因为这个没有起到作用,所以唯一隐藏Pivot页面(并且不让它占据空间)的方法就是把它从Pivot页面集合中删除。
根据Windows Phone设计原则,如果用户有方法可以往空白的Pivot页面中添加信息的时候,我们不应该把这个Pivot页面删除。相反,我们应该显示这个空白的Pivot页面,或者是在上面放置一条说明性的信息,就像TODO List中页面的处理方式。
➔ 在OnNavigatedTo函数(在设置页面中调整记录的可见性以后,返回时调用该函数)中,Pivot里面显示的记录根据当前的设置进行添加或者删除。
➔ Pivot对于其页面删除的处理并不优雅。如果Pivot页面被删除,使得之前选择的索引大于刚刚选择的索引的话,会抛出ArgumentOutOfRangeException的异常。即使在删除Pivot页面之前,将SelectedIndex属性设置为0,这种情况也会发生,推测这是由于旧页面切换到新页面时的动画过渡引起的。这是Windows Phone将来的版本中需要解决的Bug。
因此,针对这个问题,本应用程序在OnNavigatedFrom函数中,设置SelectedIndex为0。通过这种方法,即使用户访问设置页面,在删除Pivot页面后快速返回主页面,仍然有充足的时间来完成页面的切换。所以,如果之前选择的页面被删除,那么Pivot会返回到第0个页面。这是通过Loaded中的逻辑实现的,该逻辑在OnNavigatedTo执行以后,恢复选择的页面。
➔ “所有”的list box与 TaskList设置建立数据绑定,“已完成”的list box与DoneList设置建立数据绑定。剩余的三个list box包含的是TaskList数据集过滤以后的数据。它们在RefreshLists中进行手动填充,因为对于过滤数据集的操作并没有自动数据绑定机制。
➔ 上下文菜单的打开和关闭事件用来对上下文菜单是否被打开进行按需检查。当这次点击引起已经打开的上下文菜单被解散时,ListBox_SelectionChanged事件凭借这个来忽略用户对页面的点击。
➔ 由于上下文菜单的处理是同一个函数,所以我们编写的代码必须对多个上下文菜单均适用。发送者将被用户点击的MenuItem发送给处理函数,所以它的DataContext属性是用来获取使用上下文菜单模板的item。
在处理上下文菜单的点击事件时,如何获取点击并且保持的菜单?
对于放置在数据模板中的上下文菜单,这个问题经常会被问到,那是因为没有办法把特定的菜单项与数据对象联系起来。这个问题的答案是使用菜单项的DataContext属性。我们开始考虑DataContext时,想到的是把它设置为一个数据对象,但对于这种情况而言,获取它的值是非常有用的。
当用户想要隐藏上下文菜单时,注意不要像往常一样处理点击事件!
理想情况下,系统为你处理这些,但事实是不会。在很多情况下,上下文菜单打开时,我们应该进行追踪,这样的话,我们可以合理地忽略那段时间里面触发的一些事件。上下文菜单的打开和关闭事件使得我们可以做到这一点。
Supporting Data Types
正如前文所述,TODO List应用程序控制着两个设置任务集合。在我们理解这个应用程序如何运行时,需要认识三个重要的类。Task类用来展示主页面list box中显示的那些记录。
➔ 主页面的Item模板包含了每个任务中Title和Star属性的值。所有的属性显示在任务明细和添加/编辑页面中,DueDate属性也用来任务列表的排序。
➔ 一方面,CreatedDate 和 ModifiedDate属性设置为DateTimeOffset类型,而不是DateTime类型,这样更加合理;另一方面,这也是为了与其他类型的匹配(我们可能会提出这样的质疑,DueDate属性应该设置为DateTime类型,它代表当前时区内的一个逻辑时间点,第20章中的Alarm Clock应用也使用了DateTime)。
➔ Star属性的值是一个字符串,它代表颜色(如红色或者黄色)。这从API的角度来看显得有些奇怪,但是它的确很实用,因为主页面的item模板和任务明细页面上的星标可以直接与属性进行绑定,而不需要值转换器。
➔ 属性更改的通知确保数据绑定的用户界面元素可以保持更新。这在主页面和任务明细页面中得到了体现。在主页面中,由于编辑任务的缘故,使得只有“done”列表需要它。这会在接下来的“添加/编辑页面”一节中介绍。
Settings.cs
➔ 前五个设置保存了主页面上Pivot控件的状态,下一项设置(CurrentTask)保存了主页面上选中的任务明细和添加/编辑任务页面。
➔ 最重要的是最后两项设置,即未完成的任务列表和已完成的任务列表。注意,这是两个不同类型的集合。DoneList是一个任务基本的可观察集合,不包含任何的排序,所以列表总是按照完成的先后次序排列。(如果用户想要更改次序,他们需要首先将任务标记为“未完成”,然后再把任务标记为“完成”。)另一方面,TaskList是一个可观察集合,它会按照DueDate属性的值,对任务按照时间顺序进行自动排序。因此,利用这个性质,对于主页面上的每一个list box(除done list以外),不再需要额外的代码来实现任务的排序了。
➔ 以上两种list的可观察特性是很重要的一点,因为在记录内容被添加或者删除时,主页面依靠集合更改通知来使得“all”和“done”两个列表中的内容保持更新。
SortedTaskCollection.cs
➔ 该类的实现中,需要重写ObservableCollection的被保护的InsertItem方法,它最终会被Add和Insert方法调用。在实现时,它忽略了传入的索引值,相反,它选择了维持list需要的排序的索引值。这对于那些尝试调用集合中带特定索引值的Insert方法的人来说,显得有些迷惑,但调用Add方法时,是没有问题的。
➔ 在这个集合的实现中,最微妙的部分是CollectionDataContract属性。该属性在System.Runtime.Serialization二进制集的System.Runtime.Serialization命名空间中定义(默认的Windows Phone应用程序模板不包含对它的引用),它对于本应用程序中设置内容的序列化起有很大的作用。但是,其中的缘由很晦涩。因为SortedTaskCollection由ObservableCollection<Task>而来,就系统内置的序列化过程而言,这两个类拥有相同的数据字段名。但是,每种被序列化的类型必须有一个唯一的数据字段名,因此,CollectionDataContract属性分配一个给SortedTaskCollection(我们甚至不需要选择一个显式的名字就可以使它实现)。
没有这个属性,在应用程序关闭或者休眠的时候,由于尝试自动序列化应用程序的设置信息,会抛出如下异常:
Type‘System.Collections.ObjectModel.ObservableCollection`1[WindowsPhoneApp.Task]’ cannot be added to list of known types since another type ‘WindowsPhoneApp.SortedTaskCollection’ with the same data contract name
‘http://schemas.datacontract.org/2004/07/WindowsPhoneApp:ArrayOfTask’ is already present.
注意,如果两个列表都是SortedTaskCollection类型的话,即使没有这个属性,设置信息可以正常序列化,因为没有出现冲突。当然,在我们设计类的时候,可以设置这个属性,一面将来出现一些不必要的麻烦。
除了CollectionDataContract属性是为集合类所设计之外,System.Runtime.Serialization也提供了DataContract属性,它可以用在普通(非集合)类中使用。
一般情况下,在数据无法序列化到隔离存储空间或者页面状态的时候,我们得到的唯一提示就是:在应用程序再次启动或者激活的时候,数据不存在了。为了能够看到数据序列化失败的详细异常信息,我们可以在Visual Studio中将程序运行在debugger状态下,并且将其设置为“捕获所有首次出现的.NET异常”。我们可以在Exceptions选项(在Debug菜单下的Exceptions选项)的“Common Language Runtime Exceptions”附近,找到“Thrown”这个复选框,并且把它选中,这样就可以实现上述功能了。
The Details Page
任务明细页面如图26.5所示,它是每项任务属性最直接的体现。任务的标题被设置为页面的标题,任务描述和日期信息显示在标题的下面。如果该条记录被设置为星级,它也会显示出来。为了方便,页面的应用程序栏上放置了按钮,可以实现主页面中提供的上下文菜单中的功能。
图26.5 任务明细页面,给出了包含星级和不包含星级的任务
➔ 为了处理描述信息较长的情况,我们把它放在scroll viewer控件中。而那个大的五角星是作为一个静态的背景,不随着内容的滚动而滚动。
➔ 当前任务的各项属性显示,均用到了数据绑定。DateConverter与第21章“Passwords & Secrets”中描述的值转换器类似,用来友好地显示DateTimeOffset数据。本应用使用的值转换器与之前的唯一不同,就在于其日期和时间之间,增加了“@”符号。
➔ 在OnNavigatedTo方法中,对显示内容作了一些调整,使得在用户点击编辑按钮将页面导航到添加/编辑页面、对记录做了更改、保存并且返回之后,当前页面中的信息能够保持更新。在背后的cs代码中,值转换器可以用来避免更改两个text block中的Visibility属性,但这就有点略显多余。
The Add/Edit Page
添加/编辑页面看上去像完全不同的页面:一个是用来添加新任务的页面,另一个是用来编辑存在的任务,但由于它们之间的类似性,我们就在同一个页面中实现了。图26.6显示了这个页面中的添加和编辑的功能。
图26.6 两种不同模式下的添加/编辑页面
➔ 该页面利用了Silverlight for Windows Phone Toolkit中的三个控件:list picker、date picker和time picker。List picker用来修饰每条任务,使它带上合适的颜色,因为数据绑定与带颜色的字符串能够自动匹配。详见图26.7所示。对于空值,该应用程序获得其字符串值为空以后,使得数据绑定失败,那么显示的矩形框中也就没有填充了。
图26.7 在List picker展开的时候,每条记录的文字旁边还显示了五角星
➔ 该页面保存了每个控件页面状态的当前值。在用户输入页面信息被打断的情况下,这种处理方式就非常有用,而且对于时间日期控件来说,这还是它的需求。因为这些控件都会将屏幕导航到其他页面,如果无法保存并且恢复这些信息的话,回归页面时,无论之前是否选择了时间和日期,填写的表格信息就被清空了。
➔ 如果在已完成任务列表中的项目被重新编辑了,那么它们的值会被直接修改。如果任务列表中的项目被重新编辑了,那么原来的任务被删除,而一个新的任务会被加入。这么做的目的就是为了任务列表中的记录按照应完成的日期来排序。如果这个日期改变了,编辑集合中已存在的任务有可能会导致排序不准确。这就是为什么任务的INotifyPropertyChanged实现只是为了满足主页面“done” list box控件的更新;添加和删除操作由可观察的集合负责报告,所以propertychanged通知只在直接编辑操作中使用。
The Settings Page
设置页面如图26.8所示,它使得用户能够关闭除了“all”以外的所有Pivot页面(页面显示了“all”复选框,但无法操作,这是为了表明这个页面不能隐藏。)这个是设置页面及其简洁的表达,其难点是支持主页面中的Pivot页隐藏。
图26.8 设置页面允许用户隐藏除第一个以外的所有Pivot页面