WP7有约(四):课程全景
Written by Allen Lee
Do I trust some and get fooled by phoniness, or do I trust nobody and live in loneliness?
– Linkin Park, By Myself
化零为整
前面三节课我们分别实现了课程表、作业本和笔记本三个主要功能,然而,它们的内容分散在三个不同的页面,试想一下,如果我想查看某门课下一次什么时候上课、今天有哪些作业要做以及今天记了哪些笔记,我就不得不在多个不同的页面之间来回切换了,这显然降低了应用程序的可用性(usability)!因此,这节课的任务是化零为整,把相关内容整合起来。
在WP7里,内容的整合一般是通过"Hub"来实现的,比如说,People Hub、Pictures Hub、Music + Video Hub、Office Hub、Games Hub以及Marketplace Hub等都是典型的代表。那么,如何创建这样的页面?非常简单!右击Projects面板里的项目节点,选择Add New Item:
图 1
在弹出的New Item对话框里选择Windows Phone Panorama Page,输入页面的名字,然后按OK:
图 2
此时,Expression Blend会为你创建一个Panorama页,里面包含了两个Panorama项。在Properties面板上把Panorama控件的Title属性设为"组织行为学",接着,添加一个Panorama项,然后,把页面上的三个Panorama项的Header属性分别设为"课程概况"、"今天笔记"和"今天作业":
图 3
这三个Panorama项分别用于显示当前课程的基本信息、今天记录的笔记以及今天要做的作业。接下来,我们将会详细探讨每个Panorama项的设计。
首先是"课程概况",我希望它能告诉我每周星期几有课以及下节课的相关信息:
图 4
看到这里,你可能会问,这是怎么弄的?你可以在Panorama项里放置两个TextBlock,也可以放置一个ListBox,你的决定将会影响到后面的实现,这里没有谁对谁错,你要做的只是权衡利弊,做出决定,然后承担责任。这里选择了后一种做法,主要是考虑到将来添加其它信息时可以更加方便。
接着是"今天笔记",要显示今天的笔记并不难,也就是一个ListBox的功夫:
图 5
然而,仅仅这样就够了吗?我们知道,新建和查看是最常用的两项操作,可以满足用户绝大多数时候的需求,我们应该尽可能让用户最快地接触到常用的功能,这意味着我们可以考虑把新建操作集成到这里,但完整的新建操作需要打开NewOrEditNotePage页才能完成啊,而CourseHubPage页的设计原则是尽可能让涉及到的操作"就地完成",并且用户可以马上看到结果,怎么处理?要回答这个问题,我们得先搞清楚用户在什么情况下会通过CourseHubPage页来新建笔记。我们知道,用户在新建笔记的时候需要提供笔记内容和笔记标签,然而,只有笔记内容是必须提供的,笔记标签的提供可以延后,如果用户要在极短时间内新建笔记,比如说在课堂上,那么暂时忽略笔记标签,只输入笔记内容显然会极大地加速整个操作过程,换句话说,用户只需一个TextBox和一个Button就可以完成新建操作:
图 6
需要注意的是,TextBox的TextWrapping属性的值默认是Wrap,这是为了让TextBox能够根据内容的长度自动调整自身的高度,然而,这个好处在这里会导致下面的ListBox被挤压,从而使得笔记内容的显示空间减少,这显然不是我们希望看到的,因此我们需要把TextBox的TextWrapping属性的值设为NoWrap。
最后是"今天作业",出于相同的理由,我也为它配备了新建功能:
图 7
然而,仅仅这样就够了吗?当然不够,首先,用户无从知晓哪些作业还没完成,其次,当用户完成一项作业时,很自然地想把它标记为已完成,然而,就目前的设计而言,用户得先退回主菜单,然后进入作业本找到并编辑这项作业,这个繁琐的过程无疑降低了应用程序的可用性。有没有办法可以简化这个过程?我们知道,作业的完成状态是通过IsCompleted属性来标识的,这个属性的类型是bool,一般而言,如果我们要在用户界面上表达这个类型的数据,我们会选择CheckBox,因此,我们不妨为每项作业配备一个CheckBox:
图 8
这样,用户既可以直观地了解作业的完成状态,又可以轻易地更改作业的完成状态,而我们唯一要做的只是创建一个双向数据绑定!
连接前端和后端
有了用户界面,接下来就是为它创建对应的ViewModel类了。首先,在ViewModel文件夹里创建一个CourseHubViewModel类,并让它继承NotificationObject类:
代码 1
接着,创建以下三个属性:
代码 2
它们将会分别绑到课程概况、今天笔记和今天作业上的ListBox。那么,我们应该如何初始化它们呢?
Assignments属性的初始化最简单,因为Assignment类里有个StartDate属性,我们可以根据这个属性在CouseHubViewModel类的构造函数里筛选出今天的作业:
代码 3
但是,Note类没有类似的属性啊,怎么办?创建一个吧:
代码 4
这样我们就可以像筛选作业那样筛选笔记了:
代码 5
最麻烦的是Overview属性,它需要我们重新组织/聚合课程的信息,比如图4的第一条信息——"逢星期一、星期二、星期五有课",其中"逢XXX有课"是固定部分,"星期一、星期二、星期五"是可变部分,这部分信息保存在Course对象的Day属性里,我们可以通过LINQ查询课程表里特定课程的所有Course对象,然后提取它们的Day属性,并按照我们期望的格式聚合起来:
代码 6
至于第二条信息,我们得先找到下节课的Course对象,怎么找?想想看,如果我们手头上有一份课程表,我们会怎么找呢?我们会先看看明天有没有这节课,如果有,那就是它了,如果没有,看看后天有没有,如此类推,下星期的今天为止。我们可以模拟这个过程查找下节课的Course对象:
代码 7
需要说明的是,某个课程(Course对象)星期几有课是以字符串的形式表示的,而某天(DateTime对象)星期几却是以DayOfWeek枚举的形式表示的,因此我们需要一个GetChineseDayName方法把DayOfWeek枚举转换成对应的中文字符串:
代码 8
现在,请思考一下,GetChineseDayName方法有没有可能抛出ArgumentException异常?如果有,什么情况下会抛出这个异常?如果没有,为什么?好了,找到下节课之后,我们就可以着手相关信息的聚合了:
代码 9
值得提醒的是,这里的做法是不具备多语言扩展的,不过,就目前而言,这已足够了。
接下来,我们将会实现"今天笔记"的新建操作,从用户界面上看,这个功能是由一个TextBox和一个Button组成的,前者只需配备一个对应的字符串属性:
代码 10
至于后者,根据上节课的经验,我们需要为它创建一个NewNoteCommand属性:
代码 11
然后在构造函数里把它初始化为一个DelegateCommand对象:
代码 12
DelegateCommand类的构造函数接受两个参数,第一个是执行这个命令时将会调用的代码,第二个是判断这个命令能否调用的代码。当用户单击按钮时,我们需要根据当前课程、笔记内容以及今天日期等信息创建一个Note对象,然后保存这个Note对象。现在的问题是,如何通知页面的ListBox更新?办法其实有很多,比如说,我们可以学第二节课那样,手动监听CollectionChanged事件,也可以学上节课那样,通过CollectionViewSource间接监听CollectionChanged事件,不过,这次我打算采用更加直接的办法,在保存Note对象之后手动把它添加到Notes属性:
代码 13
需要说明的是,这里不是通过Add方法把Note对象添加到Notes属性的末尾,而是通过Insert方法把Note对象添加到Notes属性的首位,为什么这样呢?想想看,当用户单击按钮时,其视线将会落在按钮及其附近,如果新建的笔记显示在ListBox的首位,那就正好落在用户的视线范围里面,这样用户就无需挪动视线寻找并确认新建的笔记了。完成这些操作之后,我们需要把TextBox清空,为下次输入做好准备,因为TextBox是绑到NoteContent属性的,所以我们只需把NoteContent属性的值重设为空字符串就行了。至于DelegateCommand类的构造函数的第二个参数,即判断这个命令能否调用的代码,也很简单,只有当TextBox里有内容,那么单击按钮才会执行这个命令:
代码 14
"今天作业"的新建操作的实现方式和这里的一样,我打算把它留给你当今天作业,值得提醒的是,在创建Assignment对象时,你需要把相关属性初始化为恰当的值,这可以参考第二节课的做法。
最后,页面的课程名称也需要一个对应的属性:
代码 15
看到这里,你可能会问,为什么这里直接使用自动属性,而不像代码10的NoteContent属性那样在set访问器里调用RaisePropertyChanged方法?这是因为,在用户访问CourseHubPage页期间,当前课程是不变的,即CourseName属性的值不会发生改变,因此简单的自动属性已经足够了。CourseName属性的初始化也非常简单,只需把传给页面的课程名称赋给它就行了:
代码 16
创建好ViewModel类之后,我们就可以着手处理它和CourseHubPage页之间的关联了。首先是设置数据绑定,需要设置的控件以及对应的绑定表达式如下表所示:
描述
类型
属性
绑定表达式
页面标题
TextBlock
Text
{Binding CourseName}
课程概况
ListBox
ItemsSource
{Binding Overview}
今日笔记
ListBox
ItemsSource
{Binding Notes}
笔记内容
TextBox
Text
{Binding NoteContent, Mode=TwoWay}
今日作业
ListBox
ItemsSource
{Binding Assignments}
作业内容
TextBox
Text
{Binding AssignmentContent, Mode=TwoWay}
表 1
看到这里,你可能会问,还有两个按钮呢?上节课我们曾经说过,SL for WP的Button控件没有Command属性,不能直接绑定命令对象,我们需要通过Behavior间接实现Button控件和命令对象之间的绑定,但Prism没有为Button控件提供现成的Behavior,怎么办?我们可以仿照Prism的ApplicationBarButtonCommand创建一个适用于Button控件的ButtonCommand,也可以直接使用Windows Phone 7 Developer Guide – Code Samples里面提供的ButtonCommand,还可以使用MVVM Light Toolkit的EventToCommand。毫无疑问,第二种方案是最简单的,而第三种方案则是最强大的,它不但可以把任意事件映射到任意命令对象,最新版本还支持把事件处理程序的参数传给命令对象。下面将会示范如何通过EventToCommand给Button控件绑定命令对象。
首先,引用GalaSoft.MvvmLight.Extras.WP7.dll类库,接着,打开Assets面板,选择Behaviors,然后把EventToCommand拖到Objects and Timeline面板的Button上:
图 9
此时,Objects and Timeline面板将会变成这样:
图 10
确保EventToCommand处于选中状态,在Properties面板上把Command属性的值设为"{Binding NewNoteCommand}":
图 11
看到这里,你可能会问,不用设置事件吗?如果你查看Properties面板上的EventName属性,你会发现它已经设为Click了,因为Button控件的默认事件就是Click。是不是很简单呢,剩下那个Button控件也是这样处理哦。
现在,万事俱备只欠……嗯,创建一个CourseHubViewModel对象,并把它赋给CourseHubPage页的DataContext属性,但是,我们从哪获取课程名称呢?我们知道,查询字符串是页面之间传递信息的一个主要途径,我们可以假设从某个页面来到CourseHubPage页时,查询字符串里包含了一个名为"coursename"的参数,然后在OnNavigatedTo方法里通过NavigationContext.QueryString来访问:
代码 17
那么,用户如何打开这个页面?
打开CourseHubPage页
什么地方最适合用来打开CourseHubPage页呢?我们知道,这个页面不能直接打开,因为在打开之前我们必须提供一个课程名称,因此,仿效前三节课在主菜单里通过菜单项打开页面的做法是行不通的。如果换了传统的桌面应用程序,我们可能会使用ComboBox和Button这个组合,在ComboBox里列出所有课程名称让用户选择,选好之后单击按钮打开CourseHubPage页,可是,这里是手机应用啊,还有没有更好的方式呢?
毫无疑问,CourseHubPage页需要一个课程上下文,哪里可以提供这个上下文呢?课程表!那么,如何设计打开CourseHubPage页的操作呢?有两种可能的方案,第一种是在课程表的Application Bar上添加一个"打开"按钮:
图 12
这样,用户可以通过单击选中某个课程,然后单击"打开"按钮打开CourseHubPage页。第二种是把编辑和删除两个操作放在课程的上下文菜单里,把保存操作改成Application Bar的按钮:
图 13
这样,用户可以通过单击课程打开CourseHubPage页,通过长按课程打开上下文菜单,然后执行编辑或删除操作。这里选择第二种方案,这样课程表的页面设计和操作体验可以与作业本、笔记本的保持一致。
打开CourseTimetablePage.xaml.cs文件,把ApplicationBarCommitMenuItem_Click方法重命名为ApplicationBarCommitIconButton_Click,然后把"保存"按钮的Click事件处理程序设为ApplicationBarCommitIconButton_Click:
代码 18
接着,打开CourseTimetablePage.xaml文件,在courseCollectionItemTemplate数据模板里添加上下文菜单,并把两个菜单项的Click事件处理程序分别设为EditMenuItem_Click和DeleteMenuItem_Click:
代码 19
那么,这次是否也学前面那样,把ApplicationBarEditIconButton_Click和ApplicationBarDeleteIconButton_Click两个方法分别重命名为EditMenuItem_Click和DeleteMenuItem_Click呢?不行哦,因为前后两种操作方式背后的实现原理是不一样的,之前的操作方式是单击选中课程然后单击Application Bar上的按钮执行相应操作,因此我们可以通过ICollectionView.CurrentItem来获取当前选中的课程,而现在的操作方式是长按课程打开上下文菜单然后单击菜单项执行相应操作,执行操作的时候目标课程不会变成选中状态,因此通过ICollectionView.CurrentItem来获取目标课程是行不通的,怎么办?上节课我们曾经说过,上下文菜单能从列表项那里继承DataContext属性的值,而这个值正是目标课程,因此我们可以通过菜单项的DataContext属性获取目标课程的相关信息:
代码 20
这样,编辑、删除和保存三个操作就改造完毕了,那么,打开CourseHubPage页的操作呢?
一般情况下,我们通过Click事件处理程序来实现单击操作的,然而,正如代码24所示的那样,列表项的最外层容器是StackPanel,它没有Click事件,怎么办?有三种可能的方案,第一种是通过StackPanel的MouseLeftButtonUp事件模拟单击操作,这种方案最简单,但不够精确,因为Click事件本身是由MouseLeftButtonDown和MouseLeftButtonUp两个事件组成的,如果你想获得更加精确的效果,可以使用Windows Phone 7 Developer Guide – Code Samples里面提供的FrameworkElementClickCommand,它不但正确处理了MouseLeftButtonDown和MouseLeftButtonUp两个事件,还提供了命令对象的绑定,最后一种是使用SL for WP Toolkit的GestureService/GestureListener组件,由于Click事件本质上是Tap手势操作,我们可以通过GestureService/GestureListener组件监听并处理这个手势操作。下面将会示范如何通过GestureService/GestureListener组件处理Tap手势操作。
首先,打开CourseTimetablePage.xaml文件,在courseCollectionItemTemplate数据模板里添加GestureService/GestureListener组件,并把GestureListener的Tap事件处理程序设为CourseItem_Tap:
代码 21
接着,在CourseTimetablePage.xaml.cs文件里创建这个事件处理程序:
代码 22
需要说明的是,sender参数指向的并非GestureListener对象本身,而是包含它的StackPanel,我们可以通过StackPanel的DataContext属性获取目标课程的课程名称,以便作为查询字符串的coursename参数的值传给CourseHubPage页。
好了,终于可以按F5了:
图 14
单击主菜单上的"课程表"进入课程表,在星期一、星期二和星期五这三天里分别创建一节"organizational behavior"课程:
图 15
值得注意的是,当你单击"上课地点"下面的编辑框,使之变成编辑状态时,整个页面会稍稍向上平移,这样做是为了避免软键盘打开时把编辑框挡住,这个特性是系统自带的,我们不必做任何事情就能拥有。不过,由于"确定"和"取消"两个功能还沿用着旧的设计,即通过传统的Button控件来实现,因此无可避免地被软键盘挡住了,我们应该改用上节课的新设计,即通过Application Bar按钮来实现,以便用户在输入完毕之后随时可以关闭页面,而不用先单击页面空白处关闭软键盘再单击确定按钮关闭页面,嗯,改造页面的工作就留给你当课后作业吧……
创建好课程之后,单击课程表上的任意一个课程打开CourseHubPage页:
图 16
从上图可以看出,哪天有课已经正确显示了,下节课的上课日期也正确计算出来了。接着,我们来看看"今天笔记":
图 17
在编辑框里输入一条笔记,然后单击旁边的新建按钮:
图 18
再新建两条看看:
图 19
嗯,很好,前面创建的笔记在下面,后面创建的笔记在上面。接下来,我们看看"今天作业":
图 20
创建三项作业,并通过旁边的CheckBox控件把其中一项标记为已完成:
图 21
现在,按两次Back键回到主菜单,然后单击"笔记本"进入笔记本:
图 22
长按第二条笔记打开上下文菜单,然后单击编辑菜单项打开编辑笔记的页面:
图 23
修改一下笔记内容,然后按确定保存并关闭页面:
图 24
好了之后按Back键回到主菜单,然后单击"作业本"进入作业本:
图 25
从上图可以看出,作业的起止时间、完成状态都正确设置了。现在,长按第一项作业打开上下文菜单,然后单击编辑菜单项打开编辑作业的页面,把这项作业标记为已完成,并按确定关闭页面:
图 26
好了之后按Back键回到主菜单,然后单击"课程表"进入课程表,接着单击课程表上的任意一个课程打开CourseHubPage页,嗯,笔记内容和作业的完整状态都正确地更新了:
图 27
图 28
嗯,非常好,只是还有一个小小的地方需要改进的,前面输入数据的时候我是手动输入每个字符的,你知道,Windows Phone 7的模拟器不支持通过电脑的键盘进行输入,因此在测试的时候需要输入大量数据会很痛苦,我希望软键盘能够提供词条联想,比如说,当我输入"Org"三个字母时,它可以提示"Organizantion"给我选择,怎样才能做到呢?很简单,把编辑框的InputScope属性设为Text就可以了:
图 29
这样,当你在编辑框里输入数据时,软键盘上方就会显示相关的词条联想了:
图 30
至此,Course Hub已经实现完毕了,但是,你有没有觉得它有点儿单调?
动起来
单调吗?那就来点儿动感吧!首先是给课程表的课程列表添加Tilt Effect,它可以为控件交互产生一种特别的视觉反馈,感觉上就像控件后面垫了一层柔软的海绵,当你单击控件时,看起来就像你把控件放后面推了一下,当你释放控件时,它会自动复原。MSDN提供了Tilt Effect的示范代码以及把它移植到你的应用的教程,不过,如果你和我一样嫌这麻烦(说到底就是懒),可以直接使用SL for WP Toolkit Feb 2011提供的Tilt Effect组件,不过,它还没修复我在第二节课里提到的bug,因此在使用之前我们需要按照那节课提到的办法修改一下SL for WP Toolkit的源代码。一切准备就绪之后我们就可以应用Tilt Effect了。打开CourseTimetablePage.xaml文件,找到pivotItemContentTemplate数据模板,然后在ListBox里添加TiltEffect.IsTiltEnabled附加属性:
代码 23
这样就行了,是不是很简单呢,如果你希望把Tilt Effect应用到页面上的所有控件,你只需在PhoneApplicationPage里设置这个属性就可以了。
接下来是Page Transitions,如果你有留意Windows Phone 7的相关视频,那么你应该已经看过Windows Phone 7的翻页效果,这是其中一种Page Transitions,怎么把它应用到我们的页面呢?非常简单,在你想应用翻页效果的页面根元素里添加如下代码:
代码 24
我们可以把这段代码分别添加到CourseTimetablePage.xaml和CourseHubPage.xaml两个页面里。不过,仅仅这样还看不到效果,因为应用程序的RootFrame还只是一个普通的PhoneApplicationFrame,我们需要把它换成TransitionFrame。打开App.xaml.cs文件,在InitializePhoneApplication方法里把RootFrame初始化为TransitionFrame:
代码 25
这样,当用户单击课程表上的某个课程打开CourseHubPage页时,就会看到翻页效果了。除了翻页效果(Turnstile),SL for WP Toolkit还提供了滚动(roll)、旋转(rotate)、滑动(slide)和回旋(swivel)等四种效果,你可以通过SL for WP Toolkit附带的示范程序看看这些效果应用到页面时会是怎样的。
最后是FluidMoveBehavior,你知道吗,我第一次看到它产生的效果时就被它深深地吸引住了,我想,用"一见钟情"来描述这种感觉一点儿都不过分!你玩过新浪微博吗,当你发一条新的微博时,现有的微博会向下平移,与此同时,新的微博会在最上面逐渐显现出来,我希望为"今天笔记"实现这样的效果。右击"今天笔记"上的ListBox,选择Edit Additional Templates\Edit Layout of Items (ItemsPanel)\Create Empty:
图 31
在弹出的Create ItemsPanelTemplate Resource对话框里输入模板名字,然后按OK关闭对话框:
图 32
进入模板的编辑状态之后,你会看到一个空的StackPanel,从Assets面板把FluidMoveBehavior拖到StackPanel里,确保FluidMoveBehavior处于选中状态,在Properties面板上把AppliesTo和Duration两个属性分别设为"Children"和"0.5":
图 33
这样就行了,是不是很简单呢,如果你希望为"今天作业"实现同样的效果,你不必重新创建一个模板,直接应用刚才那个就行了:
图 34
那么,如何让新的笔记会在最上面逐渐显现出来呢?如果你看过刚才那个视频,你可能会说,用最后介绍那个办法不就行了?非常遗憾,不行哦,LayoutStates这组可视状态是Silverlight 4的新特性,而Silverlight for Windows Phone 7是基于Silverlight 3的,尚未支持它们,因此无法通过它们实现我们想要的效果,怎么办?
想想看,这个效果的本质是什么?一个动画!这个动画只做一件事情,使新的笔记逐渐呈现出来,换句话说,使它的Opacity属性的值从0逐渐变成1。现在,右击"今天笔记"上的ListBox,选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Edit Current:
图 35
在Objects and Timeline面板上选中StackPanel,并在Properties面板上把它的Opacity属性的值设为0,因为新的笔记最初是不显示的。接着,在Objects and Timeline面板上创建一个名为FadeIn的动画,并把播放指针拖到0.5秒处:
图 36
然后在Properties面板上把StackPanel的Opacity属性的值设为100:
图 37
需要说明的是,Opacity属性的类型是double,Expression Blend为了免除输入小数点的麻烦在界面上把这个值扩大了100倍,换句话说,在Properties面板上设100相当于在XAML里设1。好了之后关闭动画,然后在Assets面板上把ControlStoryboardAction拖到Objects and Timeline面板的StackPanel上:
图 38
最后,在Properties面板上把EventName和Storyboard两个属性的值分别设为Loaded和FadeIn:
图 39
这样就大功告成了。虽然这可以实现我们想要的效果,却无可避免地带来了一个新的问题:我怎样把这个效果应用到"今天作业"呢?
最直接的办法是在"今天作业"上重做一遍上面的步骤,额,这显然不是一个办法,我不想重复劳动!这个时候,我们可以创建一个Behavior,把上面的逻辑封装起来,然后直接应用到StackPanel上,从而实现重用。现在,右击Utils文件夹,然后选择Add New Items,在弹出的New Item对话框里选择Behavior,并把它命名为FadeInWhenLoading:
图 40
我们知道,FrameworkElement类是整个继承体系里最先同时拥有Opacity属性和Loaded事件的,因此,我们需要把FadeInWhenLoading类的声明改成这样:
代码 26
接着,在OnAttached方法里把Opacity属性的值设为0,并订阅Loaded事件:
代码 27
而AssociatedObject_Loaded方法所做的事情仅仅是创建并播放使Opacity属性的值从0逐渐变成1的动画:
代码 28
最后,在OnDetaching方法里取消订阅Loaded事件:
代码 29
好了之后,重新编译一下,然后右击"今天笔记"上的ListBox,选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Edit Current进入模板编辑状态,撤销前面所做的操作(包括设置Opacity属性的值、创建动画以及应用ControlStoryboardAction),然后在Assets面板上把FadeInWhenLoading拖到Objects and Timeline面板的StackPanel上:
图 41
大功告成!现在,你可以把FadeInWhenLoading直接应用到"今天作业"上了。
动画效果是Windows Phone 7用户体验的一个重要组成部分,它使得应用程序不再单纯地从某个状态突然跳到另一个状态,比如Tilt Effect,它使得控件不再只有按下和释放两种状态,当你单击控件时,它会向你单击的那个地方倾斜,非常有趣;又比如Page Transitions,前进到一个页面和返回相同页面会产生不同的动画效果,想象你看书的时候翻到下一页和回到上一页的情景,这使你不会在这个过程中迷失;再比如FluidMoveBehavior,它使你清楚的了解到当前查看的内容是如何变化的,所有这些,目的只有一个,提高用户体验,当然,前提是运用得当。
下课了……
特别感谢PRO为本系列文章精心打造了一个WP7手机外壳,并合成了上面这幅图。