WP7有约(五):回到主页
Written by Allen Lee
In this farewell, there is no blood, there is no alibi, cause I've drawn regret from the truth of a thousand lies.
– Linkin Park, What I've done
最重要的问题
还记得当初我们开发这个应用的目的吗?是为了随时随地可以查看课程信息。但你会闲来无事打开课程表查看课程信息吗?我就不会了!那用户要课程表来干嘛?一般情况下,用户查看课程表是为了回答以下问题:
我希望在主页上提供直接获取上述问题的答案的途径,比如说,我们可以在主页上添加下面这个格子(Tile):
图 1
它可以清楚地告诉用户下一节课是什么,几点开始,在哪里上。至于第二、三个问题,我们可以在主页上添加下面两个格子:
图 2
值得注意的是,这两个格子的右上角都有一个箭头记号,这是为了提醒用户单击它们可以获取相关信息。我们不可能在主页上罗列今天和明天的所有课程,这样会导致信息泛滥,同时也会加重用户识别有用信息的负担,因此我们在这里提供一些捷径,以便用户更快到达包含所需信息的地方。此外,用户最常打开的页面应该是当前课程的Course Hub了,在那里用户可以完成当前课程最常用的操作,因此我们可以在主页上再添加一个格子,帮助用户直接到达那个页面:
图 3
值得注意的是,与几点上课相比,用户更加关心几点下课,因此当前课程显示的是下课时间而不是上课时间。
我们应该把这些格子放在主页上,以便用户随时了解最重要的信息:
图 4
那么,怎样创建这样的用户界面呢?其实不难,上面的刷新按钮可以直接使用Coding4Fun Windows Phone Toolkit的RoundButton控件,而下面的四个格子由于内容结构基本相同,很容易让人想到把它们放到一个集合型的控件里,这里将会选择ItemsControl作为它们的容器。
创建用户界面
首先,在主页上添加一个Panorama项,并把标题设为"课程":
图 5
新创建出来的Panorama项默认包含一个Grid,我们可以通过Properties面板上的RowDefinitions属性把它分成上下两行,上面那行用来放置刷新按钮,高度设为自动适应,而下面那行用来放置ItemsControl,高度使用默认设置。
好了之后,从Assets面板上把RoundButton控件拖到Grid里,默认自动放在Grid的第一行(注意,在使用RoundButton控件之前请先引用Coding4Fun.Phone.Control.dll文件):
图 6
设置ImageSource属性,把图标换成图4里面那个,然后把Content属性的值设为"刷新":
图 7
默认情况下,RoundButton控件的文字是位于图标下方的,而且无法直接通过设置某个属性使它的文字放在图标右边,想要达到图4的效果,我们需要修改它的样式。右击RoundButton控件,选择Edit Template\Edit a Copy进入模板编辑状态,此时,你的Objects and Timeline面板将会变成这样:
图 8
确保StackPanel处于选中状态,然后在Properties面板上把Orientation属性改为Horizontal。接着,选中StackPanel下面的ContentBody,并在Properties面板上把字体大小设为PhoneFontSizeMedium。好了之后退出模板编辑状态,此时,你的RoundButton控件应该是这样的:
图 9
看到这里,你可能会问,为什么要在模板编辑状态里设置字体大小呢?这是因为直接修改RoundButton控件的字体大小不起作用,我认为这是一个bug。我在CodePlex上提了这个问题,第二天他们就发布了修正的版本,动作好快!
接着,在里面添加一个ItemsControl,在Properties面板上把它的Row属性设为1,并使它充满Grid的第二行。由于ItemsControl默认使用StackPanel来排列它的子元素,这会导致格子从上到下排成一列,要使格子按照图4所示的那样排列,我们需要把StackPanel替换成WrapPanel。右击ItemsControl,选择Edit Additional Templates\Edit Layout of Items (ItemsPanel)\Create Empty:
图 10
在弹出的Create ItemsPanelTemplate Resource对话框里输入模板名字,然后单击OK关闭对话框:
图 11
此时,你的Objects and Timeline面板将会变成这样:
图 12
把里面的StackPanel删掉,然后从Assets面板上把WrapPanel拖到里面:
图 13
好了之后退出模板编辑状态。
接下来,我们将会定制每个格子的内容结构。再次右击ItemsControl,选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Create Empty,然后在弹出的Create DateTemplate Resource对话框里输入模板名字,然后单击OK关闭对话框:
图 14
此时,你会看到屏幕中间有个空白的Grid:
图 15
由于这次我们没有使用设计时数据,数据模板的设计将会比较考究想象力,不过我相信你能跟得上的。确保Grid出于选中状态,把它的Width和Height两个属性都设为190(这个数值是经过多次微调得到的最佳结果),并把它的背景色设为PhoneAccentBrush:
图 16
然后把Grid的Margin设成这样:
图 17
接着,在Grid里添加相应的控件,使它变成这样:
图 18
此时,你的Objects and Timeline面板应该是这样的:
图 19
值得提醒的是,在调整控件的大小和位置时,你可以暂时为控件硬编码一些内容,比如上图的"当前课程"和课程信息。好了之后退出模板编辑状态。
实现内部逻辑
现在是时候为那些格子创建对应的ViewModel类了,不过,在开始之前我们需要搞清楚这些格子的工作方式:
噢,看起来有点复杂哦,不过不用担心,我们一起有步骤地地把它们实现了。
首先,在ViewModels文件夹里创建一个TileViewModelBase抽象类,并让它继承NotificationObject类:
代码 1
这里将会放置被四个格子的ViewModel类共用的代码,那么,哪些代码是共用的呢?根据上面的讨论,每个格子都需要提供一组信息,里面可能包含课程信息或者格子的状态信息,我们可以把这些信息放在一个集合里,并在构造函数里初始化这个集合:
代码 2
其次,每个格子都有一个标题,但这个标题的内容不会改变,因此我们可以通过自动属性来实现:
代码 3
这个属性的初始化需要在每个格子的ViewModel类的构造函数里进行,因为每个格子的标题都不同。至于格子右上角的图标,我们只需提供一个布尔型属性,指示是否显示这个图标就行了,不过,由于这个图标的显示和格子的状态紧密相连,我们需要为这个布尔型属性提供通知机制,以便在格子的状态发生改变时可以通知用户界面做出相应的调整:
代码 4
此外,格子还需要支持单击操作和刷新操作:
代码 5
由于每个格子在计算应该显示的信息时都要获取今天或者明天的全部课程,我们可以把这部分代码放在基类里:
代码 6
有了这个基类,我们就可以分别为那些格子创建对应的ViewModel类了。
我们先挑一个简单的来试一下,根据前面的讨论,"明天课程"最简单,因此我们先为它创建ViewModel类。在ViewModels文件夹里创建一个TomorrowCoursesTileViewModel类,并让它继承TileViewModelBase抽象类:
代码 7
当用户单击这个格子将会打开课程表并显示明天的课程,为了执行这个操作我们需要知道明天是星期几,因此我在里面添加了一个_day私有字段,用来存放这个信息。接着,我们需要在构造函数里初始化相关的属性:
代码 8
其中,打开课程表的代码将会放在(1)里,而初始化格子的代码则会放在(2)里。看到这里,你可能会问,如何在ViewModel类里打开一个页面?一直以来,我们都是在一个页面里通过NavigationService的Navigate方法打开另一个页面,而在ViewModel类里,NavigationService是访问不了的,怎么办?要解决这个问题,我们得先搞清楚Windows Phone的页面导航模型是怎样的,Windows Phone的导航模型是由一个PhoneApplicationFrame和一个或多个PhoneApplicationPage共同组成的,前者提供与导航相关的事件和Navigate方法,而后者则包含应用的内容,它们共享着同一个NavigationService。虽然在ViewModel类里无法访问页面的资源,但是我们可以通过App类获取当前应用的PhoneApplicationFrame对象,继而调用它的Navigate方法:
代码 9
事实上,MVVM模式认为ViewModel层不应该依赖于View层的任何东西,更好的做法应该是创建一个INavigationService接口把导航服务隔离开来,然后在ViewModel类里通过依赖注入的方式来获取和访问导航服务,如果你打算为你的ViewModel类创建单元测试,那么这种隔离就更加有必要了。不过,我不想在这里把事情弄得太复杂,因此采用代码9这种简单直接的做法。此外,需要说明的是,仅当格子右上角显示箭头图标时单击格子才会执行相应操作,因此在初始化TileCommand属性的时候我们需要告诉它根据ShowNextIcon属性的值来判断是否允许执行相应的操作。而刷新方面,我们只需要看看明天是否有课,然后根据情况更新相关的信息就行了:
代码 10
需要说明的是,GetChineseDayName方法是一个扩展方法,用来生成与DayOfWeek枚举值对应的中文星期名称,事实上,它是从上节课的CourseHubViewModel类里提取出来的。
接着,我们来看看"今天课程",它和"明天课程"没有太大区别,我们只是需要多处理一个状态,以及把显示课程总数改为显示剩余课程的数目。在ViewModels文件夹里创建一个TodayCoursesTileViewModel类,并让它继承TileViewModelBase抽象类:
代码 11
刷新的时候,如果今天的课程数目为0,当然是显示"今天没课"了:
代码 12
否则,看看是否所有课程都结束了,如果是就显示"放学了":
代码 13
否则,显示剩余课程的数目:
代码 14
需要说明的是,这里把剩余课程定义为还没开始的课程,已经开始但还没下课的不包含在里面。此外,Course类的StartTime和EndTime两个属性的值是通过SL for WP Toolkit的TimePicker设置的,由于它只关心时间部分,因此把年、月和日的值都设为1了,而DateTime.Now包含了今天的年、月和日,会对后面的比较造成影响,所以我另外创建了一个thisTime。
不过,要让打开的课程表正确显示我们想要的课程,还得修改一下课程表的代码。打开CourseTimetablePage.xaml.cs文件,在构造函数里把Loaded事件处理程序的代码改成下面这样:
代码 15
当用户打开课程表时,默认显示今天的课程,如果查询字符串里面包含了day参数,则显示该参数指定的那天课程。需要说明的是,GetDayOfWeekValue是一个扩展方法,用来生成与中文星期名称对应的DayOfWeek枚举值,本质上它是GetChineseDayName方法的反向实现。
接着,我们来看看"下一节课"。在ViewModels文件夹里创建一个NextCourseTileViewModel类,并让它继承TileViewModelBase抽象类:
代码 16
根据前面的讨论,这个格子不显示箭头图标,单击也不执行任何操作,因此我们把ShowNextIcon属性的值设为false,把TileCommand属性初始化为"空"命令。看到这里,你可能会问,为什么要这样初始化TileCommand属性呢?因为将来设置数据绑定的时候,我们是在格子的模板里统一设置的,如果某个格子的TileCommand属性为null,那么将来用户单击这个格子时可能会抛出NullReferenceException。刷新的时候,如果今天的课程数目为0就显示"今天没课"了,如果所有课程都结束了就显示"放学了":
代码 17
否则,我们需要从剩余课程里找出下一节课。如果此时正在上最后一节课呢?前面我们把剩余课程定义为还没开始的课程,那么现在的状态既没有剩余课程又不是已经放学,怎么办?这是一个临界状态,我们可以增加一个状态来描述这种情况,如果现在是最后一节课,那么我们可以显示"最后一节了",否则显示下一节课的相关信息:
代码 18
最后,我们来看看"当前课程",这是四个格子里面最麻烦的,也是状态最多的。在ViewModels文件夹里创建一个CurrentCourseTileViewModel类,并让它继承TileViewModelBase抽象类:
代码 19
刷新的时候,如果今天的课程数目为0就显示"今天没课"了,如果所有课程都没开始就显示"还没上课",如果所有课程都结束了就显示"放学了":
代码 20
否则,看看此时是不是正在上课,如果是就显示课程信息,如果不是就显示"课间休息":
代码 21
看到这里,你可能会问,每次刷新格子都要通过LINQ做很多次遍历,这样会不会降低应用的效率?嗯,是的,目前的做法不是很好,比较好的做法是获取某天的课程,然后对它们进行排序,并缓存到一个集合里,接着从前往后做一次遍历,看看当前时间在哪个位置,从而判断格子处于什么状态。但即使这样,每次刷新还是需要一次遍历,更好的做法应该是每次找到当前时间的位置,就用一个变量做个记号,有点像书签一样,下次刷新时就从这个位置开始往后遍历,这样的话,理想状态下一天就只需做一次遍历了。不过,所有这些都是以一个完整且稳定的课程表为前提的,如果你打算让你的课程表支持临时调课、插课或者和远程服务器进行同步,那么这些优化措施可能会让事情变得糟糕。我不建议太早进行性能优化,因为这样可能会导致设计僵化,我无法告诉你什么时候适合优化性能,你必须根据自己的设计和计划作出决定。但有一点我是可以肯定的,如果性能问题超出了用户的容忍程度,那么无论如何我们都要采取行动了,因为用户无法直接看到你的设计有多灵活,却非常清楚他们内心此刻的负面感受。
现在,打开MainViewModel.cs文件,它是我们创建这个项目的时候Expression Blend自动为主页创建的ViewModel类,不过里面有很多我们不需要的代码,在继续之前先把它们清理掉,但保留INotifyPropertyChanged接口的实现代码。好了之后,在里面创建下面两个属性:
代码 22
接着,在构造函数里初始化这两个属性:
代码 23
初始化完这两个属性之后别忘了"刷新"一下哦,否则主页上的那些格子不会显示任何东西的。至此,ViewModel类全部创建完毕了,接下来,我们将会把它们关联到对应的View上。
把ViewModel连接到View上
如果你是在Visual Studio里写代码的,那么现在是时候回到Expression Blend了。确保主页上的ItemsControl处于选中状态,在Properties面板上单击ItemsSource属性右边的小正方形,选择Custom Expression,然后在弹出的Custom Expression对话框里输入"{Binding TileViewModels}"。接着,右击ItemsControl,选择Edit Additional Templates\Edit Generated Items (ItemTemplate)\Edit Current进入模板编辑状态,根据下表用同样的办法设置TextBlock和ItemsControl的数据绑定:
控件 |
属性 |
绑定表达式 |
TextBlock |
Text |
{Binding Title} |
ItemsControl |
ItemsSource |
{Binding Contents} |
表 1
至于格子右上角的箭头图标,我们需要把它的Visibility属性绑到ShowNextIcon属性,不过,它们的类型并不一样,因此我们需要使用转换器。在Properties面板上单击Visibility属性右边的小正方形,选择Data Binding,在弹出的Create Data Binding对话框里选中Data Context选项卡,然后勾选Use a custom path expression,并在旁边输入ShowNextIcon:
图 20
接着,单击Value converter右边的省略号按钮:
图 21
然后在弹出的Add Value Converter对话框里选择Coding4Fun.Phone.Controls.Converters下面的BooleanToVisibilityConverter:
图 22
好了之后单击OK关闭对话框。
接下来,我们将会把命令对象绑定到相关控件上。首先是格子的单击操作,每个格子最外层是一个Grid,它没有Click事件,因此我选择了MouseLeftButtonUp事件来代替,我希望把它关联到TileViewModelBase的TileCommand属性,怎么关联?还记得上节课介绍的EventToCommand吗?现在又轮到它出场了!从Assets面板上把EventToCommand拖到Grid上,此时,你的Objects and Timeline面板将会变成这样:
图 23
确保EventToCommand处于选中状态,在Properties面板上把EventName属性设为MouseLeftButtonUp,并把Command属性的绑定表达式设为"{Binding TileCommand}"。好了之后退出模板编辑状态,用同样的办法给RoundButton控件添加EventToCommand,然后把Command属性的绑定表达式设为"{Binding RefreshTilesCommand}"。当我们把EventToCommand拖到一个控件上时,EventName属性会被自动设为目标控件的默认事件,因为Click事件刚好是RoundButton控件的默认事件,所以我们无需对这个属性做任何修改。
终于可以看效果了,哎呀,我等这个时候等到花儿也谢了,事不宜迟了,按F5吧:
图 24
从上图可以看到,今天是星期天,没课,明天是星期一,有三节课,单击"明天课程"将会打开课程表,并显示星期一的课程:
图 25
到了星期一早上,如果我们在所有课程开始之前打开应用,主页上的格子就会变成这样:
图 26
过了一会,在第一节课开始之后刷新一下,主页上的格子就会变成这样:
图 27
单击"当前课程"将会打开Course Hub,并显示当前课程的相关信息,包括课程概况、今天笔记和今天作业:
图 28
到了下午放学之后再打开应用,主页上的格子就会变成这样:
图 29
非常好!不过,有一个小小的地方还需要改进一下,目前每个格子里的内容和标题的字体大小是一样的,这意味着它们是同等重要的,但事实上用户更加关注内容而不是标题,因此我们应该增加内容的字体大小,从而更加突出内容的重要性,事实上,通过不同的字体大小区分不同内容的重要性是Metro的设计原则之一。嗯,这个改进的实现就留给你当课后作业吧。
最近的作业完成得怎样?
通过作业本,用户可以清楚地看到每天有什么作业,哪些已经完成,哪些还没完成,但如果用户希望直观地了解每天的作业量有多少,已经完成的占多少,还没完成的又占多少,每天的作业量是否均匀、能否跟得上呢?前面那组是微观层面的问题,而后面那组是宏观层面的问题,要回答宏观层面的问题,我们需要借助统计工具,比如说,我们可以通过下面这个图表回顾过去五天的作业情况:
图 30
从上图不难看出,最近作业量增大了,还没完成的作业比重也在增大,是时候找找原因了。不过,查明这个原因不是我们的任务,实现这个功能才是我们的任务。那么,如何实现这个功能?
实现这个功能需要从两个方面入手,第一,实现一个算法计算统计结果,第二,使用一个控件显示统计结果。我们先来看看第一个方面,这个算法的基本思路是先找出过去五天的作业,然后把它们分成已完成和未完成两组,接着分别对每组进行统计,计算每天的作业有多少。换句话说,我们的统计结果将会产生两组二维数据,我们可以通过两个集合来保存它们:
代码 24
这两组数据的计算过程基本上是一样的:
图 31
唯一的区别在于一个要筛选出已完成的作业,另一个要筛选出未完成的作业,这个"变数"可以通过方法的参数隔离开来。根据图31,我们可以创建一个ComputeStatistic方法:
代码 25
我们可以通过方法的参数指定计算哪组数据,此外,在执行任何具体计算代码之前,我们需要知道过去五天的日期是什么。看到这里,你可能会问,每次调用这个方法都要创建这样一个集合会不会很低效?是的,你可以把它保存在一个属性里,然后在构造函数里对它进行初始化,这样每次调用这个方法时只需访问那个属性就行了,不过,如果你决定这样做,你需要额外考虑一个问题:当用户在使用的过程中时间跨越了零点,应用需要重新根据新的"今天"重新创建这个集合,诚然,这种情况发生的几率不大,但若放任不管,它就会成为一个潜在的bug。接下来,执行图31所示的五个计算步骤:
代码 26
看到这里,你可能会问,如果现有的作业不足五天呢,比如说一开始没有任何作业?这种情况下,我们应该为没有作业的那些天按顺序"补零",以便横坐标可以正常显示过去五天:
代码 27
有了ComputeStatistic方法,我们就可以实际计算代码24那两个属性的值了:
代码 28
那么,什么时候调用这个方法呢?我们知道,作业的统计数据不会因为时间的流逝而自动改变,因为作业的新建和编辑都需要用户手动完成,而主页上面也没有提供任何直接操作的途径,所以我们无需在主页上为这个统计图表提供一个单独的刷新按钮,只需在页面加载的时候刷新一下就行了:
代码 29
接下来,我们将会把统计结果以统计图表的方式呈现出来。
这里选择Mindscape Phone Elements的Stacked Bar Chart,当然,你也可以使用其它熟悉的图标控件。下载并添加Mindscape Phone Elements的类库的引用,然后在主页上添加一个Panorama项,并把标题设为"作业":
图 32
接着,从Assets面板上把Chart控件拖到Grid里,并使之充满整个Grid:
图 33
我们的统计图表包含两组统计数据,因此我们需要在Chart控件里添加两个StackedBarSeries控件,此时,你的Objects and Timeline面板应该是这样的:
图 34
这两个StackedBarSeries控件将会分别绑到FinishedStatistic和UnfinishedStatistic两个属性。怎么绑呢?选中第一个StackedBarSeries控件,在Properties面板上按照下表设置相关属性的绑定表达式/值:
属性 |
绑定表达式 |
ItemsSource |
{Binding FinishedStatistic} |
XBinding |
{Binding Key} |
YBinding |
{Binding Value} |
SeriesBrush |
#FFDB843D |
表 2
需要注意的是,ItemsSource属性的类型是IList,这正是为什么我把代码24的两个属性的类型声明为IList,当然,你也可以把它们声明为List<KeyValuePair<string, int>>。至于第二个StackedBarSeries控件,你只需把ItemsSource的绑定表达式设为"{Binding UnfinishedStatistic}",并为SeriesBrush属性换一种颜色就行了。
现在,按F5看看效果:
图 35
进入作业本添加一些作业,然后回到主页看看:
图 36
如果作业本里保存了过去五天的作业数据,那么统计图表将会显示成这样:
图 37
目前,我们是硬性规定显示过去五天的统计结果,这个设定显然无法满足所有用户,比如说,有些用户希望查看过去七天甚至过去十天的统计结果,这个时候可以考虑在应用设置里给出选项。如果你决定给予用户这个选择权,那么你就需要考虑在保证统计图表清晰可辨的情况下最多能够显示多少天的统计结果,你需要给定一个选择上限,同时向用户解释这样做的原因。
哪些内容是重点?
教育的一个重要方面是不断强调,这主要表现为重要的内容经常重复出现。当我们的笔记本记满了笔记,并且通过标签做好分类,我们很自然就会问,哪些标签的出现频率最高,因为这些拥有这些标签的笔记通常都是课堂重点/考试要点。这个时候又轮到统计工具出场了,我们可以通过饼图列出七个出现频率最高的标签,剩下的统一归入"其它"类别,像这样:
图 38
为什么是七个呢?这和大脑的短期记忆容量有关,长期研究表明,这个容量是7±2。事实上,当我们在设计Windows Phone应用的用户界面时,我们应该把短期记忆容量考虑进去,不要在上面放置太多东西,以免用户产生心理饱和。
事不宜迟了,我们动手吧!创建一个Windows Phone Page,并把它命名为TagStatisticPage.xaml:
图 39
然后把应用程序标题和页面标题分别改为"笔记本"和"标签统计":
图 40
接着,从Assets面板上把PieChart控件拖到页面的ContentPanel里,并使之充满整个ContentPanel:
图 41
我们的饼图包含了一组统计数据,因此我们需要在PieChart控件里添加一个PieSeries控件,此时,你的Objects and Timeline面板应该是这样的:
图 42
接下来,我们需要为这个页面创建一个ViewModel类。
在ViewModels文件夹里创建一个TagStatisticViewModel类,并在里面创建一个TagStatistic属性:
代码 30
我们将会在构造函数里初始化这个属性,由于标签统计是针对每个课程而不是所有课程来做的,因此我们需要通过构造函数的参数传递课程名称:
代码 31
这个属性的计算过程并不复杂:
图 43
前五个计算步骤可以通过LINQ轻松实现:
代码 32
至于最后一步,我们需要分情况处理,如果统计结果包含的分组在七个或者以内,那么我们只需直接显示统计结果;如果超过七个,那么我们需要把剩余的分组聚合到"其它"分类:
代码 33
创建好ViewModel类之后,我们就可以把它关联到TagStatisticPage页了。
打开TagStatisticPage.xaml.cs文件,在里面创建一个OnNavigatedTo方法:
代码 34
这里假设查询字符串里面包含了一个名为coursename的参数。
最后,我们需要在NoteBookPage页里添加一个Application Bar菜单项,用于打开TagStatisticPage页:
图 44
并在它的Click事件处理程序里添加相关代码:
代码 35
值得注意的是,如果课程表没有任何课程,那么我们就不应该打开TagStatisticPage页了,不过,我们需要通过消息框向用户说明一下。
现在,按F5,然后进入笔记本,打开TagStatisticPage页:
图 45
嗯,很好!不过,有几个地方需要调整一下:
第一个问题很容易解决,我们只需在TagStatisticViewModel类里创建一个Title属性,并在构造函数里把它初始化为课程名称,然后把它绑到PieChart控件的Title属性就行了。第二个问题也很好解决,我们只需把PieSeries控件的StartAngle属性的值改为270就行了。至于第三个问题,我们需要设置PieSeries控件的Brushes属性,按顺序添加相应颜色的SolidColorBrush对象,你可以直接写XAML,也可以通过Blend的Brush Collection Editor来设置:
图 46
我采用的是第二种做法,因为它允许我通过取色器直接从图38上获取颜色,当然,如果你有自己的配色方案,并且知道那些颜色的HTML代码,那么直接写XAML也很方便。改好之后重新运行,然后打开TagStatisticPage页:
图 47
对比图45,图47给人一种相容有序的感觉,这在很大程度上得益于配色方案的选择,这些颜色都是基于同一色相的,因此能够给人一种相容的感觉,同时,这些颜色根据明度(明亮程度)从小到大顺时针排列,因此给人一种有序的感觉,此外,不同明度能使颜色产生不同的"重量",明度较低的颜色给人感觉较重,因此用在出现频率较高的标签上,而明度较高的颜色给人感觉较轻,因此用在出现频率较低的标签上,这样用户可以直观地感知内容的重要程度。一个好的应用除了能够帮助用户解决相关的问题,还应该照顾到用户的无意识需求,这不但对于用户来说是一件好事,对于开发者来说也是一件好事,最自然的操作是凭直觉的操作,最自然的选择是符合直觉的选择,这是一种无需经过思考的条件反射,当用户的无意识需求被照顾到了,他们也会不经意地选择你的应用,他们甚至很难解释清楚为什么就要选择你的应用。
你一直想要的东西
从《WP7有约》系列的第一篇文章发布至今,我收到了无数封索要代码的邮件,但都一一回绝了。如果你在学习这些文章的过程中遇到任何问题,我都乐意为你解答,你可以在文章下面留下评论,我会尽快回复你,这样做的好处是其他人遇到相同/类似问题的同学也可以看到这些交流内容,当然,我也欢迎你通过其它方式和我交流。如果你的问题确实很难描述清楚,你也可以把你的代码发给我,我帮你看看到底哪里出了问题,我之所以愿意花费这个时间是因为你认真对待了这些文章的学习。为什么我会这么肯定呢?因为这个系列的所有代码都是以图片形式呈现的,这导致你无法简单地把它们复制粘贴到你的工程里,你必须亲自输入一遍,这是故意的,因为仅仅看一遍文章虽然可以让你知道那么一回事儿,但这些知识只会暂存在大脑的短期记忆里,过一段时间就会变得模糊,而亲自动手做一遍练习却可以帮助你把知识转存到大脑的长期记忆里,同时,动手的过程中会遇到各种各样的问题,这些问题可以激发你的思考,这对巩固你学到的知识是很有帮助的,因此,当你拿着亲自输入的代码来问我问题时,我也会认真地回答你。
当然,前来索要代码的同学并不都是为了学习这些文章,每个人都有自己的想法和目的,这我能理解,因此,在这个最后的时刻,没错,本文是这个系列的最终回了,我把代码开源出来,你可以到iridescent.codeplex.com下载,这样,你就可以:
事实上,我也鼓励你根据自己的需求对这个应用进行定制,在这个过程里,你已经学到的知识将会得到巩固,因为你需要充分理解这些知识才能找到恰当的切入点,同时,为了实现新的需求,你会去学习新的知识,并在实践的过程中把它们整合到现有的知识体系里,此外,你的定制版本还可以帮助你以及有类似需求的潜在用户,实乃一箭三雕!
除了代码,另一个被提及最多的要求就是提供这些文章的打包下载。会有的,我会把这些文章整合起来,添加目录,适当排版,然后生成PDF提供下载,不过,这需要一些时间,敬请耐心等待。
最后,终于到了真正的最后,感谢各位一直以来的支持,特别感谢PRO为本系列文章精心打造了一个WP7手机外壳,以及他在图片合成方面为本系列文章提供的帮助!
下课了……