WP7有约(一):课程安排
[2] WP7有约(一):课程安排
[3] WP7有约(一):课程安排
[4] WP7有约(一):课程安排
[5] WP7有约(一):课程安排
[6] WP7有约(一):课程安排
[7] WP7有约(一):课程安排
把前端和后端连接起来
还记得Expression Blend是怎么做的吗?它为MainPage页创建一个与之对应的MainViewModel类,并在代码隐藏文件里把后者的实例绑到前者的DataContext属性上,而剩下的事情就交给数据绑定来处理。接下来,我们将会模范这种做法,把前端和后端连接起来。
我们知道,一个课程表包含若干列,每列都包含了一个标题和一组当天的课程,整个课程表对应于CourseTimetablePage页,里面的每列对应于一个Pivot项,其中,列的标题将会作为Pivot项的标题显示,而列所包含的那组当天的课程将会在Pivot项所包含的ListBox里显示。为了方便理解,我们把它们之间的映射关系制成下表:
页面 |
页面的抽象模型 |
CourseTimetablePage页 |
CourseTimetableViewModel类 |
Pivot项 |
CourseTimetableColumnViewModel类 |
Header属性 |
Header属性 |
ListBox控件 |
Courses属性 |
表 3
我们先来看看CourseTimetableColumnViewModel类。在ViewModels文件夹里创建一个CourseTimetableColumnViewModel类,并让它实现INotifyPropertyChanged接口,然后为它添加一个Header属性:
代码 17
这个属性将会在构造函数里初始化:
代码 18
这些都不难,稍微有点麻烦的是Courses属性,我们该如何实现这个属性呢?目前,JsonCourseStore类的Courses属性返回的是全部课程,而CourseTimetableColumnViewModel类的Courses属性仅返回当天的课程,显然,我们要做过滤,此外,由于JsonCourseStore类返回的课程没有固定顺序,我们还要排序。一个可能的方案是通过LINQ从源集合筛选当天课程,并根据时间进行排序,然后添加到目标集合,我们还需要监听源集合的相关事件,以便在源集合的内容发生更改时把更改反映到目标集合。感觉上这个方案需要不少代码,还有没有别的方案呢?有,我们可以使用CollectionViewSource类,它能根据我们指定的过滤和排序条件提供集合视图,我们可以通过它的View属性访问这个视图:
代码 19
_courses的初始化也是在构造函数里进行:
代码 20
现在,请思考一下,我们是不是直接给Source属性创建一个JsonCourseStore对象呢?回忆一下JsonCourseStore类的设计思路,CRUD四个操作都是直接在它的Courses属性上进行的,而C和U这两个操作是发生在另一个页面的(NewOrEditCoursePage页),这意味着我们需要一个全局的JsonCourseStore对象。解决方案有很多,你可以把JsonCourseStore类设计成单例模式,也可以使用依赖注入容器,而最简单的做法莫过于在App类里添加一个静态属性了:
代码 21
这样,Source属性的初始化就可以通过下面这句来完成了:
代码 22
诚然,这并不是什么好的做法,你可能会坚持使用依赖注入容器,因为这样能更好的降低对象之间的耦合度,如果你已经知道怎么做,请不要犹豫,立即行动,鉴于这篇文章的内容已经很多了,所以我想把这个内容留到后面的文章。
接着来看CourseTimetableViewModel类,在ViewModels文件夹里创建一个CourseTimetableViewModel类,并让它实现INotifyPropertyChanged接口。毫无疑问,它应该包含一组CourseTimetableColumnViewModel对象,我们可以创建一个Columns属性来存放它们:
代码 23
此外,我们还需要一个属性来跟踪和当前显示的Pivot项绑定的是哪个CourseTimetableColumnViewModel对象:
代码 24
这个属性将会和Pivot控件的SelectedIndex属性进行双向绑定。这两个属性都会在构造函数里初始化:
代码 25
我们知道,用户体验很重要,我们应该尽可能减少用户获取所需信息的步骤,当用户打开课程表时最想看到的应该是今天的课程,这正是为什么我要把SelectedColumnIndex属性的值初始化为DateTime.Today.DayOfWeek属性的值。然而,当一种观点产生的时候,反面观点也会随之而来,有人可能建议每次打开课程表都能看到相同的东西,比如说,显示一周第一天的课程,也有人建议和上次离开该页面时的东西保持一致。事实上,这些观点没有谁对谁错,它们的目的都是为了改善用户体验,你可以按照你认为正确的去做,或许,我们也可以考虑在后续的版本里通过选项设置让用户选择。
现在,我们可以着手处理绑定了。打开CourseTimetablePage.xaml,切换到XAML模式,把现有的两个Pivot项删掉或者注释掉,因为我们不再单独处理个别Pivot项,而是把CourseTimetableViewModel类的Columns属性绑到Pivot控件的ItemsSource属性上,由Pivot控件动态创建Pivot项。此外,我们还要把CourseTimetableViewModel类的SelectedColumnIndex属性绑到Pivot控件的SelectedIndex属性上,并设置为双向绑定。设置好绑定后,Pivot控件的XAML应该是这样的:
代码 26
但是,Pivot控件又从何得知应该创建怎样的Pivot项呢?事实上,它无从知晓,也不知道该如何使用CourseTimetableColumnViewModel 类Columns属性,所以我们必须通过某种方式告诉它我们希望它怎么做,而这种方式就是数据模板。我们需要创建两个数据模板,一个用于Pivot项的标题,另一个用于Pivot项的内容,这两个数据模板将会放在页面的资源字典里。第一个数据模板很简单,只需创建一个TextBlock,并把Header属性绑到它的Text属性上:
代码 27
另一个数据模板也不难,你可以把其中一个Pivot项的XAML代码复制过来,并修改ItemsSource属性的绑定:
代码 28
之前Expression Blend为我们生成的Course类的属性采用全小写命名方式,而现在已经改为Pascal命名方式,所以courseCollectionItemTemplate数据模板的相关绑定也要改过来:
代码 29
数据模板创建好之后就可以应用到Pivot控件了:
代码 30
由于我们不再使用Expression Blend生成的示例数据了,所以你可以把包含Pivot控件的Grid的DataContext属性去掉,然后在代码隐藏文件的构造函数里设置DataContext属性:
代码 31
好了,我知道你很心急了,按F5吧,然后单击中间的按钮……噢!出错了!
图 43
怎么回事呢?经过一番调查,我发现问题出在代码25的设置SelectedColumnIndex属性那行,如果你去掉这行,你会发现什么问题也没有,如果你尝试为它硬编码一个值,你会发现同一个值有时候会抛异常,有时候不会,非常不稳定,为什么呢?原来,当我们设置SelectedColumnIndex属性的时候,它会把我们给它设的值同步到Pivot控件的SelectedIndex属性,但此时Pivot控件有可能还没完全构建好,于是就出错了。解决方法是把代码25出错的那行删掉,在代码31设置DataContext属性的那行后面加上这句:
图 44
值得提醒的是,通过Loaded事件来设置SelectedColumnIndex属性是必须的,因为此时页面及其包含的控件已经构建好了,如果你只是把设置SelectedColumnIndex属性的代码从代码25复制粘贴到代码31,那么问题依旧存在。现在,当你打开课程表时,你会看到Pivot控件快速滑到今天的课程:
图 45
哎哟,课程表空荡荡的,怎么检查课程的显示格式是否正确?添加几个看看吧。但是,添加课程的功能还没有……