65.9K
CodeProject 正在变化。 阅读更多。
Home

宿主和工作流:通信的两个世界 IV

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2008年10月4日

CPOL

9分钟阅读

viewsIcon

33487

downloadIcon

454

第四部分:通信类的组织:通信管理器、wca.exe 工具和 Wwca.exe - wca.exe 的 Windows 前端

引言

这是关于主机工作流通信系列文章的第四篇。本系列文章旨在展示实现主机与工作流之间通信的各种可能性,从最简单的案例到复杂的案例。我并不打算穷尽所有细节,但会努力提供一个总体概览,以便我们能对该主题有一个很好的认识。

因为我也不喜欢冗长的文章,所以我将其分为以下几个部分:

第一部分:最简单的通信案例:主机 -> 工作流通过参数通信.
第二部分:工作流 -> 主机通过 CallExternalMethod 活动相互通信
第三部分:主机 -> 工作流通过 HandleExternalEvent 活动进行互通信.
第四部分:通信类的组织:通信管理器、wca.exe 工具和 Wwca.exe - wca.exe 的 Windows 前端
第五部分:使用相关性参数与工作流实例进行内部通信。
第四部分:通信类的组织:通信管理器、wca.exe 工具和 Wwca.exe - wca.exe 的 Windows 前端

背景

到目前为止,我们一直强调工作流和主机之间如何进行通信,现在我们将侧重于使用什么工具来让我们的工作更轻松,以及如何更面向对象地组织我们的工作。

正如我们在之前的文章中所见,可以直接使用 `CallExternalMethod` 和 `HandleExternalEvent` 活动,但当工作流稍微复杂时,直接使用这些活动可能会导致混淆,并且不能很好地反映活动的作用。

工作流 SDK 提供了一个工具,可以自动创建所有必需的自定义 `CallExternalMethod` 和 `HandleExternalEvent` 活动。这可以使我们的工作更轻松,并且使工作流更容易理解。

`wca.exe` 工具通过直接从包含我们定义的外部通信接口的 .dll 文件中获取必要信息来创建自定义活动。然后,该工具需要一些选项来生成自定义操作。由于命令行程序运行起来总是有点复杂,因为数据或程序本身的路径越来越复杂,所以我编写了一个小型 Windows 前端,您可以免费在此处获取:WWCA.ZIP 

您可以在下图看到 WWCA 的图形界面。

在使用该程序之前,您必须在 WWCA.exe 的配置菜单中存储 `wca.exe` 的路径和文件名。

正如您所见,该程序仅从输入文件中获取路径,并为您提供了轻松快速地检查不同命令行参数选项的可能性。

在之前的示例中,我们开发了代码,在没有面向对象的情况下将工作流和通信服务插入主机。这样做是因为当时的想法是解释通信的本质,而不涉及对象提供的封装级别。

在本部分,我们将创建一个 `Communication Manager` 类来驱动与通信相关的所有事务。

好的,我们以一个控制简单阀门的程序为例。

阀门可以通过一个按钮供电,另一个按钮驱动阀门的打开或关闭状态。

作为简单的业务规则:阀门只能在关闭时才能断电。如果阀门已打开,则不应断电。

如果阀门已关闭,则可以断电。

我们将使用状态工作流来控制阀门状态,用户界面必须是一个 Windows 程序。

在下图,您可以看到应用程序的总体架构图。

使用代码

1. - 通信管理器

从上图,您可以了解到主机和工作流之间的通信是如何进行的。

  • 从工作流到主机

您需要一个外部方法来将状态信息返回给主机。您可以将返回的状态定义为字符串:CLOSE、OPEN、EXIT、INIT。

  •  主机到工作流的通信

这里有两种可能性:为每个状态更改设置一个事件,或者设置一个带有新状态信息的参数的事件。

我在这里选择了为每个状态设置一个事件。使用此解决方案,我无需创建特殊的外部事件参数即可使用该事件,可以直接使用 `ExternalDataEventArgs`,因为我不需要在参数中传递信息。

然后,我们需要声明三个事件:Open、Close、Exit。无需声明 Init,因为您可以在创建工作流时进行初始化。

在接下来的代码片段中,我们有用于创建的代码。

[ExternalDataExchange]
public interface ICommunicationValve
{
#region Communication WF -> Host
  /// <summary>
  /// Used by CallExternalEvent Argument to pass the valve
  /// status to Host
  /// </summary>
  /// <param name="status">Actual statis for valve</param>
  void StatusValve(Guid wfGuid, string status);
#endregion

#region Communication Host -> WF
  /// <summary>Use to pass the valve operation to workflow
  ///</summary>
  event EventHandler<ExternalDataEventArgs> CloseValve;
  /// <summary>Use to pass the valve operation to workflow
  ///</summary>
  event EventHandler<ExternalDataEventArgs> Exit;
  /// <summary>Use to pass the valve operation to workflow
  ///</summary>
  event EventHandler<ExternalDataEventArgs> OpenValve;

#endregion

}//end ICommunicationValve

