(翻译)《WF编程系列之47》 第八章 工作流中的通信

    独立存在的工作流并不是很多。大多数工作流还是需要和本地或远程的服务通信来完成它们的工作。在前面的章节中,我们已经看到了在WF中的一些基本的构造块。这些构造块包括了诸如HandleExternalEvent和WebServiceInput这样的活动。

    在本章,我们深入研究WF中通信的细节。我们将看到宿主进程是如何与工作流中特定的活动进行通信的,还会研究让工作流工作的底层队列机制。最后,我们会研究使用web服务的远程通信。在本章的结尾,我们将了解到为了创建一个连接良好的应用程序所需要的知识。

8.1 本地通信服务回顾

    我们知道,本地通信服务允许工作流和它们的宿主进程交换数据。在第3章,我们定义了一个服务来触发BugAdded事件到正在运行的工作流,这将依次调用服务的AssignBug方法。该服务通过事件发送数据到工作流,而工作流则通过调用方法发送数据到服务。

image

    上面的截图展示了工作流运行时是如何扮演本地通信服务和工作流实例之间的那道屏障的。运行时从本地服务截取事件,并沿着这些事件跳转到恰当的工作流实例上。这种交互是必须的,因为等待事件的工作流实例可能已经从内存中被卸载了,并持久化到数据库表中。运行时可以在发送事件的时候要求持久化服务重新加载工作流,但它首先会需要一个工作流实例ID。即使工作流仍然在内存中,运行时还是需要一个实例ID来定位合适的工作流。运行时所使用的这个实例ID,是ExternalDataEventArgs对象在事件传递期间所需要的。

    在许多方面,我们可以把实例ID认为是在寄送包裹上的街道地址。给出一个街道地址,我们就可以将包裹寄送到正确的房子。如果有人居住在这里,我们就能完全肯定我们已经把包裹寄送给了预定接收者。然而一旦街道地址把我们带到了一件办公楼又如何是好呢?我们并没有充足的信息来把包裹与它的预定接收者联系起来。

    我们在第3章的简单范例只具有一个等待事件的活动。运行时不需要任何额外的信息发送这些有效内容。并不是所有的工作流都能这样简单。我们需要学会,一旦存在等待一个事件的多个活动,那么我们该如何把这些消息联系起来

8.1.1 相关性参数

    WF使用相关性记号来确定在工作流中的特定活动和宿主中的本地通信服务之间的会话。通信接口默认是不具有相关性的,并且在工作流具有多个活动并发等待即将到来的事件时,我们只需要确定这些相关性记号就可以了。让我们看一个例子:

    想象一下为我们的bug跟踪的应用程序开发一个工作流,这将要求小组中的成员对即将到来的一个bug进行投票。投票yes表示小组成员接受系统中的这个bug。投票no表示小组成员想要关闭这个bug。我们可能会像下面这样设计接口和事件参数类:

[ExternalDataExchange]
public interface IBugVotingService
{
    void RequestVote(string userName);
    event EventHandler<VoteCompletedEventArgs> VoteCompleted;
}

[Serializable]
public class VoteCompletedEventArgs : ExternalDataEventArgs
{
    public VoteCompletedEventArgs(Guid instanceID, string userName, bool isYesVote)
        : base(instanceID)
    {
        _userName = userName;
        _isYesVote = isYesVote;
    }

    private string _userName;
    public string UserName
    {
        get { return _userName; }
        set { _userName = value; }
    }

    private bool _isYesVote;
    public bool IsYesVote
    {
        get { return _isYesVote; }
        set { _isYesVote = value; }
    }
}

    工作流将会调用外部方法RequestVote,并把用户名作为参数传递。服务有责任通知用户并等待收集用户的投票。服务还能将投票结果打包到一个事件参数对象中并触发VoteCompleted事件。工作流将会接受这个事件,截取该事件的参数,并决定下一步要做什么。

    现在想象一个工作流,就像下面截图中的那样:

