工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信


作者  朱先忠

    [摘要]

      在本篇中,首先详细分析本地服务有关概念,探讨本地服务在工作流运行时、工作流实例及工作流宿主间的地位及作用。然后,通过一个简单的例子来说明使用本地服务在宿主和工作流之间的具体通信方法。

一、简介

      WWF中的服务可分为核心服务和本地服务。核心服务由WWF定义,而本地服务(也称为“数据交换服务”)则是开发人员自定义的。本地服务可以是任何想在WWF中实现的服务,通常的用处是:使用本地服务在工作流实例与宿主之间进行通信。比如,数据最初从宿主应用程序的某个运行结果中传入工作流,工作流实例在处理完数据后返还给宿主。
      在本篇中,我想首先详细分析本地服务有关概念,探讨本地服务在工作流运行时、工作流实例及工作流宿主间的地位及作用。然后,通过一个简单的例子来说明使用本地服务在宿主和工作流之间的具体通信方法。

二、与本地服务相关的两个重要活动-HandleExternalEventActivity和 CallExternalMethodActivity

      首先,让我们看一下MSDN中有关解释。
       Windows Workflow Foundation支持工作流的承载环境中的本地通信和使用 Web 服务在工作流之间进行的通信。

有关在工作流中使用 Web 服务的实例,将在后面文章中陆续给出。

       WWF工作流通信服务向工作流开发人员公开了一个用户自定义服务类,通过在此服务类中定义一系列的方法和事件处理程序,以达到简化工作流与宿主间数据通信的建模目的。
      实际开发中,实现本地服务的目的要根据当前应用程序的需求来定,比如需要查询或设置应用程序的状态,从数据库获取或更新数据,或者是与其他不属于工作流的对象组件进行通信,等等。当将功能实现为本地服务后,本地服务对多个工作流实例都可以有效。
       下图演示本地(通信)服务如何与其主机(也称“宿主”,即Host)应用程序之间进行通信(图片改编自MSDN)。
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

      从上图中可以看出,工作流实例上的定义的两个活动HandleExternalEventActivity和 CallExternalMethodActivity活动(也就是说,典型情况下,在创建我们的工作流时需要加入这两个“特殊”的活动)的任务是:与在自定义接口(请看本文后面的介绍)中声明并在自定义本地服务中实现的事件和方法交互。 
      具体来看,[1]HandleExternalEventActivity 活动响应由主机应用程序引发且由本地服务实现的特定事件。 [2]CallExternalMethodActivity负责调用本地服务中定义的方法。 
      有关两个活动HandleExternalEventActivity和 CallExternalMethodActivity活动,请继续阅读下面来自于MSDN的重要解释。

来自MSDN:
      [1]HandleExternalEventActivity 活动与  CallExternalMethodActivity 活动结合使用,可输入或输出与本地服务的通信。 可以直接对一般通信使用这些活动。 或者,可以创建 HandleExternalEventActivity 和 CallExternalMethodActivity 类的子类,以创建严格绑定到某个接口上的特定事件和方法的活动,并具有  ExternalDataExchangeAttribute 属性。
      
      HandleExternalEventActivity 基类阻止工作流,直到通过 WorkflowRuntime 注册的相应本地服务引发由 InterfaceType 和 EventName 属性指定的事件。 引发该事件后,或者如果该事件在活动开始执行前引发,则将传入数据分配给在 ParameterBindings 集合中定义的绑定位置。
 
       [2]CallExternalMethodActivity 活动和 HandleExternalEventActivity 活动可用于与本地服务进行输入和输出通信。 您可以直接使用这些活动进行一般通信,也可以创建 CallExternalMethodActivity 和 HandleExternalEventActivity类的子类以创建一些活动,这些活动严格绑定到具有 ExternalDataExchangeAttribute属性的接口上的特定事件和方法。
 
      CallExternalMethodActivity 基类调用由向 WorkflowRuntime 注册的相应本地服务的 InterfaceType 和 MethodName 属性指定的方法。 此调用是使用从绑定位置的 ParameterBindings 集合中收集的参数以同步方式执行的。 如果该方法具有返回值,则会在活动执行完毕前将这些值设置为绑定位置。


三、局部步骤归纳

 
    为了让工作流运行时与本地服务进行交互,需要完成如下步骤:
    (1)使用标准的C#接口定义一个服务契约(你必须先定义符合这一特征的一个接口),在该接口中定义工作流实例中可以使用的方法和事件。
    (2)在接口前面修饰以ExternalDataExchangeAttribute特性,使该接口能被识别为一个本地服务接口。
接口定义举例(来自MSDN):
[ExternalDataExchange]
public interface ICommunicationService
{
void HelloHost(string message);
event EventHandler<ExternalDataEventArgs> HelloWorkflow;
}


    (3)编写一个实现了第2步定义的接口的标准C#类(即自定义本地服务类)。
