工作流设计
http://www.uml.org.cn/workclass/work.asp#1
工作流引擎的作用是解释流程定义,创建、执行和控制流程实例。对于本项目研究来讲,工作流引擎要接受外部请求事件,并按照业务过程的需要执行响应动作。具体来讲,包括接受工作人员启动业务流程的请求,解释流程定义,创建新的流程实例以及流程的首任务实例;响应用户的完成任务动作,生成下一步的任务实例;响应用户的流程回退请求,将流程实例回退到前面的任务实例重新执行等。总之,工作流引擎遵循前面讨论的过程控制模型,接受外部指令,驱动和控制流程在各业务节点的流转。
本文的项目实践基于政府机关部门的统一政务系统,各项审批业务包括业务流程定义、业务运行、业务流程监控均在统一的工作流管理平台上运行和管理,并由一个轻量级引擎驱动。这里的工作流引擎只是政务系统的一个从属组件,并不是一个独立的工作流产品。引擎的功能包括,配合工作流定义工具,通过对数据库的访问与操作,实现对业务流程的保存与修改;通过创建流程实例以及流程的首任务实例,实现对业务流程的启动;通过创建和修改后续的各节点任务实例,驱动流程实例的运行或回退等。
4.1 工作流引擎的数据模型设计
4.1.1 工作流定义的数据库设计
要实现对政府审批过程的管理与监控,需要预先定义和保存业务过程。过程定义可以通过上一章提到的图形定义工具进行,通过过程节点和连接节点的迁移线段,绘制业务过程,并将过程及其节点、迁移等元素的图形属性分别保存到记录过程定义、节点定义和迁移属性的数据库表中。有了过程定义,下一步就可以创建过程实例,启动业务过程的具体执行。
1、工作流过程定义:
字段名 |
字段类型 |
字段描述 |
ProcessID |
整数(标识) |
过程定义编号 |
ProcessName |
字符串 |
过程定义名称 |
RelatedTable |
字符串 |
与当前过程定义相关联的业务数据表名称 |
Alias |
字符串 |
业务数据表中文名称 |
IdentifiedField |
字符串 |
业务数据表的标识字段名称 |
IsDefineCompleted |
字符(Y/N) |
当前过程是否已被完整定义(只有定义完整的过程才可用) |
IsSuspended |
字符(Y/N) |
当前过程是否被挂起(被挂起的过程不可用) |
DueDate |
整数 |
业务过程的承诺办理期限(单位为天) |
过程定义表所包含的字段中,过程定义编号、过程名称等字段是过程定义的基本属性;关联的业务数据表名称等是它的扩展属性,其中IdentifiedField字段保存了关联业务数据表的标识字段名称。后面在定义过程实例时将详细介绍这些字段的作用。
2、过程节点定义:
字段名 |
字段类型 |
字段描述 |
TaskID |
整数(标识) |
节点(任务)编号 |
TaskName |
字符串 |
节点(任务)名称 |
ProcessID |
整数 |
所属过程定义编号 |
NodeType |
字符串 |
节点类型(开始、结束节点或任务节点) |
ProcessLogic |
字符串 |
节点的过程逻辑属性(是否分支或合并节点) |
AssignedRole |
整数 |
分配的任务角色编号 |
AboutAttached |
两位字符 |
是否可以上载或下载附件 |
ReadableFields |
字符串 |
当前任务可读的业务数据表字段名集和 |
WritableFields |
字符串 |
当前任务可编辑的业务数据表字段名集和 |
DueDate |
整数 |
当前任务的承诺完成期限(天) |
PrintedTable |
字符串 |
当前任务可打印的表格编号集合 |
节点定义中的字段NodeType有三种取值:Start、End和TaskNode,分别表示开始、结束和任务节点;ProcessLogic字段的取值包括:空值、AndSplit、AndJoin、OrSplit、OrJoin等,分别代表任务节点所包含的过程路由属性。如AndSplit表示当前任务节点具有“与分支”的过程逻辑属性,当前任务的完成将触发其后续节点任务的同步执行。在后面章节介绍程序逻辑时还会进一步分析。
节点定义的AboutAttached字段记录当前任务是否可以上载文件附件或是否可以阅读前面任务所上传的附件,其取值包括:L、R、LR,分别对应只能上传、只能下载阅读、既可以上载又可以阅读等权限;ReadableFields和WritableFields字段记录了当前任务可以浏览和编辑的关联业务数据表的字段名称集合,字段名城间以逗号隔开,在任务处理页面,程序将根据这两个字段向用户列表显示业务数据,并提供数据编辑控件供用户数据和修改业务数据;PrintedTable字段存储了当前任务可以打印的业务相关表格的编号集合。与业务表相关的这些字段,以及业务数据表、打印表格等内容,在后面分析任务处理程序是还会详细介绍。
AssignedRole字段记录了具有当前任务处理权限的角色编号。系统采用基于角色的任务分配机制,在过程执行时,任务将分配到具有对应角色的多个用户的任务列表中,谁先选择了任务,就由谁来执行,有效地模拟了现实世界机关业务的办理机制。有关角色的设置与分配等内容,在后面的用户权限管理部分还会详细介绍。
3、迁移定义:
字段名 |
字段类型 |
字段描述 |
ID |
整数(标识) |
迁移(有向线段)的编号 |
ProcessID |
整数 |
迁移所属过程定义的编号 |
ToNodeName |
字符串 |
迁移指向的节点名称 |
FromNodeName |
字符串 |
迁移的起始节点名称 |
与工作流图形定义工具中绘制的业务流程图对应,迁移定义表的作用是表达节点及任务之间的路由关系及执行顺序,通过与节点定义的ProcessLogic字段配合使用,可以充分表达业务过程的执行路径。
4.1.2 工作流实例的数据库设计
有了保存在数据库中的工作流过程定义,通过政务处理系统,用户就可以根据需要启动业务过程的执行。每一次启动某个业务过程,工作流引擎都会使用保存在数据库中的对应的过程定义,创建一个新的过程实例,也就是在过程实例表中创建一条新的记录;在创建过程实例的同时,工作流引擎还将查找过程节点数据表中的首任务节点,并创建首任务实例,也就是在任务实例数据表中创建一条新的任务实例记录。这样,具有首任务处理权限的角色用户在登陆政务系统后就会在其可处理任务列表中看到该任务。首任务的处理完成,将触发工作流引擎在节点定义表中查找其后续任务,并创建其后续的任务实例,直至当前业务过程执行完毕。
1、过程实例的数据模型
字段名 |
字段类型 |
字段描述 |
ProcessInstanceID |
整数(标识) |
流程实例编号 |
Description |
字符串 |
业务实例的内容描述 |
ProcessDefinitionID |
整数 |
流程定义编号 |
StartTime |
日期 |
流程实例的创建日期 |
EndTime |
日期 |
流程实例的结束日期 |
StartTaskInstanceID |
整数 |
开始任务实例编号 |
RelatedTable |
字符串 |
过程相关的业务数据表名称 |
IdentityFieldValue |
数字 |
关联业务表的记录编号值 |
IsSuspended |
字符 |
Y(N) 是否冻结当前流程实例 |
SuspendedTime |
日期 |
冻结时间 |
IsCanceled |
字符 |
Y(N) 是否取消当前流程 |
CanceledTime |
日期 |
取消流程的时间 |
CanceledReason |
字符串 |
取消流程的原因 |
Message |
字符串 |
记录每个环节的任务名称与完成人,以及用户审核意见 |
Description字段由用户在启动流程时填写,用于记录流程实例的描述,如正在处理的审批项目名称、提出申请的单位(个人)等,用于在流程监控时,配合开始、结束时间等字段作为查询项使用。
RelatedTable与过程定义中的同名字段一样,保存了过程执行中要处理的业务表名称,把业务表名称复制到这里,是为了以适当的数据冗余换取效率和方便性(当然,不设置该字段,而使用过程定义中的同名字段也完全可以);IdentityFieldValue字段记录了流程所关联的业务表(业务表名称等于RelatedTable字段值)的一条记录编号。当启动流程,生成流程实例时,工作流引擎根据流程定义,自动生成关联业务表的新记录,并将记录编号保存到流程实例中。这样,通过RelatedTable字段、过程定义的IdentifiedField字段、IdentityFieldValue字段,工作流引擎就可以方便地找到与当前过程实例对应的一条业务数据记录,使得不同的的任务实例可以按照不同的数据字段编辑和查询权限(通过对照节点定义的ReadableFields和WritableFields字段),查询或编辑业务表数据。
IsSuspended字段记录流程实例是否被冻结,被冻结的流程只有在解冻之后才能继续执行;IsCanceled字段记录流程实例是否被取消,取消流程实例的操作,会激发引擎删除关联业务表记录、删除所有任务实例,只保留流程实例供监控。因此取消流程实例操作是不可逆的,用户在取消流程时需要填写取消原因,而且只有被冻结的流程实例(而且冻结时间超过一定期限)才可以被取消;Message字段用于保存不同环节任务的完成人姓名,以及用户审核意见、沟通信息等,如任务回退的原因、下一步任务需注意的问题等。用户可以随时浏览沟通信息栏,并添加自己的意见、建议等。
2、任务实例的数据模型
字段名 |
数据类型 |
字段描述 |
TaskInstanceID |
整数(标识) |
任务实例编号 |
TaskDefinitionID |
整数 |
任务定义编号 |
ProcessInstanceID |
整数 |
对应的流程实例编号 |
StartTime |
日期 |
任务开始时间 |
EndTime |
日期 |
任务结束时间 |
TaskUserID |
整数 |
拾起任务的用户编号 |
NextTaskInstance |
字符串 |
后续任务实例编号集合(逗号隔开) |
PreTaskInstance |
字符串 |
前驱任务实例编号集合(逗号隔开) |
TaskState |
字符串 |
任务执行状态 |
前面已经提到,新创建的任务实例将出现在具有任务角色权限的所有用户的任务列表中,谁先选择接受任务,对应的用户编号将保存到任务实例的TaskUserID字段。
TaskState字段是任务实例的执行状态。工作流引擎对流程的控制实际上是对任务实例的完成状态进行管理,通过改变任务的完成状态来驱动流程的继续执行或回退。任务实例的状态字段包括“JustCreated、Running、Completed”等3种取值,分别对应任务刚被创建、任务正在进行和任务已完成等3种任务执行状态。PreTaskInstance和NextTaskInstance字段的设置,也是为了方便引擎控制流程的回退和重新执行。
PreTaskInstance字段保存了当前任务实例的前驱任务实例集合。根据当前任务实例(所对应的任务节点定义)是否包含过程路由属性,其PreTaskInstance字段的值有所不同。如当前节点为一般任务节点(其过程逻辑属性字段ProcessLogic字段值为空,即只是一般任务节点,不负责过程路由),则它只有一个前驱任务实例;如果当前任务节点是一个并行分支的合并节点(其ProcessLogic字段值为AndJoin,只有所有的并行分支都执行完毕,才会执行到该节点),则该节点任务实例的PreTaskInstance字段值则要包含多个前驱任务实例编号(有几个分支,就有几个前驱任务实例),并以逗号隔开。
NextTaskInstance字段与此类似。如当前节点为一个并行路由的分支节点,则实例的extTaskInstance字段值也要包含多个后继任务实例编号。
4.2 工作流引擎组件及核心程序逻辑
4.2.1 工作流引擎的组件类设计
图4.1描述了WFEngine工作流引擎包的核心类设计。
WFEngine软件包是构建政务管理系统的核心组件。政务系统通过调用WFEngine组件的类方法,对存贮在关系数据库中的工作流控制模型数据和信息模型数据进行编辑,实现对流程定义、流程运行和流程监控的统一管理,并在此基础上实现对业务信息数据的进一步管理和应用。
图4.1 WFEngine工作流引擎包的核心类设计
4.2.2 工作流引擎的核心程序逻辑
1、引擎完成不同任务的程序逻辑
任务节点的执行重点要区分顺序执行的任务、并行执行的任务以及流程沿不同路径的执行等情况。
在讨论节点任务定义的数据模型时提到,任务定义包括字段ProcessLogic,代表任务节点所包含的过程路由属性。在处理不同类型任务时,根据当前任务节点的不同类型,用户的任务处理界面将出现不同的结束任务按钮。如当前节点为与分支任务节点时,将出现“结束当前任务”按钮,点击该按钮将触发引擎创建当前节点的所有后继任务实例;当前节点为或分支任务节点时,将出现以后继任务为按钮名称的不同的迁移按钮,点击不同按钮将触发引擎创建不同的后继任务实例,使流程沿着不同的路径执行(对于或分支中存在的旁路迁移,即迁移直接连接了或分支任务节点和或连接任务节点,这里出现的就是以或连接任务为按钮名称的迁移按钮,点击该按钮将触发引擎直接创建或连接任务实例,类似电路中的“短路”现象)。当前任务为一般任务时,用户的任务处理界面也将出现“结束当前任务”按钮,按钮的点击将触发引擎的后续处理,即判断当前任务的后继节点类型,如果后继节点是“And Join”任务节点,则还要判断“And Join”节点的所有前驱任务是否都已完成,如果都已完成,则创建该“And Join”节点任务实例,继续流程的执行;如果“And Join”节点存在有前驱任务尚未完成的情况,则只设置当前任务的状态为“已完成”,并等待其它并行任务的完成。
引擎对包含不同逻辑属性任务的处理逻辑如图4.2所示:
图4.2 引擎处理不同类型任务的程序逻辑
2、流程的回退处理
流程回退与WfMC的循环路由有所不同,它是指后续环节(任务)能够有选择地将流程回退到特定的前驱环节重新执行,以满足修改输入错误、补充审批所需资料等特定需求。
流程回退的另一个作用是取消正在进行的业务过程。当流程被回退到发起业务的首任务节点时,参考回退的不同原因,首任务用户有权选择继续业务的执行或者取消当前正在执行的业务。
完整的流程回退处理包括以下几方面的内容:
(1)列举当前任务的所有前驱任务环节,供用户选择回退:
该功能的程序逻辑见图4.3:
图4.3 创建节点的所有前驱任务实例列表的程序逻辑
该功能的实现既要区分当前的任务节点类型,还要判断它的前驱节点类型。如果当前节点的过程逻辑属性字段ProcessLogic不为空,即当前节点包含了过程逻辑属性,如或连接任务节点等,则在该节点实例之前创建的所有任务实例都可以作为流程回退的目标;如果当前任务节点的ProcessLogic字段为空,即当前节点为一般任务节点,则还要循环判断其前驱节点类型,如果前驱节点为一般任务节点,则将它添加到流程回退的目标环节列表中,然后继续判断,直到前驱节点为逻辑任务节点或已到达首任务节点为止。首任务节点不设置回退功能。
(2)修改回退目标的任务完成状态:
修改发起回退的任务实例的TaskState为“Completed”,同时将用户选择的回退目标任务实例的TaskState修改为“Running”,使得回退任务重新显示到对应用户的任务列表中。
(3)回退目标的后续任务处理:
回退目标的后续任务处理需要引擎判断当前回退目标的任务节点类型,如果是“OR Split”任务,引擎还要判断用户选择点击的路由按钮,如果选择的路由不是原来的执行路径,则删除原路径的所有后续任务实例,并清空相应实例所编辑的业务字段内容,同时删除相应实例所上传的附件,按照用户所选路径创建新的任务实例,开始流程在新路径上的执行。
如果是其它节点类型,或用户选择的路由为流程原来的执行路径,当用户点击按钮结束当前任务时,引擎会修改当前任务的TaskState为“Completed”,修改其NextTaskInstance字段所指向的所有任务实例的TaskState为“Running”,使流程得以按照原来的执行路径重复执行,直到将发起回退的任务实例状态也设置为“Running”,继续流程的执行。
3、用户任务列表
工作流引擎的一个很重要的功能是提供用户可以处理的任务列表,就是在用户登录后,应用程序通过调用引擎,列表显示登录用户可以处理的任务实例,供用户选择处理。
对于用户对列表中任务实例的选择和执行,系统将采用类似JBPM[1][2][3]的“抢占”方式,即新创建的任务实例将出现在具有相应角色权限的所有用户的任务列表中,谁先选择了任务,该任务实例就由谁处理,拾起任务的用户编号会被记录到该任务实例的TaskUserID字段,并修改任务实例状态为“Running”,其他相同角色用户将不能再处理该任务实例,在用户完成拾起的任务后,流程继续进行,否则流程进入等待状态。
用户任务列表中可以处理的任务主要包括如下几个部分:
(1)启动业务流程
根据登录用户的角色,找出所有首任务角色等于登录用户角色的流程,将流程首任务名称列表显示在用户的“启动业务流程”任务列表中。用户点击启动按钮会激发引擎生成新的流程实例及其首任务实例。
(2)新生成的任务实例
即前面的任务完成后由引擎创建的新的任务实例。这种任务实例的特点是其任务状态TaskState为“Just Created”,而且其TaskUserID为空,同时要判断任务对应角色是否在登录用户的角色列表中。
(3)临时保存的任务实例
即用户选择任务后点击了保存任务按钮。这种情形的任务实例其特点是TaskState为“Running”, TaskUserID等于当前登录用户。
(4)回退的任务实例
对于前面任务节点出现输入错误、提供资料不全的情况,后面的任务节点可以将任务退回到特定任务实例。这种任务实例的特点是其TaskState被引擎重新从“Completed”修改为“Running”,而且其NextTaskInstance不为空、其TaskUserID等于当前登录用户。
系统登录用户的可执行任务列表,通过调用ProcessDefinitionDo类的GetProcessListByUserID方法、TaskInstanceDo类的GetTaskListByUserID方法等实现。
4.3 工作流引擎的核心代码实现
4.3.1 电子政务系统的基础类
电子政务系统的基础组件类,包括完成数据库访问功能的基础类,以及完成字符串截取、字符串连接等功能的工具类。
1、完成数据库访问功能的基础类
工作流引擎以及政务系统都要对数据库进行访问。为增强数据访问的安全性,也是为了提高代码编写效率、方便数据访问代码的统一修改,减少代码编写错误,在这里设计和实现了一个专门用于数据访问的基础类Base(其命名空间为DataAccess,对应于第三章图3.4中的DataAccess类包),对数据库的读写操作则以类方法(操作函数)的形式出现。这样,在需要对数据库进行读写操作时,只要创建一个Base类对象,并调用对象的相应操作即可。
下面给出了Base类所包含的主要方法(操作),部分核心方法给出了主要实现代码:
//BaseCode.cs文件
......
namespace DataAccess
{
public class Base
{
private SqlConnection mAppCon = null;//数据库连接对象
private string SqlErrDes = "";//数据库访问错误描述字符串
public Base()
{
mAppCon = new SqlConnection();
......
//使用ASP.NET配置文件Web.config中的数据库连接配置字符串
//......
//<configuration>
// <appSettings>
// <add key="DBsqlConn" value="data source=(local);initial
// catalog=myworkflow;password=mxh;user id=mxh;persist security
// info=True;"/>
// </appSettings>
//......
string connstring = ConfigurationSettings.AppSettings["DBsqlConn"];
mAppCon.ConnectionString = connstring;
}
//执行sql修改、添加、删除操作
public bool SQLExeNonQuery( string Sql )
{
try
{
//打开数据连接
if( mAppCon.State != System.Data.ConnectionState.Open )
mAppCon.Open();
SqlCommand cmd = new SqlCommand( Sql , mAppCon );
cmd.ExecuteNonQuery();
mAppCon.Close();
return true;
}
catch(System.Exception ex)
{
//关闭数据库连接
if( mAppCon.State == System.Data.ConnectionState.Open )
mAppCon.Close();
//设置类对象属性--sql操作异常描述字符串
SqlErrDes = ex.Message;
return false;
}
}
//执行事务处理(多语句修改、添加、删除操作)
public bool ExeSQLNoResultTransaction( string Sql )
{
try
{
if( mAppCon.State != System.Data.ConnectionState.Open )
mAppCon.Open();
//初始化SQL事务处理对象
SqlTransaction st = mAppCon.BeginTransaction();
//以事务处理对象为参数初始化SQL命令对象
SqlCommand cmd = new SqlCommand( Sql , mAppCon , st);
try
{
cmd.ExecuteNonQuery();
//提交事务,完成多个操作
st.Commit();
}
catch(System.Exception ex)
{
//回滚事务,取消所有操作
st.Rollback();
if( mAppCon.State == System.Data.ConnectionState.Open )
mAppCon.Close();
SqlErrDes = ex.Message;
return false;
}
if( mAppCon.State == System.Data.ConnectionState.Open )
mAppCon.Close();
return true;
}
catch(System.Exception ex)
{
if( mAppCon.State == System.Data.ConnectionState.Open )
mAppCon.Close();
SqlErrDes = ex.Message;
return false;
}
}
//调用无参数的存储过程,返回datareader
public SqlDataReader SQLExeDataReader_proc(string procName)
......
//sql语句查询,返回sqldatareder对象
public SqlDataReader SQLExeDataReader(string Sql)
......
//sql语句查询,返回dataset
public DataSet SQLExeDataSet(string Sql)
......
//sql语句查询,返回dataset
public DataSet SQLExeDataSet(SqlCommand cmd)
......
//sql语句查询,返回dataview
public DataView SQLExeDataView(string Sql)
......
//执行删除、添加、修改等操作
public bool SQLExeNonQuery_proc( SqlCommand cmd )
......
//判断是否存在记录
public bool IfExistRecord(string Sql)
......
//判断是否存在记录
public bool IfExistRecord(SqlCommand cmd)
{
try
{
cmd.Connection=mAppCon;
if( mAppCon.State != System.Data.ConnectionState.Open )
mAppCon.Open();
SqlDataReader dr=cmd.ExecuteReader();
bool existRecord=false;
if(dr.Read())
existRecord=true;
mAppCon.Close();
return existRecord;
}
catch(System.Exception ex)
{
SqlErrDes = ex.Message;
if( mAppCon.State == System.Data.ConnectionState.Open )
mAppCon.Close();
return false;
}
}
//插入空白记录,返回包含所有记录的数据集
public DataSet InserBlankRecord(string TableName)
{
string Sql="select * from "+TableName;
try
{
if( mAppCon.State != System.Data.ConnectionState.Open )
mAppCon.Open();
SqlDataAdapter adp=new SqlDataAdapter(Sql,mAppCon);
SqlCommandBuilder cb = new SqlCommandBuilder(adp);
DataSet dataset=new DataSet();
adp.Fill(dataset);
DataRow row=dataset.Tables[0].NewRow();
dataset.Tables[0].Rows.Add(row);
adp.Update(dataset);
dataset.AcceptChanges();
//重新获取新记录
adp.Fill(dataset);
mAppCon.Close();
return dataset;
}
catch(System.Exception ex)
{
......
return null;
}
}
//插入空白记录,返回包含新记录的数据行
public DataRow InserBlankRecord_returnrow(string TblName)
{
string Sql="select * from "+TblName;//定义sql查询语句
try
{
if( mAppCon.State != System.Data.ConnectionState.Open )
mAppCon.Open();
SqlDataAdapter adp=new SqlDataAdapter(Sql,mAppCon);
SqlCommandBuilder cb = new SqlCommandBuilder(adp);
DataSet dataset=new DataSet();
adp.Fill(dataset);
DataRow row=dataset.Tables[0].NewRow();
dataset.Tables[0].Rows.Add(row);
adp.Update(dataset);
dataset.AcceptChanges();
//重新获取新记录
adp.Fill(dataset);
int count=dataset.Tables[0].Rows.Count;
row=dataset.Tables[0].Rows[count-1];
mAppCon.Close();
return row;
}
catch(System.Exception ex)
{
......
return null;
}
}
//插入空白记录,返回新记录的自增的标识字段值
public string InserBlankRecord(string TableName,string identityname)
{
......
string Sql="select * from "+TableName;
......
dataset.AcceptChanges();
//重新获取新插入的记录
Sql="select top 1 * from "+TableName+" order by "+identityname+" DESC";
adp=new SqlDataAdapter(Sql,mAppCon);
//dataset要重新初始化,不然会包含原来的记录集内容
dataset=new DataSet();
adp.Fill(dataset);
mAppCon.Close();
string identityvalue=
dataset.Tables[0].Rows[0][identityname].ToString();
dataset=null;
return identityvalue;
......
}
}//Base类定义结束
}//DataAccess命名空间定义结束
2、工具类
在工作流引擎及政务系统软件中经常会用到字符串的截取操作等代码,为减少代码行数,降低代码出错率,把这些操作代码以工具类Tools(命名空间为CommonTools,对应于第三章图3.4中的CommonTools类包)的及其方法的形式提供。这样,在需要时只需创建工具类对象,并调用其相应的方法函数。
下面给出工具类的主要方法函数:
using System;
namespace CommonTools
{
public class Tools
{
public Tools(){}
......
//截取字符串得到字符串数组的方法
public string [] StringSplit(string source,string seprators)
{
if(source !=null & seprators !=null)
{
char [] delimiter = seprators.ToCharArray();
string [] SplitResult = null;
SplitResult=source.Split(delimiter);
return SplitResult;
}
else
return null;
}
......
}
}