WM有约II(六):分级限制
Written by Allen Lee
等级制度
在上一集里,我们把发送查询短信息的人分为"联系人"和"陌生人"两大类,应用程序会自动回复"联系人",忽略"陌生人",在这一集里,我们将会引入另外两种类型:"白名单"和"黑名单"。如果你看过上一集,你应该知道"联系人"和"陌生人"是互斥的,它们之间的区别是手机号码是否存在联系人里。"白名单"和"黑名单"是在"联系人"上发展出来的两个细分类型,它们也是互斥的,"白名单"允许执行所有查询,而"黑名单"则禁止执行所有查询。至于"陌生人",我们将会给它一个"注册"的机会,让它可以请求把自己加到联系人里。这四个等级将会通过AccessLevel枚举来表达:
代码 1
在这一集里,我们将会在这个"等级制度"的基础上实现如下效果:
访问级别 | 允许执行的操作 |
Whitelist | PingStatus、PingSchedule |
Contact | PingStatus |
Stranger | SignUp |
Blacklist | N/A |
白名单 & 黑名单
因为白名单和黑名单本质上只是两组姓名,所以我们选择最简单的文本文件来存储,每行一条记录。白名单和黑名单的管理分别由WhitelistManager和BlacklistManager来负责:
图 1
而读/写文本文件的任务则交给ReadAllLines和WriteAllLines两个方法:
代码 2
因为白名单和黑名单是互斥的,所以当我们向任一名单添加一个姓名时,必须确保该姓名不会出现在另一个名单里,比如说,当我们向白名单添加一个姓名时,如果该姓名已包含在黑名单里,则应先从黑名单删除该姓名,由于我们使用了BindingList<T>,于是检查工作可以在ListChanged事件触发时展开:
代码 3
接着,我们需要两个窗体来编辑白名单和黑名单:
图 2
是不是觉得这两个窗体很眼熟?事实上,它们仿照了Status Texts Editor(参见的图1)的做法,所以这里就不详述了。
最后,我们需要在主窗体放置两个菜单项,以便打开Whitelist Editor和Blacklist Editor:
图 3
现在,我们来看看运行效果,一开始,白名单和黑名单都是空的,我们打开Whitelist Editor,单击Add按钮:
图 4
单击TextBox右边的"..."按钮,ChooseContactDialog对话框将会打开:
图 5
选中一个联系人,ChooseContactDialog对话框将会关闭,选中的联系人的姓名将会出现在Whitelist Editor下面的TextBox里:
图 6
单击OK把该姓名添加到白名单里,重复上面步骤添加另一个联系人:
图 7
单击Whitelist Editor右上角的OK按钮关闭窗体并保存白名单。接着,打开Blacklist Editor,参照上面的步骤把Allen Lee添加到黑名单:
图 8
由于白名单和黑名单是互斥的,此时白名单应该没有Allen Lee了,打开Whitelist Editor验证一下:
图 9
如何界定访问级别?
首先,发送方要么在联系人里,要么不在,如果不在,那么他/她的访问级别就是Stranger,如果在,我们还要看看他/她是否在白名单或者黑名单里,如果也在,那么他/她的访问级别就是Whitelist或者Blacklist,否则就是Contact。就实现方式而言,我们应该优先考虑检查白名单和黑名单,因为从集合的角度来看,它们均是联系人的子集,如果发送方在任一名单里,我们就可以立即返回他/她的访问级别,而不必遍历所有联系人,于是,我们可以这样获取发送方的访问级别:
代码 4
那么,查询操作的访问级别又如何获取呢?我们知道,应用程序(目前)只支持三种查询操作,每种操作所需的最低访问级别如下表所示:
查询操作 | 所需的最低访问级别 |
PingStatus | Contact |
PingSchedule | Whitelist |
SignUp | Stranger* |
*SignUp只针对陌生人开放。
不难看出,上表包含了查询操作和访问级别的映射关系,于是我们可以这样获取查询操作所需的最低访问级别:
代码 5
- A:且慢!为何要用switch?
- B:别激动,就目前而言,switch已经可以满足我们的需求了……
- A:目前?难道你不打算为将来做些什么吗?
- B:你是活在将来的吗?如果不是,你怎么知道将来会变成怎样?
不知道从什么时候开始,我也把思维的战线拉长了,比如说,当我写下上面那个表格时,我的脑子里就出现了代码5,接着,我直接在脑子里对它进行重构,想着设计模式,思考着如果不修改ISmsProcessor接口,要如何设计才可以使各个查询操作及其访问级别的关连变得更自然平滑,如果要让用户可以配置每个查询操作的访问等级,又要添加哪些类型,而这些配置信息又该以什么样的形式存储,如何管理等等。人们似乎对预测未来乐此不疲,也发明了各式各样的预测方法,或许,人们讨厌在毫无准备的状态下迎接未来的到来,但未来在到来之前只不过是一个虚幻,而且不稳定,如果我们过度依赖对未来的预测,那将会束缚现在,也会抹杀其它可能的未来……
回到我们的节目,假设我们继续使用代码5的GetSmsProcessorAccessLevel方法,我们如何才能判断发送方能否执行某个查询操作呢?很简单,我们可以通过GetSenderAccessLevel方法获取发送方的访问级别,接着判断这个级别是否在某个查询操作所需的最低访问级别之上(或者两者的访问级别相同),如果是,则可以执行该查询操作,于是,我们可以把SmsProcessorBase.IsAuthorized方法改成这样:
代码 6
好了,又到测试的时候了,沿用上面的测试结果,下表列出了本次测试所用的4个手机号码及其访问级别:
姓名 | 手机号码 | 访问级别 |
Jay Chou | 15834561144 | Whitelist |
Eason Chan | 15933449394 | Contact |
Leehom Wang | 13813572468 | Stranger |
Allen Lee | 13733449394 | Blacklist |
我们将会通过Cellular Emulator分别用这4个手机号码发送如下两个查询短信息:
- {Trombone:PingStatus}
- {Trombone:PingSchedule(2/15/2009 3:30 PM)}
图 10
从上图可以看到,只有3个自动回复,这是预期的结果,再来看看模拟器上的应用程序:
从上图可以看到,其它发出的查询短信息都被成功忽略了。
请把我加为好友……
下面,我们将会实现SignUp操作。首先,我们要定义这个操作的短信息,很明显,它包含的数据将用于创建Outlook联系人,最低限度应该提供姓名和手机号码:
{Trombone:SignUp(Leehom,Wang,13813572468)}
用户界面也需要做出相应调整:
图 11
这个用户界面仅向用户收集姓名,那么,我们如何获取本机的号码?你可以通过P/Invoke调用SmsGetPhoneNumber函数,也可以像我这样,使用OpenNETCF的。下载、安装Smart Device Framework 2.3,添加对OpenNETCF.Phone.dll的引用,接着就是修改Ping按钮的代码了(原本的代码参见的代码14):
代码 7
好了,运行一下看看效果:
图 12
再来看看Cellular Emulator,很好,注册短信息顺利发出了:
图 13
接下来就是注册短信息的处理了,因为它只针对陌生人,而且无需自动回复,所以我们不必遵守SmsProcessorBase类的套路,直接让SignUpProcessor类实现ISmsProcessor接口就可以了。在实现SignUpProcessor.Process方法之前,我们需要一个辅助方法来提取注册短信息里的数据:
代码 8
因为注册短信息的处理需要用户的参与,用户有权决定把发送方添加到联系人还是忽略他/她,所以应用程序应该把截获的注册短信息添加到某个队列里,以便用户过后查询和处理,避免用户在应用程序截获注册短信息时不得不马上处理的尴尬情景。为此,我们需要一个RegistrationQueue来暂存待处理的注册请求,由于每个注册请求都包含了接收时间、姓名和手机号码,于是我们用一个Registration类来存放这些数据:
代码 9
由于这些数据有可能跨越多个应用程序会话,于是我们需要把它们持久化到文件里,这里我还是选择使用XML:
代码 10
因为RegistrationQueue的实现和InterceptionHistory(参见)相似,所以我就不在这里一一细说了。
回到SignUpProcessor.Process方法,在截获注册短信息时,我们需要判断发送方是否陌生人,若是,把相关数据添加到RegistrationQueue,否则,忽略该注册短信息。另外,我们还需要在InterceptionHistory里等级本次截获:
代码 11
最后,我们需要为此配备一个用户界面:
图 14
当应用程序启动时,我们要把RegistrationQueue绑定到上面的DataGrid:
代码 12
当DataGrid上选中的行发生改变时,下面的两个TextBox的内容要更新为当前选中行的姓名:
代码 13
当用户单击Add按钮时,应用程序将会用当前选中的注册信息来创建Outlook联系人,创建完毕后将会把注册信息从RegistrationQueue里删除:
代码 14
而当用户单击Remove按钮时,应用程序将会直接把当前选中的注册信息从RegistrationQueue里删除。
好了,又到了测试的时候了(在测试之前,别忘了把SignUpProcessor挂接到InterceptorManager里!),通过Cellular Emulator发送如下注册短信息:
{Trombone:SignUp(Leehom,Wang,13813572468)}
图 15
现在,我们把注意力集中到模拟器上:
图 16
从上图可以看到,注册短信息已被成功截获,单击Add按钮,应用程序将会用选中的注册信息创建Outlook联系人:
图 17
此时,如果我们通过Cellular Emulator使用刚才的手机号码发送如下查询短信息:
{Trombone:PingStatus}
将会收到应用程序的自动回复:
图 18
而主界面上的截获历史也能正确显示发送方的姓名了:
图 19
你还想要什么?
有一次,我和一个六年级的学生一起看动画片,当我们看到正义的代表向怪兽发动反击时,我问他:"你现在看到这种情节还会觉得体内有一股激动在奔流吗?"他说没有。曾几何时,当我们碰到可以使用新技术的机会时,我们也会异常激动……
或许,在很多人的眼里,技术只不过是用来解决问题的工具罢了,然而,我更倾向于通过学习/使用技术寻找乐趣。下一集,要不我们试一下db4o吧,然后,再试一下Windows Mobile上的Notification,就是当你收到短信息时屏幕下面弹出的提示框……