image

    当我们设计我们的服务接口时,我们假设工作流只会寻找到一个单独的投票。上面截图中的工作流在平行地寻找两个投票。该工作流将调用服务来请求小组技术组长的投票。该工作流还将调用服务来请求来自小组分析师的投票。然后在这个平行的活动完成之前,它会等待这些事件的到达。

注意:ParallelActivity活动,我们在第4章介绍过,并没有提供真正的平行处理。运行时只允许在一个工作流实例中每次执行一个线程。这里的平行活动将使用一个线程按照不确定的顺序执行这两个分支,直到它们阻塞或等待一个事件。既然这与投票小组中的一个成员要在很短的时间内作出投票响应是不同的,这两种平行活动的分支将会到达它们被阻塞的地方或等待它们各自事件的到达。

设想一下,小组的技术组长是第一个响应投票的。服务将会触发一个事件,工作流运行时将会截取这个事件。然后运行时将找到我们的工作流实例并发送这个事件。问题是——运行时将会把技术组长的投票发送到哪个分支的HandleExternalEvent活动呢,是左边的那个分支,还是右边的呢?我们不能肯定,技术组长的投票可能结束右边的分支,也就是我们为处理分析师的投票而设计的分支。这种场景代表了我们必须为运行时提供更多信息才能解决的问题。

8.1.1.1 相关性特性

    当工作流是一个RequestVote方法时,它会包括一个重要的信息——用户名参数。如果我们请求Scott的投票,那么我们应该在下面的活动中得到Scott的投票回复。用户名参数可以和活动结合在一起——我们只需要通知工作流这个参数的重要性。下面的代码是我们的服务接口的修正版本,以及事件参数类:

[ExternalDataExchange]
[CorrelationParameter("userName")]
public interface IBugVotingService
{
    [CorrelationAlias("userName", "e.UserName")]
    void RequestVote(string userName);

    [CorrelationInitializer]
    event EventHandler<VoteCompletedEventArgs> VoteCompleted;
}

[Serializable]
public class VoteCompletedEventArgs : ExternalDataEventArgs
{
    public VoteCompletedEventArgs(Guid instanceID, string userName, bool isYesVote)
        : base(instanceID)
    {
        _userName = userName;
        _isYesVote = isYesVote;
    }

    private string _userName;
    public string UserName
    {
        get { return _userName; }
        set { _userName = value; }
    }

    private bool _isYesVote;
    public bool IsYesVote
    {
        get { return _isYesVote; }
        set { _isYesVote = value; }
    }
}

    在我们的服务接口上,我们具有三个新的特性。这些特性包括了运行时用于在我们的服务和工作流中独立的活动之间建立会话的相关性元数据。

 相关性参数

    我们把CorrelationParameter特性添加到了我们的接口中。这个特性指定了参数的名称,运行时根据它将一个事件映射到一个特定的HandleExternalEvent活动。我们将userName指定为我们的相关性参数。运行时将会在通信服务接口中所有的方法和事件上寻找带有userName名称的参数。当它发现了这样一个参数,它就会把这个参数用于相关性。

    如果有多个相关性参数,那么CorrelationParameter特性就可以在一个接口上多次出现,我们只能在接口类型上使用这个特性。

相关性初始化

    我们已经对RequestVote方法使用了CorrelationInitializer特性进行装饰。工作流运行时会把对RequestVote的调用识别为工作流和通信服务之间会话的开始。运行时还会把用户名参数识别为相关性参数,并保存这个值。随后,当事件到达工作流时,工作流将会对保存的值和即将收到的相关性参数进行比较。

    有一个明显的问题,对于一个事件而言,相关性参数是什么?工作流又是如何知道的?这里并没有用户名参数——我们在事件参数中封装了用户名。这就是第三个特性CorrelationAlias所在的地方。

相关性别名

    我们可以把CorrelationAlias特性应用到服务接口中的方法和事件上,从而在独立的成员上覆盖CorrelationParameter特性。我们把这个特性放在VoteCompleted事件上。这个特性告诉运行时从参数e的UserName属性中取出用户名的相关性参数。

    我们建立了我们在服务接口中所需要的所有元数据。工作流运行时将具有充分的信息来把传递scott用户名的CallExternalMethod活动与等待scott投票结果的HandleExternalEvent活动相关联。我们下一步要做的是优化工作流中的相关性元数据。

