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

WP7有约(二):课后作业

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

  插曲 #2

      究竟发生了什么事?是数据没有添加进去?是事件通知没有发出?还是出现线程安全的问题?我调试了一下,数据已经正确添加进去了,事件通知也正确发出去了,所有操作都在UI线程里执行,而且没有出现并发问题,那么问题到底出在哪里呢?

      带着这个疑问,我从codeplex.com上下载了SL for WP Toolkit的最新代码(Change Set 57505),然后调试进去看看。在调试的过程中,我发现每次从NewOrEditAssignmentPage页返回AssignmentBookPage页时,LongListSelector控件都会调用Balance方法,但每次都会"跳过"本应执行的大部分代码,一开始我没怎么留意,觉得这个方法一下子就返回实在太神奇了,仔细观察,原来它是通过第一个if里的return悄悄返回的:

代码 43

  难怪LongListSelector控件什么也没显示,因为Balance方法后面那些负责调整显示的代码一句都没执行。为什么会这样?关键在于IsReady方法,因为它每次都返回false。当我单步进入IsReady方法时,发现_itemsPanel和ItemsSource都不为null,但ActualHeight的值却为0.0,从而导致IsReady方法返回false:

代码 44

  为什么会这样?这是因为,当我们打开NewOrEditAssignmentPage页时,由于AssignmentBookPage页暂时无需显示,Silverlight会把它从主对象树移除,于是ActualHeight会被"清零",当我们从NewOrEditAssignmentPage页返回时,Silverlight需要重新测量每个控件的大小(包括页面本身),并安排它们的位置,ActualHeight的值为0.0意味着Silverlight还没完成布局处理的工作,换句话说,LongListSelector控件还没准备好,IsReady方法返回false是正确的。奇怪的是,每次我们从NewOrEditAssignmentPage页返回时,Balance方法里的IsReady方法没有一次返回true的,这可能意味着Balance方法的调用时机不对,那什么时候调用才对呢?控件加载完毕的时候,即Loaded事件触发的时候,那么,LongListSelector控件在Loaded事件触发的时候做了些啥呢?其实没什么,只是简单地把_isLoaded设为true,然后调用EnsureData方法:

代码 45

  这么看来,问题的关键就在于EnsureData方法有没有正确调用Balance方法了。我们来看看EnsureData方法的代码:

代码 46

  FlattenData和Balance是两个很重要的方法,前者负责从ItemsSource把数据初始化到_flattenedItems,而后者则负责确定哪些数据需要显示以及如何显示。显然,当我们从NewOrEditAssignmentPage页返回时,如果我们创建了作业,if里面的语句是不可能执行的,因为_flattenedItems里面包含了我们的作业!?这听起来很别扭,不是吗?毫无疑问,LongListSelector控件没有考虑我们的情况,即打开一个另一个页面操作数据源,这是不应该的,你不可能指望我们把所有事情都放在同一个页面里处理吧?

      既然知道了原因,问题就不难解决了,把LongListSelector控件的Loaded事件处理程序改成下面这样:

代码 47

  看到这里,你可能会问,_isLoadedRaisedBefore是干嘛的?我们知道,第一次进入AssignmentBookPage页和从NewOrEditAssignmentPage页返回时都会触发Loaded事件,这是两种需要区别处理的情况,因为Balance方法里包含了重设_resolvedFirstIndex和_resolvedCount的代码(参见代码43),如果我们在后面那种情况下执行这行代码,LongListSelector控件的显示就会乱掉,因为它计算不出正确的显示索引,_isLoadedRaisedBefore的存在就是为了防止这种情况的发生。接着,在Balance方法里用if把重设_resolvedFirstIndex和_resolvedCount的那行代码包围起来:

代码 48

  值得提醒的是,每次调用FlattenData方法都会重设_flattenedItems,这对于从NewOrEditAssignmentPage页返回的情况来说是没有必要的,所以Loaded事件处理程序里的FlattenData方法需要放在if里,否则,使用ObservableCollection就会变得毫无意义了。

      改好之后,编译一下。注意,如果你是通过MSI安装SL for WP7 Toolkit的话,你需要先在项目属性里修改一下版本再编译,否则待会重新添加引用的时候Visual Studio会自作聪明的引用原来那个dll文件,因为MSI在注册表里做了手脚

      一切准备就绪之后就可以按F5了。单击Application Bar上的新建按钮打开NewOrEditAssignmentPage页:

图 36

  输入作业内容,然后按确定返回:

图 37

  噢,终于看到我的作业啦!

  编辑作业本·续

      回到作业本的操作,接下来我们要实现编辑和删除两个操作。前面提到,我打算把它们放在上下文菜单里,那么,如何创建上下文菜单?非常简单,我们可以使用SL for WP Toolkit的ContextMenu控件:

