WP7有约(二):课后作业

📅 2026/7/5 3:24:30
WP7有约(二):课后作业
上节课布置的作业有做吗没人吭声啊看来大家都忘了哦没事我们这次弄个作业本出来大家就有地方记作业了。在开始设计应用程序之前我们先来看看通常的作业本是怎样记作业的图 1从上图可以看到作业本有点像日记本每次记录时都会写下当天的日期每天的作业又会根据课程进行归类。慢着我怎么知道这些作业什么时候交一般情况下中小学生的作业都是第二天上课时交的但大学生就不同了他们的作业可能第二天交也可能一周之后交有时甚至几周之后才交更重要的是不同的作业可能在不同的时间交。换句话说我们的应用程序还需要支持记录交作业的时间。此外每当完成一项作业我们可以在旁边做个记号这样当我们打开作业本时即使作业再多也能马上知道哪些还没做完。现在用Visual Studio打开项目在Models文件夹里创建一个Assignment类和上节课的Course类一样它也需要实现INotifyPropertyChanged接口。由于我们有很多类都需要实现INotifyPropertyChanged接口为了避免不必要的重复你可以考虑创建一个类专门实现这个接口然后让有需要的类继承这个类。这个需求似乎比较常见因此Prism提供了一个NotificationObject类我们只需继承它就行了代码 1继承之前别忘了引用Bin\Phone\Microsoft.Practices.Prism.dll类库和Microsoft.Practices.Prism.ViewModel命名空间哦。根据前面的讨论Assignment类应该包含以下属性属性名字属性类型备注IdGuid唯一标识CourseNamestring课程名称StartDateDateTime创建日期DueDateDateTime截止日期Contentstring作业内容IsCompletedbool完成状态表 1我们知道Id属性作为唯一标识其值一旦生成就不会改变因此我们只需在构造函数里初始化它就行了代码 2而其它属性则需要在它们的set访问器里调用从NotificationObject类继承过来的RaisePropertyChanged方法比如说我们可以这样实现IsCompleted属性代码 3看到这里你可能会说作业的状态应该不止已完成和未完成两种啊比如说当老师刚把作业布置下来时它应该是未开始当我们开始做某项作业时它应该是进行中有时候准备工作还没好我们不得不把作业推迟此时它应该是已推迟有时候老师可能大发慈悲说某些作业不用做了此时它应该是已取消等等等等。照这样说我们是否也该考虑把现在的两个日期细化为计划开始日期、计划结束日期、实际开始日期和实际结束日期然后加上一个作业进度什么的千万不要这样没有学生愿意采用这么细致的作业管理方案再说这样做也会分散他们的注意、加重他们的负担作业本的主要目的只有一个就是让学生对要做哪些作业一目了然所有功能的设计都应该围绕这点展开所有功能的取舍也应该以此为标准。保存作业本数据存储方面我打算仿效课程表的做法通过JSON序列化把作业本的数据保存到独立存储区实现这个并不难你可以照搬课程表的做法创建一个IAssignmentStore接口和一个JsonAssignmentStore类。当你实现完JsonAssignmentStore类之后你将会发现它和JsonCourseStore类有99.9%的代码是相同的事实上你可以把JsonCourseStore.cs文件复制一份并重命名为JsonAssignmentStore.cs然后把里面的Course字眼都替换成Assignment就可以了。不过这种重复着实让人不爽啊看来是时候重构一下了。ICourseStore接口和IAssignmentStore接口的区别只在于集合元素的类型和集合属性的名字前者可以通过泛型统一起来至于后者我们可以把属性的名字统一为Items这样两个接口就能统一起来了代码 4而实现方面我们可以创建一个JsonDataStoreT类并让它实现IDataStoreT接口代码 5需要说明的是之前我们把文件名硬编码在JsonXXXStore类里那是因为它对于JsonXXXStore类来说是固定的、一对一的而现在的JsonDataStoreT类不再仅仅对应一个文件因此我们把它保存在一个私有字段里。其它的和JsonAssignmentStore类没有太大出入。看到这里有些同学可能会问ICourseStore接口和JsonCourseStore类已经投入使用了现在换用IDataStoreT接口和JsonDataStoreT类会不会造成很大影响这个问题问得好如果你确实不想修改其它代码那你可以把JsonCourseStore类改造成JsonDataStoreCourse类的马甲代码 6需要说明的是我们通过继承JsonDataStoreCourse类获得Rollback和Commit两个方法的实现此外由于其它代码是通过ICourseStore接口间接使用JsonCourseStore类的实例的于是我们保留了ICourseStore接口并把Courses属性重定向到Items属性。不过就项目现在的规模而言我们可以把重构做的更彻底一些我们可以把ICourseStore.cs和JsonCourseStore.cs两个文件删除如果你想保险一点可以先把它们从项目排除出去然后重新编译此时Visual Studio会告诉你找不到ICourseStore接口和JsonCourseStore类分别把它们替换成IDataStoreCourse接口和JsonDataStoreCourse类调用后者的构造函数时记得提供文件名即Courses.json重新编译此时Visual Studio会显示一堆错误全部都是说找不到Courses属性的把它们都替换成Items属性重新编译好了如果那两个文件还没删除的话现在可以安全删除了。原型现在是时候考虑一下用户界面了仔细观察我们的作业本图1是否觉得这种布局方式有种似曾相识的感觉如果你一直关注WP7的相关消息你可能已经看过类似的用户界面了——People Hub的联系人列表。下面我们把它们两个放在一起看看图 2从上图可以看到作业列表和联系人列表刚好能够对应起来课程名称对应姓氏首字母作为分组标题而作业内容则对应联系人作为分组内容。看到这里你可能会问WP7的Silverlight貌似没有这样的控件啊难道要我们自己动手弄一个原本是没有的不过十一月发布的SL for WP Toolkit已经增加了这个控件名字叫做LongListSelector。上节课我们使用了Silverlight for Windows Phone Toolkit的TimePicker控件当时引用的是九月份发布的版本现在你可以下载新的版本然后重新引用一下。仔细观察上图你会发现作业列表上面有个日期没法对应到联系人列表我们该怎么处理这个日期呢这个问题问得好事实上这正是作业列表和联系人列表的最大区别我们知道联系人列表只有一份但作业列表却会有很多份每份都会有一个不同的日期这些作业列表共同组成了一本作业本。如果把每份作业列表看作一个由标题和LongListSelector控件组成的页面那么整个作业本就可以看作由N个这样的页面组成的应用程序了但我们不必真的创建N个这样的页面我们可以仿效课程表的做法利用Pivot控件的特点让每个Pivot项显示一份作业列表这样Pivot项的标题可以用来显示作业列表上面的日期而标题下面则通过LongListSelector控件显示每个课程的作业。不过这样的设计是否真的妥当呢试想一下如果我们首先通过日期来划分Pivot项接着通过课程来划分作业那么每次我们要新建作业的时候我们可能得先创建一个Pivot项如果对应今天的Pivot项还没有的话接着指定作业所属的课程最后才填写和作业相关的信息这个过程显然有点繁琐我们应该尽可能简化其中的步骤。说到这里有些同学可能会建议不如让应用程序自动创建今天的Pivot项这样至少可以省掉一个步骤。嗯这个主意值得考虑不过并非每天都会有作业比如说今天是星期天我进入作业本只是看一下这个周末有哪些作业但应用程序却自动为我创建了今天的的Pivot项而这并非我想要的这意味着应用程序不得不在退出的时候把这个空的Pivot项删除。事实上对于大学生来说尤其是大三、大四的今天有课明天没有是很常见的难道要让用户设置哪天有课哪天没课或者干脆直接解释课程表的数据看看哪天有课哪天没课从上面讨论不难看出日期这个因素很不稳定不太适合用来划分Pivot项但课程就不同了一旦课程表创建好了作业本上会有哪些课程的作业也就定下来了既然这样何不把分组的顺序换一下如果我们通过课程来划分Pivot项那就不用考虑Pivot项的创建和删除了因为用户在访问作业本的过程中会涉及到哪些课程是确定的此外当用户新建作业时也无需额外的步骤来指定今天的日期因为这可以从DateTime的Today属性获取这样我们就为用户省下两个步骤了。从这里我们可以看到应用程序的设计绝对不是把控件堆砌起来显示数据就完事了它包含的是一组完整的用户体验而不同的组织方式可能会产生完全不一样的用户体验有时候多一两个步骤好像没什么大不了但假如这一两个步骤要重复十次的话用户就要额外执行十几二十个这样的步骤了要么你为用户省下这些步骤要么你让竞争对手为用户服务。现在让我们切换到Expression Blend创建一个Windows Phone Pivot Page并把它命名为AssignmentBookPage.xaml完了之后把Pivot控件的Title属性设为作业本把两个Pivot项的Header属性分别设为数学和英语最后把一个LongListSelector控件拖到第一个Pivot项里图 3接下来我们要为LongListSelector控件定制作业的显示方式而执行这个任务的最佳场所是Expression Blend但要发挥Expression Blend的潜能我们需要准备一些示例数据那么我们是否可以像上节课那样导入一些XML数据然后把它们拖到LongListSelector控件上呢很遗憾不行因为LongListSelector控件对于需要进行分组显示的数据源有特别要求。你可能以为我们只需把一个作业集合赋给ItemsSource属性然后指定集合元素的某个属性作为分组依据LongListSelector控件就会自动为我们分组但事实并非如此LongListSelector控件要求我们先把数据分组好然后把这些分组凑成一个集合赋给ItemsSource属性而且硬性规定每个分组至少实现IEnumerable接口否则初始化时将会因为转换失败而抛出InvalidCastException异常此外为了便于显示分组标题每个分组最好有个属性保存标题的内容那么我们如何创建这样的数据源其实创建这样的数据源并不难LINQ的group XXX by YYY完全可以胜任这项任务难处在于我们还想让它在Expression Blend的设计器上显示所以我们得费一点儿周折了。首先切换到Visual Studio在ViewModels文件夹里创建一个AssignmentListViewModel类并让它继承NotificationObject类代码 7接着创建一个GetAssignments方法返回一些Assignment对象代码 8然后再创建一个AssignmentGroups属性通过LINQ选取全部数学作业并根据创建日期进行分组代码 9做好这些准备工作之后我们就可以着手把示例数据关联到用户界面上了。打开AssignmentBookPage.xaml文件创建一个资源字典并在里面创建一个AssignmentListViewModel对象代码 10好了之后就把第一个Pivot项的DataContext属性设为上面创建的AssignmentListViewModel对象并把LongListSelector控件的ItemsSource属性绑到这个对象的AssignmentGroups属性代码 11此时如果你切换到Expression Blend它会提示你重新加载文件因为刚才我们在Visual Studio里做了修改。加载完毕之后你会看到LongListSelector控件里多了一些东西图 4从上图可以看出示例数据已经绑上去了但为什么显示出来的是Iridescent.Models.Assignment而且每个都是一样这是因为LongListSelector控件并不知道如何显示Assignment对象所以直接调用它们的ToString方法获取可以显示的内容而我们在创建Assignment类的时候并未重写ToString方法所以LongListSelector控件调用的是从Object类继承下来的版本这个版本返回的是对象的类型的完全限定名也就是我们刚才看到的Iridescent.Models.Assignment。那么分组标题又哪去了事实上分组标题并未显示出来因为LongListSelector控件并不知道分组的哪个属性表示分组标题。换句话说LongListSelector控件压根不知道如何使用我们提供的数据而把使用方法告诉它正是我们的责任。定制数据模板首先是定制分组标题的数据模板右击LongListSelector控件里的任何地方选择Edit Additional Templates\Edit GroupHeaderTemplate\Create Empty图 5在弹出的Create DataTemplate Resource对话框里输入模板名字然后按OK关闭对话框图 6进入模板的编辑状态之后你会看到一个空的Grid从Tools面板把一个TextBlock拖到Grid里确保TextBlock处于选中状态而不是编辑状态单击Text属性右边的小正方形并选择Data Binding图 7在弹出的Create Data Binding对话框里选中Use a custom path expression并在旁边的编辑框里输入Key图 8为什么输入Key呢因为通过LINQ的group XXX by YYY创建的分组对象实现了IGroupingTKey, TElement接口而这个接口有个Key属性保存了分组的依据——创建日期也就是这里需要的分组标题了。当你按OK关闭对话框之后你将会看到图 9奇怪了我们明明提供了示例数据啊而且数据绑定也没弄错啊为什么TextBlock没有任何显示仔细观察Text属性下面的DataContext属性图 10此时的值应该是分组对象而不是AssignmentListViewModel对象啊我怀疑LongListSelector控件没有正确处理DataContext在设计时的传递bug导致Expression Blend无法获取正确的数据。既然这样我们只好再弄点示例数据了单击Text属性右边的编辑框选择Reset然后把Text属性的值改为2010/11/29。接着在Objects and Timeline面板上选中Grid单击Background属性右边的小正方形并选择System Resource\PhoneAccentBrush图 11此时你的Artboard应该是这样的图 12退出模板的编辑状态保存所有修改然后重新编译项目好了之后就能看到分组标题了图 13不要奇怪分组标题都是2010/11/29这是我们刚才为了编辑的方便硬编码上去的结果暂时忍耐一下吧。接下来是列表项的数据模板右击LongListSelector控件里的任何地方选择Edit Additional Templates\Edit ItemTemplate\Create Empty在弹出的Create DataTemplate Resource对话框里输入模板名字itemTemplate然后按OK关闭对话框。现在我们要思考的问题是如何更好地显示作业数据呢回顾表1Id属性为了便于应用程序搜索Assignment对象而创建的用户并不需要知晓它的存在所以我们不必把它呈现在用户面前Pivot项的标题已经显示了CourseName属性分组标题也显示了StartDate属性剩下的就是DueDate、Content和IsCompleted三个属性了那么我们应该如何显示这三个属性此时我的脑子里浮现出的第一个想法是这样的图 14整个Grid分为两个Column左边是作业内容自动换行右边从上到下分别是截止日期的月、日和完成状态一般情况下创建日期和截止日期的年份都是一样的所以我们没有必要提供重复的信息即使碰到跨年的情况用户也不会因为缺少年份而感到疑惑除非有个老师布置了一个跨越两年或以上的作业。想到这里我的脑子里突然闪出一个问题表示完成状态的TextBlock能否去掉并以其它方式表达这个信息呢此时我的脑子里迅速浮现出各种各样的图标但是还有更好的方式吗颜色突然这个词儿从我的脑子里掠过一般而言与文字相比我们的大脑对颜色的反应更快更准。有鉴于此我把列表项的模板改成这样图 15右边部分将会根据作业的不同状态显示不同底色。退出模板的编辑状态保存所有修改然后重新编译项目好了之后就能看到效果了图 16显然字体的大小、控件之间的间距还不能让人满意我们需要调整一下这个过程可能有点反复和枯燥但这却是我们体贴用户的重要途径我们不但要让用户的眼睛感到满意还要让用户的手指感到满意别忘记我们开发的是触屏应用程序哦下面是我调整之后的效果图 17现在我们可以再次进入模板的编辑状态为对应的控件设置数据绑定了做法和前面为分组标题设置数据绑定的一样图7和图8各个控件对应的自定义路径表达式如下图所示图 18好了之后就可以看到我们前面准备的示例数据了图 19噢分组标题我希望只显示日期而且是符合中国区域设置的短日期格式还有月份的显示我希望是十一月而不是11。这个时候又轮到转换器出场了。首先切换到Visual Studio在Utils文件夹里创建下面两个类代码 12代码 13需要说明的是因为我们的绑定是单向的所以没有必要实现ConvertBack方法。接着在AssignmentBookPage.xaml的资源字典里创建它们的实例代码 14看到这里你可能会问这两个转换器的Convert方法都使用了culture这个参数但我们没有直接调用Convert方法啊那我们怎么把这个参数传给它这可以通过设置绑定表达式的ConverterCulture属性做到现在把那两个TextBlock的Text属性的绑定表达式改为{Binding Key, Converter{StaticResource dateConverter}, ConverterCulturezh-CN}和{Binding DueDate.Month, Converter{StaticResource monthNameConverter}, ConverterCulturezh-CN}。剩下的就是截止日期的底色了既然转换器可以把DateTime对象转换成字符串它也应该可以把Assignment对象转换成SolidColorBrush对象不过在创建这个转换器之前我们得先弄清楚什么状态对应什么底色。前面我们说过作业本的主要目的是让学生对要做哪些作业一目了然而未完成的作业里可能存在一些已经过了截止日期的这类作业需要马上处理所以我们应该单独为这类作业设置一种底色以便用户及时知晓并采取行动。假设这三种状态及其对应的底色如下表所示你也可以换成其它底色状态底色已逾期Red未完成#FF1BA1E2已完成Green表 2那么转换器的Convert方法可以这样实现代码 15接着在AssignmentBookPage.xaml的资源字典里创建它的实例参考代码14并把那个StackPanel的Background属性的绑定表达式改为{Binding Converter{StaticResource assignmentToBrushConverter}}。好了之后就编译一下没问题的话就可以看到效果了你也可以在Visual Studio里看图 20看到这里你可能会问未完成的底色和分组标题的底色是一样的为什么不直接使用PhoneAccentBrush这个系统资源呢这是因为用户有可能在手机的Settings里把Accent Color设成和其它状态一样的颜色这会导致两种不同的状态应用相同的底色而用户也有可能因此获得错误的信息。