WM有约II(七):番外篇
Written by Allen Lee
别让软键盘遮住屏幕!
如果你的手机带有数字键盘或者Qwerty键盘,那么你可能不会遇到这种问题;但若你和我一样偏爱全触摸手机,那么你可能已经受到这种问题困扰多时了。当我们打开软键盘时,它会把屏幕的下部遮住,继而妨碍我们的操作:
图 1
这时候就轮到Orientation Aware Control的SipAwareContainer出场了,用法非常简单,把SipAwareContainer"夹"在父窗体和子控件中间就行了!我们来看看使用了SipAwareContainer的主窗体是怎样的(注意:每个TabPage的AutoScroll属性的值都已设为true):
图 2
这次,窗体懂得显示一个滚动条了,而且TabControl的选项卡也没有被软键盘遮住了,很好!我们再来看看Whitelist Editor:
图 3
从上图可以看到,没有控件被软键盘遮住,只是控件和边缘之间的距离没有了,有点不好看,如果此时我把软键盘关闭,将会出现一个很奇怪的现象:
图 4
难道这是……一个BUG?但在主窗体上却又一切正常,我试过在SipAwareContainer和其它控件之间再放一个Panel,并把Panel的AutoScroll属性的值设为true,结果还是一样。无奈之下,只好到官方论坛逛一下,虽然论坛上的帖子少的可怜,不过我还是惊喜地发现有人提到SipAwareContainer有问题,贴子上有人建议把控件的Anchor属性调整为Top和Right,事不宜迟,我们来试一下这个建议能否帮上忙。
经过一番调整,Whitelist Editor终于正常了:
图 5
关闭软键盘,控件回到原来的位置上:
图 6
在调整的过程中,我发现把所有控件的Anchor属性都设为Top和Right并非最佳方法,因为这样会导致有些控件超出窗体左边缘,而水平滚动条却没有出现,经过一番测试,我发现使用下面的设置效果最佳(即图5和图6的效果):
控件
Anchor属性的值
"Whitelist:"Label
Top、Left
ListBox
Top、Left、Right
Add按钮
Top、Right
Remove按钮
Top、Right
"Contact name:"Label
Top、Left
TextBox
Top、Left、Right
"..."按钮
Top、Right
OK按钮
Top、Right
Cancel按钮
Top、Right
表 1
有没有想过直接存取对象?
到目前为止,应用程序的所有数据都是存储在XML或TXT文件里的,今天,我想换一下口味,试一下db4o。下载并安装db4o 7.4 for .NET 3.5,接着向项目添加对Db4objects.Db4o.dll的引用,默认情况下,这个DLL在"C:\Program Files\Db4objects\db4o-7.4\bin\compact-3.5"目录里。
接下来,我会拿InterceptionHistory类来开刀,如果你不清楚它之前是如何实现的,可以先看看《WM有约II(五):区别对待不同的手机号码》。在修改任何代码之前,先把db4o的命名空间引用进来:
using Db4objects.Db4o;
InterceptionHistory类有一个LoadInterceptions方法,之前,我们用它来读取XML里的数据,创建Interception对象并添加到BindingList<Interception>里,现在,我们要把它改造成从db4o数据库里读取Interception对象,并添加到BindingList< Interception >里:
代码 1
首先,我们通过Db4oFactory.OpenFile方法创建数据库对象,接着,我们通过下面这行代码获取所有类型为Interception的对象:
db.Query<Interception>()
Query方法返回的已经是IList<Interception>集合了,为什么还要调用ToList方法呢?这是因为返回的集合是只读的,如果直接把它传给BindingList< Interception>集合的构造函数,那么当我们通过Add方法向BindingList< Interception>集合添加对象时将会引发异常。为什么会这样呢?我们可以用Reflector反编译BindingList<T>的构造函数,发现它只是简单地把参数传给它的父类:
代码 2
而当我们通过"base"追踪到Collection<T>的构造函数时,发现它也只不过是把参数简单地赋值给类型为IList<T>的私有成员,而不是像List<T>那样做个浅拷贝:
代码 3
于是,当我们调用Add方法时,将会引发NotSupportedException:
代码 4
这个问题在MSDN里并非没有描述,但比较隐晦,如果我没中招,可能这辈子都不会想到这么一个问题了:
图 7
当BindingList<Interception>发生改变时,我们只需把发生改变的对象保存到数据库就可以了:
代码 5
最后,别忘了把数据库的路径改过来:
m_FilePath = Helper.MapPath("InterceptionHistory.yap");
好了,又到测试的时候了,下面通过Cellular Emulator使用若干手机号码发送查询短信息:
图 8
接着,我们来看看应用程序的主窗体,嗯,历史纪录也对了:
图 9
到此为止啦?呃,我才不要呢,我还想为主窗体的历史纪录加一个过滤功能,通过这个功能,用户可以选择查看所有历史纪录或者今天的。首先,我们需要在主窗体上添加一个ComboBox:
图 10
过滤选项将会使用FilterOptions枚举来表达:
代码 6
当应用程序启动时,我们需要向上面那个ComboBox填充FilterOptions:
代码 7
当ComboBox的当前选中项发生更改时,将会通知InterceptionHistory,并重新绑定历史纪录:
代码 8
当FilterOption属性的值改变时,InterceptionHistory将会根据过滤选项重新装载数据:
代码 9
而LoadInterceptions方法也会做出相应的调整:
代码 10
因为Query方法不接受null作为参数,所以我们没办法把代码10里的两个条件分支统一起来。目前,过滤条件是在FilterOption属性的set访问器里构建的,将来如果有其它过滤需求,我们可以用一个Dictionary<FilterOptions, Predicate<Interception>>来存放过滤条件,然后在FilterOption属性的set访问器里根据属性值获取对应的过滤条件,并传给LoadInterceptions方法。
好了,又到测试的时候了,首先通过Cellular Emulator使用若干手机号码发送查询短信息,应用程序的主窗体默认显示所有历史纪录:
图 11
把系统时间修改为明天,然后把过滤条件改为Today看看:
图 12
嗯,很好!不过,在我更改过滤条件时,明显感觉得出应用程序的"迟钝",这可能是和我们每次操作都重新打开数据库连接有关,于是,我修改InterceptionHistory的实现,在构造函数里初始化一个IObjectContainer,并以私有成员的方式把它存到InterceptionHistory里,然后让InterceptionHistory实现IDisposable接口,并在Dispose方法里调用IObjectContainer.Close方法。重新运行应用程序,哇,情况不是一般的改善!另外,我还在网上找到一篇有趣的文章:
- 《db40 indexing and query performance》
由于目前数据库的数据不多,无法体会出上面这篇文章所给的建议的好处,但我想历史纪录应该不会很多,因为每条纪录背后都可能意味着"一脚"的付出……说到这里,我们不难想象有些用户会要求应用程序支持自动回复的限额功能,至少在到达某个水平时发出警告……
别把我的短信耗光了!
目前,应用程序会不加限制地挥霍短信,这使部分用户感到担忧,他们非常希望应用程序能够按照他们的意愿节制一点,于是……废话少说,先来看看新的选项窗体:
图 13
请把注意力集中在窗体的下半部分,我们看到一个CheckBox和两个TextBox,那个CheckBox是用来启用配额策略的,上面那个TextBox是只读的,它告诉用户应用程序已经发出了多少条自动回复,它的右边有个Reset按钮,可以把计数清零,下面那个TextBox则是用于设置自动回复的配额上限。这些配置信息将会存储在Options.xml里:
代码 11
当选项窗体打开时,它会通过OptionManager读取配置信息;当用户单击OK菜单项时,它会通过OptionManager保存配置信息,这些功能的实现和之前的一样,如果你不清楚如何实现,可以先看看《WM有约II(三):整合Outlook Mobile的约会信息》。
那么,如何使用这些配置信息?一个最简单的做法就是在自动回复时检查是否超标(即usedReplyQuota的值大于totalReplyQuota的值),若是,什么也不做,否则,照常回复。但如果我们这样做的话,用户可能又要发飙了(是这个"飙"吗?):要不要回复应该由我说了算!
好吧,我们隆重请出今天的主角(确切地说应该是本节的主角)——NotificationWithSoftKeys!首先,确保我们的配额设置没有问题:
图 14
接着,通过Cellular Emulator使用若干手机号码发送查询短信息,当我发到第三条时,通知气球就冒出来了,当我发送第四条时,通知气球更新它的标题栏,告诉我们一共有多少等待发送的自动回复以及当前是第几条:
图 15
通知气球右上角的"2 of 2"旁边有两个导航按钮,分别用于向左和向右浏览等待发送的自动回复,而下面有两个菜单项,通常被称为Soft Key,分别用来发送和忽略当前显示的自动回复。默认情况下,通知气球的显示时间是10秒,在通知气球消失之后,用户可以通过屏幕最上面的通知图标重新打开通知气球。
那么,这个东西怎么实现呢?首先,我得感谢Christopher Fairbairn,要不是这个家伙搞了个NotificationWithSoftKeys,恐怕今天我的日子就难过了!下载他提供的压缩包,里面只有源代码没有DLL,你可以把它们编译成DLL,然后添加到你的项目里,你也可以直接把源代码添加到你的项目里。由于NotificationWithSoftKeys里我们现在的要求还有一段距离,所以我创建了一个NotificationQueue来扩展它。NotificationQueue应用了Singleton模式,我们在它的构造函数里配置NotificationWithSoftKeys:
代码 12
当用户单击通知气球右上角的导航按钮时,我们首先要判断能否执行对应的操作,即在第一页时不能向左导航,在最后一页时不能向右导航,接着更新当前索引和通知气球的标题和内容:
代码 13
其中,更新通知气球的标题和内容的工作由UpdateNotification方法来负责:
代码 14
当用户单击Send菜单项时,当前索引指向的自动回复将被发送,并更新UsedReplyQuota配置信息,然后删除通知气球的当前页;当用户单击Ignore菜单项时,将会删除通知气球的当前页:
代码 15
DeleteNotification方法会删除当前等待发送的自动回复,接着,如果没有等待发送的自动回复,就关闭通知气球,否则,更新当前索引,并更新通知气球的标题和内容:
代码 16
现在,万事俱备,只欠"排队"了:
代码 17
另外,NotificationQueue还实现了IDisposable接口,并在Dispose方法里调用NotificationWithSoftKeys.Dispose方法,以确保资源得到妥善的释放。
最后,我们还需要修改一下SmsProcessorBase.Process方法:
代码 18
至此,故事似乎有了一个完满的结局了,然而,就在此时,我却意外地发现,如果我一直放着通知气球不管,当应用程序退出时,它没有消失!
图 16
我明明调用了NotificationWithSoftKeys.Dispose方法,那为啥它还阴魂不散?原来,NotificationWithSoftKeys是通过把Visible属性设为false来关闭通知气球的:
代码 19
再来看看Visible属性的set访问器的代码:
代码 20
当我单击主窗体的Exit菜单项关闭应用程序时,通知气球是隐藏的,否则Exit菜单项那个位置应该是Send菜单项,换句话说,Visible属性的值是false的,此时,Dispose方法把Visible属性的值设为false将会怎样?Visible属性判断当前值和待设值是一样的,于是跳过整个代码20!难怪应用程序关闭后通知气球还健在……了解症结后,问题就不难解决了:
代码 21
至此,故事终于有个完满的结局了!
你还想要什么?
故事发展到现在,我想现有功能应该可以满足我老爸了吧(嘘,暂时还不能让他知道),慢着,我爸不看英文!噢,那么,下一集,我们来看看多语言支持?