代码 49

  正如你所看到的,ContextMenu控件只需嵌入目标对象就能工作了,非常方便。

      接下来的问题是如何实现它们的事件处理程序。我们知道,这两个操作有一个共同点,就是要获取用户当前选中的作业,怎么获取呢?有些同学可能会建议,在AssignmentListViewModel类里添加一个SelectedAssignment属性,并为它和LongListSelector控件的SelectedItem属性设置双向绑定,这样,一旦用户选中某项作业,我们就可以通过SelectedAssignment属性获取作业的Id了。你可以这样做,不过,这个做法会带来一个小小的问题,就是用户在长按某项作业之前得先单击一下。什么意思?我们知道,手机没有鼠标右击的概念,我们是通过长按(Touch and Hold)打开上下文菜单的,但从触摸手势的角度来看,长按和单击(Tap)是两个不同的触摸手势。LonglistSelector控件只会在单击的时候设置SelectedItem属性,它不处理长按,所以当我们通过长按打开上下文菜单时,SelectedItem属性可能为null或者之前选中的其它作业,前者会引发异常,而后者则会为用户带来困扰。为了避免这些问题,要么我们再次修改LongListSelector控件的代码,要么用户不得不执行一步额外的操作,显然,这都不是什么好办法,还有没有别的选择?

      当然有!你知道吗,DataContext属性是一个很特别的属性,子元素可以从父元素那里继承这个属性的值,对照代码49来看,这意味着MenuItem的DataContext和Grid的有着相同的值,而这个值正是我们苦苦寻找的作业!换句话说,只要我们获取到用户单击的MenuItem对象,就可以通过它的DataContext属性获取用户想要操作的作业。我们知道,事件处理程序的第一个参数就是引发该事件的对象,于是我们可以通过这个参数来访问MenuItem对象:

代码 50

  这样,我们既不需要在AssignmentListViewModel类里添加一个SelectedAssignment属性,也不需要修改LongListSelector控件的代码,更不需要委屈用户执行额外的操作,真是一举三得啊!

      现在只剩一个操作了——撤销所有更改,我相信这对于你来说不是问题,所以我决定把它留给你当课后作业。

      好了,又到看效果的时候了!按F5运行应用程序,新建三项作业:

图 38

  长按第三项作业,你会看到这项作业以外的所有东西都缩小了,给人一种向后移动的感觉,这个动画生动地突出了正在操作的作业以及上下文菜单,不过,不知道是不是动画的bug,第三项作业的截止日期上面有个瑕疵(试了几次都是这样):

图 39

  单击编辑将会打开NewOrEditAssignmentPage页,修改一下截止日期:

图 40

  然后按确定返回,你会看到刚才修改的截止日期:

图 41

  接着,长按第二项作业(Textbook. P20. Ex 2),并选择删除:

图 42

  作业成功删除。但是,如果你尝试删除(剩下的)第二项作业,你会发现它还在那里!为什么!?我调试了一下,发现此时MenuItem对象的DataContext属性的值居然是已故的前任第二项(Textbook. P20. Ex 2),而不是我们期望的现任第二项(Textbook. P21. Ex 3)!因为前任第二项已被删除,所以Remove方法不会触发CollectionChanged事件,LongListSelector控件自然不会更新显示。如果你现在尝试删除第一项作业(既是前任也是现任),你会成功的,但是,在删除之后,如果你再次尝试删除剩下的唯一一项作业,你会发现此时MenuItem对象的DataContext属性的值变成刚故的前任第一项(Textbook. P10. Ex 9, 10)!从此以后,剩下的唯一一项作业就再也删除不了了,除非你返回MainPage页重新打开AssignmentBookPage页。由于编辑操作采用了相同的实现思路,如果一项作业删除不了,那么它也编辑不了。

      究竟发生了什么事?是ContextMenu控件的bug吗?我另外创建了一个新的项目,在同等条件下,分别在ListBox和LongListSelector上测试了ContextMenu,结果,Listbox一方表现正常,而LongListSelector一方问题依旧,有趣的是,即使不用打开新的页面,结果还是一样。这让我不得不再一次怀疑是LongListSelector控件的问题。

      我重新运行应用程序,然后单步执行第一次删除的整个过程。在这个过程里,我发现一个很奇怪的事情,当我删除第二项时,LongListSelector控件先把第三项的ContentPresenter和Assignment分离开来,并把分离出来的ContentPresenter推入内部的_recycledItems(类型为Stack<ContentPresenter>),接着对第二项做相同的事,然后把第二项从_flattenedItems里删除,最后重新关联第三项的ContentPresenter和Assignment,问题就出现在最后一步,它居然直接使用_recycledItems顶部的ContentPresenter,换句话说,它把第二项的ContentPresenter和第三项的Assignment关联了!见鬼!此时,如果我删除第一项的话,它会把第一项的ContentPresenter和第三项的Assignment关联!从这里不难看出,它应该在关联之前把_recycledItems顶部那个垃圾扔掉!既然知道了原因,问题就不难解决了,在OnRemove方法的相应地方加上红框里面那句:

代码 51

      重新编译所有东西,然后运行应用程序,这次没问题了。

      写完这篇文章之后,我的第一感觉是LongListSelector控件远未达到产品级别的质量,它的问题导致我无法专注于应用程序本身的功能设计和实现,如果你是本着学习和研究的态度去用它,那没问题,如果你想用它来做产品,那你就要做好心理准备了。不管怎样,这次我还是学到了不少东西。LongListSelector控件的补丁我已经提交到codeplex.com了,在官方发布修正版本之前,我只能使用自己修改的版本了→_→

  下课了……

1
0
标签:WP7

手机开发热门文章

    手机开发最新文章

      最新新闻

        热门新闻