多表单系统框架设计
题记:之前写了两篇关于多表单系统分析的短文,只是觉得将多表单系统框架设计的问题分开写不是很合理,于是就将前两篇短文合并在此篇,以完整地叙述多表单系统开发框架。在前两篇短文的评论中,园子里的兄弟推荐了一些不错的工具,这里先谢谢你们。
摘要:在企业呆了3年,接触最多的就是表单了,各式各样的表单,千变万化,好在它们之间有共同点,就是“表单内容+动作名称”。企业办公最常见的业务就是和表单打交道,不管什么请假申请单,权限申请单等等,都需要经过一系列的工作流程来完成它。由于此类业务频繁,在信息化发展迅速的今天,开发此类系统是很有必要的。
1.多表单系统是什么?
“多表单系统”,顾名思义,就是多个不同形式的表单构成的一个系统。它在企业内部也是一种常见的管理信息系统,每个表单的内容和流程不尽相同,也可以说每个表单是一个子系统。在企业内部,各单位的需求不一致,以至于设计一个通用的表单系统非常困难。尽管不能通用,并且也避免不了新表单的增加和已有表单的修改,但是还是可以抽象出一些共同的东东来,如表单的动作名称,因此,我们可以将不变的部分抽象出来,以方便维护和增加新需求。
2.多表单系统的需求
(1).需求概述
某单位主管向软件开发部提出,我们单位有一个申请单,想把它做成Web网页形式的。流程是:
- 我们部门工程师填写申请单;
- 然后由我来审核,审核不通过,则退回申请人重新申请,审核通过,则跑到处理单位主管那里审核;
- 处理单位主管审核不通过,则退回申请人重新申请,审核通过,则分派给处理人员处理;
- 处理人员处理完成,则通知处理单位主管结案、通知申请人。
这个表单流程就算跑完了。流程图:
(2).功能需求
根据以上需求,我们可以设计如下功能:
- 表单申请:申请表单1、申请表单2、……、申请表单n
- 表单查询:申请表单查询、历史表单查询
- 表单审核:审核工作、代理审核
- 代理设置:默认代理设置、代理区间设置
- 表单管理:工作分派、工作清单、处理状况
- 报表管理:报表1、报表2、……、报表n
- 系统管理:表单设置、角色管理、流程管理、处理单位管理、(包括表单共同部分设置)
(3).表单动作需求
有了系统功能结构,但不同表单还有不同动作,概括起来,表单共有以下动作:
- 申请 (Apply)
- 修改 (Modify)
- 重新申请 (Return)
- 取消 (Cancel)
- 检视 (View)
- 审核 (Sign)
- 分派 (Assign)
- 处理 (Process)
- 撤单 (Revoke)
- 结案 (Close)
审核可能有好几步,如上面例子,申请人主管和处理单位主管审核,有的表单有可能在处理单位主管审核前还有好几步审核。
(4).UI需求
首先看看UI抽象图:
从UI层面来分析,表单系统也只有“表单内容”(也就是上图中阴影部分)在变化,各个表单的内容不一样,有简单的,有复杂的。简单的表单,也就是一个实体(可理解成一张数据库表)就可以满足要求。复杂的表单,那就需要n个实体的组合才能满足需要。
关于UI逻辑,对于不同表单动作,有不同的UI界面,也就是要考虑UI的显示逻辑,例如某审核页面需要能修改某个栏位,如“希望完成时间”,甚至有的审核页面可以修改所有内容,另外表单处理的页面和表单检视的页面也不一样,处理页要上传处理报告,还有根据当前登录者的角色判断什么栏位显示,什么栏位不显示,什么功能显示,什么功能不显示,这就是操作权限和UI的联系。
关于UI需求应该还有很多,没有想出来,盼谢大家帮忙补充一下。
(5).业务需求
从业务层面来看,每个表单的动作不一样,也就是每个动作处理的逻辑不同,简单的动作只要将资料插入一张表或将某个字段值改一下之类的简单操作,复杂的动作就是要操作n张表,包括判断,插入,修改,删除等等。
业务逻辑这东西不知道怎么讲好,这块在写起来是比较复杂的,也是系统的最难写的部分,呵呵,个人体会。
(6).变化需求
像这样的多表单系统,最烦人的就是用户不断地变更需求,这在开发企业管理信息系统中是不可避免的。下面就简单描述一下几个常见的变化:
- 增加栏位。这点只有修改代码,想不修改也难,除非自动生成表单什么的,但是自动生成有其局限性,不能完全满足需求。
- 增加功能。这点也要修改代码。
- 增加审核流程。这点需要增加页面和动作处理逻辑了,这点只要系统的扩展性好(采用接口设计),加一个流程也不是很麻烦。
面对需求变化,我们应该有应对变化的方法,尽量减少我们修改代码的地方。
3.多表单系统的设计
(1)技术框架
我们采取大家熟悉的.NET平台来构建B/S模式的多表单系统,以下是主要技术框架:
- .NET Framework 3.5
- ASP.NET WebForm
- ASP.NET Ajax
- JQuery
由于企业应用需求变更频繁,实体修改在所难免,为了修改方便,避免更改多处代码,所以没有使用ORM技术,而是直接SQL的方式编写。
(2)目录结构
首先看看目录结构图:
从图中我们可以看出工程中有三个项目Booking、Common、Workflow。
- Common类库项目是存放系统公共的业务。
- Workflow类库项目是存放系统工作流业务。
- Booking项目是WebApplication,几个主要文件夹和页面如图所示。
(3)系统关键设计
系统的关键技巧就在于,每个表单子系统所有业务都使用上面图中几个页面来完成作业的,不必为每个表单添加相应功能的页面。每个页面(除了一些List页面)有一个Panel控件,其是每个表单内容呈现的容器,真正的内容是通过动态载入Editor和Viewer来实现的,每个表单有两个用户控件(Editor和Viewer)和一个Control类,Editor是表单编辑者,Viewer是表单的监视器,Control是表单的控制者。
下面是各控件和页面之间的继承关系图:
图中的FormControl的FormApply等表单动作方法都是虚方法,真正的业务逻辑是在Editor和Viewer中覆写的。FormControl中GetShowMode方法默认申请、修改、重新申请页面为载入Editor控件,其他页面载入Viewer控件。若其他页面需载入Editor控件,则可以在每个表单的Control类中覆写GetShowMode方法。
下面是几个主要类别的代码:
public class FormControl : BaseControl { /// <summary> /// 取得页面显示模型 /// </summary> /// <param name="pageType">页面类型</param> /// <param name="signStep">审核步骤</param> /// <returns></returns> public virtual string GetShowMode(Type pageType, string signStep) { if (pageType == typeof(Web.FormApply) || pageType == typeof(Web.FormReturn) || pageType == typeof(Web.FormModify)) { return "Editor"; } else { return "Viewer"; } } public virtual void FormApply() { throw new NotImplementedException(); } public virtual void FormReturn() { throw new NotImplementedException(); } public virtual void FormModify() { throw new NotImplementedException(); } public virtual void FormCancel(string cancelReason) { throw new NotImplementedException(); } public virtual void FormSign(bool isAgree, string signComments) { throw new NotImplementedException(); } public virtual void FormAssign() { throw new NotImplementedException(); } public virtual void FormProcess() { throw new NotImplementedException(); } public virtual void FormRevoke(string revokeType, string revokeReason) { throw new NotImplementedException(); } public virtual void FormClose(string closeType) { throw new NotImplementedException(); } } public class FormControlFactory { public static FormControl GetControl(string type) { string className = String.Format("Known.Booking.{0}Control", type); Type controlType = Type.GetType(className); if (controlType == null) { return new FormControl(); //throw new Exception(String.Format("找不到{0}表单控件类型!", type)); } FormControl control = Activator.CreateInstance(controlType) as FormControl; if (control == null) { throw new Exception(String.Format("找不到{0}表单控件类型!", type)); } return control; } } public class FormPage : BasePage { public string FormNo { get { return Request.QueryString["formno"]; } } public string SignStep { get { return Request.QueryString["step"]; } } public string SignRole { get { return Request.QueryString["role"]; } } public string FormType { get { string formType = Request.QueryString["type"]; if (String.IsNullOrEmpty(formType)) { formType = DataService.GetFormType(FormNo); } return formType; } } public string FormName { get { return FormType + "申请单"; } } private FormControl formControl; public FormControl FormControl { get { if (formControl == null) { FormControl form = FormControlFactory.GetControl(FormType); string format = SysSetting.Instance.FormModelPath.EndsWith("/") ? "{0}{1}/{2}.ascx" : "{0}/{1}/{2}.ascx"; string modelPath = String.Format(format, SysSetting.Instance.FormModelPath, FormType, form.GetShowMode(PageType, SignStep)); formControl = LoadControl(modelPath) as FormControl; if (formControl == null) { throw new Exception( String.Format("请确认{0}是否继承自FormControl!", modelPath)); } } return formControl; } } } public class Test1Control : FormControl { public override string GetShowMode(Type pageType, string signStep) { if (pageType == typeof(FormSign) && signStep == "AL") { return "Editor"; } return base.GetShowMode(pageType, signStep); } }
4.这样开发优劣在哪里?
优势:新增一个表单时,只需要在UControls下面添加像Test1一样的控件,表单的内容和逻辑都在控件中实现,这样写不需要为每个表单增加相应的页面,维护时也不用改动多个页面,这是集中开发,集中维护。
劣势:这样写就将一切逻辑都集中在一起,造成代码很长,这点局限也可以让它有所改变,就是将表单对象数据的组装和呈现写在控件中,将业务处理抽出来写在Control类中。
5.结束语
这个系统的设计,我可能没有讲的太透彻,但是原理是很简单的,就是动态加载用户控件,我们把控件看成是一个表单对象,表单对象的动作就在控件中实现,页面中的功能按钮事件响应控件中的方法。关键的一个类就是FormControl,一切表单的动作都定义在此类中。