下一步是实现此通信接口。因为我们使用的是 Windows 应用程序,所以在 `StatusValve` 过程中需要实现一个内部事件,通过一个事件将信息传递给窗体。

public class CommunicationValve : ICommunicationValve
{
  #region WF -> Host Communication
  public event EventHandler<ExternalValveEventArgs>
  EventValveStatus;

  /// <summary>
  /// Used by CallExternalEvent Argument to pass the valve 
  ///status to Host
  /// </summary>
  /// <param name="status">Valve status OPEN / CLOSE / EXIT</param>
  public void StatusValve(Guid wfGuid, string status)
  {
     if (EventValveStatus != null)
     {
        ExternalValveEventArgs e = new ExternalValveEventArgs(wfGuid);
        e.Status = status;
        EventValveStatus(this, e); //Raise the event
  }

}

#endregion

#region Host -> WF
  /// <summary>
  /// Use to pass the valve operation to workflow
  /// </summary>
  public event EventHandler<ExternalDataEventArgs> CloseValve;

  /// <summary>
  /// Use to pass the valve operation to workflow
  /// </summary>
  public event EventHandler<ExternalDataEventArgs> Exit;

  /// <summary>
  /// Use to pass the valve operation to workflow
  /// </summary>
  public event EventHandler<ExternalDataEventArgs> OpenValve;

#endregion

#region Auxiliar procedures

  /// <summary>
  /// Raise the event Exit
  /// </summary>
  /// <param name="instanceId">workflow instance</param>

  public void RaiseExit(Guid instanceId)
  {

     if (Exit != null)
     {
        ExternalDataEventArgs e = new ExternalDataEventArgs(instanceId);
        e.WaitForIdle = true;
        Exit(null, e);
     }
  }

  /// <summary>
  /// Raise the event Open
  /// </summary>
  /// <param name="instanceId">workflow instance</param>
  public void RaiseOpen(Guid instanceId)
  {
    if (OpenValve != null)
    {
       ExternalDataEventArgs e = new ExternalDataEventArgs(instanceId);
      OpenValve(null, e);
    }
  }
  /// <summary>
  /// Raise the event Close
  /// </summary>
  /// <param name="instanceId">workflow instance</param>
  public void RaiseClose(Guid instanceId)
  {
    if (CloseValve != null)
    {
       ExternalDataEventArgs e = new ExternalDataEventArgs(instanceId);
       CloseValve(null, e);
    }
  }
#endregion
}//end CommunicationValve

正如您在此处看到的,我们将引发事件的方法直接封装在接口的实现中,然后您就不需要从外部引发事件了。

正如您在前一部分所见,您必须将该服务注册为工作流运行时中的通信服务。然后,创建一个对象来封装所有这些活动是一个好主意。因此,我们创建了 `CommunicationManager` 类。该类必须包含 `CommunicationValve` 类的一个实例,以及对工作流运行时的引用。

请看下面的代码

public class CommunicationManager 
{

  /// <summary>
  /// Single onject of the communicationValve class.
  /// </summary>
  private static CommunicationValve Comvalve = null;

  /// <summary>
  /// Reference to the workflow runtime.
  /// </summary>
  private WorkflowRuntime runtime = null;

  /// <summary>
  /// Return the CommunicationValve instance.
  /// </summary>
  public CommunicationValve Valve
  {
    get
      {
        return Comvalve;
      }
  }

  /// <summary>Constructor Communication manager</summary>
  /// <param name="wfRuntime">Runtime instance</param>
  public CommunicationManager(WorkflowRuntime wfRuntime)
  {
    runtime = wfRuntime;
    if (Comvalve == null)
    {
      Comvalve = new CommunicationValve();
    }
  }

  /// <summary>
  /// Procedure to register the communication service.
  /// </summary>
  public void RegisterCommunicationService()
  {
    //Declare a ExternalDataExchangeService class
    ExternalDataExchangeService dataservice = new
    ExternalDataExchangeService();
    //Add to workflow runtime
    runtime.AddService(dataservice);
    //Add to the ExternalDataService
    dataservice.AddService(Comvalve);
  }
}//end CommunicationManager

代码中包含了所有与通信相关的内容。

我们也可以应用外观模式(Facade),并在该管理器中声明 `ValveCommunication` 类中的引发事件过程,而不直接调用 `ComValve` 实例。

就这样,我们的通信管理器就准备好了。您可以在下图看到完整的类图。

2. – 工作流应用程序

创建一个新的状态工作流库项目。我们应该做的第一件事是创建自定义活动。

打开 `WWCA.EXE` 程序,输入在上一步构建 `CommunicationManager` 时创建的 .dll 文件。点击第一个文本框旁边的省略号按钮,然后选择工作流创建项目所在的目录作为输出目录。查看图 1:选择“包含发送者”选项以及您在项目中使用的命名空间名称。

然后选择“操作”->“执行 wca”,并在结果文本框中查看操作结果是否正确。(文本框会捕获 `wca.exe` 工具发送的控制台信息)。

