上一篇:【翻译】WF从入门到精通(第七章):基本活动的操作例
学习完本章,你将掌握: 1.创建并调用你的工作流外部的本地数据服务 2.理解怎样使用接口来为宿主进程和你的工作流之间进行通信。 3.使用设计的外部方法在你的工作流和宿主应用程序之间传输数据。 4.在一个正执行的工作流中调用其它工作流
在写前面的章节时,我自己不断地思考,“我不能再等了,我要弄清楚在哪里可把(工作流中的)真实数据返回到宿主应用程序中!”为什么?做了这么多的活动和工作流的演示,但都没有实际返回某些感兴趣的东西给宿主应用程序。我不知写过多少我们感兴趣的工作流的实例和演示,但至多只是仅仅处理过数据的初始化(就像第一章-WF简介中你看过的邮政编码的例子)。但事情变得更加有趣,坦率地说,当我们启动工作流,然后从外部源中寻找并处理数据、返回处理后的数据给我们的主应用程序要更加接近现实。 为什么不这样呢?公开一个对象,来从执行的工作流中传给宿主应用程序,或者从宿主应用程序传给工作流不就行了吗?其实,使用现有的串行化技术,如.NET Remoting或者XML Web服务,就可完成这些事。串行化,也叫序列化,它可把数据从原有的形式转换成合适的形式,以在不同进程甚至不同计算机之间进行传输。 为什么谈到序列化呢?因为你的工作流是在你的宿主进程中的不同线程上执行,不同线程之间传送数据,如不进行适当的序列化,将会引发灾难,具体原因超出了本书的讨论范围。其实,你的工作流能在一个持久化的状态下发送它的数据。这并没有在不同线程上,甚至它不在执行中。 但我们想在我们的工作流和正控制该工作流的宿主进程间传送数据时,使用.NET Remoting或者XML Web服务这样的技术为什么并没有认为是多余的呢?其实这绝对有必要!我们将创建local通信,本章将以此出发。我们将搭建必须的体系来满足线程数据序列化,以进行计算机之间或进程之间的数据传输。
创建ExternalDataService服务
当工作流和它的宿主进行通信时,在它发送和接收数据的时候,工作流要使用队列和消息。WF为我们做的越多,我们就可把重点更多的放到应用中特定任务的解决上。
工作流内部进程通信 对于简单的通信任务,WF使用“abstraction layer”来在工作流和宿主之间进行缓冲。抽象层像一个黑盒,你为它提供输入,它会执行一些神奇的任务,然后信息流出到另一边。但我们不用知道它是如何工作的。 在这种情形下,该黑盒就是一个知名的“local communication”服务。和WF术语中的任何一种服务一样,它也是另一种可插拔服务。区别是它不像WF中的那些已预先创建好的服务,你需要写出这个服务的一部分。为什么呢?因为你在宿主应用程序和你的工作流之间传递的数据有一定的特殊性。更进一步说,你可创建各种各样的数据传输方法,你可使用你设计的各种方法从宿主应用程序发送数据,然后在工作流中接收数据。 备注:这里有些事情你需要进行关注,那就是对象或集合的共享问题。因为宿主应用程序和工作流运行时在同一个应用程序域执行,因此引用类型的对象和集合就是通过引用而不是值进行传递。这意味着宿主应用程序和工作流实例在同一时间会访问和使用同一个对象,多线程环境下这会产生bug,出现数据并发访问错误。因此,对于可能要进行并发访问的对象或集合,你可考虑传递一个对象或集合的副本,或许这可通过实现ICloneable接口,或者考虑亲自序列化该对象或集合并传递序列化后的版本。 你可写这种local service,把它插进工作流,然后打开连接,发送数据。这些数据可以是字符串,DataSet对象,甚至可以是你设计的任何可被序列化的自定义对象。通信可以是双向的,尽管在本章我没有演示它。(这里,我仅仅是把数据从工作流中传回给宿主应用程序。)从工作流的角度来说,我们使用工具生成活动的目的是发送和接收数据。从宿主应用程序的角度来说,接收数据等同于一个事件,而发送数据就是在一个服务对象上的方法的简单调用。 备注:我们在后面几章看到更多的活动后还会重温该双向数据传输的概念。工作流活动从宿主应用程序中接收数据基于一个HandleExternalEvent活动,我们将在第10章“Event活动”中看到。我们也需要更深入地了解这些概念间的相互关系,这在第17章“宿主通信”中将进行介绍。对于当前,我们只是在工作流实例完成它的任务后,简单地返回复合数据给宿主。 我们需要做的还不仅仅是这一点,我们最终需要添加ExternalDataService服务到我们的工作流运行时中。ExternalDataService是一个可插拔的服务,它方便了工作流实例和宿主应用程序之间进行序列化数据的传输。在紧接下来的一节我们将写出的该服务的代码将做很多事(包括序列化数据的传输)。让我们来看看大体的开发过程。
设计并实现工作流内部进程通信 我们先决定将传送些什么数据。它是一个DataSet吗?是一个像整形数字或字符串之类的系统直接支持的对象吗?或者是一个由我们自己设计的自定义对象吗?无论它是什么,我们都将设计一个ExternalDataService能够绑定的接口。这个接口将包含我们设计的一些方法,这些方法能分别从工作流实例的角度上及宿主的角度上来发送数据和接收数据。使用该接口中的方法,我们就可来回传送数据。 我们然后需要写一些代码:外部数据服务的一部分。它表述了连接或者称作桥接代码,宿主和工作流将使用它来和WF提供的ExternalDataService进行交互。假如我们正涉及一个XML Web服务,Visual Studio会为我们自动地创建代理代码。但对于工作流来说没有这样的工具,因此我们需要亲自设计这个桥接代码。我们这里使用的“桥”实际上由两个类组成:一个connector类和一个service类。你可用你喜欢的名称来命名它们,但我推荐使用这样的名字来命名它们。connector类管理数据管道(状态维护),而service类被宿主和工作流用来直接进行数据交换。 在创建好接口后,我们将使用一个工具:wca.exe,它的位置通常是在你的“Program Files\Microsoft SDKs\Windows\v6.0A\Bin”目录下。该工具叫做Workflow communications Activity generator utility,该工具的作用是,给出一个接口,它将生成两个活动,你能使用它们去把该接口和你的工作流实例进行绑定。一个用来发送数据,为invoker,另一个用来接收数据,为sink。一旦它们创建好后,你就能从Visual Studio工具箱中把它们拖拽到工作流视图设计器上,它们也和任何其它工作流活动一样进行工作。但前面我已经提到过,我们没有一个工具创建连接桥代码,这样的工具在工作流方面一定很有用。 提示:从项目的角度考虑,我倾向于为宿主应用程序创建一个或一组项目,为前面提到的接口和连接桥创建另一个项目,为工作流代码再单独创建一个项目。这可让我方便地从宿主应用程序和工作流中添加对该接口和桥接类的引用,做到了在程序集之间进行简洁的功能隔离。 我们有了这些程序集后,我们需要连通我们的工作流和宿主应用程序之间的通信。在执行时,通过使用ExternalDataService整个过程被简化了。我们先快速看看本章中的最基本的应用程序实例(就它而言,它比我们目前看到过的例子都有复杂),然后开使创建我们需要的工作流外部数据通信代码。
机动车数据检查应用程序
本示例应用程序是一个Windows Forms应用程序,它提供了一个用户界面,上面集中了指定驾驶员的机动车数据。该应用程序本身已是很有意义的,我不想再重复创建它的每一个细节。相反,你将使用这个已经提供好了的样本代码来作为本章的起点。但是,我将展示怎样把它们绑进工作流组件中。 主用户界面窗体见图8-1。下拉列表框控件包含了三个驾驶员的姓名,选择其中一个的姓名都会生成一个新的设计好的工作流的实例来对该驾驶员的机动车信息进行检索,并返回一个完整的DataSet。该DataSet然后被绑定到两个ListView控件,一个是违规信息。图8-1 MVDataChecker窗体的主用户界面 当你点击“Recrieve MV Data”按钮时,你就会初始化一个新的工作流实例,用户界面会禁用该检索按钮及驾驶员下拉列表框控件并显示一个“searching”通知,如图8-2所示。你在该窗体底部看到的picture box控件是一个动画图片文件。该应用程序根据情况对其中的label控件和picture box控件进行隐藏或显示。图8-2 MVDataChecker窗体的“searching”用户界面 当工作流实例来完成了它的工作后,它会使用我们将要创建的一个活动来激发一个事件,宿主应用程序会截获该事件,该事件把数据已准备好的消息通告该宿主应用程序。因为Windows窗体的ListView控件不能直接绑定到DataTable对象,因此我们从工作流中检索到数据后将一行一行地把数据插入到该控件中,如图8-3所示。图8-3 MVDataChecker窗体检索数据后的用户界面 在应用程序执行到此时,你可选择是检索另一个驾驶员的信息还是退出程序。假如你在查询过程中退出该应用程序,正执行的工作流实例会被异常终止。 然后我们来看看需要添写完成所有这些任务的代码,首先我们需要为WF提供一个接口,以便它能激发我提过的“数据已准备好”的事件。
创建服务接口
该服务接口完全要由你创建,它应基于你想在你的工作流实例和你的宿主应用程序之间进行通信的数据之上。对于本示例,想像你需要设计一个工作流来从各个源数据中检索驾驶员的信息,然后你想把这些信息整理为一个单一的数据结构:带多个表的DataSet,一个表是车辆标识信息,一个表是驾驶员违规信息。我们将简单地使用虚拟的数据,以便更侧重于把焦点放到工作流自身上。在宿主应用程序中,我们将在两个ListView控件中显示这些(伪造的)数据。 你要把驾驶员的名字传入工作流实例中,该工作流实例使用它来查找驾驶员和车辆的信息。在获取了这些数据后,工作流实例通知宿主应用程序数据已经准备好了,然后宿主应用程序读取并显示这些信息。 因此实际上在我们的接口只需要一个单一的方法:MVDataUpdate。我们知道需要发送一个DataSet,因此我们把这个DataSet作为方法的参数传入到MVDataUpdate中。
创建一个工作流数据通信接口 1.该MVDataChecker示例应用程序,同前面的例子一样,包含两个版本:练习版本(MVDataChecker目录中)和完整版本(MVDataChecker Completed目录中),它们可在本章的源代码中进行下载。我们现在就使用Visual Studio打开练习项目中的解决方案。 2.在该解决方案中包含三个项目。在Visual Studio解决方案浏览器中展开MVDataServic项目,然后打开IMVDataService.cs文件。 3.在MVDataService名称空间中添加下面的代码并进行保存。
这样就大功告成了!这就是所有你需要为创建一个接口所要做的工作。不过,我们需要添加一个属性,以使这个接口适合于WF的使用,我们将在下面的一节介绍。
使用ExternalDataExchange特性
尽管有了接口:IMVDataService,但我们仍不能把该接口提供给WF,以让WF真正使用它来进行数据通信。为此,我们需要添加ExternalDataExchange特性。 ExternalDataExchange特性是一个简单的标记,WF使用它来指明接口可适合于本地通信服务使用。记得我提到的wca.exe工具吗?它和Visual Studio都使用这个特性来指明接口可被你的工作流实例使用。我们就来添加ExternalDataExchange特性。 备注:不要让词语“特性标记”所欺骗,你不要认为该ExternalDataExchage特性不是一个关键组成部分。它相当重要。当工作流运行时试图进行外部数据传送时会寻找该特性。没有它,工作流和宿主之间进行数据传输就不可能。
创建一个工作流数据通信接口 在Visual Studio中打开IMVDataService.cs文件,为前面定义的接口添加下面的代码: [ExternalDataExchange] IMVDataService接口的完整代码在下面的清单8-1中。此时不要担心该应用程序编译出错。在编译无错之前,我们还需要添加更多的代码。
使用ExternalDataEventArgs
我在前面提到过,宿主应用程序和正执行的工作流之间使用事件进行通信。宿主应用程序无法事先准确地知道工作流实例准备好数据的时间,对该数据进行轮询效率又低得可怕。因此WF使用异步模式,当数据准备好了的时候激发一些事件。宿主应用程序捕获这些事件然后读出数据。 因为我们想把信息发送给事件的接收者,因此我们需要创建一个自定义事件参数的类。假如你在前面的工作中已创建过一个自定义事件类,你或许就是使用System.EventArgs作为基类。 但是,WF外部数据事件需要带一个(和上述)不同的参数作为基类,以便该事件能承载工作流实例的实例ID。我们应使用的基类是ExternalDataEventArgs,它从System.EventArgs类派生,这样我们就熟悉了它的背景。另外,还有两点要求:我们需要提供一个以该实例ID(一个Guid)作为参数的基本的构造器,该构造器又把实例ID传给基类构造器,第二点是我们必须使用Serializable特性来标记我们的类,以表明我们的类是可序列化的。我们现在就来创建我们所需要的外部数据事件参数类。
创建工作流数据事件参数类 1.使用Visual Studio打开MVDataService项目,定位在MVDataAvailableArgs.cs文件上,打开该文件准备进行编辑。 2.在该文件所定义的名称空间中,添加下面的代码:
3.最后我们需要添加一个构造器,以便把工作流实例ID传给基类:
完整的事件参数类如清单8-2所示。
创建外部数据服务
我们现在来到了更加复杂的一节,我们的任务是为外部数据服务创建桥接代码。宿主必须有这些代码,它才能访问到工作流实例试图传递过来的数据。我们将使用工具来为工作流创建活动(这在下一节介绍),但对于宿主这边的通信连接来说,却没有现成的工具。 在这里,我们将创建一个稍微简化的连接桥版本(这是对于完整的连接桥架构来说)。该版本仅仅支持工作流到宿主的通信。(当我们学到17章时,我们将会创建一个可重用的通用双向连接桥。)我们在此将创建的连接桥被分成了两个部分:一是connector,它实现了我们前面已经开发好了的接口;二是service,除了别的事情外,它有一个职责是激发“data available”事件以及提供一个“read”方法,使用该方法来把数据从工作流中取出。 提示:该代码应由你而不是WF来提供。我在写本地数据交换服务时提供了该代码,但你要写的代码可以有所不同。唯一要求是本地数据交换服务实现了通信接口并提供一种机制,用于检索需要交换的数据。 为什么如此复杂?和传统的.NET对象不同,工作流实例在工作流运行时的范围内执行。因此进出工作流实例的事件都由工作流运行时进行代理。工作流运行时必须做这些工作,因为你的宿主应用程序不能把数据发送给已经被持久化或不处在执行状态下的工作流实例。 回到我们的连接桥上,该连接类包含一个字段,工作流将使用要被传回的数据来填充该字段。对于我们正在创建的本示例应用程序来说,我们不允许并发执行工作流实例,但这仅仅是出于方便。通常情况下,并没有阻止我们执行并发执行的工作流实例,这些我们将在第17章看到。 当然,每一个工作流实例可能会返回不同的数据,至少它传递的驾驶员会和另一个工作流实例不同。连接类的职责是实现我们开发的在宿主这边接口,以及不间断地保持这些数据。当宿主请求该数据时,连接类根据工作流实例ID来确定应正确返回的DataSet是否已经到达。 该服务类为你处理一些任务。首先,它使用工作流运行时注册该ExternalDataService,以便我们可在宿主和工作流实例间进行通信。它维护一个连接类的单例副本,并把它自己作为服务提供者绑定到该连接类。该服务类也充当了工厂(设计模式)的角色,确保我们有一个且仅有一个连接类(实例)。(假如我们实现了双向的接口,该服务类也会提供一个“write”方法。)我们现在就来创建这些类。
创建桥接器(bridge connector)类 1.在Visual Studio中打开MVDataService项目,定位到MVDataCnnector.cs文件,最后打开该文件。 2.在所定义的名称空间中添加下面的代码:
字段_dataValue用来容纳工作流实例产生的数据。字段_service用来容纳数据服务对象的单一实例。_syncLock对象仅仅用来进行线程的同步。 3.下面,我们添加一个static属性来访问该服务对象的单一实例。代码如下:
4.我们需要添加一个属性来访问该DataSet,代码如下:
5.因为连接器类从IMVDataService派生,因此我们必须实现MVDataUpdate方法:
工作流使用这个方法来把DataSet保存到_dataValue字段中。它激发事件以通知宿主数据已经准备好了。该桥接器类的完整代码参见清单8-3。注意我们这时并没准备去编译整个应用程序,我们还有更多的代码需要添加。
创建桥接服务(bridge service)类 1.再次在Visual Studio中打开MVDataService项目,定位到WorkflowMVDataService.cs文件,打开该文件准备进行编辑。 2.我们创建好了MVDataConnector类,我们还要把下面的代码复制到WorkflowMVDataService.cs文件中:
3.我们需要具有从类的外部访问_instanceID的能力,因此添加下面的属性:
4.我们现在要添加一个静态的工厂方法,我们将用它去创建本类的实例。我们这样做是为了确保在我们创建本桥接服务的时候,所有重要的事情都已完成。例如,我们需要确保ExternalDataService服务已被插入到了工作流运行时中。我们也将添加刚才已经创建好了的桥接器类,并把它作为一个可插拔服务以便工作流能访问到该数据连接器类。因此,我们在上面一步所添加的属性下面还要添加下面的方法: