UML软件工程组织

 

 

最先进的技术:Windows Workflow Foundation
 
2007-12-21 作者:Dino Esposito 来源:微软
 
本页内容
常见业务方案 常见业务方案
常见业务方案 应用 Windows Workflow Foundation
常见业务方案 技术支持解决方案
常见业务方案 人为因素
常见业务方案 技术支持前端
常见业务方案 状态持久性
常见业务方案 恢复工作流实例
常见业务方案 结论

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

常见业务方案

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

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

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

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

应用 Windows Workflow Foundation

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

技术支持解决方案

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

[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 的服务类,如 图 1 所示。HelpDeskService 类将添加到工作流运行时,表示宿主应用程序和工作流之间的接触点。宿主应用程序将调用该类的公共方法以激发对工作流的外部事件。激发的事件会以信号形式通知将指导操作流的特定于域的事件。IHelpDeskService 接口上的方法仅表示工作流可以通过 InvokeMethod 活动在外部组件上调用的部分代码。以此方法计算出的 HelpDeskService 的逻辑能够在很大程度上增加工作流的灵活性,因为它基本上可以使工作流回调宿主,以真正实现基本操作。通过包含实现 IHelpDeskService 接口的类,不同宿主可以使用不同算法和存储媒体创建、解决或升级票证,同时保持工作流逻辑不变。用户可以在其他程序集中编译接口和类,也可以仅将这些文件保留在定义工作流的同一程序集中。

Figure 1 Implementing Workflow with HelpDeskService

using System;
using System.Threading;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Messaging;

namespace MySamples
{
    public class HelpDeskService : IHelpDeskService
    {
        // Implement events
        public event EventHandler<HelpDeskTicketEventArgs> TicketClosed;
        public event EventHandler<HelpDeskTicketEventArgs> 
            TicketEscalated;

        public void RaiseCloseTicketEvent(Guid instanceId)
        {
            // Raise the event to the workflow
            HelpDeskTicketEventArgs args = 
                new HelpDeskTicketEventArgs(instanceId, "");
            if (TicketClosed != null) TicketClosed(this, args);
        }

        public void RaiseEscalateTicketEvent(Guid instanceId)
        {
            // Raise the event to the workflow
            HelpDeskTicketEventArgs args = 
                new HelpDeskTicketEventArgs(instanceId, "");
            if (TicketEscalated != null) TicketEscalated(this, args);
        }

        void IHelpDeskService.CreateTicket(
            string description, string userRef, string createdBy)
        {
            // Fill up a ticket: same ID as the workflow
            string ticketID = 
                WorkFlowEnvironment.CurrentInstanceId.ToString();

            // Create ticket on the DB
            HelpDeskHelpers.CreateTicket(
                ticketID, description, userRef, createdBy);
        }

        void IHelpDeskService.CloseTicket(string ticketID)
        {
            // Update ticket on the DB
            HelpDeskHelpers.UpdateTicket(ticketID, TicketStatus.Closed);
        }

        void IHelpDeskService.EscalateTicket(string ticketID)
        {
            // Update ticket on the DB
            HelpDeskHelpers.UpdateTicket(ticketID, 
                TicketStatus.Escalated);
        }
    }
}

图 1

HelpDeskWorkflow 是 Windows 窗体应用程序驻留的顺序工作流。图 2 显示的是其图形模型。它开始于 InvokeMethod 活动,该活动导致应用程序基于用户提供的信息创建票证。然后,票证将放置在数据库中,等待技术人员选取,以进行解决或升级。


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

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

人为因素

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

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

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


图 3 EventSink 属性

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

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

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

技术支持前端

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


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

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

状态持久性

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

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

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

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

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 中的代码将其打开。

Figure 5 Workflow Persistence Service

private WorkflowRuntime InitWorkflowRuntime()
{
    // Get a new workflow runtime
    WorkflowRuntime wr = new WorkflowRuntime();

    // Add custom HelpDesk service
    theHelpDeskService = new HelpDeskService();
    wr.AddService(theHelpDeskService);

    // Add system SQL state service
    SqlWorkflowPersistenceService stateService = 
        new SqlWorkflowPersistenceService("Data Source=localhost;" + 
        "Initial Catalog=WFState;UID=...;");
    wr.AddService(stateService);

    // Start
    wr.StartRuntime();
    return wr;
}

private void btnCreate_Click(object sender, EventArgs e)
{
    // Fill the Parameters collection for this instance of the workflow
    Dictionary<string, object> parameters = 
        new Dictionary<string, object>();
    parameters.Add("TicketDescription", txtDesc.Text);
    parameters.Add("TicketUser", txtUser.Text);
    parameters.Add("TicketCreatedBy", txtCreatedBy.Text);

    // Get the type of the workflow
    Type type = typeof(MySamples.HelpDeskWorkflow);

    // Start the workflow instance
    WorkflowInstance inst = 
        theWorkflowRuntime.CreateWorkflow(type, parameters);

    // Feedback to the user
    string msg = String.Format("The ticket '{0}' has been successfully " + 
        "created!", inst.InstanceId);
    MessageBox.Show(msg, "HelpDesk", MessageBoxButtons.OK, 
        MessageBoxIcon.Information);
    FillGrid();
}

图 5

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

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

恢复工作流实例

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


图 6 问题规划求解

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

WorkflowRuntime wr = new WorkflowRuntime();
wr.WorkflowLoaded += OnWorkflowLoaded;
wr.WorkflowCompleted += OnWorkflowCompleted;

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

string workflowID = (string)ticketList.SelectedValue;
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 事件处理程序调用 UpdateList。Form 类的 Invoke 方法可以确保在正确线程上调用 FillList,以便能够安全地刷新所有控件。

结论

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

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

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

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

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号