8.1.1.2 相关性记号

    当我们把CallExternalMethod活动拖放到工作流设计器中时,我们至少要配置InterfaceType和MethodName属性。这些属性会告诉WF我们想要活动调用什么服务和服务方法。如果我们配置的接口具有一个CorrelationParameter特性,那么设计器将添加一个新的属性到Properties窗口——CorrelationToken属性(参见下面的截图)。

image

    WF在会话中使用相关性记号来连接活动。我们需要为这个记号指定名称和所有者。记号的名称是随意的,但由于它标志了该记号的身份,我们应该选择一个有意义的名称。所有者必须是当前活动的祖先。在上面的截图中,我们将这个相关性记号标记为TechLead,并将它的祖先——平行活动的分支指派为这个记号的所有者。

    HandleExternalEvent活动是这个分支中的下一个活动,而这个活动应该处理技术组长的投票。一旦我们在这个活动中指派了InterfaceType和EventName属性,我们将再次看到CorrelationToken属性出现。我们将要在记号的下拉清单中选择TechLead。

image

    我们在左边的分支里把相关性记号与CallExternalMethod活动和HandleExternalEvent活动连接起来。我们也能在右边的分支中为这对活动创建一个新的记号。工作流运行时将使用这些记号和用户名参数来保证它把投票发送到合适的事件处理程序。

    尽管相关性记号在连接活动时很有用,我们不应该将其作为安全特性而依赖。只为,如果一个投票说它是来自技术组长,但并不表示这个投票真的来自于技术组长。在下一节,我们将学会如何使用HandleExternalEvent活动的另一个属性——Role。

8.1.2 基于角色的授权

    保护计算机资源的过程包括两个步骤。第一步是验证。验证确定了用户的身份。验证可能很简单,就像询问一个人的用户名和密码那样,或者更多地利用相关的生物特征信息,如指纹识别和视网膜扫描。WF不验证用户,但是当需要验证时,运行时将会依赖于软件中的验证机制。例如,以ASP.NET应用程序为宿主的工作流可能需要集成Windows验证的支持,或者可能依赖于ASP.NET的membership provider来对用户进行验证。

    一旦用户的身份得到确定,我们就可以决定允许用户执行什么操作。这就是第二步——授权。授权规则通常听起来像是——只有经理才能审批报销单,或只有管理员才能取消操作。注意到这些规则涉及到像经理或管理员这样的用户分组。这是因为我们代表性地为用户指派角色并对它们基于角色的请求进行授权(这里,“基于角色的授权”是一个术语)。

    在软件的世界中,角色管理软件别有一番风味。对于Windows领域中的软件而言,我们可能从它们的活动目录组成员中得到用户的角色。在面向公众的 ASP.NET的web应用程序中,我们可以从ASP.NET的Role Provider中得到用户的角色。

    为了支持不同的角色实现,WF提供了一个可扩展的角色管理方案。WF提供了一个名为WorkflowRole的抽象基类。这个类的内嵌实现为Windows活动目录和ASP.NET 2.0角色提供者提供了角色管理:

image

8.1.2.1 角色和活动

    对基于角色的授权提供支持的两个WF活动是HandleExternalEvent活动和WebServiceInput活动,它们都暴露了具有WorkflowRoleCollection类型的Roles属性。如果我们想要使用基于角色的授权来保护这些活动,我们将需要邦定Roles属性到一个有效的集合上。在下面的代码中,我们声明了一个名为ValidRoles的公有字段。在工作流的Initialized事件处理程序中,我们新添加了一个单独的WebWorkflowRole实例到validRoles集合中。

public partial class BugFlowWithRoles : SequentialWorkflowActivity
{
    public WorkflowRoleCollection validRoles = new WorkflowRoleCollection();

