我读MSDN文章:《最先进的技术:Windows Workflow Foundation》

 

红色——原文重点地方;蓝色——我自己的理解和注释;绿色——不理解的地方

发布日期: 2006-4-28 | 更新日期: 2006-4-28

下载本文的代码:CuttingEdge0603.exe (663KB)

2006 1 月号中,Don Box Dharma Shukla 介绍了 Windows® Workflow Foundation,并讨论了框架的整体体系结构及其构成组件(请参阅 WinFX Workflow:Simplify Development With The Declarative Model Of Windows Workflow Foundation [英文](中文叫做《WinFX 工作流:使用WWF的声明性模型简化开发》,中文MSDN上有)。这篇文章促使我想进一步讨论这个主题,并介绍如何使用 Windows Workflow Foundation 来处理自动进程与人工活动贯穿相交的这种常见业务方案。它为开发和执行基于复杂过程的多种应用程序提供了框架。典型示例包括文档管理应用程序、企业对企业应用程序和企业对消费者应用程序。用户可以使用 Visual Studio® 2005 帮助设计基础工作流以及有关的顶级应用程序和程序集。

 

常见业务方案

对于订单处理、采购申请、差旅费用之类的任务,各组织通常会设有许多内部进程。工作流使这些独立进程以透明、动态、有效的方式按顺序进行。

依我的经验,在国内能够让工作流各步骤“顺序执行”的公司似乎不多见~规范的工作流产品很难满足需要啊~呵呵。所以我认为,使用WWFOA产品,采用状态机工作流还是比较明智的选择(当然,状态机内部也是顺序的!)。这可以通过事件的触发,在各状态之间任意跳转  ^o^

业务流程描述:让我们看一个典型的技术支持工作流进程。在技术支持人员接到客户电话,并开立记录客户姓名、来电时间和问题简要说明的票证时,进程即算开始。创建票证之后,该技术支持人员会将这件事放在一边,并等待其他来电。下班时,他将注销计算机,然后回家。此时,在另一个部门中(可能位于其他城市),一组技术人员正专注解决这些未解决的问题。每个工作的技术人员要选取申请,然后解决该申请或将该申请升级到第二级帮助(可能加签)。如何编写代码来实现此进程?

可以使用 Windows 窗体应用程序来收集电话的相关输入数据Host App,并在数据库中创建一个记录:即包含时间、说明、状态和唯一 ID 的票证。第二个 Windows 窗体应用程序的用户将看到待处理申请的实时列表(待办列表),然后选取一个申请。然后,接线员将尽量解决问题(回电给客户、检索申请的信息、发送电子邮件或执行一些远程活动),并指明问题是已解决还是需要进一步研究。此决策可由一个命令性操作表示,例如单击某个按钮更新同一基础数据库中的票证。最后,如果还涉及其他类别的用户,自定义前端将使这些用户能够指明问题已成功关闭或中止。

尽管此过程明确地表达了工作流需要用户进行某些决策,但可以使用以标准编程语言和数据库编写的传统顺序代码轻松实现。

 

应用 Windows Workflow Foundation

如果用户具备由各活动组成的基于工作流的系统(如 Windows Workflow Foundation),则可利用命令性代码和声明性活动地图的强大组合以及绑定它们的声明性规则来实现应用程序(即工作流建模过程。说的很本质了)。主要好处在于用户可以为解决方案建模(甚至以直观方式建模),将 Windows Workflow 嵌入运行时服务器来解释图表,并使 Windows Workflow 遵循在创建块中定义的链接。进程越复杂,为其设计和实现的流程就越简单。进程动态更改越容易,用户需要编写和维护的代码数量就越少。让我们了解一下如何实现技术支持方案的 Windows Workflow Foundation 解决方案。

 

技术支持解决方案

通过创建票证,创建的技术支持工作流程即开始,然后在等待连接用户或技术人员给予响应时停止。无论票证是关闭还是升级,工作流都将获得外部事件,并更新应用程序的内部状态以跟踪该事件(响应外部事件。这些事件是在LocalSvc的接口中定义的)。因此,工作流需要与外界进行交互(很重要的一句话!看交互方法!)。这类异步活动是 Windows Workflow Foundation 解决的实际工作流进程的固有问题之一。因为需要与系统外部的实体进行交互,所以宿主应用程序和工作流可以定义约定,以进行任何必要的数据交换。此处显示的 IHelpDeskService 接口说明了在工作流及其宿主之间建立的通信接口:

因为如果需要引发的事件不同,则需要的LocalSvc就是不同的。那么,是否每定义一个工作流,都一定需要单独定义一个LocalSvc以包含如下的接口呢?

这可能是一个影响产品化的问题!因为不可能让用户去写接口,虽然这很简单  -_-||

[DataExchangeService]  //关键部分,必须这样声明

public interface IHelpDeskService

{

//定义可以为工作流引发的事件

event EventHandler<HelpDeskTicketEventArgs> TicketClosed;

event EventHandler<HelpDeskTicketEventArgs> TicketEscalated;

 

//定义工作流可以调用的方法

void CreateTicket(

string description, string userRef, string createdBy);

void CloseTicket(string ticketID);

void EscalateTicket(string ticketID);

}

现在,开始编写名为 HelpDeskService 的服务类(这是LocalSvc中的另一部分,用来实现刚才定义的接口),如 1 所示。HelpDeskService 类将添加到工作流运行时,表示宿主应用程序和工作流之间的接触点。宿主应用程序将调用该类的公共方法以激发对工作流的外部事件(为工作流引发事件!然后工作流中就可以使用HandleExternalEvent Activity来处理事件)。激发的事件会以信号形式通知将指导操作流的特定于域的事件。IHelpDeskService 接口上的方法仅表示工作流可以通过 InvokeMethod 活动在外部组件上调用的部分代码(注意:工作流和用户界面不在一个线程上!所以要invoke。以此方法计算出的 HelpDeskService 的逻辑能够在很大程度上增加工作流的灵活性,因为它基本上可以使工作流回调宿主,以真正实现基本操作。通过包含实现 IHelpDeskService 接口的类,不同宿主(注意!)可以使用不同算法和存储媒体创建、解决或升级票证,同时保持工作流逻辑不变。用户可以在其他程序集中编译接口和类,也可以仅将这些文件保留在定义工作流的同一程序集中。

HelpDeskWorkflow Windows 窗体应用程序驻留的顺序工作流。图 2 显示的是其图形模型。它开始于 InvokeMethod 活动,该活动导致应用程序基于用户提供的信息创建票证。然后,票证将放置在数据库中(在DB中需要为每个流程单独建立一张表,以存储该流程各实例的数据!),等待技术人员选取,以进行解决或升级。


图 2 Visual Studio 2005 中的技术支持工作流

创建票证之后,工作流将进入等待状态,可能会等待很长时间 - 甚至几小时或几天(如老板出差且没带电脑 -_-|| 。这段时间内会出现什么情况?是否应该将工作流实例加载到内存中(问得好!)?如果工作流空闲,可以将其从内存中卸载 - 这一进程称为钝化(重要概念!)本地服务(如技术支持服务)保留宿主应用程序和休眠工作流之间的主要接触点。

 

人为因素

当人为因素与宿主应用程序进行交互并执行一些操作来唤醒工作流时,本地服务(就是我前面说的LocalSvc将向运行时发布请求来恢复钝化的工作流Windows Workflow Foundation 工具箱包含一个称为 Listen 的活动Listen Activity,该活动只是使工作流空闲并侦听传入的唤醒呼叫。图 2 中标有 WaitForSolution 的块是 Listen 活动的实例。

如果没有一个或多个子分支(每个分支表示一个可挂接在此点的可能发生的事件),Listen 活动基本上没有意义。在该技术支持示例中,Listen 活动包含两个分支 - 票证关闭和票证升级。每个分支都是一个 EventDriven 活动。Listen 活动和 EventDriven 活动都不需要特别设置,它们只是子活动的容器。要捕获外部事件,工作流需要 EventSink 组件。(看到这里,这篇文章似乎是基于WWF beta1写的  -_-||

在图 2 中,TicketClosed 块是绑定到本地技术支持服务中的 TicketClosed 事件的 EventSink。图 3 列出了此处设置的 EventSink 属性。首先,请选择提供事件的服务接口。必须设置 InterfaceType 实现,此外,仅能选择以 [DataExchangeService] 属性修饰的接口,如 1 所示


图 3 EventSink 属性

设置数据交换接口后,将使用接口中找到的所有事件预填充 EventName 属性。用户选取选择的事件并设置其参数。如果希望在事件得到处理后得到进一步的通知,还要设置 Invoked 属性。

在等待人为干预的一段空闲时间之后,EventSink 将控制任务返回给工作流。事件发生后,继续进行与工作流相应的其他任何活动。例如,在技术支持应用程序中调用本地服务的另一个方法,以更新票证数据库中的票证状态。

在实际情况下,工作流至少需要与一个数据库直接或间接交互。在此示例中,使用的是 SQL Server™ 2000 Tickets,其中每行都对应于一个正在处理的票证。票证 ID 特意设置为与用于管理票证的工作流 ID 相匹配(即“流程实例ID”与“文件实例ID”相同)

 

技术支持前端

工作流经编译后,只是一个可重用的程序集,因此可以被任何类型的基于 .NET 的应用程序引用。现在,让我们为技术支持接线员来构想一个前端应用程序。该应用程序是 Windows 窗体程序,允许用户填充表单,并创建票证以启动工作流,如图 4 所示


图 4 技术支持前端应用程序

工作流调用本地服务的 CreateTicket 方法,并使服务在票证数据库中添加新记录(不通过工作流层,直接将文件内容写入数据库。然后获取生成的GUID,创建一个工作流实例)。在接线员开立新票证之后,工作流将开始并进入空闲状态等待人为干预,而该接线员将继续接听电话并回复电子邮件。每个票证都由工作流的不同实例来表示。为了方便起见,工作流 ID 也是票证的 ID

 

状态持久性(这部分是我关注的重点)

在一天结束的时候,工作流成为一个活动树,如何管理其保留时间? Windows Workflow Foundation (总述:)运行时引擎会1管理所有工作流的执行,使工作流长时间保持活动状态,甚至在重新启动计算机时也不会受到影响运行时引擎2由可插拔式服务提供支持,这些服务提供了完整丰富的执行环境 - 事务、持久性、跟踪、计时器和线程

除非有某些干预使工作流继续进行,否则工作流不能保留在主机进程的内存中。如前所述,许多实际基于人为干预的工作流可能需要等待几个小时(甚至更长时间)才能继续。在前面的示例中,技术支持接线员启动了工作流,并在前端应用程序进程中加载了工作流类。然后,该接线员就可以关闭计算机回家了。但是,票证必须保持激活状态,并且对于其他接线员(甚至其他应用程序内的接线员)可用。因此,工作流必须支持对长效存储媒体的序列化

工作流可能是一个长时间运行的操作,不适用于那些在内存中连续几天保持激活状态的对象。首先,主机进程通常不能为连续几天活动所积累的所有对象提供缓存;其次,主机进程可能会多次关闭或重新启动。(!!)

解决方案是配置运行时,以在工作流空闲时将其卸载。在这种情况下,宿主程序将使用以下代码初始化运行时:

WorkflowRuntime wr = new WorkflowRuntime();wr.StartRuntime();

用户还可以在 WorkflowRuntime 类上设置几个事件处理程序,以便在工作流空闲、持续或被卸载时运行某些代码。以这种方法配置的运行时,将在工作流遇到 Listen 活动或进入等待状态时,将工作流自动序列化存入到存储媒体中(自动保存状态)。另一个方法是指示宿主程序以编程方式将工作流保持在已知的执行点处(手动保存状态)。在这种情况下,可以调用 WorkflowInstance 类的 EnqueueItemOnIdle 方法。用户将获得 WorkflowInstance 类的实例,作为运行时类的 CreateWorkflow 方法的返回值:

WorkflowInstance inst = theRuntime.CreateWorkflow(type);

...

inst.EnqueueItemOnIdle();

inst.Unload();

 

但是请记住,调用 EnqueueItemOnIdle 不一定会立即保持工作流。只有工作流处于空闲或挂起状态时才立即保持。否则,当工作流处于挂起或空闲状态时,运行时会予以注意并在以后保持工作流实例。请注意,EnqueueItemOnIdle 仅限于保存工作流实例的状态,不会将其从内存中卸载。要卸载工作流实例,需要调用 Unload 方法。工作流运行时支持的一种标准运行时服务是工作流持久性服务。可以通过 5 中的代码将其打开。

(本文重点部分:)WorkflowPersistence 服务将序列化功能添加到在给定运行时引擎内执行的所有工作流实例。工作流服务核心功能由 WorkflowPersistenceService 类表示。Windows Workflow Foundation 通过 SqlWorkflowPersistenceService 实现它。此类可以将工作流数据保存到具有已知架构的 SQL Server 2000 SQL Server 2005 数据库中(这需要做个Demo练习一下)。用户可以使用许多脚本和基于对话框的实用程序轻松创建目标数据库。可以从 Windows Workflow Foundation 网站 下载所需内容。必须创建 SQLWorkflowPersistenceService 的实例并向运行时注册,才能实际启动运行时。持久性服务的构造函数使用连接字符串作为其唯一的参数。

如前所述,运行时服务是整体 Windows Workflow Foundation 体系结构的可插入部分,因此用户可以创建自己的服务,并使用自己的服务替换标准服务。

 

恢复工作流实例

在所述方案中,需要由其他接线员(技术人员)接手任何开放的票证,并尽可能去解决它们或将它们提交给上一级。技术人员将使用其他应用程序,或者至少使用图 4 中的应用程序的其他实例(即有可能使用的是不同的Host App。在这两种情况下,都必须通过持久性支持创建工作流运行时的其他实例,并且必须加载和恢复正确的工作流状态。如您所见,只有在已经保存空闲工作流的 ID 的情况下才发生这种情况。在技术支持示例应用程序中,票证 ID 特意设置为与工作流 ID 相匹配,每个票证都对应于被创建用于处理票证的工作流的一个实例


图 6 问题规划求解

问题规划求解应用程序(请参阅图 6)通过使用与图 4 中的技术支持前端应用程序几乎相同的代码来初始化工作流运行时。唯一的微小差别在于在问题规划求解应用程序中,我是在运行时上为 WorkflowLoaded WorkflowCompleted 事件注册处理程序:

WorkflowRuntime wr = new WorkflowRuntime();

wr.WorkflowLoaded += OnWorkflowLoaded;

wr.WorkflowCompleted += OnWorkflowCompleted;

接线员从显示的列表中选择票证,并与提出票证的用户协同工作。完成后,她将进行单击以解决票证或升级票证,从而终止工作流。事件处理程序将基于票证 ID 检索保存的工作流实例,然后将其从空闲状态唤醒。以下是用户所需的代码:

// ListView控件的选中项目中读取一个value

// 这个value就是workflow instanceticket instanceID

string workflowID = (string)ticketList.SelectedValue;

// 注:stringGUID间无法隐式转换

Guid id = new Guid(workflowID);

Type type = typeof(MySamples.HelpDeskWorkflow);

try {

// 注意下面工作方式部分

WorkflowInstance inst = theWorkflowRuntime.GetLoadedWorkflow(id);

theHelpDeskService.RaiseCloseTicketEvent(inst.InstanceId);

} catch { ... }

前面代码的核心部分为调用 GetLoadedWorkflow 方法(工作方式:)GetLoadedWorkflow 采用表示工作流实例的 GUID,如果当前内存中没有 GUID,则从已配置的持久媒体中进行检索(!!!)。在这种情况下,工作流将加载到内存中,并被安排执行时间。即使工作流实例以前已中止,仍会发生这种情况。工作流加载回内存后,WorkflowLoaded 事件将激发。

GetLoadedWorkflow 将返回 WorkflowInstance 类型的对象,用户可以使用该类对象检查工作流的当前状态及其活动。此时,将激发工作流等待的一个事件。相应的 EventSink 活动将捕获事件并继续,直至工作流结束或遇到后续等待点。

工作流完成时,将自动从 Windows Workflow Foundation 持久性数据库中删除(删了干啥?多有用的步骤数据啊!)。当工作流结束时,WorkflowCompleted 事件将激发。在技术支持方案中,工作流将在接线员单击解决或升级之后完成(请参阅图 6)。

从开发的角度而言,关键要注意以下两点。第一,要将事件引发到工作流,必须将申请发布到池线程:(不明白!)

public void RaiseCloseTicketEvent(Guid instanceId) {

ThreadPool.QueueUserWorkItem(JustCloseTheTicket,

new HelpDeskTicketEventArgs(instanceId, "Jim"));

}

public void JustCloseTheTicket(object o) {

HelpDeskTicketEventArgs args = o as HelpDeskTicketEventArgs;

if (TicketClosed != null) TicketClosed(null, args);

}

第二,不能在主 Windows 窗体线程上引发 WorkflowCompleted 事件,因此不能直接更新任何 UI 控件。(这是由于 Windows 窗体控件不是线程安全控件。)确实可以在创建 Windows 窗体控件的线程之外的线程中更新 Windows 窗体控件,但是需要间接调用更新控件的方法:

private delegate void UpdateListDelegate();private void UpdateList() {

this.Invoke(new UpdateListDelegate(FillList));

}

可以从 WorkflowCompleted 事件处理程序调用 UpdateListForm 类的 Invoke 方法可以确保在正确线程上调用 FillList,以便能够安全地刷新所有控件。

 

结论

Windows Workflow Foundation 使用户可以直观地设计复杂的算法,从而解决业务问题并为进程建模。工作流是用于说明数据和操作流的工具。因此,任何需要 IF WHILE 语句的方案都可以是工作流。但是,任何人都不能使用仅包含 IF 语句的工作流,工作流运行时确实具有成本(即WorkflowRuntime,该成本可以在流复杂性超出给定阈值时分摊。

如何界定工作流的使用是否高效低耗的界限?在本专栏实现的技术支持方案对于工作流可能过于简单,可以通过使用票证数据库(甚至是工作流通过技术支持服务处理的同一票证数据库)以等效的方式实现。

基于工作流的解决方案的真正优点是使复杂进程更易于建模和实现,更重要的是使其更易于改进和扩展Windows Workflow Foundation Windows Workflow 程序提供了托管执行环境,还为程序提供了持续时间、可靠性、挂起/恢复以及补偿特征。在某种意义上,活动类似于中间语言 (IL) 操作码或程序语句,但包含特定领域的知识。简而言之,Windows Workflow Foundation 使程序语义具有声明性并且十分准确,使用户能够为接近实际进程的应用程序建模。它是最适合此工作的工具。用户无需使用 IL 编写前端可视应用程序,而是使用 RAD 开发工具和更具人类可读性的语言。Windows Workflow Foundation SDK 提供了广泛的编程语言,专门用于为复杂的业务程序建模,特别是在这些程序可能随着时间而改进的情况下。在这种情况下,主要好处在于用户可以添加特殊活动来进行工作流,并使用内置行为活动来控制该工作流。用户可以专注于任务,其他事情由运行时进行处理。如果要进一步了解 Windows Workflow Foundation 或查找其他信息,请访问 Windows Workflow Foundation

请将您的疑问和意见通过 cutting@microsoft.com 发送给 Dino

Dino Esposito Solid Quality Learning 顾问,并且是 Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005) 的作者。Dino 居住在意大利,经常就世界范围内的行业事件进行发言。请通过 cutting@microsoft.com 或者加入博客 weblogs.asp.net/despos Dino 联系。

本文摘自《MSDN Magazine2006 3 号。

转到原英文页面

© 2006 Microsoft Corporation 版权所有。保留所有权利。使用规定。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值