就这样!您将在工作流应用程序中获得两个文件的自定义活动。在 VS 解决方案资源管理器中选择工作流项目,选择:显示所有文件。您将看到创建的文件:`ICommunicationValve.Invokes.cs` 和 `ICommunicationValve.Sinks.cs`,将两者都包含到项目中,然后编译项目。(如果使用的命名空间未包含在工作流项目中,您可能会遇到错误。插入对 `CommunicationManager` 项目的引用或更正输入的命名空间,然后构建项目)。

打开工作流设计器,并打开工具箱。您将在工具箱中看到创建的自定义活动,其名称为 `IValveCommunication` 中的方法和事件。

我们创建了一个新的状态工作流,包含 4 个状态,以解决我们的问题。请参阅下图。

您可以在附件代码中查看实现的详细信息。我们仅演示了自定义活动用于发送状态和接收事件的使用,正如您在下图中所见。

这里您将 `StatusValve` 用作 `CallExternalMethod` 活动。嗯,`StatusValve` 继承自 `CallExternalMethod`。并且它是为我们的应用程序定制的。如果您想查看使用的代码,请查看生成的文件。

下图说明了自定义 `HandleExternalEvent` 活动的使用情况。

正如您所见,自定义活动的使用与原始活动相同,只是您无需声明接口名称和使用的(请参阅前面照片中的属性窗口)。

工作流仅用于演示目的,唯一代码操作是在状态更改时更新状态变量。当从主机接收到确定事件以更改状态时,状态会更改。

3. – 主机应用程序。

用户必须与应用程序交互,观察阀门状态并在必要时进行更改。已实现以下用户界面来满足这些要求。

初始状态下,阀门电源关闭,阀门处于非运行状态。如果您单击“电源开启”,阀门将激活,并将状态初始化为“关闭”。

如果您单击阀门“打开”,阀门按钮将显示“点亮”,如果阀门已打开,您可以断开系统电源,此时“开启”按钮将禁用。构建并运行代码以了解更多程序功能。

我们将阀门代码封装在用户控件中。并在控件中封装与工作流的交互,正如您在以下代码中所见。

代码量太大,无法在此处完整包含,因此我只包含了与通信相关的部分。

/// <summary>
/// Constructor
/// </summary>
public Valve()
{
  InitializeComponent();
  lName.Text = "-";
}

/// <summary>
/// Initialization of activities
/// </summary>
/// <param name="name">Decorative name for the
/// valve</param>
/// <param name="wr">runtime reference</param>
/// <param name="cm">communication manager reference</param>

public void InitializeControl(string name, WorkflowRuntime wr, CommunicationManager cm)
{
  wi = null;
  wi = wr.CreateWorkflow(typeof(WFValveControl));
  this.wr = wr;
  this.cm = cm;
  lName.Text = name;
  cm.Valve.EventValveStatus += new  EventHandler<ExternalValveEventArgs>
(Valve_EventValveStatus);
  wi.Start();
}

当阀门激活(电源开启)时,将使用 `initializeControl` 方法。此时,工作流将被实例化,从工作流接收事件状态将被连接,并且工作流将被启动。

触发 OPEN、CLOSE 的命令由内部的单击事件触发,正如您在以下代码片段中所见。

/// <summary>
/// Send order to Workflow
/// </summary>
private void bControl_Click(object sender, EventArgs e)
{
  string dg = Color.DarkGreen.Name;
  switch (bControl.BackColor.Name)
  { 
    case "DarkGreen": {cm.Valve.RaiseOpen (wi.InstanceId); break; }
  case "LightGreen":{cm.Valve.RaiseClose(wi.InstanceId); break; }
  }
}

项目的其余部分是控制电源按钮的正常逻辑,与通信相关的其余部分在类构造函数中完成,首先创建工作流运行时,然后将通信服务注册到工作流运行时。

public Form1()
{
  InitializeComponent();
  // initialize form...
  runtime = WFRuntime.GetRuntime();
  manager = new CommunicationManager(runtime);
  manager.RegisterCommunicationService();
  bActive.BackColor = Color.DarkRed;
  gbValvePanel.Enabled = false;
  valve1.Name = "VALVE I";
}

您可以看到,服务通信的注册操作封装在通信管理器中。

多个工作流实例示例

我们这里描述的方法也适用于同一工作流的多个实例或多个工作流。您可以在附件代码中看到一个控制 4 个阀门的完整示例程序。

恢复

使用 `wca.exe` 工具或开发的 Windows 前端 `WWCA.exe` 使您能够创建自定义活动来管理主机与工作流之间的输入输出操作。生成的自定义操作具有配置更简单、程序更容易理解的优点。

将通信服务封装在管理器类中,可以使程序更易于理解,并且出错的可能性更小。您也可以用最小的更改重用代码到其他代码中。

是的,我们对主机与工作流之间的通信有了一个很好的概览。只剩下非常具体的情况需要处理了。

工作流在本地化正确的工作流实例方面没有问题,因为它使用 `idInstance` 在 `eventArguments` 中。您可以在伴随代码中看到一个示例。

我们没有涵盖的情况是,同一个事件在工作流的两个或更多部分并行消耗的情况。我们将在本文的最后一部分讨论这一点。

历史

首次发布 2008.10.04 
© . All rights reserved.