您的位置:知识库 » 手机开发

WP7有约(二):课后作业

作者: Allen Lee  来源: 博客园  发布时间: 2010-12-20 23:06  阅读: 2767 次  推荐: 1   原文链接   [收藏]  

  插曲 #1

      究竟发生了什么事?示例数据和绑定表达式应该都没问题啊,否则Expression Blend和Visual Studio的设计器也不会正常显示,那么问题到底出在哪里呢?突然,一个想法在我的脑子里闪过,如果我在DateConverter类的Convert方法里设个断点,你觉得会怎么样?试一下吧……结果是,没有到达这个断点,换句话说,Convert方法根本没被调用!这种情况有点像数据绑定找不到分组对象的Key属性,比如说,我故意把绑定表达式的Key改为Key1,结果Expression Blend的设计器就变成这样了:

图 24

      我们知道,分组对象实现了IGrouping<TKey, IElement>接口,因此Key属性肯定存在,否则编译器会报错,那么,什么情况下这个属性是不可见的,或者说,有什么办法可以让它不可见?想到这里,一个词儿突然在我的脑子里冒出来——显式接口实现!如果Key属性是显式实现的,仅当变量的类型是IGrouping<TKey, IElement>时Key属性才是可见的。看到这里,你可能会说,Silverlight不可能直接调用分组对象的Key属性,它应该是通过反射获取这个属性的。没错,当我们在绑定表达式里以字符串的形式给出属性路径,PropertyPathConverter对象将会把这个字符串转换成PropertyPath对象,那么,PropertyPath对象又是如何找到对应的属性呢?在微软公开的.NET Framework 4.0源代码里,我找到了PropertyPath类的实现,里面有个GetPropertyHelper方法负责获取指定的属性:

代码 17

  如果Key属性是显式实现的话,GetProperty方法就会返回null!换句话说,数据绑定和显式实现的属性一起工作的话会出问题。那么,group XXX by YYY返回的分组对象是不是显示实现Key属性的呢?我们知道,使用group XXX by YYY实质上就是调用Enumerable类的GroupBy方法,经过一番查找,我发现它返回的分组对象就是Lookup类内部的Grouping类的实例,但Grouping类的Key属性是隐式实现的,有趣的是,Key属性上方有一段注释:

代码 18

  除了Key属性之外,Grouping类的其它属性都是显式实现的,我猜Key属性原来也是显式实现的,后来由于数据绑定的问题才改为隐式实现。

      这些代码是WPF 4.0的,而Key属性上面的注释也明确提到了WPF,这是不是说Key属性的值在WPF里可以正确显示?我们可以设计一个简单的实验来验证一下:

  1. 创建一个ListBox。
  2. 定制ListBox的ItemTemplate,里面只放一个TextBlock。
  3. 把TextBlock的Text属性设为"{Binding Key}"。
  4. 通过GroupBy方法创建分组对象的集合,并把它绑到ListBox的ItemsSource属性。
  5. 按F5。

  我分别在WPF 4.0、SL 4.0和SL for WP7上执行这个实验,发现只有WPF 4.0能够正确显示Key属性的值,其它两个的ListBox是一片空白的。我怀疑SL的分支是在这个问题得到修复之前创建的,但我没有代码证实这个猜想。

      还有一个问题我没弄明白的,为什么设计器能够正确显示而程序真正运行的时候却不能?难道设计器对显式实现的属性有什么特别的照顾?为了验证这个猜想,我又做了一个实验,我不直接返回分组对象,而是通过下面这个Grouping类包装一下再返回:

代码 19

  结果,设计器也不显示了……我不知道为什么设计器能够正确显示GroupBy方法返回的分组对象的Key属性,这里面肯定有些东西是我不知道的,如果你知道原因,或者先我一步找到原因,那你一定要告诉我哦!

  连接前端和后端

      既然显式实现的属性会对数据绑定造成不良影响,那我们就换成隐式实现吧。首先,在ViewModels文件夹里创建AssignmentGroupViewModel类,并让它继承ObservableCollection<Assignment>类:

代码 20

  为什么要继承ObservableCollection<Assignment>类呢?前面说过,LongListSelector控件硬性规定分组对象至少实现IEnumerable接口,不过,要想获得更好的效果,仅仅实现IEnumerable接口是不够的,LongListSelector控件通过内部的GetItemsInGroup方法来获取分组内容:

代码 21

  从上面代码不难看出,如果分组对象实现了IList接口,那么每次获取分组内容时都会免掉一次遍历。此外,我们还希望当分组内容发生改变时,比如新建/删除一项作业,分组对象能够自动通知LongListSelector控件做出相应的更新,为了实现这个效果,分组对象需要实现INotifyCollectionChanged接口。毫无疑问,能够一次过满足我们所有要求的最简单做法就是继承ObservableCollection<Assignment>类了。

      看到这里,你可能会问,IGrouping<TKey, TElement>接口不用实现吗?不用,LongListSelector控件没有规定分组对象必须实现这个接口,我们只需简单地创建一个Key属性,配合绑定表达式里的属性路径就行了:

