从 AJAX 触发 WorkflowEvents






4.93/5 (18投票s)
本文介绍了一种松散耦合的连接方式,可以从 AJAX/JSON 驱动的浏览器触发工作流事件活动。
目录
注意:本文是使用 .NET Framework 3.5 (Orcas) Beta 2 版本编写的。
引言与概念
基于逻辑连接模型 (LCM) 构建事件驱动的应用程序解决方案,以封装物理连接端到端的业务层,需要跨所有层使用适当的技术。典型的具有私有中间层的多层架构正在平滑地转变为基于面向服务架构 (SOA) 的分布式中间层。
AJAX (Asynchronous JavaScript and XML) 等最新技术使得 LCM 的物理连接可以转移到浏览器客户端。这个过程通过浏览器中托管的 XmlHttpRequest ActiveX 对象来实现,该对象以异步方式发送和处理来自服务器的请求和响应,而无需回发。
XmlHttpRequest
使用 REST 架构风格以文本格式交换数据。基于 Content-type,文本(数据)可以在浏览器和服务之间进行编码/解码。这种高效且语言无关的数据交换由 JSON (JavaScript Object Notation) 格式描述,与使用 XML 文档导向风格相比,它可以极大地简化实现任务。
因此,在现代 Web 事件驱动架构中,AJAX/JSON 这个流行语经常被提及。AJAX/JSON 启用的网页可以将页面分解成小部分,并以异步方式单独处理它们。换句话说,用户控件组可以在不回发到 Web 服务器的情况下异步处理,用于所有页面更新过程。
从服务器端来看,服务必须启用 AJAX/JSON 通信。这将在 .NET Framework 3.5 的下一个版本中实现,称为 Orcas,目前为 beta 版本。本文基于此 beta 版本,使用了新的 webHttpBinding
和 JSON 编码。稍后您将看到更多详细信息。
我假设您将阅读更多关于 AJAX、JSON、XmlHttpRequest、REST 的详细信息,以了解它们在前端层,特别是浏览器客户端的风格、架构和位置。现在,是时候解释 WCF 和 WF 技术在浏览器/工作流连接系统中的作用了。
下图将帮助我展示这些技术在事件驱动的企业解决方案中的位置。
上述解决方案由元数据驱动,元数据包含端到端活动定义的描述,例如 WCF 服务的业务逻辑,Web 页面的呈现,其中包含组件、接口和数据契约的定义。每个 Web 页面在知识库中都有自己的入口定义,用于创建页面并为事件驱动任务启用其组件。当然,在此基础上,可以有一个根定义来导航所有站点 - 产品等。在页面激活期间,工作流状态机实例标识符将发送到浏览器,以基于事件兴趣发送事件消息。
它是如何工作的?
- 当用户在页面上的特定组件上创建事件(例如,单击按钮)时,由 JavaScript 实现的其逻辑将创建一个 JSON 格式的请求对象(事件消息),此过程激活 AJAX 回调以获取服务响应,并调用神奇的
XmlHttpRequest
对象以异步方式将请求发送到特定端点。 - 在服务器端,我们有一个启用了 AJAX/JSON 通信的 WCF 服务端点。传入的消息在 JSON 反序列化后,根据接口契约转发到工作流实例队列。注意,所有层也由知识库配置。
- 来自服务的响应以非阻塞方式在 AJAX 回调处理程序中接收。
在 .NET Framework 3.5 (beta 版本) 中,托管和连接 WCF 和 WF 模型非常顺畅。为基于接口契约的发送和接收消息提供了两个额外的上下文驱动的工作流活动。
从逻辑连接的角度来看,来自浏览器的事件兴趣通过 AJAX/JSON/WCF/WF 技术传递到由工作流活动表示的业务逻辑 - 事件接收器。所有这些通信在事件兴趣的发布者和其消费者之间都是完全透明且可配置的。
通过向服务添加额外的端点,可以实现 WCF 服务后端业务活动的重用性。在我的文章 从 WCF 触发工作流事件 中详细描述了通过 WCF 服务触发 WF 活动的概念,本文将使用该程序集。
本文的主要目标是关注如何通过 WCF 服务(例如另一个客户端)从 AJAX/JSON 启用的页面触发事件到工作流活动。请注意,这方面的实现工作量不大,因为我上面提到的文章中已经提供了神奇的可重用代码 Fire WorfklowEvents from WCF。
好的,我们开始吧。
下图展示了由工作流活动表示的事件接收器与位于浏览器客户端的事件源之间的逻辑连接。在这种情况下,对于长时间运行的工作流,浏览器将以“即发即弃”的方式引发一个事件。
为了让事件驱动的活动接收事件消息,必须使用特定于应用程序的事件处理程序声明 ExternalDataExchange
接口契约。
事件委托具有以下行的标准 .NET 签名。
public void EventHandler(object sender, ExternalDataEventArgs eventArgs);
其中 eventArgs
代表工作流事件兴趣,例如 InstanceId、Identity、应用程序特定数据等。
事件处理程序(JSON 格式)将具有以下表示法。
{"sender":"myName",
"e":{"batchworkHandler":null,
"batchworkItem":null,
"identity":null,
"instanceId":"00000000-0000-0000-0000-000000000000",
"waitForIdle":true
}
}
正如您所见,事件消息可以在浏览器端轻松创建,并传递到服务终结点以反序列化为 CLR。
我们需要执行 3 个简单的步骤来完成 AJAX/JSON/WCF/WF 连接。
步骤 1:服务器 - 使用 .svc 文件(内联代码)托管和连接 WCF + WF 技术
JSON 启用的服务
@ServiceHost
指令允许调用由 Factory
属性声明的宿主激活类型。注意,这个自定义类必须派生自 ServiceHostFactoryBase
类,以便调用其抽象方法来创建 ServiceHost
和 WorkflowRuntime
对象。有关此指令的更多详细信息,请参见 此处。
没有 CodeBehind
属性,因此所有实现都位于此 .svc 文件中(内联),用于运行时编译。服务契约,在此示例中,类 ServiceTest1
是一个常规 WCF 服务,带有一个操作方法,用于传入(单向)消息请求。通过自定义属性 RKiss.WorkflowEventService.WorkflowFireEventAttribute,
装饰此方法,操作调用程序将根据接口契约和 e.InstanceId
值将消息转发到工作流队列。注意,方法参数可以是任何可序列化的复杂类型,派生自 ExternalDataEventArgs
。有关调用工作流并将消息传递到其队列的更多详细信息,请参见我之前的文章 从 WCF 触发工作流事件;此处我们使用其程序集。
此步骤就完成了。下一步是为 AJAX/JSON 启用的服务终结点创建配置节。
步骤 2:服务配置 - 创建 AJAX/JSON 端点
我们需要使用 webHttpBinding
绑定来启用支持 AJAX/JSON 对话的端点,使用端点行为中的 EnableWebScript
,并设置 JSON 编码的数据格式,以便在 JavaScript 中使用 eval 函数反序列化服务响应。注意,即发即弃事件消息(无返回值)不需要设置 messageEncoding
属性。
现在,服务端的各项工作都已完成,我们已准备好接收 AJAX/JSON 请求。
让我们进行最后一步 - 浏览器客户端。
步骤 3:客户端 - AJAX/JSON 启用的 JavaScript
以下代码片段展示了使用 JavaScript 实现的简单 AJAX/JSON 启用的网页。有 5 个步骤来处理将触发的事件发送到目标端点。
// 1: AJAX enabled client
var xmlHttp ;
try {xmlHttp=new XMLHttpRequest();}
catch (e) {try {xmlHttp=new ActiveXObject("Msxml2.XMLHTTP");}
catch (e) {try {xmlHttp=new ActiveXObject("Microsoft.XMLHTTP");}
catch (e) {alert("This sample only works in browsers with AJAX support");
return false;}}}
// 2: callback
xmlHttp.onreadystatechange=function()
{
if(xmlHttp.readyState==4 /*complete*/)
{
// todo - for example: var response=(eval("("+xmlHttp.responseText+")").d);
}
}
// 3: endpoint
var url = "service11.svc/ajaxEndpoint/Approve";
// 4: message
var body =
'{"sender":"AjaxClient",
"e":{"batchworkHandler":null,
"batchworkItem":null,
"identity":null,
"instanceId":"2c6b451e-0d7d-4e8b-bc71-8f51e5bf54b0",
"waitForIdle":true
}
}';
// 5: action
xmlHttp.open("POST",url,true);
xmlHttp.setRequestHeader("Content-type","application/json");
xmlHttp.send(body);
首先,我们需要创建一个 ActiveX
对象来以异步方式处理所有神奇的请求和响应(回调)数据交换。接下来,JavaScript 将创建通信参数,例如 URL、JSON 格式的请求体,并设置 JSON 转换的内容类型标头。之后,调用 send
方法。注意,该方法会立即返回,而不会阻塞线程以获取响应调用,这就是为什么我们设置回调函数的原因。换句话说,浏览器已准备好在页面上进行下一次事件触发,依此类推。
正如您所见,所有步骤都非常轻量级。基于这种 AJAX/JSON/WCF/WF 连接,我们可以看到浏览器客户端更多的功能。例如:页面可以在逻辑对话中处理一组触发事件(类似于广播消息给特定服务契约),并等待所有响应在一个事件接收器(回调函数)中收集,然后将它们分发回页面。这种情况在使用长流程工作流时非常有用。
触发工作流的预/后处理
将事件消息从事件源(浏览器)转发到工作流实例队列需要知道实例标识符(guid
)和接口契约来创建工作流队列名称。ExternalDataEventArgs
具有 instanceId
属性用于此目的,因此事件源应以某种方式获取此“票证 ID”。否则,服务负责在触发事件到工作流之前插入票证 ID。WorkflowFireEventAttribute
具有通过执行操作契约中的逻辑来预处理或后处理事件的“即发即弃”功能。
以下代码片段展示了如何通过类型实现预处理的工作流触发。注意,浏览器和服务之间的操作契约可以包含更多特定于应用程序的参数来执行特定的预处理。
预处理触发事件
正如您所见,在上面的代码片段中,可以选择准备一个流程来触发工作流事件。在此示例中,已基于特定工作流类型创建了工作流实例。
我们还可以将一些初始参数传递给服务或从浏览器创建的工作流。来自服务的返回值将返回到浏览器,在将事件转发到工作流之后。此操作可被视为与工作流业务逻辑的对话的开始。
任何其他触发事件都可以是即发即弃的,直到最后一个操作契约关闭此对话并从工作流返回响应。
下图展示了事件驱动的 AJAX 页面到状态机工作流的示例。
事件驱动的 AJAX 与工作流之间的实现示例可以如下面的代码片段所示。
[ServiceContract]
public class ServiceTest12
{
// PRE-PROCESS
[WorkflowFireEvent(Option = WorkflowFireEventOption.After,
EventName = "Approve",
EventType = typeof(IWorkflowSimpleEvents))]
[OperationContract]
public string BeginApprove(object sender, ExternalDataEventArgs e)
{
// Get a reference to the WorkflowRuntime
WorkflowRuntime wr =
OperationContext.Current.Host.Extensions.Find<WorkflowRuntime>();
// Create workflow
WorkflowInstance wi = wr.CreateWorkflow(typeof(Workflow1));
// instance id
e.InstanceId = wi.InstanceId;
// done
return e.InstanceId.ToString();
}
// PROCESS
[WorkflowFireEvent(EventType = typeof(IWorkflowSimpleEvents))]
[OperationContract(IsOneWay = true)]
public void Approve(object sender, ExternalDataEventArgs e)
{
}
// POST-PROCESS
[WorkflowFireEvent(Option = WorkflowFireEventOption.Before,
EventName = "Approve",
EventType = typeof(IWorkflowSimpleEvents))]
[OperationContract]
public string EndApprove(object sender, ExternalDataEventArgs e)
{
string retVal = "error";
AutoResetEvent waitHandle = new AutoResetEvent(false);
// Get a reference to the WorkflowRuntime
WorkflowRuntime wr =
OperationContext.Current.Host.Extensions.Find<WorkflowRuntime>();
// events from workflow runtime
wr.WorkflowTerminated += delegate {waitHandle.Set();};
wr.WorkflowCompleted += delegate(object sender1,
WorkflowCompletedEventArgs e1)
{
if(e1.WorkflowInstance.InstanceId.Equals(e.InstanceId))
{
int counter = (int)e1.OutputParameters["Counter"];
retVal = string.Format("{0}, counter={1}",
e.InstanceId.ToString(), counter);
waitHandle.Set();
}
};
// wait for finish a job in the workflow
waitHandle.WaitOne();
// done
return retVal;
}
}
浏览器和工作流之间存在更多可能的对话场景,但让我们看看单独程序集中 WCF 服务的连接。
AJAX/WF 由接口契约驱动的连接
从连接的角度来看,工作流代表一个事件接收器,WCF 客户端是事件源(订阅者兴趣)。下图展示了这种逻辑模式。
AJAX、服务和工作流这三个层之间存在两个实际的连接。服务是一个常规的 WCF 服务,带有一个或多个端点。每个端点都可以根据地址、绑定、契约和行为进行配置,以实现特定目的。
第一个连接(1),AJAX 和服务之间的连接,在我们的示例中由 IFireEventTest
操作契约(方法)描述。我们可以使用此契约让服务接收事件消息。通过 webHttpBinding
装饰一个用于 AJAX 连接的端点,将允许接收来自 AJAX 启用浏览器的请求,并将其分派到服务操作(方法)。
第二个接口契约由工作流完成。请注意,这些契约是完全独立的,它们并不期望相互协作。它们是在各自的技术内独立设计的 - 请参阅它们的装饰,例如 ServiceContract
和 ExternalDataExchange
。事件驱动的工作流活动 (HEEA) 会根据接口契约元数据和工作流实例 ID 创建一个工作流队列。这是我们连接概念的关键点,允许跨接口契约传递消息体。
通过用 WorkflowFireEventAttribute
装饰特定的操作契约,常规操作调用程序将被 WorkflowFireEventOperationInvoker
对象替换,以透明地处理到工作流的传入事件消息。
集成 WCF 和 WF 的第一步是使其能够托管。下面的 .svc 文件代码片段展示了托管 ServiceTest2
的示例。
Factory
属性描述的类型将创建 ServiceHost
和 WorkflowRuntime
实例。可以通过主机扩展获取 WorkflowRuntime
引用。
WorkflowRuntime wr =
OperationContext.Current.Host.Extensions.Find<WorkflowRuntime>();
正如我之前提到的,有两个接口契约和一个服务。下面的代码片段展示了这些项的示例,包括自定义 ExternalDataEventArgs
。
// WCF service contract
[ServiceContract]
public interface IFireEventTest
{
[OperationContract(IsOneWay = true)]
[WorkflowFireEvent(EventType = typeof(IWorkflowSimpleEvents2))]
void Approve(object sender, MyEventArgs e);
// ...
}
// Workflow interface type
[ExternalDataExchange]
public interface IWorkflowSimpleEvents2
{
event EventHandler<MyEventArgs> Approve;
}
// WCF Service
public class ServiceTest2 : IFireEventTest
{
public void Approve(object sender, MyEventArgs e)
{
}
// ...
}
// Source event object
[Serializable]
public class MyEventArgs : ExternalDataEventArgs
{
// properties
}
如上面的代码片段所示,连接主要需要声明接口及其属性。繁重的工作由 WorkflowFireEventAttribute 完成。
这就是与标准 .NET Framework 3.0 工作流和 HandleExternalEventActivity
的 AJAX/JSON/WCF 连接的全部内容。
让我们测试一下连接概念。
测试
可以通过以下解决方案演示从 AJAX 启用的浏览器触发工作流。
在上述解决方案中,有四个 Web 测试页面和服务用于不同的场景。在实际测试之前,请确保所有服务都已安装在虚拟目录 WFService 中。成功编译后,可以在解决方案中执行以下检查。
- 每个服务都应该可以通过浏览器打开。
- Workflow1 应该可以通过设计器打开。
要查看服务器端(包括工作流)发生了什么,请下载 DebugView 实用工具。此实用工具将显示所有跟踪消息。另外,我注意到,在 ~/bin 文件夹中有来自我之前文章 从 WCF 触发工作流事件 的 WorkflowEventService
程序集。当然,必须安装 Orcas,包括 SQL 脚本。
好的,现在我们将从 AjaxClient12 页面触发事件。下图显示了事件源和工作流状态机。
出于测试目的,测试解决方案包含一个简单的状态机工作流,有三个状态。两个状态是事件驱动的,最后一个是超时状态,在 10 秒无活动时完成此过程。
上述测试页面将向 Worfklow1
生成一个 Approve(object sender, ExternalDataEventArgs e)
事件。页面允许修改事件消息的一些参数。当按下 BeginFireEvent
时,Approve
对象以 JSON 格式创建(参见文本框),并通过 XmlHttpRequest
发送到 service12.svc。基于 e.InstancedId
(最初为空),服务将在触发事件之前插入一个预处理调用。此即发即弃预处理负责创建工作流实例。在将事件触发到工作流之后,工作流实例 ID 将返回给客户端并显示在文本框中。上图显示了此状态。
事件触发的过程可以在 DebugView 窗体中看到,如下图所示。
现在我们应该按下 FireEvent
按钮生成另一个触发事件到工作流。注意,来自客户端的触发事件的生命周期是十 (10) 秒,否则状态机会完成工作流 - 请参阅 DebugView
Trace。每次单击 FireEvent
按钮都会发送一个事件消息。
要完成与工作流的此对话,请按下 EndFireEvent
按钮。在这种情况下,将调用即发即弃后处理调用,以阻塞方式等待服务端的响应。超时后,工作流将在页面上输出一条文本消息,显示发送到工作流的事件数量。
我们可以通过单击 Clear 按钮来开始一个新的触发对话到工作流。
基于此测试,请打开其他客户端页面进行类似的测试,例如页面 AjaxClient2.htm,其中事件消息包含一个自定义对象。
我们的测试已完成。
结论
本文介绍了使用 Microsoft 前沿技术向工作流引发事件的功能。在此解决方案中,我们可以使用标准的 .NET Framework 3.0 工作流。即将推出的新版本 Orcas (.NET framework 3.5) 将允许在工作流上下文中触发事件。AJAX/JSON/WCF/WF 代表了一种由元数据驱动的连接模型,其中业务层被正确封装并根据业务需求进行部署。
参考文献和有用链接
- http://www.json.org/
- http://www.json.org/js.html
- Orcas/B1 示例
- http://developer.yahoo.com/common/json.html
- http://www.developer.com/lang/jscript/article.php/10939_3596836_2
- http://ajax.schwarz-interactive.de/csharpsample/default.aspx
- http://ajaxmatters.com/archive/2007/02/17/ajax-tutorials.aspx
- http://www-128.ibm.com/developerworks/views/webservices/libraryview.jsp
- http://www-128.ibm.com/developerworks/webservices/library/ws-wsajax2/?ca=dgr-jw22SOAP-AJAX2
- http://jibbering.com/2002/4/httprequest.html
- http://www.microsoft.com/technet/sysinternals/utilities/debugview.mspx