自定义本地服务类举例(来自MSDN)
public class CommunicationService : ICommunicationService 
{
public event EventHandler<ExternalDataEventArgs> HelloWorkflow;

public void HelloHost(string message)
{
Console.WriteLine("This is the message: {0}", message);

//引发HelloWorkflow事件
HelloWorkflow(nullnew
ExternalDataEventArgs(WorkflowEnvironment.WorkflowInstanceId));
}
}

    (4)创建一个系统提供的本地服务类ExternalDataExchangeService的实例,再创建上面自定义服务类的实例,把本地服务类ExternalDataExchangeService的实例添加到工作流运行时引擎中,然后把自定义服务类的实例加载到本地服务类ExternalDataExchangeService的实例中。这个过程需要在工作流运行时引擎初始化期间(在正式启动工作流之前)进行创建。
   
在启动工作流之前需要完成的任务示例:
ExternalDataExchangeService externalService = new ExternalDataExchangeService();
workflowRuntime.AddService(externalService);
externalService.AddService(new CommunicationService());

      注意:所有本地服务必须经由接口类型进行唯一的识别。也就是说,每个服务接口仅允许有一个实例。但是可以在工作流运行时引擎中注册多个本地服务,只要每个服务实现了不同的接口。
注意:因为每个服务只能具有单个实例,因而有可能有多个工作流实例同时调用服务中的方法和事件。因此在设计时需要考虑线程安全的数据访问问题。

    当本地服务向工作流运行时引擎注册后,便可以被任何工作流引擎使用。有两种方法可以调用本地服务中的方法。
  [1]使用GetService()方法获取服务的引用,并调用定义在服务接口中的方法。
  [2]使用CallExtemalMethodActivity活动以声明性方式调用一个方法,使之作为工作流的一个步骤。当使用工作流时,不需要工作流或者是活动的代码。

四、案例分析


一)创建控制台顺序工作流示例框架

说明:本文创建的LocalServiceDemo示例演示了如何在一个状态机工作流内部调用另外的一个工作SubWorkflow,并且定义了本地服务接口实现,使用HandleExternalEvent活动调用外部事件以等待被调用的工作流实例执行完成。该活动需要等待一个事件的触发才能够继续工作流的运行,而在Program.cs中,设置了只有当指定非宿主工作流执行完毕后,才触发事件。因此这实现了一种等待被调用工作流执行完成才继续执行的效果。

请遵循如下步骤创建一个控制台状态机工作流示例程序:
1. 启动VS2008,单击菜单”文件“|”新建“|”项目“,选择“Sequential Workflow Console Application”
模板创建一个名字为LocalServiceDemo的控制台状态机工作流示例程序。
2.之后,系统自动打开工作流设计器界面。
3. 从工具箱中拖动几个活动到工作流设计器中得到如图所示的情形。
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

在上图中,我们依次把三个活动:Code,CallExternalMethod和Listen拖动到工作流设计器中,其他没有作任何更改,具体的修改操作将在后面步骤中给出。

(二)创建自定义事件参数类


创建自定义事件参数类(用于在宿主与工作流间传递参数之用):

class CustomServiceEventArgs:ExternalDataEventArgs 
{
    private string name;
    //这个公共属性用于在宿主与工作流间传递参数之用,可以是复杂的类,也可以是简单的字符串
    public string Name
    {
        get { return this.name; }
    }

    public CustomServiceEventArgs(Guid instanceID, string name)
        : base(instanceID)
    {
        this.name = name;
    }
}




(三)定义本地服务接口


本地服务接口定义:
[ExternalDataExchange]
internal interface ICustService
{
    event EventHandler<CustomServiceEventArgs> Approved;
    event EventHandler<CustomServiceEventArgs> Rejected;

    void CreateBallot(string name);//产生一次新的投票

}




(四)定义本地服务类


IVotingService接口的类。该类将实现CreateBallot(),并触发这两个事件,代码如下所示。

定义本地服务类:
internal class CustServiceImpl : ICustService
{

    #region ICustService 成员

    public event EventHandler<CustomServiceEventArgs> Approved;

    public event EventHandler<CustomServiceEventArgs> Rejected;

    public void CreateBallot(string name)
    {
        Console.WriteLine("现在为{0}投票。", name);
        ShowDlg(new CustomServiceEventArgs(WorkflowEnvironment.WorkflowInstanceId, name));
    }
    #endregion

    public void ShowDlg(CustomServiceEventArgs args)
    {
        DialogResult result;
        string name = args.Name;

        result = MessageBox.Show(string.Format("是否同意,{0}", name),
            string.Format("当前为{0}投票", name), MessageBoxButtons.YesNo);

        if (DialogResult.Yes == result)
        {
            EventHandler<CustomServiceEventArgs> approved = this.Approved;
            if (approved != null)
                approved(null, args);
        }
        else
        {
            EventHandler<CustomServiceEventArgs> rejected = this.Rejected;
            if (rejected != null)
                rejected(null, args);
        }
    }
}