    private void BugFlowWithRoles_initialized(object sender, EventArgs e)
    {
        validRoles.Add(new WebWorkflowRole("TechLeads"));
    }
}

    在角色的位置上,我们需要配置我们的活动来利用角色。

image

    当一个本地通信服务宿主为工作流触发一个事件时,它可以在ExternalDataEventArgs对象的Identity属性中传递Windows身份参数。该活动将会对指派了身份的角色和Role集合中的角色进行比较。如果匹配,那么活动将继续执行;如果不匹配,那么活动将抛出一个WorkflowAuthorizationException异常。我们可以在工作流中使用错误处理来管理这个异常,或者让该异常终止工作流。注意到如果本地通信服务没有在事件参数中显示地传递一个身份,那么工作流将使用与当前线程相关联的身份。

    当我们使用CallExternalMethod和HandleExternalEvent活动来和宿主进行通信时,我们正工作在一个比较高的抽象级别上。像基于角色的授权和交互性这样的内嵌特性,可以解决很多与消息系统关联的头痛之事。然而,在某些情况下,当一个设计需要额外的弹性时,就需要更接近本质(close to the metal)。在下一节,我们将看一下在这些高级通信活动之下的队列机制。

8.1.3 工作流队列

    如果你回顾一下本章开头的图表,就会记得关于工作流运行时截取事件的会话。在一个特定的工作流实例中把事件发送到恰当的活动,这是运行时的职责。但是工作流如何发送一个事件呢?运行时不能只在一个任意的线程上触发事件——在一个实例中每次只有一个线程能够执行。

    答案是一个查询服务,它是工作流运行时的一部分。活动使用这个服务来创建队列,这个队列可以保存即将到来的数据。当一个项到达队列中时,一个活动就会订阅这个通知。这些队列成为工作流的一部分,在持久层服务序列化并保存这个工作流实例时,和工作流一起被序列化。这就是为什么我们的数据交换的事件参数被标记为可序列化的原因之一。

    当运行时发送一个事件到工作流时,它会为该事件取出正确的队列,并在这个队列上添加事件参数。当选择了正确的队列时,每个队列都会暴露一些信息,包括那些允许运行时使类型和相关性参数匹配的信息。在下面的截图的底部,我们暗示了运行时将使用工作流队列名称来路由(route)事件。我们将在下一节中看到关于队列名称的细节。

image

8.1.3.1 WorkflowQueueWorkflowQueueInfo

    下面的截图显示了WorkflowQueue类,它代表了在工作流实例中的一个队列;还显示了WorkflowQueueInfo类,它描述了一个队列。代表性地,当我们使用诸如HandleExternalEvent或Delay这样的事件侦听活动时,我们不需要知道这些底层的队列。然而,这些队列不能支持大量的场景,它们在更高级的抽象上是不可能的。让我们看一些例子。

image

8.1.3.2 查找和等待活动

    让我们回到本章开始的那个工作流。在工作流中,我们在Parallel活动的两个分支中使用了HandleExternalEvent活动。这些事件处理程序等待来自本地通信服务的投票事件到达,并使用相关性记号来保证投票到达正确的分支。我们将在事件处理程序中为运行时的WorkflowIdled事件调用下面的代码。当工作流被阻塞并等待事件到达时,运行时触发这个事件。

static void DumpQueueInfo(WorkflowInstance workflow)
{
    ReadOnlyCollection<WorkflowQueueInfo> queueInfos;
    queueInfos = workflow.GetWorkflowQueueData();

    Console.WriteLine("Queue Info for {0}", workflow.InstanceId);

    for (int i = 0; i < queueInfos.Count; i++)
    {
        Console.WriteLine();
        Console.WriteLine("Queue #{0}", i.ToString());
        Console.WriteLine(queueInfos[i].QueueName);
        Console.WriteLine("Subscribed activities: ");

        ReadOnlyCollection<string> names = queueInfos[i].SubscribedActivityNames;

        foreach (string name in names)
        {
            Console.Write("{0} ", name);
        }

        Console.WriteLine();
        Console.WriteLine();
    }
}

    对于在工作流实例中的每个队列,我们会打印出队列的名称和一组被订阅的活动。输出看起来是下面这样的:

image

    从这个输出中,我们可以看到在工作流实例中有两个队列。在我们打印队列名称的地方,我们会看到Message Properties输出。它是这些队列的名称,描述了它们正在等待的事件类型。记住,在工作流中的队列名称对于运行时发送事件到正确的队列中而言,必须是唯一的。这两个队列在它们各自的名称中都包括了相同的Iterface Type和Method Name(事件名称)。不幸的是,Correlation Value,它们也是队列名称的一部分,是不同的。这些Correlation Value是由userName参数组成的值,我们把它们传递到当前带有CorrelationInitializer特性的服务方法中(RequestVote)。

注意:如果我们想要以一种强类型的方式和队列名称协同工作,我们可以将队列名称转换为EventQueueName类的实例。这个类提供了InterfaceType和MethodName属性来把队列的名称拆散为它所选择的部分。这里有一个方法(GetCorrelationValues)来截取这个相关性的值。

    现在这应该是明显的了——当工作流运行时需要发送事件到一个工作流活动时,它不会去寻找一个特定的活动,而是寻找一个特定的队列。运行时能够对引入的事件信息(接口、名称和相关性的值)和这些队列名称进行比较,从而为该事件找到恰当的队列。活动只需要等待事件到达队列中。

    从下面截图的输出可以看出。我们还能推断出那个活动在等待事件。这些活动将出现在被订阅的活动的清单中。比如说,分析师立即投票,我们的工作流会返回到空闲状态,以等待技术组长来进行投票。我们的队列转储看上去将会是下面这样的:

image

    现在我们下至一个单独的阻塞活动。我们的应用程序可以使用队列信息的这个类型来诊断和援助被阻塞的工作流(我们可以使用跟踪信息)。

8.1.3.3 取消一个正在等待的活动

    如果我们恰好想要取消一个等待活动,那将会发生什么呢?因为我们的公司解雇了所有的技术员工,可能投票将不会向前进行。这是队列操作有用武之地的另一个场景。

    WorkflowInstance类提供了一个EnqueueItem方法。给定一个队列名称和一个对象,EnqueueItem方法将会把对象放进给定名称的队列中。如果我们把一个异常对象放进队列,那么我们可以取消一个带有错误的HandleExternalEvent活动:

ReadOnlyCollection<WorkflowQueueInfo> queueInfos;
queueInfos = workflowInstance.GetWorkflowQueueData();

foreach (WorkflowQueueInfo queueInfo in queueInfos)
{
    workflowInstance.EnqueueItem(queueInfo.QueueName, new Exception(), null, null);
}

    响应这个异常的工作流将会依赖于工作流的设计。如果在一定范围内,该工作流有一个错误处理程序,那么新的异常将会向上冒泡到这个错误处理程序。否则,那么该异常将导致工作流终止。

8.1.3.4 与队列进行通信

    工作流队列适合于来自外部的(inbound)所有形式的通信。当我们使用HandleExternalEvent和Delay活动时,即使我们没有直接与它们相互作用,这些队列也是在工作的。引进高级别的抽象是以定义和坚持过去的通信接口为代价的。

    可能会有工作流在它们的通信场景中需要更多的弹性。在这些情形中我们可以创建队列,这可能是从我们自己的自定义活动中,这些活动能够直接从宿主进程中接收数据。这种弹性的代价是需要额外的代码来管理和定义我们的自定义队列。

8.2 Web Service通信

    并不是所有的通信是本地通信。WF提供了Web Service互操作能力作为一种额外的特性。WF允许我们把工作流暴露为一个Web Service,并使用来自一个工作流的Web Service。

