虽然 Windows workflow 是实施业务流程处理的一个优秀框架,但它却缺乏对人工活动的直接支持。 微软虽然发布了 [ 1 、 2 ] 几种方法来解决这个问题,但这些方法却显得不够通用。本文将定义一种完全通用的方法,在 WF 中实现对人工活动的支持。
支持人工交互的复杂性带来众多的挑战,如下所列,可见一斑:
- 用户的响应时间(用户活动的执行时间)是不可预知的。
- 当请求发生的时候用户可以不连接到系统,因此需要存储请求,并当用户登录到系统之时提交给用户。
- 在不同的机器上可以有多个同时运行的工作流程。但是用户通常需要一个所有任务的统一视图。
业界已经认识到了这些问题,并制定了两个主要的规格来解决这些问题 [ 3 、 4 ]。我们将根据以下思想来构建人工活动的实现,而不是纸面上的规格说明书。
一个解决方案的组件视图
整体解决方案的主要组件如图 1 所示。
图 1 解决方案组件
解决方案的核心是一个工作队列管理器。这是一个集中的服务,负责跟踪系统所有用户的所有任务。任何需要人工交互活动的工作流程(或者服务 / 应用程序所包含的工作流程),都去调用一个自定义的工作流活动 [ 5 ],以通过它将请求提交到工作队列管理器,并对其进行持久化,同时允许系统的其他组件与这些请求一同协作。这样,工作队列管理器就成为了工作流引擎和人工活动执行之间的解耦层。在工作流执行期间,如果用户并不存在于系统中,这种方法同样提供了支持。同时,工作队列管理器通过形成的集中服务,可以将所有任务与指定的用户结合,而不用考虑是从哪里初始化的流程。因为不同的用户任务可以要求不同的输入信息以及产生不同的输出,工作流和人工活动之间的通信采用了 XML 进行输入输出,从而可以处理任何可能的请求和响应。虽然使用 XML 似乎会增加实现上的复杂度,但.NET 对 XML 序列化的良好支持,使得 XML 和对象之间的映射易如反掌。工作队列查看器是一个 GUI 应用程序,允许用户直观地查看输入的已经准备就绪的所有任务。该应用程序是通用的,仅仅显示了任务的基本要素,包括名称,类型,优先级,创建者等等。根据队列中的这些信息,用户可以决定执行某一项特定的任务。任务的实际处理过程是通过一个在功能上支持给定任务的任务应用程序来完成的。一个工作流队列管理应用程序提供了一个用户界面,用来支持对工作队列管理器的管理。它可以查看和修改现有的人工任务,查看它们的历史记录等。最后,一个人工活动就是一个自定义活动(参看边栏“Windows Workflow Foundation 组件模型”,它实现了与任务队列管理器的通信,并为工作流开发人员展现了一个非常简单的针对人工任务执行的编程模型。从人工交互的角度来看,这与常规的服务调用并无二致。
Windows Workflow Foundation 组件模型 正如 [ 11 ] 所描述的,Windows Workflow Foundation (WWF) 的实现不同于当下主流工作流实现的可执行工作流语言(域语言)。在 WWF 中,“过程图中的活动关联了一个实现该活动运行时行为的组件,组件由一种通用编程语言实现。过程语言中的每一个活动都对应一个实现组件。例如,一个 Web 服务调用活动,一个人工任务活动或一个电子邮件活动都对应一个实现组件 ”。
因此,我们能够非常容易地通过引入新的活动类型扩展 WWF,以实现特定情况下的(在我们的案例中,就是人工任务的活动)运行时行为,通过实现新的活动这种方法,使得构建或者扩展现有的过程语言非常简单。
组件之间的整体交互如序列图所示(图 2)
图 2 序列图
我们可以看到,上面的整体解决方案里包含两种类型的组件:
- 通用的,包括人工活动,工作队列管理器和工作队列查看器。这些组件运行于人工任务的“标准”特性(attributes )之上,并将任务的输入输出视为通用的 XML。
- 特殊的,包括工作流本身和处理业务流程的应用程序,实现了特定业务对象的 XML 序列化 / 反序列化,并使用这些对象实现他们的功能。
本文只讨论通用组件的解决方案。
工作队列管理器
服务接口和功能
正如我们在前面所定义的那样,工作队列管理器是一个集中的服务。它的功能基于数据契约,如图 3 所示。
这个数据模型的主要组件包括:
- 人工任务 - 这个数据类型定义的主要元素是一个人工任务(参见注释 [ 3 ]),包括:
- TaskID ── 一项任务的唯一标识
- Task Name ── 一项可读的标识性任务名称 (非唯一的)
- Task status ── 任务状态。任务可能的状态都定义在枚举 TaskStatusEnum 中,数据可以是: Ready, Reserved 和 completed。一个简化的任务状态迁移图如图 4 所示。
图 4 任务状态迁移图
当任务提交到工作队列管理器中时,它的状态是 Ready。一旦应用将请求提交,状态就迁移到 Ready 状态,而在应用程序处理完任务之后,任务状态则迁移到 completed 状态,它标志着工作队列管理器将任务完成的响应信息返回给人工活动。如果任务被取消或者超时,任务则退回到 Ready 状态。
-
任务的优先级可以在创建任务的时候分配给一个任务,用于表示任务的重要性。 工作队列查看器根据任务的优先级展现任务,因为高优先级的任务会放到队列的顶部。
-
CreatedOn 和 createdBy 属性指定任务的创建时间戳和任务的创建人。它们用来描述队列中任务的处理顺序。
-
Task type 是任务的类型,用于选择适合的处理任务的业务处理程序。目前我们是使用流程 / 任务的名称相结合的方式(任务类型的数据元素)。将任务类型与相关的数据类型分开,简化了切换到另一种定义任务类型定义的方式(如有需要)。
-
ReservedBy,ReservedOn 用于锁定并确保一次只能有一个用户处理一项任务。
-
CompletedBy 和 CompletedOn 元素不影响任务的处理,更多地是用来登记,以保证服务可以用于对应用的维护以及 / 或者报告。
-
Potential Owners 允许列表中的用户可以查看 / 处理这个任务。目前,一个可能的用户可以被定义为一个特定用户或者用户组 (用户数据类型)
-
Escalations 用于标示一项任务的升级清单。每次升级 (升级数据类型) 可以被定位为 notification(notification 数据类型) 或者 reassignment (reassignment 数据类型)。这两种类型的升级在其中一个升级应用后会包含一个超时 (超时来自于任务的创建),邮件 (通知,notification) 和一个新的用户名单 (再分配,(reassignment )。目前的实现不支持升级。
-
Escalated flag 标示任务是否已经升级。
-
Task request 和 reply 字段包含任务请求和应答的字符串版本。
-
CallbackInfo 是人工活动用来指定工作队列管理器所需的特定信息的数据类型,工作队列管理器发送一个回复给人工活动,以标志任务执行完成。该类型包括类别、名字以及包含了工作队列管理器进程的消费者类型的服务版本 [ 10 ]。实施取决于服务所注册的解决该信息的实际端点地址、绑定和绑定参数。
-
WorkflowParameters 数据类型是用于指明将回复传递到对应工作流实例的适当位置。它包含了工作流实例 ID (确定工作流实例) 以及一个关联关系 (确定工作流步骤 / 活动)(因为自定义人工活动是基于工作流队列,所以我们当前用工作流队列的名字作为一个关联)。
-
HumanTaskView 是一个具体的人工任务子集数据类型, 用于为任务队列查看器返回任务信息。
-
WorkItemQuery 数据类型被任务队列查看应用程序用作有关当前任务的请求信息。
-
MaintenanceQuery 数据类型被维护应用程序用于查询和查看人工任务。
除了数据类型,数据模型还定义了故障数据类型,参见图 5。
图 5 故障数据类型
工作队列管理器公开了三组服务──工作流服务,用户服务和维护服务。
工作流服务节(图 6)定义了一个服务,人工任务活动可以利用它为一个执行提交一个任务,同时还定义了一个工作流服务公开的接口,该接口被工作队列管理器用来标识执行完成。这两个服务提供了人工活动之间的集成,并运行在工作流和工作队列管理器中。
图 6 工作流服务
提交给由工作队列管理器实现的执行服务,从作为不同工作流其中一部分的人工活动处接收请求。一旦接收到请求,就会存储到数据库里做进一步的处理。
completeExecution 契约虽然在此处定义,但却是在工作流中实现的。这个服务用于通知工作流一个特定任务的执行已经完成,工作流可以执行了。
用户服务(图 7)用于支持用户与工作队列管理器之间的交互。WorkWithItems 服务支持三种操作:
- 工作队列查看器使用 GetItemsList 方法为给定用户返回’Ready’状态的任务列表(可以选择一个给定的类型)。
- 业务应用程序使用 WorkWithItem 方法获取它要处理的任务的完整信息。这里会对用户凭证(credentials )和任务的潜在所有者列表进行比较,如果用户不属于该组,就会抛出一个未经授权的异常。一旦业务应用程序开始处理一项任务,则该请求还会触发任务状态的改变,修改 Reserved 和 reservedOn 以及 reservedby 字段,这些字段会被填充为适当的信息。这是一种简单的预定(锁)机制,能够确保在某个特定时间只有一个操作任务的用户。如果一个业务应用程序试图访问已被预定的任务,操作将引发一个错误,表明这个任务正在被另一个用户处理(错误提供了用户的信息和他 / 她开始处理任务的时间)。为了避免任务无限期的锁定,服务是一个超时锁,如果接收一个请求到处于 Reserved 状态的任务清单或者工作项的时间超过 30 分钟,任务状态将修改为 Ready。
- 业务应用程序使用 CompleteExecution 方法通知工作队列管理器任务处理完成,业务应用程序返回完成状态,完成状态可以是 completed 或 aborted。如果返回的状态是 Completed,任务的执行状态就修改为 Completed,并且服务会通知人工活动任务完成。如果返回的状态是 aborted,任务的状态就修改为 Ready。
维护服务(图 8)用于支持工作队列管理器的维护应用程序。
查看任务方法使用 MaintenanceQuery 数据类型定义的过滤器返回现有任务的一个列表。在看到任务之后,这些任务可以被修改和改变,可以通过更新任务方法将任务返回给工作队列管理器。
底层数据库和数据访问
服务的数据库设计(图 9)包含表,以及用于持久化与人工任务相关的所有信息的必要内容。主表为人工任务表,包含了所有人工任务的基本信息,包括任务名称,ID(由服务分配),优先级和任务生命周期事件,以及规定执行事件的主体和时间的补充信息。
额外的表包含任务的额外信息,包括:
- Callback 和 TaskCallback 表。Callback 表包含了服务的信息,它由一个工作流程实现并负责监听从人工任务管理器传来的请求。表的设计依赖于服务注册的方法,因此它使用了服务的名称和版本来充分解析服务的终结点地址和绑定信息。多个人工任务可以使用相同的回调服务,这就是为什么我们把它放在一张单表中的原因。TaskCallback 表是 HumanTask 和 Callback 表的连接表。
- TaskType 和 TaskTaskType 表。TaskType 表包含任务的类型信息。因为我们拥有相同类型的多个人工任务,我们将该信息分离到各自的表中。TaskTaskType 是 HumanTask 表和 TaskType 表的连接表。
- WorkflowReference 表包含的信息是连接人工任务到工作流的特定实例 / 活动。它包含工作流实例 ID 和实例所使用的工作流队列的名称(见下文)。因为这一机制在将来可能发生变更,我们把它的信息分离到它自己的一张表中。人工任务和工作流引用之间总是一对一关系,因此不需要使用连接表,我们使用外键连接 WorkflowReference 表和 HumanTask 表。
- PotentialUser 和 TaskPotentialUser。每个任务都有一个潜在用户清单,而在系统中的每个用户都有一个有权操作的任务的清单。因此我们用 PotentialUser 信息来跟踪用户,用连接表 TaskPotentialUser 连接用户到任务。
- Notification, Reassignment, TaskNotification, TaskReassignment 和 ReassignmentPotentialUser 表。这些在实现升级的时候用来支持升级。
为了优化数据的访问,我们还实现了几个存储过程和视图。存储过程是:
AddTaskCallback
存储过程能够添加回调信息和分配人工任务(使用下面的InsertTaskCallback
存储过程)。它首先检查是否已经有回调存在,如果存在则使用存在的回调记录,否则在Callback
表中创建一个回调记录,然后调用InsertTaskCallback
存储过程创建一个连接。InsertTaskCallback
存储过程用于生成TaskCallback
表,这样就能够创建给定回调与人工任务之间的关系。AddTaskType
存储过程用于添加任务类型信息和关联它与给定人工任务的关系(使用下面的InsertTaskTypeTask
存储过程)。它首先检查是否已经存在任务类型,如果存在则使用存在的任务类型记录,否则在 TaskType 表中创建一条新的记录,然后调用InsertTaskTypeTask
创建一个连接。InsertTaskTypeTask
存储过程用于生成 TaskTaskType 表,这样就能够创建给定任务类型和人工任务之间的关系。AddTaskPotentialOwner
存储过程负责添加潜在用户信息,以及将用户信息与给定的人工任务建立关联(使用下面的InsertTaskPotentialUser
存储过程)。它首先检查是否存在一个潜在的用户,如果存在则使用已经存在的用户记录,否则在PotentialUser
表中创建一条记录,然后调用InsertTaskPotentialUser
创建一个连接。InsertTaskPotentialUser
存储过程用于生成TaskPotentialUser
表,这样就能够创建用户和人工任务之间的关系。
使用这些存储过程通过两方面的因素减少了数据库的访问次数,同时通过消除重复数据以减少数据库的数据量。
数据库视图(图 10)简化了数据库数据的读取。分配给用户的所有任务信息(包括任务类型)可以从单个视图获得。
持久层类图实现了仓储模式(repository pattern),参见图11。
服务实现
服务的整体实现非常直接。一旦某个特定方法被调用了,它就会执行委派给实现了所有必须的数据库访问的仓储类(图11)。
人工活动
人工活动是一个实现了人工交互的自定义工作流活动。这个活动隐藏了工作流设计器与工作队列管理器的通信,并允许象调用普通服务那样处理用户交互。
这个活动的实现基于工作流队列[ 7 ] 提供的活动同外部交互的异步通信:活动注册到队列的接受消息中,服务则通过队列发送消息。一个自定义活动可以使用这个模型处理外部事件以及与异步活动执行完成的通信。它允许一个活动执行到一个点,然后等待触发使得它可以继续执行。这种实现的整体交互参见图 12。
图 12 使用工作流队列实现活动
一旦启动,只要允许,活动执行就会继续。随着到达需要外部执行的活动点,活动就会注册到工作流队列中。然后工作流实例进入等待状态,并可能会被钝化(持久化)。一旦外部执行完毕,它就会将信息压入到队列中。此时,运行时就可以重新激活工作流,继续执行。
人工活动的实际实现包含两部分──活动本身和一个回调服务。
一个人工活动发送一个消息到人工任务管理器去启动一个新的人工任务开始处理。如果该服务的调用是成功的,活动会将自身注册到工作流队列中,并进入等待状态。如果执行服务出现任何错误,活动会抛出一个工作流故障,它能够被工作流处理。
当服务接收到答复时,它会将回复消息压入活动的工作队列中。队列消息唤醒一个活动,弹出回复消息并完成它的执行。
回调服务实现完成执行契约(图 6),同时必须被一个实现了工作流的服务 / 应用程序所启动。为了使服务正确工作,WorkflowRuntime
必须被设置,可以使用executionComplete
类的一个公共静态变量来实现。
人工活动的执行需要几个参数 - 任务类型,优先级等等。这些参数被定义为DependencyProperty
[ 8 ]。暴露的DependencyProperty
可以被工作流设计器设置 [ 9 ],从而使得设置活动参数可视化,而无需编程(参见图 13)。
工作队列查看器
一个工作队列查看器是作为一个用户控件(图14)实现的,可以将它放到任一表单中。该控件使用了一个dataview 控件,并接收一个委托作为参数。当行的任意单元格被点击,委托就会传递一个任务ID 和任务类型而被调用。图14 显示了一个简单的委托,弹出一个消息框。
结论
尽管推动了业务流程的完全自动化,但人工活动依旧不可或缺,并仍然会在业务流程实现上继续扮演着重要的角色。正如我们在文章前言一开始所定义的那样,引入用户的交互会带来许多额外的关注点,这些关注点与工作流的实现没有直接的关系。本文介绍的彻底解耦的实现方法,能够将工作流开发与用户交互开发之间的关注点进行分离。此外,在人工任务管理器中对人工任务支持的集中化减少了对给定用户的任务管理的聚合。
致谢
非常感谢 Paul Rovkah 和 Rob Sheldon 为本文提供的帮助。
参考资源
1 Jeremy Boyd. Integrating Windows Workflow Foundation and Windows Communication Foundation . MSDN January 2007.
2 Windows Workflow Foundation Web Workflow Approvals Starter Kit? . Microsoft Downloads.
3 Web Services Human Task (WS_HumanTask).
4 WS BPEL extensions for People .
5 Matt Milner. Build Custom Activities To Extend The Reach Of Your Workflows MSDN Magazine, December 2006,
6 Dare Obasanjo XML Serialization in the .NET Framework . January 2003,
7 Serge Luca. Using the Windows Workflow Foundation Queuing system .
8 Glenn Block Attached Properties and the Workflow Designer .
9 Dennis Pilarinos. Getting DependencyProperty RegisterAttached properties to appear in the Property Browser redux .
10 B.Lublinsky, Implementing a Service Registry for .NET Web Services . January 2008, InfoQ,
11 Tom Baeyens. Process Component Models: The Next Generation In Workflow? February 2008, InfoQ.
评论