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





4.00/5 (2投票s)
第四部分:通信类的组织:通信管理器、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` 中。您可以在伴随代码中看到一个示例。
我们没有涵盖的情况是,同一个事件在工作流的两个或更多部分并行消耗的情况。我们将在本文的最后一部分讨论这一点。