8.2.1 作为Web Service的工作流

    在本节中,我们将创建一个工作流,并将其作为一个Web Service来使用。我们的开始项目将会是一个顺序工作流库。理想地,一个库适合于宿主在一个ASP.NET的web应用程序中。在新的项目中,我们可以将Workflow1.cs重命名为HellpWorldWorkflow。

    就像本地通信服务一样,Web Service需要一个定义了Web Service成员的契约。契约是一个接口,但不需要我们在前面的通信接口中使用的任何数据交换特性。对于我们的HelloWorld工作流而言,这个接口如下所示:

interface IHelloWorldService
{
    string GetHelloWorldMessage(string name);
}

8.2.1.1 WebServiceInput活动

    用于Web Service工作流的第一个活动是WebServiceInput活动。WebServiceInput活动代表了在我们的Web Service契约中的一个方法,它需要从Web Service请求中接受数据。把WebServiceInput活动拖入到设计器中并进行配置,这多少有点类似于拖动并配置一个HandleExternalEvent活动。我们需要设置InterfaceType和MethodName属性。我们的IHelloWorldService接口只需定义了一个单独的方法:GetHelloWorldMessage。为了实现这个方法,我们只需要将WebService活动配置为如下所示:

image

    在WebServiceInput活动中另一个重要的属性是IsActivating属性。这个属性告诉工作流——调用GetHelloWorldMessage这个Web方法将开始我们的工作流的执行。在一个活动中,当我们希望看到一系列的Web Service调用来完成工作流时,我们可以拥有多个WebServiceInput活动。只有第一个输入活动应该将IsActivating属性设置为true。

    工作流的设计者将检查我们所实现的Web Service方法来呈现输入参数。设计者将使得输入参数在绑定到Properties窗体时是可用的。在上面的揭图中,我们使用绑定把引进的名称参数放入到工作流的_name字段中。

    注意到在我们的输入活动中,我们还有一个有效性错误(在图形的右上角有一个红色的惊叹号)。WebServiceInput活动是不完整的,除非我们把这个活动和WebServiceOutput活动结合起来。

8.2.1.2 WebServiceOutput活动

    WebServiceOutput返回对Web Service客户端的响应。InputActivityName是在这个活动上的一个必须的属性。在下面的截图中,我们把一个输出活动作为第二个活动放入我们的工作流中,并使用我们的输入活动来配置InputActivityName。这两个活动现在都经过了有效性检查。

image

注意:WebServiceFault活动的存在,用于在Web Service中对异常进行建模。这个活动将返回一个SOAP异常到客户端。我们在这个工作流中没有WebServiceFault活动。

    在上面的截图中,我们还配置了SendingOutput事件处理程序,并把返回值(Web Service的输出)绑定到了工作流类中的_result字段。这个工作流类的完整列表如下所示。在SendingOutput事件处理函数中,我们创建了对客户端的一个相应,并把结果放在_result字段中。

public sealed partial class HelloWorldWorkflow : SequentialWorkflowActivity
{
    public HelloWorldWorkflow()
    {
        InitializeComponent();
    }

    public string _name = String.Empty;
    public string _result;

    private void SendingOutput(object sender, EventArgs e)
    {
        _result = String.Format("Hello World! Hello, {0}", _name);
    }
}

    一旦我们确定这个工作流编译了,我们就可以通过发布Web Service来测试服务是有效的。

8.2.1.3 发布Web Service工作流

    将工作流发布为一个Web Service是一个相对简单的操作。我们需要右击我们的工作流项目并选择Public as Web Service。这个操作将启动Visual Studio中的一系列步骤。Visual Studio将创建一个新的Web Service项目,并添加该项目到我们的解决方案中。新的项目的项目名称将具有_WebService的后缀。

    新的项目只包括一些新的文件,和一个指向我们的工作流项目的引用。一个文件是asmx文件,这里asmx是ASP.NET的Web Service终结点的通用扩展名。这个文件的生成内容看上去是下面这样的:

<%@ WebService Class="Chapter8_WebService.HelloWorldWorkflow_WebService" %>

    上面的所有代码是把工作流发布为一个Web Service时所必须的。生成的项目还将包括一个web.config文件。配置文件的一部分如下所示。注意到,为了让代码与页面相适,其中一些命名空间已经被移除了。

