尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

WP7有约(二):课后作业

WP7有约(二):课后作业
📅 发布时间:2026/7/5 3:34:18

上节课布置的作业有做吗?没人吭声啊,看来大家都忘了哦,没事,我们这次弄个作业本出来,大家就有地方记作业了。在开始设计应用程序之前,我们先来看看通常的作业本是怎样记作业的:

图 1

从上图可以看到,作业本有点像日记本,每次记录时都会写下当天的日期,每天的作业又会根据课程进行归类。慢着!我怎么知道这些作业什么时候交?一般情况下,中小学生的作业都是第二天上课时交的,但大学生就不同了,他们的作业可能第二天交,也可能一周之后交,有时甚至几周之后才交,更重要的是,不同的作业可能在不同的时间交。换句话说,我们的应用程序还需要支持记录交作业的时间。此外,每当完成一项作业,我们可以在旁边做个记号,这样,当我们打开作业本时,即使作业再多也能马上知道哪些还没做完。

现在,用Visual Studio打开项目,在Models文件夹里创建一个Assignment类,和上节课的Course类一样,它也需要实现INotifyPropertyChanged接口。由于我们有很多类都需要实现INotifyPropertyChanged接口,为了避免不必要的重复,你可以考虑创建一个类专门实现这个接口,然后让有需要的类继承这个类。这个需求似乎比较常见,因此Prism提供了一个NotificationObject类,我们只需继承它就行了:

代码 1

继承之前别忘了引用Bin\Phone\Microsoft.Practices.Prism.dll类库和Microsoft.Practices.Prism.ViewModel命名空间哦。根据前面的讨论,Assignment类应该包含以下属性:

属性名字

属性类型

备注

Id

Guid

唯一标识

CourseName

string

课程名称

StartDate

DateTime

创建日期

DueDate

DateTime

截止日期

Content

string

作业内容

IsCompleted

bool

完成状态

表 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

而实现方面,我们可以创建一个JsonDataStore<T>类,并让它实现IDataStore<T>接口:

代码 5

需要说明的是,之前我们把文件名硬编码在JsonXXXStore类里,那是因为它对于JsonXXXStore类来说是固定的、一对一的,而现在的JsonDataStore<T>类不再仅仅对应一个文件,因此我们把它保存在一个私有字段里。其它的和JsonAssignmentStore类没有太大出入。

看到这里,有些同学可能会问,ICourseStore接口和JsonCourseStore类已经投入使用了,现在换用IDataStore<T>接口和JsonDataStore<T>类会不会造成很大影响?这个问题问得好,如果你确实不想修改其它代码,那你可以把JsonCourseStore类改造成JsonDataStore<Course>类的"马甲":

代码 6

需要说明的是,我们通过继承JsonDataStore<Course>类获得Rollback和Commit两个方法的实现,此外,由于其它代码是通过ICourseStore接口间接使用JsonCourseStore类的实例的,于是我们保留了ICourseStore接口,并把Courses属性重定向到Items属性。

不过,就项目现在的规模而言,我们可以把重构做的更彻底一些,我们可以把ICourseStore.cs和JsonCourseStore.cs两个文件删除,如果你想保险一点,可以先把它们从项目排除出去,然后重新编译,此时Visual Studio会告诉你找不到ICourseStore接口和JsonCourseStore类,分别把它们替换成IDataStore<Course>接口和JsonDataStore<Course>类,调用后者的构造函数时记得提供文件名,即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创建的分组对象实现了IGrouping<TKey, 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关闭对话框。现在,我们要思考的问题是,如何更好地显示作业数据呢?回顾表1,Id属性为了便于应用程序搜索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}, ConverterCulture=zh-CN}"和"{Binding DueDate.Month, Converter={StaticResource monthNameConverter}, ConverterCulture=zh-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设成和其它状态一样的颜色,这会导致两种不同的状态应用相同的底色,而用户也有可能因此获得错误的信息。

相关新闻

  • DMDUL:达梦数据库离线抽取数据工具
  • 中国 AI 冲击正在撼动硅谷——GLM-5.2 让硅谷大佬纷纷转向中国模型
  • thinkphp连接远程redis提示 Fatal error: Uncaught RedisException: WRONGPASS invalid username-password

最新新闻

  • AI智能体开发指南:从入门到实战应用
  • 第三方平台信息不一致,企业说明材料该怎么写才稳?
  • OpenCV+YOLO实战:快速搭建机器人视觉感知系统
  • Cypress vs Playwright:端到端测试框架实战选型与迁移指南
  • Yale OpenHand:重新定义机器人抓取的开源硬件革命
  • 开题写作告别反复改稿,okbiye 一站式 AI 开题功能打通科研起步全流程

日新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号