在开发系统时,经常会面对各种问题进行决策。比如,日志是写到文本文件中,还是写到数据库;邮件提醒功能是由系统自动发送还是由用户主动发送。下面列举我经常遇到的问题,以及我的应对策略。
1 在系统开始运行前,如何初试化系统的用户帐号
这个问题很常见。以前开发系统时,先建立一个User表,把自己的帐号创建好,对应的权限也设置为最高级的管理员权限,然后开始写代码,建立User表的操作直接用SQL企业管理器完成。后来的进步是先为系统开发用户角色管理工具,直接用管理工具来创建用户,这样的效率要比直接操作SQL企业管理器要高。这样还有一个好处是,可以把管理用户的职责交给其他人,方便别人,也方便自己。
再到后来,对于用户管理这个功能,几乎所有的应用程序都需要,干脆做一个通用的用户角色管理工具,不断的完善。到现在,如果开始一个新项目,创建用户的第一个步骤不是打开SQL企业管理器创建User表,而是直接用通用的管理工具,依次创建角色和用户,并给用户分配权限。这样的方式使我的工作一直比较轻松,不用加很多班。
这种模式在用了一段时间后发现有问题,就是有时候你不知道自己的潜在用户的基本信息,甚至连唯一识别用户的数据也没有。以我们公司为例,公司采用域模式来管理公司的网络。做好Web程序之后,放到内部网上,设置Windows验证方式。当有用户访问我们公司的系统时,除了知道用户的Login ID外,其它的信息都无法获取,角色权限信息也无从得到。向公司的前辈请教,参考公司的其它系统的用户管理模式,需要写一个向导程序。当发现当前登陆的用户在系统用户表中不存在,自动转到向导页面,引导用户填写自己的信息,比如部门,角色。用户点击提交按钮后,会有一封邮件自动转发给系统管理员,管理员登陆审核(以电话和邮件的方式确认)当前申请的用户。
在通用的用户角色管理工具的基础上,再写一个向导程序,以引导用户创建自己的角色权限信息,自己在后台只需要审核确认。相比其它的策略,比如发现系统表中不存在当前用户时,直接转到出错页面,或是干脆throw一个异常。相比这几种方式,上面的方法会智能很多,也让用户感觉好很多。
2 系统连不上数据库或是发生异常,如何处理
有以下几种处理方式:程序中没有任何处理,如果连不上数据库,直接弹出异常页面。
可以在Web.config中有设置出错处理配置节,跳到指定的错误页面。设置如下
<customErrors mode="Off" defaultRedirect="Error.aspx">
<error statusCode="403" redirect="NoAccess.htm"/>
<error statusCode="404" redirect="FileNotFound.htm"/>
</customErrors>
当出现错误时,弹出错误提示页面。比如博客园的出错处理方式
这个方法相对于以前的处理策略,进步很大。
记得以前在进去一个系统时,发生错误,系统只会提示出错之类的文字或图片,但是没有像上面的图片那样的,放上一些正确的可以点击的链接,不至于让用户无所适从。通常的错误,用户都不知道怎么办。
有的系统出错后,会在指定的时间把浏览器重定向到系统的其他页面。比如CSDN论坛的灌水区,有时候看见一个贴子的主题很吸引人,于是点进去,发现这个贴子已经被管理员删除了,出错页面在在几秒后,重定向到论坛的首页。
记录错误日志是必然的,我的处理方法中还会给系统管理员自动发送一封邮件。邮件中有异常的详细信息,我只需要检查一下邮件的内容,就知道系统有没有异常。用户的电话一打来,我查看一下邮件就知道发生了什么异常,然后快速的解决问题。一般系统运行几个月后,很少收到异常通知的邮件。
3 如何处理系统的字符串资源
我的意思是指当某项操作完成或失败时,提示给用户的字符串的存放位置。如下的代码
MessageBox.Show(“EXCEL数据格式错误,找不到指定的EXCEL工作表单。请重新选择EXCEL文件”, “XX –System”, MessageBoxButtons.OK, MessageBoxIcon.Information);
这段代码告诉用户,用户的操作有错误,执行没有成功。
我以前写的代码,通常都是直接把字符串传给MessageBox.Show方法。这样的处理方式,在程序运行方面一点问题也没有。
把界面操作提示字符串分布在界面的各个文件中,这种做法有个问题。如果系统不是自己写的,有时候用户电话打过来,搞开发的也搞不懂这个提示是什么意思。解决的办法通常要直接调试到代码中去,如果系统有多个地方有相似的提示,还需要再现用户的错误,找出问题会复杂一些。于是重构成下面的方法
private string Title = "XX -System";
private string FormatError = "EXCEL数据格式错误,找不到指定的EXCEL工作表单。请重新选择EXCEL文件";
MessageBox.Show(FormatError , Title, MessageBoxButtons.OK, MessageBoxIcon.Information);
相对上一个版本,这种做法的进步很明显。当有用户问到我们问题时,查找错误的原因会容易一些。可以直接根据字符串值,找到代码所在的位置,判断是什么问题。
再到后来,发现有个朋友的系统中是这样处理字符串资源提示
把所有的提示信息写到一个静态类中,并且按照顺序编号。这种做法比上面的方法又有进步。
人的进步真的是需要时间的,急也急不来。
这个做法还有个问题,就是不能适应有多国语言需求的程序。如果是做一个支持中英双语的程序,用户可以根据喜好选择自己的语言。如果用户选择英语,则应该提示英语的消息,习惯中文的用户则应该在需要的时候提示中文的信息。
这个需求可以借助于resx资源文件解决,您可以在网上查阅相关的内容。我补充一种方法,这个技巧是我在一个开源系统中学到的。 文章的地址如下
应用开源项目StringResourceTool2 实现.NET多国语言方案
使用resx文件的苦恼是无法使用VS 的智能提示功能。上面的ApplicationMessage类,在敲MessageBox.Show方法的参数时,可以借助于IDE的智能提示,找到相应的MSGxx.
如果选择使用StringResourceTool2,不仅仅可以像ApplicationMessage那样方便,使用IDE的智能提示,还支持多国语言的需求。如下的代码所示
[strings]
Raw = Raw string
StringArg(string name) = With name argument {0}
[strings.zh-cn]
Raw = 原始字符串
StringArg(string name) = 简体中文参数 {0}
这个方案是我目前找到的,关于存储字符串资源的最完美的方案。一般我都会把各个项目需要使用的字符串资源单独编译成一个程序集,供各个项目使用。
4 日志是写到文本文件中,还是写到数据库中
这个问题要看具体的应有场景。系统业务操作日志,我会写到数据库中。如果是异常日志,以日志发生的日期为文件名,把异常日志写到文本文件中。在每个月固定的时间段,根据文件名得到系统最近一个月发生的异常,集中解决。
对于分层的应用程序,比如系统中使用ORM中间件,如果要写日志,会直接写到文本文件,一般会把出错的SQL语句直接写到应用程序的执行目录中,方便诊断问题,也方便中间件类库被使用。
MS在.NET 框架中的做法是throw一个异常,这是做系统框架的策略。它每次抛出的异常日志文本,都很规范,通常直接拷贝到google中搜索就能找到答案。
5 对于日期类型的值,在数据库中用字符串类型(varchar),还是用日期时间型(datetime)
也许这个问题问得比较幼稚,呵呵,先听我说应用场景。
在一个生产管理系统的工作指令单输入窗体中,窗体中有一个字段是FOB,字段的含义是输入该零件的交货日期。对于知道交货日期的零件,直接选择一个时间就可以了。但是,有时候不知道哪一天交货,可能需要与物流部开会讨论才知道。经理又要求你先填写工作指令单,先开工开料,然后再去确认FOB日期。
(注:工厂在收到工作指令单后,才能根据指令单上面的要求,开始加工,否则工厂是不敢加工生产的)
问题就产生了,转化成程序员的语言,就是现在要写一个值到数据库中,这个值暂时为空,但是稍后会更新为日期值,是选择字符串类型(varchar),还是选择日期时间型(datetime)作为该列的字段类型?
如果选择字符串,当不知道FOB日期时,直接放空值(NULL),或string.Empty,在界面上再转话为日期值类型显示。如果涉及到报表查询,比如查询最近一个月已经组FOB的订单,还需要把这个字段转化为日期时间类型才能做比较。做报表查询的时候不方便。
如果选择日期时间类型,在不知道FOB日期时,可以放空值(NULL)。如果数据库中规定这个字段不允许为空,可以放上一个“遥远的”日期,没有实际作用,相当于占位符的作用。在稍后的会议讨论中,如果知道具体的FOB日期,再把它更新为实际的日期。在界面上读取这个日期值时,发现它是一个特殊的值,要把它转换一下才行。
如果不知道这个时间值,而数据库中又必须填写一个日期时间值,有两种方案。选择一个比当前的可能值大很多的值,或是选择一个比当前值小很多的值。选择最小值的时候要注意,.NET中的DateTime.Min的最小值是公元元年,而SQL Server中日期类型的最小值是美国的建国日,1753-01-01。对于空值,SQL Server 2000中datetime 的空值即默认值为1900-01-01 00:00:00,而C#中DateTime的空值是最小值0001-01-01 00:00:00。
在程序中读取时,如果日期是空值,可以定义DateTime?这样的类型来存放日期空值。
还有一种方案是用整型来存储日期值。使用DateTime.InternalTicks属性,可以获取该日期类型对应的长整型值。以前在用Delphi5调试程序的时候,Watch窗口中把日期类型值显示为一个整数,需要转换一下才能看懂具体表示的时间。.NET框架的DateTime类型也是以长整形来处理日期时间的。
6 对于界面,做成UserControl,还是做成Form
这个问题也适用于ASP.NET Web中,是把界面做成ascx的用户控件,还是直接做成Web Form。
先说Win Forms,这个的区别不大。做成UserControl的原因通常是想重复使用,直接嵌套进另一个窗体中,但是UserControl不能单独测试,必须有一个Form主动加载它才能运行。把界面做成Form,可以单独运行测试,使用下面的技巧,也可以把它嵌套在另一个窗体的Pannel控件中
FrmAbout frm=new FrmAbout();
frm.TopLevel=false;
frm.Parent=panel;
frm.Dock=DockStyle.Fill;
frm.Show();
如下图,右边的区域信息管理是一个独立的窗体,它被加载在右边的pannel控件中。
既方便测试,又可以嵌套进另一个窗体中复用,所以,Win Form中我推荐把界面做成Form.
再来看一下Web, 把部分界面放到ascx文件,也是为了复用。但是,ascx在Web中有两个问题
1)如果ascx中有引用图片脚本资源,它寻找资源的路径是以加载它的Web Form的相对路径开始。
2)每次回发(PostBack),都需要重新加载ascx控件。
与Win Forms的UserControl同理,ascx的用户控件也不能直接运行,它必须被Web Form加载才能运行。我没有比较使用ascx与Web Form的性能,但是我有个体会。
没事的时候我会去看看朋友的QQ空间,我发现,只要记住了朋友的QQ号码,就可以找到所有关于他的日志,相册的地址。QQ空间的地址格式是:36xxxx.qzone.qq.com。我只需要记住这个地址,就可以找到他最近的日志,他的心情,还有他最近结婚的相片。按照程序设计的方法,显示相片和显示日志肯定是在两个页面中完成。但是,从地址栏中看起来,它们是同一个页面。这个功能可以用frameset,也可以用ascx控件。
QQ空间中用的方法frameset,如果用.NET做QQ空间,可以选择用ascx达到同样的效果。
我的结论就是:如果要改善用户体验,特别是对URL地址都非常宝贵的系统,推荐用ascx控件。
7 数据验证方案
界面部分一般都会有一部分代码是用来验证用户输入的。尽管用SqlParameter可以省略验证用户输入的字符串中是否包含单引号,还是有相当的代码,执行业务方面的验证。验证用户输入的名字是否合法。
如果验证不通过,发现用户输入的数据不合乎要求,通常有两个办法
1) 直接提示用户输入的数据不合法,要求用户重新输入
2) 把用户输入的数据存成草搞,先帮用户保存。待用户去查阅相关的手册,或是问同事知道为什么数据有问题后。用户再把之前输入的草稿数据调出来,重新调整后再转化为可用的业务数据。
这个问题来自于以下情景。
假设用户新建采购申请单,申请购买10台电脑,在采购系统中输入采购申请单。输完之后,提交保存,发现无法保存,系统提示采购单编号错误。对于这个错误,申请买电脑的人不知道如何处理,因为他不懂采购单编号的生产规则,也不需要懂。他会打电话到开发部,或是直接到开发部向相关人员寻求帮忙,弄明白后,回到自己位置上,信心满满的unlock电脑,重输入采购单号,点击保存按钮,系统提示会话过期,要求重新输入用户名和密码。系统很聪明,它知道半小时内客户端没有连接响应就中止会话,但是用户丢失输入的那么多数据,都被系统的聪明之举给折腾的,用户又需要重新完整的输入数据。
本来用户就很反对这种数据输入窗体,没有EXCEL方便,现在又出现这个问题,下次让他在用户反馈表中填写关于系统的意见,他肯定第一个发表反对意见。
验证数据的时候,如果发现数据不合法,可以把它先存成一个临时文件。比如用二进制序列化方式把当前数据存成一份临时文件,方便用户直接调出来重新修改,修改之后数据验证通过,再把它存到数据库中。
然而,这个思想我知道的太晚,我的很多系统也是采用第一种数据验证方案,验证不通过直接拒绝保存。
人的学习是需要过程的,有时候急也急不来。接触想法,消化理解,运用,不是一朝一夕能做到的。
有本书中说了一句话,当遇到麻烦和问题时,要对自己说,Boy, Slowly.
把这句话送给大家,与大家共勉。