image

    注意到,web.config将默认的工作流调度器替换为手动的工作流调度器。我们在第6章介绍过这些服务。手动的工作流调度器是在ASP.NET场景中首选的调度器,因为它可以在与引进的web请求相同的那个线程上同步执行工作流。

    WorkflowWebHostingModule支持路由一个客户端到现有的使用了客户端cookie的工作流实例上。在我们的场景中,工作流实例不需要跨越多个web请求,因此这个服务不会被使用到。

    现在我们可以右击asmx文件并选择来自上下文菜单的View In Browser。用于ASP.NET web开发的Web服务器将会宿主web服务。我们应该看到一个浏览器页面,以及一个指向GetHelloWorldMessage的链接一起出现的。如果我们沿着这个链接而行,我们将会看到如下所示的截图,它代表了手动测试该方法的一个时机。

注意:用于ASP.NET web开发的Web服务器(WebDev)会挑选一个随机的端口,用于提供内容和Web Service。这是用来测试web站点开发的一个好策略,但可能使得它难于引用来自另一个.NET项目的Web Service(因为端口号在会话期间会发生改变)。最好的办法是,在IIS中创建一个新的虚拟目录,并把这个虚拟目录映射到Web Service项目的根目录上。另一个选择是为WebDev选择一个特定的端口——进入到web项目的属性中,设置use dynamic port为false,并选择一个端口号。

image

    填充名称并点击Invoke按钮,我们的工作流得以成功执行。结果如下所示:

image

    我们的Web Service现在可以使用了。在下一节我们将创建一个工作流来调用这个Web Service。

8.2.2 作为Web Service客户端的工作流

    为了使用我们创建的工作流,我们可以返回到我们之前的项目,或创建一个新的控制台模式的工作流应用程序。任何项目都可以使用Web Service,甚至是一个类库或其他的Web Service项目。在下面的截图中,我们添加了一个新的Sequential Workflow(with code separation)到命名为HelloWorldClient的项目中。

image

8.2.2.1 InvokeWebService活动

    工作流设计器一打开,我们就可以拖动并放置一个InvokeWebService活动到工作流中。设计器将打开一个对话框询问我们来创建一个Web Service。我们可以在URL文本框中输入我们之前创建的Web Service的URL。此外,如果Web Service在同一个解决方案中,我们可以在对话框内这个解决方案的链接中点击Web Service。一旦对话框定位到这个Web Service,显示将会改变为下面这样:

 image

    你可以看到我们已经将web引用名称改变为HelloWorldService。这个值设置了Web Service代理类的名称(Visual Studio会为我们自动生成)。我们可以通过代理来调用Web Service。点击Add Reference按钮将添加这个代理到我们的项目中。

    返回到设计器,我们现在可以配置我们的InvokeWebService活动了(参见下图)。ProxyClass属性是一个在项目中包括了所有Web引用代理类的下拉列表。在截图中,我们选择一个代理并从下拉列表中选择MethodName。我们还要配置我们的参数,并绑定到一个事件处理程序。

image

    我们的工作流的后台代码如下所示:

public partial class HelloWorldClient : SequentialWorkflowActivity
{
    public string _helloWorldResult;

    private void invokeHelloWorld_Invoke(object sender, InvokeWebServiceEventArgs e)
    {
        Console.WriteLine("Hello world returned " + _helloWorldResult);
    }
}

    运行这个工作流将导致SOAP这个Web Service调用我们最后的工作流。

8.3 小结

    在本章我们研究了WF的通信能力。我们看到了相关性特性和相关性记号是如何与相关的活动联系在一起的,并讨论了基于角色的授权。在事件驱动的活动之下,我们看到了工作流队列是如何管理事件和到达工作流的数据。我们可以为我们自己的通信意图而使用这些队列,或者查询队列,看一看哪个活动在等待工作流中的事件。最后,我们通过创建一个简单的Web Service,并在客户端使用这个Web Service,讨论了WF中的Web Service能力。

原文地址:https://www.cnblogs.com/Jax/p/1419352.html