学习完本章,你将掌握:
1.了解工作流关联(correlation)以及在什么地方必须去使用它、它为什么是重要的
2.使用工作流关联参数(correlation parameters)
3.生成并使用相关的本地通信服务
贯穿本书你看过的应用程序普遍都是基于单一相同架构的,通过WF的支持在工作流实例中执行任务。这些都是在应用程序和它的工作流实例间进行一对一的通信。假如你和一个工作流实例进行通信,你这样做保证了无论以任何方式在应用程序和工作流之间传送数据都不会被混淆。一个应用程序对应一个工作流。
但是至少有一种情况也是有可能的,这是应用程序和工作流在同一个应用程序域(AppDomain)中执行的时候。你单独的一个应用程序会调用同一个工作流的多个副本。来回传送数据会发生什么呢?
显然,有些人需要了解工作流和哪些数据协同工作的。通常我们不能混淆并要进行匹配比较。一旦创建了工作流实例并入队执行后,如果它绑定到一个指定的数据标识符,用工作流对不同的数据标识符进行信息的处理可能存在数据完整性的问题。
其实,WF提供了一些内部簿记来帮助我们防止数据完整性的问题。在WF术语中,它被叫做关联(correlation),WF提供了非常强大的关联支持但它也很容易地使用。
宿主和工作流的本地通信
在我们进入关联的论题之前,让我们简要地回顾一下宿主和工作流的整个通信过程。在第8章“调用外部方法和工作流”中,介绍了CallExternalEvent活动并使用了一个本地通信服务来把数据从工作流发送到宿主应用程序中。在第10章“事件活动”中,使用了HandleExternalEvent活动来进行相反过程的处理:宿主也能把数据发送到工作流中。
不论数据以哪种方式进行传送,我们首先都要创建一个接口。接口中的方法注定最终会成为CallExternalEvent活动,而接口中的事件则最终会成为HandleExternalEvent活动。我们使用wca.exe工具来为我们生成这些基于我们的接口的自定义活动。(我们也可以直接使用CallExternalEvent活动和HandleExternalEvent活动,它提供出要处理的每一个接口、方法或者事件,但是在我们的工作流中创建自定义活动是我们所强烈推荐的。)
随着手头有了接口,我们然后就创建了一个本地服务并把它插入进了工作流运行时中,让它去管理我们的本地通信需求。本地服务由一个数据连接器和一个服务类组成。
当应用程序需要发送数据到工作流中时,它需要得到这个来自工作流运行时的服务,然后激发由接口提供的事件。假如你把该事件处理器(event handler)拖进了工作流中并在适当的时候调用了该事件的话,你的工作流运行时会处理这些事件。
另一方面,对本地通信服务来说,工作流却没有查询工作流运行时的必要。拖拽一个CallExternalMethod活动到你的工作流的处理路径中以后,在数据抵达的时候会自动通知宿主——这里再次假设宿主应用程序把一个接收数据事件的事件处理程序(event handler)连接到了本地通信服务上。工作流运行时保持工作流实例和本地通信服务以及宿主应用程序之间的联系。
关联
再次回味上一段内容。工作流实例不需要到处搜寻和宿主应用程序进行通信的服务。但是宿主应用程序还是需要查询本地通信服务。尽管如此,在某种程度上,由于宿主和工作流运行时之间交互的性质,处理过程也要强调宿主应用程序和工作流实例之间的一对多的关系。
宿主应用程序需要识别出它想和哪一个工作流实例进行通信,因为可能有很多个选择。但是,一个工作流实例却没有这样的选择:因为它只可能属于一个宿主应用程序。
宿主为了数据通信总是要通过查询工作流运行时来获取服务,本地通信服务正是一个你或许想去访问的服务之一。相反的过程无疑也是同样的。工作流被绑定到本地通信服务上而不用关心宿主应用程序的身份,这是架构设计上的必然结果,因为一个工作流实例只能属于一个宿主应用程序(它们之间是一对多的关系),它不可能属于一个以上的应用程序,所以不需要识别出应用程序的身份。综上所述,工作流运行时由此为工作流实例提供了本地通信服务,工作流实例可随意地调用外部方法。
那么,对于宿主来说,使用工作流实例的标识符来传送和其相关的数据流的这种方式可行吗?也就是说,假如你掌握了一个工作流实例,然后试图和工作流往返发送和接收数据,难道仅仅有了一个工作流实例的ID号还不能足够唯一地识别出工作流实例和其关联的数据吗?
是的,假如你的数据流不是单一的数据流的话。因为在你的工作流中有多个数据路径进出。为此,关联诞生了。
当你使用相关的工作流通信的时候,工作流运行时最终会为识别出我们谈到的工作流和数据所必需的大量信息创建一个存储容器。当宿主和工作流来回传送数据的时候需要咨询相关令牌(correlation token),假如相关令牌指明了两边的会话是同步的,则意味着正确的工作流实例和绑定的活动正在和正确的一批数据进行通信,能继续进行通信。但是,假如相关令牌指出一个问题,则工作流运行时就不允许继续进行数据通信并抛出一个异常。问题可能包括正使用一个不正确的工作流实例、正和错误的数据通信、调用了绑定到不同的相关令牌上的活动、或者试图在没有首先创建相关令牌的情况下发送数据。
相关令牌由CorrelationToken类维护。当你拖拽CallExternalMethod或者HandleExternalEvent活动到你的工作流中的时候,假如关联被调用,你就需要指定一个相关令牌。相关令牌通过名称进行共享,因此从数据会话的观点来看,通过为超过一个以上的相关活动指定相同名称的相关令牌,你可有效地把这些活动绑定到一起。令牌的名称只不过是一个字符串,除了进行识别外它的值没有什么意义。
为什么本书不更早介绍相关令牌呢?这是一个好问题。毕竟,我们在前面的工作中无疑已经使用过CallExternalMethod和HandleExternalEvent活动。
答案是我们选择的是不去调用关联。关联在所有的情况下并不都是必须的,直到本章你才真正创建了这样的工作流。当你的应用程序和工作流实例之间是一对一的映射的时候,关联不是必须的,你可忽略它并享受由此带来的性能上的略微改进。
即使当你的单一的宿主应用程序有多个工作流实例的时候,没有关联你也能正常工作。但是,当你使用关联的时候,WF会防止你在不经意间混淆数据,在许多情况下,这是一个非常可取的特性。
为了激活关联的使用基础,当你创建你的宿主通信接口的时候你要使用一个特定的基于WF的特性。一个好消息是处理执行宿主通信不会由此改变很多,但是在你的工作流上产生的效果却是激动人心的。
CorrelationParameter特性
假如你考虑在单一宿主应用程序的环境下可能会出现多个工作流实例的话,你可能将会发现传送数据的事件和方法也会传送某种唯一的标识符。一个订单处理系统可能要传送一个客户ID,或者一个包装系统可能要传送一个批号。这个唯一标识符的类型是确定数据唯一实例的完美候选,事实上,的确如此。
当你在你的通信接口中设计方法和事件的时候,你也要为其设计一个数据相关ID的签名。数据相关ID在所有的空间和时间情况下并不保证必须是唯一的。但是,假如它不是一个Guid,它就必定要保证在工作流实例执行期间要被唯一地使用。
也许令人惊讶的是,假如你创建了两个相关的工作流实例,它们使用了同一个参数值(即创建了两个使用了同一客户ID的工作流)在同一时间运行的话,这并不是一个错误。关联仅仅和使用了单一的关联参数值的单一的工作流实例联系起来。使用一个关联参数值创建的工作流在调用方法和事件进行数据交换时使用了不同的关联值的地方则是错误,在这些地方WF可帮助你防止产生错误。
你要在你的接口定义中包括CorrelationParameter特性来通知WF哪些方法参数要承载这个数据关联ID值(把它们放在ExternalDataExchange特性的旁边)。当数据传递的时候WF能够检查参数的内容。例如,假如你的逻辑试图混淆客户或者(包装)批号的话,WF将抛出System.Workflow.Activity.EventDeliveryFailedException。
这个异常对你很有帮助,因为它指出了你的处理逻辑部分存在明确的不匹配的数据。例如,一个客户却为其它客户买单,显而易见,这种结果是不期望发生的。假如你接收到一个异常,你就需要去检查你应用程序中不正确的逻辑处理操作。
CorrelationParameter特性在它的构造器中接收一个字符串。这个字符串代表了你的接口所使用的包含了唯一ID的参数的名称。假如你要为某一指定的方法进行对该参数进行重命名,你就可通过使用CorrelationAlias参数来为这些事件和方法进行参数的重命名。你将在本章的稍后部分读到关于这个参数的更多知识。
CorrelationInitializer特性
当数据通信开始进行的时候,WF也需要初始化相关令牌。为了方便地完成这个工作,你可在方法或事件上加入CorrelationInitializer特性来进行数据通信,这可能有多个事件或方法需要添加这个特性。在执行该方法或事件前进行相关数据的来回传送的任何企图其结果都是产生一个异常,并会在这个异常中标记该关联初始化器。
CorrelationAlias特性
当你创建相关的服务后,CorrelationParameter特性通过名称来识别出被用来传送数据相关标识符的方法参数。对于你的接口方法来说,这意味着你必须有一个方法参数使用了和相关参数名相同的名称来命名。
假如你的代理(delegate或称委托)以相关参数定义在代理中的方式创建的话,这没有任何问题。但是这对于事件来说就不适用了。
当你使用一个包含了事件参数的代理并且这些事件参数要传送相关参数的时候问题出现了。例如,设想你的相关参数被命名为customerID,然后考虑一下下面这个代理:
delegatevoidMyEventHandler(objectsender,MyEventArgse);
假如使用了这个委托的事件被放进你的通信接口中后,customerID参数没有在事件处理程序中出现,当你执行你的工作流的时候,WF会抛出一个异常来指出关联被错误地使用。但是,假如MyEventArgs有一个包含了客户ID的属性的话,你就能使用CorrelationAlias特性来指出它。对本例子而言,假如MyEventArgs的客户ID属性被命名为CustomerID的话,相关参数的别名(alias)就将是e.CustomerID。
一个重要的事情是要牢记住一旦你为某一单一的工作流实例初始化了一个相关数据路径的话,你就不能在该工作流实例的生命周期中改变该数据的关联ID而不会出现错误。例如,一旦你和一个用客户的ID号作为联系的工作流实例通信的话,以后你就不能使用其他客户的ID号来和同一个工作流实例进行数据通信。意思就是,就像把信息插入进一个数据表中新行的时候,假如你的处理过程涉及到创建客户的ID号,你就需要预先生成该客户的ID号。你不能让数据库为你生成它们,或者开始默认为“空”(“empty”)然后在后来使用一个新生成的ID号,因为这样你的通信过程就会在没有客户ID的情况下被初始化。提到的这些ID号如果有所不同的话,你的工作流就会抛出一个异常。
创建相关工作流
在本章中我介绍了关联的概念并只提到了三个特性。这就是所有的东西吗?
对的。但是我们的本地服务变得更加复杂,因为我们必须考虑不同的数据流。记住,本地通信服务在工作流运行时中是一个单例(singleton)服务,因此各个不同的工作流实例所请求的全部数据都必须通过这个本地通信服务。该服务不可避免地必须知悉各个工作流实例和关联参数的细节以便当宿主从一个指定的工作流请求数据的时候,该服务能返回正确的数据。
备注:你怎样架构你的本地通信服务取决于你。在本章的稍后部分我将为了展示我是怎么创建它们的,但最终没有任何规则要让你也必须像我所做的一样去创建你的服务。唯一的要求是你要能从你的服务中返回正确的相关数据。
为了让你能理解下面更大的图片,我将首先介绍你将使用的应用程序并解释它为什么使用关联。
相关工作流的典型例子是一个订单处理系统,它使用唯一的客户ID号去了解客户的订单信息。本章的示例应用程序模拟了一个货运公司可能用来跟踪其车辆的应用程序。
今天,许多长途卡车都装备了全球定位系统(GPS),它能把卡车的位置报告给运输公司。无论卡车发生了什么,你都能跟踪它并监控其对于目的地的进展状况。
这个范例模拟了这种类型的跟踪应用程序,它的用户界面如图17-1所示。图中展示了四个卡车,它们到达各个不同的终点(通过活动卡车列表显示出来)。所有卡车自身都是动态的,它们从起点移动到终点。当它们到达各自的终点时,它们会被从活动卡车的列表中移除。
图17-1TruckTracker应用程序用户界面
你会看到每一辆卡车都由它自己的工作流实例支持。工作流定时、异步地对卡车的地理位置进行更新。当更新进行的时候,工作流和应用程序会为计算新的坐标进行通信,然后可视化地在用户界面中对卡车的位置进行更新。当然,我们正模拟的是GPS的接受效果,所模拟卡车的移动速度远远大于实际的卡车能够达到的速度。(运行本范例4天时间后才去看卡车是否真正从加利福尼亚抵达新泽西是非常愚蠢的行为。)应用程序真正关键的地方是当和宿主应用程序进行数据通信时使用了相关的工作流实例。
卡车按照指定的路线向它们各自的终点前进,它会在地图上穿越其它城市。当你点击“Add Truck”后,通过如下图17-2所显示的对话框,你可以选择卡车的路线。卡车的路线都保存在一个XML文件中,当应用程序加载时把它们读出来。例如,此行从萨克拉门托到特伦顿,卡车将穿过凤凰城,圣菲,奥斯汀,以及塔拉哈西。
图17-2 “Add Truck”对话框
主应用程序现在已经为你完成了,剩下的任务是要完成对应的服务和工作流。我们首先将创建服务接口。
为你的应用程序添加相关通信接口
1.本范例同样为你提供了两个版本:完整版本和非完整版本。你需要下载本章源代码,打开“TruckTracker”目录中的解决方案。
2.本解决方案包含两个项目:TruckTracker(主应用程序)和TruckService。在Visual Studio的解决方案资源管理器中找到TruckService项目,打开ITruckService.cs文件准备进行编辑。
3.在接口的大括号中,添加下面这些代码:
// Workflow to host communication
[CorrelationInitializer]
void ReadyTruck(Int32 truckID, Int32 startingX, Int32 startingY);
void UpdateTruck(Int32 truckID, Int32 X, Int32 Y);
void RemoveTruck(Int32 truckID);
// Host to workflow communication
[CorrelationAlias("truckID", "e.TruckID")]
event EventHandler<CancelTruckEventArgs> CancelTruck;
[CorrelationInitializer]
[CorrelationAlias("truckID", "e.TruckID")]
event EventHandler<AddTruckEventArgs> AddTruck;
4.正好在ExternalDataExchange特性的前面(你会发现它用来对接口进行修饰),插入CorrelationParameter特性。
[CorrelationParameter("truckID")]
5.保存本文件。
回头看看你在第3步所添加的代码,它也被复制到了列表17-1中,你能看到在本章中所讨论过的每一个特性。名称为truckID的方法参数传递一个唯一的卡车标识符,它存在于该接口的所有方法中。然后,CorrelationParameter特性通知WF这个方法参数的作用是为了关联。
列表17-1 ITruckService.cs的完整代码
ITruckService接口的完整代码
AddTruck和CancelTruck两个事件使用了一个CorrelationAlias特性来把关联参数的名称由truckID重新指定为e.TruckID,因为事件参数(arguments)为这些事件携带了关联标识符。对于本范例来说使用的是e.TruckID,但是任何事件参数(argument)都能被用来携带关联参数。也就是说,你能把truckID的别名指定为任何也携带了关联值到工作流中去的参数。
在这个接口中有两种来对关联机制进行初始化的方式:工作流可以调用ReadyTruck,或者宿主应用程序调用AddTruck事件。任何一个都会开始进行相关通信(correlated communications),因为两者都使用了CorrelationInitializer特性进行修饰。在此之前调用任何其它的方法或者事件进行初始化其结果是工作流运行时产生异常。
服务项目通常都带有本地通信服务,这个范例的应用程序也并没有什么不同。因为连接器(connector)类TruckServiceDataConnector由ITruckService派生,因此现在是该完成它的时候了。
完成关联的数据连接器(correlated data connector)
1.再次回到TruckService项目,找到TruckServiceDataConnector.cs文件并打开它准备进行编辑。
2.TruckServiceDataConnector类现在是空的,但是它明显派生自ITruckService。因此,你至少要把接口中的方法和事件添加到这个类中。但是在你完成这些之前,我们先添加一些需要的代码。首先,正好在这个类的左大括号后面添加下面的字段。
3.因为数据连接器需要与数据项保存联系并且在工作流运行时中是一个单例模式(singleton),因此我们将添加一对静态方法,它们用来对数据服务进行注册并对已经注册的数据服务进行检索。
WorkflowTruckTrackingDataService方法和RegisterDataService方法
4.当一个新的工作流实例被启动的时候,在主应用程序中都要对数据服务进行注册(每个工作流实例对应一个数据服务),一旦数据服务被注册后,它就在数据连接器中保存关联数据。我们需要有一个去检索该数据的方式。以前,我们使用的是一个属性,但是对于我们现在来说这已经不适用了,因为我们必须传入工作流实例ID和关联值(在本例中是一个卡车的标识符)二者。为了检索该数据,在静态的注册方法(RegisterDataService方法)的下面添加如下这个方法:
RetrieveTruckInfo方法
5.有了这最后的一个方法后,我们现在就去添加来自于ITruckService接口的方法。它们的位置紧跟在前一步骤所添加的检索数据的方法下面。
继承自ITruckService接口的方法
6.在ITruckService中的方法的后面是一些事件,因此也要添加它们,它们的位置在你第5步所添加的方法的下面。
继承自ITruckService接口的事件
7.回头看看在第5步中插入的方法,你会看到一个用来把关联数据插入到对应的数据字典中去的方法(UpdateTruckData)。数据本身必须被转换为XML,因此以其把下面这些代码都扩散到上面的三个方法中,那还不如添加一个UpdateTruckData方法,然后把它们都放在UpdateTruckData方法中。现在就添加该方法,把它放在你前面添加的事件的下面:
UpdateTruckData方法
8.保存该文件。
列表17-2中列出了TruckServiceDataConnector类的完整代码。在强调一次,这个类的作用是保存来自各个工作流实例的关联数据。该数据被存储到一个Dictionary对象中,键值对把工作流实例标识符和卡车标识符关联起来。该数据连接器类在工作流运行时中是一个单例服务。
备注:你可能会问:为什么数据要转换为XML而不直接使用数据对象本身?这是因为,当在工作流和宿主之间来回传送数据的时候,WF并不对对象进行序列化。因此,并不会创建对象的副本(毫无疑问是为了提高性能)。对象的传递是通过引用实现的,由此工作流和宿主都在同一个对象上工作。假如你不想这样(注:指对象引用),又不想使用本范例中的方法(注:指转换为XML)的话,你就可以对对象进行序列化或者实现ICloneable来传递对象的副本。假如这种行为(注:指对象引用)并不影响你的设计,你就不需要再去做任何事情而直接(在工作流和宿主之间)来回通过引用的方式去传递你的对象。虽然如此,也要牢记你的对象将会被两个不同的线程所共享。
列表17-2 TruckServiceDataConnector.cs的完整代码
TruckServiceDataConnector类的完整代码
你可以回忆一下第8章,本地通信服务也有一个服务组件,它被工作流和接口间的数据连接器使用。事实上,这个WorkflowTruckTrackingDataService服务就是用我们刚刚创建的连接器来注册的。该服务主要实现了一些基础的有用的方法去触发宿主边的一些事件,例如数据可用的事件和关联正使用的事件等,它有助于直接保持值的相关性。
完成相关数据服务(correlated data service)
1.在TruckService项目中,你会找到一个名称为WorkflowTruckTrackingDataService.cs的源文件。你需要打开它准备进行编辑。
2.第一件事是添加该服务执行它的任务时所需要的私有字段。在WorkflowTruckTrackingDataService类的左大括号的后面添加下面这些代码:
3.在这些私有字段的下面是该服务将触发的事件。这些事件被触发的结果就是工作流实例将调用一个CallExternalMethod活动(你可在TruckServiceDataConnector类中看到它):
4.接下来添加两个字段以及对应的属性,你需要它们去对相关的服务实例进行识别:
5.现在我们需要添加两个静态方法:一个是在工作流运行时中注册该服务并对它进行配置,另一个是去对已经注册的服务实例进行检索:
CreateDataService静态方法和GetRegisteredWorkflowDataService静态方法
6.现在添加构造器和析构器:
类的构造器和析构器代码
备注:你可以回忆一下第8章,构造器的作用是防止在服务和连接器类之间出现循环链接引用。而对于析构器的作用来说,只是实现IDisposable并不能完成相应的任务,因为当从工作流运行时中移除该服务的时候并不调用Dispose方法。