代码 22

  需要说明的是,ObservableCollection<Assignment>类也实现了INotifyPropertyChanged接口,所以我们可以直接使用它的OnPropertyChanged方法。

      接下来是分组对象的初始化,这个过程的主要任务有两个:

  1. 查询数据源,把满足条件的作业内容添加到自身。
  2. 监听数据源,把满足条件的内容更改反映到自身。

  执行这两个任务的前提是有个可用的数据源,我们可以仿效课程表的做法,在App类里通过静态属性提供JsonDataStore<Assignment>对象:

代码 23

  有了数据源我们就可以着手执行第一个任务了:

代码 24

  需要说明的是,这里把判断条件单独提取出来了,因为执行第二个任务时还要用到:

代码 25

  需要说明的是,e参数的NewItems和OldItems两个属性看起来好像可能包含多个元素,但事实上它们只会包含一个,因为NotifyCollectionChangedEventArgs类的构造函数限制了这个可能,不过这个限制仅存在于Silverlight的现有版本(SL3、SL4、SL for WP7)。另外,这里使用了Lambda语句来创建CollectionChanged事件的处理程序,虽然你也可以通过一个单独的方法做到,但使用Lambda语句可以利用闭包的特点重用前面的判断条件,当然,使用匿名方法的语法也是可以的。

      还差什么呢?噢,对了,LongListSelector控件内部会调用分组对象的Equals方法进行判等,我们可以重写AssignmentGroupViewModel类的Equals和GetHashCode两个方法,使之根据Key属性来判等以及获取哈希值。这个任务留给你当课后作业吧。

      既然分组对象的类型改了,那AssignmentListViewModel类的AssignmentGroups属性也得做出相应的调整吧:

代码 26

  由于AssignmentListViewModel类对应用户界面上的Pivot项,我们还需要给它创建一个Title属性:

代码 27

  有了这些准备,我们就可以着手实现AssignmentListViewModel类的构造函数了:

代码 28

  看到这里,你可能会说,这条LINQ语句看起来有点复杂嘛!其实不然,想想看,我们的最终目的是什么?创建分组对象并把它们添加到AssignmentGroups属性。那创建分组对象需要什么条件?课程名称和创建日期。课程名称已经有了,创建日期来自哪里?来自数据源。那我们对创建日期有些什么要求?我们只要和指定课程相关的,而且不要重复的。现在,你再看看上面这条LINQ语句,从上往下看,有没有觉得它像下面这条"流水线"?

图 25

  前面我们说过,当用户新建一项作业时,它会自动添加到"今天"的分组里,但如果"今天"的分组还没创建出来呢?那AssignmentListViewModel类就应该为这项新的作业创建"今天"的分组,并把它添加到AssignmentGroups属性:

代码 29

  当用户删除一项作业时,如果这项作业是所属分组的唯一一项作业,LongListSelector控件会自动隐藏这个分组。而当用户撤销所有更改时,AssignmentListViewModel类得把AssignmentGroups属性清空。

  到目前为止,AssignmentBookPage页里的每个组成部分都有对应的ViewModel类了,现在是时候为它创建一个了。在ViewModels文件夹里创建一个AssignmentBookViewModel类,并创建一个AssignmentLists属性:

代码 30

  AssignmentBookViewModel类的任务是读取课程表的数据,然后创建对应的AssignmentListViewModel对象:

代码 31

  看到这里,你可能会问,为什么这里不用监听数据源的更改?如果你要编辑课程表,一定要进入课程表的用户界面,一旦离开课程表的用户界面,课程表的数据就会冻结下来,换句话说,在AssignmentBookViewModel对象的整个生命周期里,课程表的数据是稳定的。

      现在,我们可以着手处理数据绑定了。打开AssignmentBookPage.xaml文件,切换到XAML模式,在页面的资源字典里添加两个数据模板:

代码 32

  接着,把现有的Pivot项删除,并在Pivot控件上设置数据模板和数据绑定:

代码 33

  最后在AssignmentBookPage的构造函数里创建一个AssignmentBookViewModel对象,并它把赋给DataContext属性:

代码 34

      好了,不知不觉又到看效果的时候了!按F5运行应用程序:

图 26

  单击"课程表"菜单项进入课程表,新建两个课程,保存,然后按Back键返回主菜单:

图 27

  在主菜单里单击"作业本"菜单项进入作业本,此时,你会看到作业本已经为刚才创建的两个课程准备了两个Pivot项:

图 28

  只是作业本上没有任何内容,也没有任何途径可以添加内容……

1
0
标签:WP7

手机开发热门文章

    手机开发最新文章

      最新新闻

        热门新闻