[注意]为了使用MessageBox和DialogResult,需要在项目上右击“引用”菜单,添加对System.Windows.Forms的引用。


(五)工作流编程


1. 添加CallExtemalMethodActivity活动

进入工作流设计视图,首先添加一个CallExtemalMethodActivity。该活动的InterfaceType被设置为ICustService,指定方法名为CreateBallot,可以在属性窗口中使用弹出式窗口选择InterfaceType,如图所示。此外,MethodName属性列出可用的方法名称供应用进行选择,选择接口中声明的方法“CreateBallot”。

[注意]后面还要进一步这个CallExtemalMethodActivity修改活动。

工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)


工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)


2. 添加ListenActivity活动

然后,在CallExtemalMethodActivity的下面添加一个ListenActivity。有关ListenActivity活动,我们后面还会专门撰文探讨。在此只需了解,ListenActivity是一个组合活动,该活动将在活动继续前使工作流等待多个可能事件中的任何一个事件发生。
然后,在ListenActivity活动中,添加2个HandleExternalEventActivity活动,分别使之负责监听接口ICustService中的Approved和Rejected两个事件。
当然,HandleExternalEventActivity也需要指定InterfaceType属性,如下图所示。
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

然后分别指定2个HandleExternalEventActivity活动的EventName属性为ICustService中定义的Approved和Rejected两个事件,如下面二图所示。
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

到目前为止,工作流设计视图如下图所示。

工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

3. 在工作流类中定义公共属性

目前为止,我们必须强调:在工作流类中需要定义了一个公共属性,以便于接收从宿主传入的投票人信息,代码如下所示。
///定义了一个公共根属性,以便于接收从宿主传入的投票人信息。
//定义一个表示投票人姓名信息的属性
private string votername;
public string VoterName
{
    set { this.votername = value; }
    get { return this.votername; }
}



4. 进一步修改CallExternalMethodActivity活动

要想使外部方法获取到工作流实例中传入的参数信息,需要进一步修改CallExternalMethodActivity活动的属性值,单击CallExternalMethodActivity活动的属性对话框中的name属性(而不是(Name)属性!!!),弹出如下图所示的对话框:
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

从图中选择前面定义的属性VoterName,单击“确定”按钮,得到如下图所示的属性对话框。
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)


最后,我们来看一下控制台宿主的编程内容。

(六)控制台宿主编程

最后,需要在工作流运行时引擎中注册本地服务,并且为工作流实例传递参数。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;

using System.Workflow.Activities;//ExternalDataExehangeService 
namespace LocalServiceDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
            {

                //加载本地服务
                ExternalDataExchangeService dataService = new ExternalDataExchangeService();
                workflowRuntime.AddService(dataService);

                //将自定义的本地通信服务加载到本地服务中
                CustServiceImpl ls = new CustServiceImpl();
                dataService.AddService(ls);

                AutoResetEvent waitHandle = new AutoResetEvent(false);
                workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) 
                {
                    waitHandle.Set();
                };
                workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
                {
                    Console.WriteLine(e.Exception.Message);
                    waitHandle.Set();
                };

                //向工作流实例传递参数,注意格式
                Dictionary<string, object> paras = new Dictionary<string, object>();
                paras.Add("VoterName", "爱因斯坦");

                WorkflowInstance instance = workflowRuntime.CreateWorkflow(
                    typeof(LocalServiceDemo.Workflow1),paras );
                instance.Start();

                waitHandle.WaitOne();

                Console.Read();
            }
        }
    }
}



注意,在上面控制台程序中代码没有特殊的内容,开始时的加载本地服务编码在以前的文章中已经作过介绍。接下来,后面的代码基本是系统自动生成的。

最后,请注意向工作流实例传递参数的格式

总体来看,上面的代码关键之处还在于前面的围绕本地服务的编程,以及工作流中相应活动的关联设置方面。而本篇编程逻辑的根本在于深入理解工作流运行时与本地服务进行交互的原理,以及理解工作流编程中的HandleExternalEventActivity CallExternalMethodActivity两个活动的重要作用。

(七)观察运行结果

按F5运行控制台程序,一般顺利的话,将得到如下图所示运行时快照。
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)
单击“是”按钮后,得到如下结果:
工作流编程循序渐进(9:使用本地服务在宿主和工作流之间通信)

最后一句:本篇是实战环境下比较重要的一篇,必须理解透彻,烂熟于心。

五、部分参考资料

1.《如何:发布符合 .NET Framework 准则的事件(C# 编程指南)》(http://msdn.microsoft.com/zh-cn/library/w369ty8x.aspx)。