相反的调用过程是宿主应用程序触发的事件被工作流捕获进行处理(尽管工作流事件处理可被用在更广泛的任务中,而不仅仅是和宿主进行通信)。在第八章中,我提到过在对工作流用来处理事件的活动进行叙述后,我们还将重温宿主/工作流之间的通信,在本章中我们将完成这件事。
http://file.ddvip.com/2008_09/1222511868_ddvip_2809.rar
学习完本章,你将掌握:
1.使用HandleExtenalEvent活动创建特定的事件处理程序
2.在你的工作流中使用Delay活动
3.在你的工作流中使用EventDriven活动
4.在你的工作流中使用Listen活动
5.理解EventHandlingScope活动在活动并发执行的情况下是怎样监听事件的
在第八章(“调用外部方法和工作流”)中,你看过工作流怎样使用CallExternalMethod活动来和宿主应用程序进行通信。当工作流调用一个外部方法时,使用一个你提供的本地通信服务,该宿主应用程序会收到一个事件,然后宿主对数据进行处理并产生一些相应的动作。
相反的调用过程是宿主应用程序触发的事件被工作流捕获进行处理(尽管工作流事件处理可被用在更广泛的任务中,而不仅仅是和宿主进行通信)。在第八章中,我提到过在对工作流用来处理事件的活动进行叙述后,我们还将重温宿主/工作流之间的通信,在本章中我们将完成这件事。
在目前为止的其它章节中,我都是单独地对某个工作流活动进行描述,然后提供一个小应用程序来演示该活动的操作过程。和这些章节不同,本章将在一个示例应用程序中对多个活动进行描述和演示。为什么这样做呢?因为我在这里要描述的这些活动都是相互关联互相依赖的。我不能演示其中一个活动而对其它的活动不进行演示。Listen活动可作为EventDriven活动的容器。在EventDriven活动的内部,你还会不出所料找到唯一的一个HandleExternalEvent活动等等。因此在本章中我将从始至终只创建一个应用程序来对这些活动进行描述和演示。“宿主到工作流”这一节是本章的主线。我们首先从HandleExternalEvent活动开始。
使用HandleExternalEvent活动
不管在你的工作流中在何处处理事件,也不管你的工作流正处于执行状态时所发现要执行的是什么样的活动组合,只要当一个事件来到了你的工作流路径当中,HandleExternalEvent活动就是最终去处理该事件的工作流活动。对我来说,.NET的强大的功能特性很多,它的触发和处理事件的能力就是这些最强大的功能中的一个。包括工作流事件的处理也同样强大。
HandleExternalEvent活动的作用是响应一个基于IEventActivity接口的事件,它有三个主要的成员:QueueName属性、Subscribe和Unsubscribe方法。QueueName表示正等待该事件的工作流队列,而Subscribe和Unsubscribe方法用来把你的事件处理程序将要接收(或者不将进行接收)的特定事件实例告知工作流运行时。
HandleExternalEvent活动本身也可和CallExternalMethod活动一起使用(我们在第8章中看到过)。工作流使用CallExternalMethod活动来把数据发送给宿主应用程序,但是在工作流执行时,工作流使用HandleExternalEvent来接收从宿主中发送过来的数据。
备注:牢记:使用外部数据交换的时机并不仅仅是在把数据从你的宿主应用程序发送到工作流的时候。当你创建你的工作流实例的时候,你可总是为其提供初始数据。但是,一旦工作流正在执行时,对于直接和你的宿主应用程序进行本地通信来说,它是唯一可使用的机制(当然也可使用更加间接的方式替代,例如使用FTP协议或者Web服务调用这些手段)。
表10-1和表10-2列出了使用HandleExternalEvent活动时经常用到的一些主要的属性和方法。注意有些方法和属性是所有活动共有的(如在第四章“活动和工作流类型介绍”中表4-1和表4-2展示的一样)。我在此展示的属性和方法无疑不是所有可使用的属性和方法,但他们却是经常要被用到的。
表10-1经常用到的HandleExternalEvent活动的属性
属性 | 功能 |
CorrelationToken | 获取或设置一个到关联标记(correlation token)的绑定。我们将在第17章(“关联和本地宿主通信”)中处理关联。 |
EventName | 活动将要处理的事件。注意如果没有对其进行设置,该活动将不会对事件进行监听并且和宿主通信也就不可能进行。奇怪的是,忽略该属性值你不会收到任何错误验证信息。 |
InterfaceType | 获取或设置进行通信所要使用的接口类型。该接口必须使用ExternalDataExchange特性进行装饰(标记)。(你或许可回忆一下第8章,你为CallExternalMethod方法提供了一个相同的接口。) |
表10-2经常用到的HandleExternalEvent活动的方法
方法 | 功能 |
OnInvoked | 这是一个有很用的保护型(protected)方法,它用来把本事件参数中的值和你工作流中的字段或依赖属性进行绑定。重写该方法(或者处理它所触发的事件)是检索来自于宿主并被保存到事件参数中的数据一个主要的机制,通常,你会创建一个自定义的事件参数来把数据嵌入进参数对象自身中。 |
使用HandleExternalEvent很容易,只需简单地在你的工作流中放入该活动,指定接口和事件名。假如你需要的话,还可为Invoked事件提供一个event handler,然后就可执行你的工作流了。假如你使用wca.exe,就可为你提供一个派生自HandleExternalEvent的活动,你可直接把它拖拽到你的工作流中,在属性窗口中添加绑定,把事件参数中的数据和一个局部字段或者依赖属性绑定在一起。
在你的工作流中有了HandleExternalEvent活动后,在等待事件发生时所有通过该顺序流的处理过程都会停止。在一定程度上,在你的工作流中放入这个活动的行为就像.NET Framework编程术语中的AutoResetEvent。和AutoResetEvent不同的是,该处理过程的线程不是暂停。它就像是一扇门或通道,只有在该事件被触发时才允许工作流处理过程沿着它的路径次序继续前进。
使用Delay活动
在本书中我们目前为止已经几次看到并使用过Delay活动,但现在我将对它进行更加正式的叙述。为什么呢?很巧,Delay活动实现了IEventActivity接口,因此,它同样也被归类为Windows Workflow Foundation(WF)的基于事件的活动。
传给Delay的是一个TimeSpan对象,它将延时指定的时间间隔。在延时时间过期后,它将触发一个事件。你可通过在Visual Studio的工作流视图设计器上,或者以编程的方式设置一个属性(TimeoutDuration)来初始化该延时的时间间隔。它也为你提供了一个event handler(InitializeTimeoutDuration),当Delay活动被初始化并获取所需的时间间隔信息时将调用该事件处理程序。
提示:延时事件和定时器(timer)事件是密切相关的。WF没有timer活动,但你能通过用While活动组合该Delay活动来创建一个timer,本章的示例应用程序就使用了这种方式。
HandleExternalEvent和Delay相对于组合(composite)活动而言,它们都是basic(基本)活动。也就是说,HandleExternalEvent和Delay都只执行一个单一的功能,它们不能作为其它活动的容器。正如你可能预料到的,这些活动的普遍用法是基于一个单一的事件来触发一系列的活动。你又如何支配这些事件序列会是怎么样的呢?答案是使用另一个WF活动:EventDriven活动。
使用EventDriven活动
EventDriven的行为就像是一个组合活动,这个组合活动以顺序执行的方式执行它所包含的一系列活动。这并不是说你不能在这个容器中插入一个Parallel(并行)活动,但在并行活动之前插入的活动和之后插入的活动都将依顺序进行执行。对于它容纳的活动的唯一限制是在执行路径上的第一个活动必须是对IEventActivity进行了处理的活动。(HandleExternalEvent和Delay就是这种类型的两个活动。)除了从基类继承而来的属性和方法外,该EventDriven再没有其它可使用的属性和方法。(它仅仅是一个容器。)
和顺序活动不同的是,EventDriven在有事件触发并被第一个活动处理前是不会允许所容纳的活动执行的。(记住,第一个活动必须处理IEventActivity。)。
EventDriven的使用还有第二个限制。它的父活动必须是Listen、State或者StateMachineWorkflow之中的一个,在有些地方你是不能把EventDriven拖到你的工作流中的,它只能拖到上述三种容器中。我们将在第14章(“基于状态的工作流”)中全面介绍State和StateMachineWorkflow活动。但现在还是来看看Listen活动吧。
使用Listen活动
假如说EventDriven的行为像是一个顺序活动的话,那Listen活动的行为就像是一个并行(parallel)活动。Listen可作为两个或更多的EventDriven活动的容器。其中的这些EventDriven活动选定的路径完全取决于它们中谁第一个收到事件。但是,一旦其中的一个对某个事件进行了处理,其它的和它并行的EventDriven活动的执行路径都会被忽略而不会被执行,它们不会再继续等待它们各自的事件,在EventDriven活动处理了相应的事件后,又将按顺序继续执行接下来的路径。在它的Activity基类所暴露出的属性和方法外,再没有我们感兴趣的属性和方法。
需注意的是在Listen活动内必须至少包含两个及以上的EventDriven活动对象,并且仅仅只有EventDriven类型的活动能直接放到Listen活动中。此外,Listen不能用到基于状态机的工作流中。为什么这里有这些规则和限制呢?
假如WF允许少于两个的EventDriven子活动的话,Listen活动的作用就值得怀疑。你更好的做法是直接使用一个EventDriven活动。假如子活动中没有EventDriven活动的话,你也就没有要去处理的事件。
在基于状态机的工作流中禁止使用Listen看起来或许是一个很奇怪的限制,其实这是出于可能产生循环的考虑。状态机中循环这一术语指的是一系列事件的触发彼此相互依赖。在一定程度上,这和多线程编程中的死锁概念相似。假如事件A依赖于事件B触发,但事件B又在等待事件A触发才能执行,我们就说产生了循环。在基于状态机的工作流中禁用并行事件处理是WF设计器用来减少产生潜在的这种循环的一种措施。
使用EventHandlingScope活动
回顾目前为止我们看到过的活动中,有处理事件的基本活动、触发事件的delay活动、能够组合顺序流的组合活动和组合并行流的组合活动。你相信会有结合了顺序化和并行化行为特点的和事件有关的活动吗?这就是EventHandlingScope活动。
EventHandlingScope是一个组合活动,它的作用是去容纳一组EventHandler活动(它本身就是IEventActivity类型的对象的容器),以及唯一一个其它的非基于事件的如Sequence或Parallel之类的组合活动。非基于事件的组合活动在EventHandler活动中所容纳的全部事件都已处理完毕前会一直执行。在所有这些事件都已触发并被处理完后,在该工作流的EventHandlingScope活动外面的下一个活动才继续执行。
宿主到工作流的通信
在介绍了WF中涉及事件的这些活动后,我现在要展示前面未完成的工作流和宿主之间的通信体系的另一半。你可以回忆一下第8章,我们通过在工作流实例中使用CallExternalMethod活动来把信息发送到宿主进程中。这个被调用的“external method”其实是一个你所提供的方法,它由一个你所写的本地通信服务暴露出来。该服务能把预定的数据传给宿主并触发一个事件,这个事件发送一个数据到达的信号,然后宿主采取措施把数据从该服务中读出(从工作流中接收到了数据后,该服务对数据进行了缓存)。
对于相反的过程,即宿主把数据发送给一个已经执行的工作流来说,也涉及到本地通信服务、事件以及为处理这些事件的事件处理程序。当你为宿主和工作流之间设计好了进行通信所要使用的接口时(就像是第8章中“创建服务接口”这一节所展示的一样),你在接口中添加的方法就是被工作流用来把数据发送到宿主所使用的方法。在该接口中添加事件能使宿主把数据发送给已经开始执行的工作流。
本章的示例应用程序将会用到我所描述过的每一个活动。一个EventHandlingScope活动将处理“stop processing(停止处理)”事件。一个Sequence活动将包含一个对股票行情更新进行模拟的工作流处理过程。当股价被更新时,新价将会被传到宿主中并在用户界面上显示出来(如图10-1所示)。本eBroker应用程序并不是真实地对每一只股票代码的当前股价进行检查,它使用一个简单的蒙特卡罗模拟法来计算最新的股价。蒙特卡罗模拟是使用了随机数字的模拟方法,它和通过掷骰子来获取相应结果的过程类似。我们这样做的目的只是为了去看看工作流和宿主之间是怎样进行通信的。
图10-1eBroker的主用户界面
该eBroker应用程序应能让工作流知道,添加的新的当前并未被监视的股票应该要被监视到,而添加时如该股票本已存在则不予考虑(目的是为了简化处理)。你可使用Add和Remove按钮来模拟股票的添加和删除。点击Add将弹出如图10-2所示的对话框。当你输完后点击OK,这个新的要被监视的股票就被添加进被监视股票的列表中了。
图10-2添加一个新的要被监视的股票
在“Ticker values”列表中选择一条记录,这会激活Remove按钮。点击该Remove按钮就可把该项从被监视的股票列表中移除。该移除动作产生的结果如图10-3。你正监视的股票被保存在应用程序的Settings文件(XML格式的配置文件)中。下一次你执行eBroker时,它将“记起”你的这些股票并重新开始进行监视。
图10-3移除一个已存在的被监视的股票
在图10-2中,你看到了应用程序需要知道你当前有多少股份以便能计算你所拥有的股份的总价值,这些数字可被用来计算当前的市值。假如你后来想要修正股份的数量(通过买卖股票),你可选中市值(market value)列表中的股票然后点击Buy!或者Sell!该对话框如图10-4所示。
图10-4需要去买或卖的股份数对话框
图10-2中的这个Add添加对话框也需要输入买或卖的“触发”条件值,当你不应该买进或卖出你目前所监视的任何公司的股票时,工作流中包含的使用了这些值的业务逻辑会通告你。假如股票价格超过了预定的触发卖价的值,则在市值列表中将显示一个红色的标记。假如股票价格低于预定的触发买价的值,将显示一个绿色的标记。你能在任何时候进行买卖...这些标记只是起提示作用,在图10-5中你可看到这组标记。
图10-5指出了买卖建议的eBroker用户界面
这四个按钮(Add、Remove、Buy!和Sell!)中的每一个都会触发一个到工作流的事件。还有第5个事件,就是Stop,它用来停止模拟的执行过程,这个事件由Quit按钮触发。
该应用程序的许多地方其实我已经为你写完了,这使你能把注意力放到和工作流相关的地方。首先,你要完成工作流和宿主将用来进行通信的接口,然后你要使用wca.exe工具来创建继承自CallExternalMethod和HandleExternalEvent的一组活动。准备好了这些,你就可使用本章中看到过的每一个活动来布置该工作流。你将看到本地通信服务是怎样把宿主应用程序和工作流通信处理进程粘合到一起的。最后,你将简要地进行检查并添加一些代码到eBroker用户界面源文件中,以指引它和工作流进行交互。我们就开始吧!
创建通信接口
我们只需要一个方法:MarketUpdate,它把市场价格信息返回到用户界面上,另外还需要五个事件,这些事件分别是AddTicker、RemoveTicker、BuyStock、SellStock和Stop,它们用来驱动工作流。这唯一的一个方法和五个事件都要添加到一个接口中,我们将首先创建这个接口。任何和本地通信服务相关的事情都依赖于这个接口。
创建一个工作流数据通信接口
1.下载本章源代码,从Visual Studio中打开eBroker应用程序解决方案。
备注:和本书中绝大部分示例应用程序一样,本eBroker示例应用程序也有两个版本:完整版本和非完整版本。非完整版是学习本示例应用程序的版本,你在此需要打开该版本以便进行本示例应用程序的练习和学习。
2.在打开的本解决方案中你将找到三个项目。展开eBrokerService项目,然后打开IWFBroker.cs文件准备进行修改。
3.定位到eBrokerService名称空间,在该名称空间中添加下面的代码并保存:
IWFBroker
[ExternalDataExchange]
public interface IWFBroker
{
void MarketUpdate(string xmlMarketValues);
event EventHandler<TickerActionEventArgs> AddTicker;
event EventHandler<TickerActionEventArgs> RemoveTicker;
event EventHandler<SharesActionEventArgs> BuyStock;
event EventHandler<SharesActionEventArgs> SellStock;
event EventHandler<StopActionEventArgs> Stop;
}
4.对本项目进行编译,假如存在编译错误,修正所有错误。
不要忘记ExternalDataExchange属性。没有它你就不能使用我在这里所描述的数据传送机制来成功地在工作流和宿主之间进行信息的传送。
在你创建通信活动(使用wca.exe工具)之前,花点时间来看看eBrokerService项目中的event arguments。MarketUpdateEventArgs实际上只不过是System.Workflow.ExternalDataEventArgs的强类型版本,StopActionEventArgs也是。System.Workflow.ExternalDataEventArgs这个event argument类不传送数据,但是,TickerActionEventArgs和SharesActionEventArgs都要传送数据给工作流。TickerActionEventArgs承载的是代表要添加和移除的股票的XML数据,而SharesActionEventArgs承载的是作为主键的股票代码以及要买或卖的股票数目。
提示:设计这些event argumeents是很重要的,因为这些event arguments把数据从宿主传给工作流。此外,wca.exe工具会检查这些event arguments并创建到派生类的绑定,使你能从event arguments中访问到这些派生类中的数据,仿佛这些数据就是这些event arguments所固有的。换句话说,假如event arugment有一个命名为OrderNumber的属性,则wca.exe创建的类就会有一个命名为OrderNumber的属性。它的值来自于事件的事件参数,并会为你自动指定该值。
现在我们就使用wca.exe工具来创建通信活动
创建通信活动
1.点击“开始”菜单,然后点击“运行”按钮打开“运行”对话框。
2.输入cmd,然后点击确定。
3.使用cd命令把起始目录定位到eBrokerService项目生成的程序集所对应的目录下,如cd "...eBrokereBrokerServicebinDebug"。
4.就如第8章中做过的一样,在命令行提示符中输入下面的命令(包含有双引号):"<%Program Files%>Microsoft SDKsWindowsv6.0Abinwca.exe" /n:eBrokerFlow eBrokerService.dll。(注意该“<%Program Files%>”表示你的Program Files目录的位置,通常是“C:Program Files”。)然后按下回车键。
5.wca.exe会加载它在eBrokerService.dll找到的程序集,然后扫描使用了ExternalDataExchange特性修饰的接口,在这个例子中这个接口是IWFBroker。被解析出的那个方法被转换成派生自CallExternalMethod活动的类并保存到名称为IWFBroker.Invokes.cs的文件中。那些事件也相似地被转换为派生自HandleExternalEvent活动的类并被放进IWFBroker.Sinks.cs文件中。在命令提示符的命令行中键入下面的命令来对该“invokes”文件重命名:ren IWFBroker.Invokes.cs ExternalMethodActivities.cs。
6.通过在命令提示符的命令行中键入下面的命令来对该“sinks”文件重命名:ren IWFBroker.Sinks.cs ExternalEventHandlers.cs。
7.使用下面的命令把当前目录下的这两个文件移到工作流项目的目录中:move External*.cs ......eBrokerFlow。
8.现在回到Visual Studio中,向eBrokerFlow工作流项目中添加这两个刚创建好的文件。
9.编译eBrokerFlow项目,在成功编译后,在工作流的视图设计器界面下的工具箱中将呈现出AddTicker、ButStock等自定义事件活动。
注意:作为提醒,假如编译了工作流解决方案后这些新活动没有在工具箱中呈现出来,就请关闭eBroker解决方案再重新打开它,以强制对这些自定义活动进行加载。在下一节我们将使用它们。
创建broker工作流
1.在Visual Studio的视图设计器中打开eBrokerFlow项目中的Workflow1.cs文件。
2.我们需要插入一个Code活动,它被用来为一个Delay活动(你稍后将会插入它)指定预期的延迟时间,并初始化一些内部数据结构。因此,拖拽一个Code活动到工作流视图设计器的界面上,然后在ExecuteCode属性中键入Initialize并按下回车键,以便在工作流代码中创建该Initialize事件处理程序。然后,回到工作流的视图设计器界面上继续添加活动。
备注:延时时间值保存在Settings(配置文件)中。
3.接下来拖拽一个EventHandlingScope到工作流视图设计器界面上。
4.记住,你需要为EventHandlingScope提供一个事件处理程序以及一个子活动,以便在它监听事件时执行。我们首先创建事件处理程序。为了存取这些事件处理程序,需要把鼠标指针移到eventHandlingScop1下面的微小矩形图标上。(这个矩形就是一个“智能标记。”)
然后这个矩形变成了一个更大、更黑并带有向下箭头的矩形。
点击这个向下的箭头,这会激活一个带有图标的四个快捷菜单:查看 EventHandlingScope、查看取消处理程序、查看错误处理程序和查看事件处理程序。
点击最下面的一个菜单项、切换到事件处理程序视图。你看到的这个用户界面和你在第七章(“基本活动操作”)中看到的和错误处理程序相联系的用户界面很相似。
拖拽一个EventDriven活动到工作流视图设计器界面上,把它放到这个矩形的中间(在这里你会看到“将EventDrivenActivity拖放至此”的文字说明)。
5.现在回到工具箱中,在eBrokerFlow组件区域中找到Stop活动。拖拽一个该活动到工作流视图设计器界面上,把它放进你在前一个步骤所添加的EventDriven活动中。假如你想对多个事件进行监听的话,在这时你还可把它们都添加进去。在我们的例子中,这里只有Stop事件是我们需要的。
6.你刚才就添加好了EventHandlingScope活动将对停止执行进行监听的事件。下面,你需要为EventHandlingScope添加子活动,当监听到Stop活动触发时EventHandlingScope将执行这个子活动。因此,我们需要通过第4步中的第一个子步骤回到eventHandlingScopeActivity1的查看 EventHandlingScope界面上,但你需要选择最上面的菜单项,而不是最下面的一个。
7.拖拽一个While活动到工作流视图设计器界面上,把它放到EventHandlingScope活动内。
8.指定它的Condition属性为代码条件而不是声明性规则条件,指定该事件处理程序的名称为TestContinue。一旦Visual Studio添加了该TestContinue事件处理程序后,需要回到工作流视图设计器上,还有更多的活动要进行添加。
9.While活动只能接受唯一的一个子活动,因此拖拽一个Sequence活动到该While活动中。
10.在这里你需要一个Code活动来对股票价值进行蒙特卡罗模拟,因此拖拽一个Code活动到视图设计器界面上,把它放进你在前一步骤所添加的Sequence活动中。在属性窗口中把它重命名为updateMarket。
11.指定updateMarket这个Code活动的ExecuteCode属性为UpdateMarketValues。在Visual Studio添加了相应的事件处理程序后回到工作流视图设计器界面上来,以便继续布置你的工作流。
12.在模拟完成后(你将添加的代码实际上就是进行模拟),你需要把这些潜在的进行了修改的值传给宿主应用程序。为此,把鼠标指针移到工具箱上,找到你在IWFBroker中创建的MarketUpdate活动,把它拖到视图设计器界面上并放到Sequence活动中的你在前一步骤中所添加的Code活动的下面。
13.MarketUpdate活动需要把一小段XML放送给宿主,要做到这一点,它必须绑定到容纳有此时将发送的XML的字段属性。为此,在Visual Studio的属性面板中选择xmlMarketValues属性,然后点击浏览(...)按钮,打开一个“将‘xmlMarketValues’绑定到活动的属性”的对话框。然后点击绑定到新成员选项卡,点击创建属性,在新成员名称中输入Updates。最后点击确定。Visual Studio就添加了Updates这个依赖属性。
14.为了让你能处理来自于宿主的事件,拖拽一个Listen活动到设计器界面上,把它放进Sequence活动中。
15.假如你回忆一下,你会记起IWFBroker接口声明了五个事件,它们中的一个是我们已经用过的Stop,还有四个事件要去处理。Listen活动目前仅仅容纳了两个EventDriven活动,但添加更多的EventDriven活动也很容易。你需要简单地拖拽多达三个的EventDriven活动进Listen活动中。为什么要添加三个而不是正好的两个呢?因为第五个EventDriven活动要包含一个行为像是定时器的Delay活动,当延时过期后,Listen活动会结束该工作流线程。然后While活动对执行条件进行检测判断,而返回的这个条件总被设置为true,于是使While活动不停地循环。在股票价值被更新并发送给宿主后,Listen活动又对新一轮来自宿主的事件进行监听。
16.在最右边的EventDriven活动中,拖拽并放入一个Delay活动,在属性面板中把它命名为updateDelay。
17.接下来从eBrokerFlow中拖拽一个SellStock活动到工作流视图设计器界面上,把它放到从右边数起的第二个EventDriven活动中。
18.在Visual Studio的属性面板中选择NumberOfShares属性,点击浏览(...)按钮,这会又一次打开一个“将‘NumberOfShares’绑定到活动的属性”的对话框。点击绑定到新成员选项卡,然后再点击创建字段,并在新成员名称中输入_sharesToSell,最后点击确定。Visual Studio就添加了这个_sharesToSell字段。
备注:我在这里选择创建_sharesToSell依赖属性而不是字段是因为字段从来不会被Workflow1类的外部访问到。它提供的基于XML格式的市场价值信息要传给宿主,因此应当把外部访问权限暴露出来。
19.Symbol属性也必须进行绑定。下面的步骤和上一步骤是一样的,只是字段名称要命名为_tickerToSell。
20.为了卖出股票,要拖拽一个Code活动放到SellStock事件处理程序的下面。在它的ExecuteCode属性中输入SellStock,在插入了对应的事件处理程序后请回到工作流视图设计器界面上来。
21.我们现在要对买股票的逻辑进行添加。拖拽一个BuyStock事件处理活动(也来自于eBrokerFlow)到设计器界面上,把它放到正中间的EventDriven活动中。
22.使用第18步的步骤,把BuyStock活动的NumberOfShares属性绑定到一个新的字段,名称为_sharesToBuy。同样,使用第19步的步骤,把它的Symbol属性也绑定到一个新的字段,名称为_tickerToBuy。
23.和你需要一个Code活动去卖股票一样,你也需要一个Code活动去买股票。重复第12步添加一个新的Code活动,设置它的ExecuteCode属性为BuyStock。
24.重复第17步至第20步两次,把RemoveTicker和AddTicker事件也添加到Listen活动中。RemoveTicker活动的TickerXML属性要绑定到一个新的名称为_tickerToRemove的字段,而为该RemoveTicker事件添加的Code活动的ExecuteCode属性指定为RemoveTicker。同样地,AddTicker活动的TickerXML属性要绑定到_tickerToAdd,和它相联系的Code活动的ExecuteCode属性指定为AddTicker。完成这些后,Listen活动的外观如下所示:
25.编译你的这个工作流,纠正任何出现的编译错误。
26.在Visual Studio中打开Workflow1.cs的源文件准备进行编辑。
27.Visual Studio为你添加了大量的代码,因此你首先定位到Workflow1的构造器并在该构造器下添加如下的代码。你插入的这些代码可被认为是初始化代码。当工作流启动时,你将把一个数据字典传给该工作流,这个数据字典包含有以股票代码(如“CONT”)作为关键字的要监视的若干股票信息的集合。你也需要指定一个轮询间隔,它是再一次对股票市值进行检测前工作流所要等待的时间值。
private Dictionary<string, eBrokerService.Ticker> _items =
new Dictionary<string, eBrokerService.Ticker>();
private string _tickersXML = null;
public string TickersXML
{
get { return _tickersXML; }
set { _tickersXML = value; }
}
private TimeSpan _interval = TimeSpan.FromSeconds(7);
public TimeSpan PollInterval
{
get { return _interval; }
set { _interval = value; }
}
28.下面定位到你在步骤2中为你的第一个Code活动添加的Initialize事件处理程序。插入下面的代码:
Initialize
// Establish the market update timeout
updateDelay.TimeoutDuration = PollInterval;
// Stuff the known ticker values into the dictionary
// for later recall when updating market conditions.
eBrokerService.Tickers tickers = null;
using (StringReader rdr = new StringReader(TickersXML))
{
XmlSerializer serializer =
new XmlSerializer(typeof(eBrokerService.Tickers));
tickers = (eBrokerService.Tickers)serializer.Deserialize(rdr);
}
foreach (eBrokerService.Ticker ticker in tickers.Items)
{
// Add the ticker to the dictionary
_items.Add(ticker.Symbol, ticker);
}
提示:为了方便,我在这个初始化方法中对该Delay活动的TimeoutDuration进行了指定。但是不要忘了,你也能使用Delay活动的InitializeTimeoutDuration方法来做同样的工作。
29.找到TestContinue事件处理程序,While活动使用它来对是否继续进行循环进行判断。插入下面的代码让While活动不停循环(不用担心...实际上它最终会停止循环的!):
// Continue forever
e.Result = true;
30.下面要插入的代码块很长,它使用了蒙特卡罗模拟来对股票市场价进行更新。找到和名称为updateMarket的Code活动(参见第10步)相对应的UpdateMarketValues事件处理程序,插入下面的代码:
UpdateMarketValues
// Iterate over each item in the dictionary and decide
// what it's current value should be. Normally we'd call
// some external service with each of our watch values,
// but for demo purposes we'll just use random values.
Random rand = new Random(DateTime.Now.Millisecond);
eBrokerService.UpdateCollection updates = new eBrokerService.UpdateCollection();
foreach (string key in _items.Keys)
{
// Locate the item
eBrokerService.Ticker item = _items[key];
// If we're starting out, we have no current value,
// so place the value at half the distance between the
// buy and sell triggers.
if (item.LastPrice <= 0.0m)
{
// Assign a price
decimal delta = (item.SellTrigger - item.BuyTrigger) / 2.0m;
// The last price must be a positive value, so add
// the delta to the smaller value.
if (delta >= 0.0m)
{
// Add delta to buy trigger value
item.LastPrice = item.BuyTrigger + delta;
} // if
else
{
// Reverse it and add to the sell trigger
// value
item.LastPrice = item.SellTrigger + delta;
} // else
} // if
// Set up the simulation
decimal newPrice = item.LastPrice;
decimal onePercent = item.LastPrice * 0.1m;
Int32 multiplier = 0; // no change
// We'll now roll some dice. First roll: does the
// market value change? 0-79, no. 80-99, yes.
if (rand.Next(0, 99) >= 80)
{
// Yes, update the price. Next roll: will the
// value increase or decrease? 0-49, increase.
// 50-99, decrease
multiplier = 1;
if (rand.Next(0, 99) >= 50)
{
// Decrease the price.
multiplier = -1;
} // if
// Next roll, by how much? We'll calculate it
// as a percentage of the current share value.
// 0-74, .1% change. 75-89, .2% change. 90-97,
// .3% change. And 98-99, .4% change.
Int32 roll = rand.Next(0, 99);
if (roll < 75)
{
// 1% change
newPrice = item.LastPrice + (onePercent * multiplier * 0.1m);
} // if
else if (roll < 90)
{
// 2% change
newPrice = item.LastPrice + (onePercent * multiplier * 0.2m);
} // else if
else if (roll < 98)
{
// 3% change
newPrice = item.LastPrice + (onePercent * multiplier * 0.3m);
} // else if
else
{
// 4% change
newPrice = item.LastPrice + (onePercent * multiplier * 0.4m);
} // else if
} // if
else
{
// No change in price
newPrice = item.LastPrice;
} // else
// Now create the update for this ticker
eBrokerService.Update update = new eBrokerService.Update();
update.Symbol = item.Symbol;
update.LastPrice = item.LastPrice;
update.NewPrice = newPrice;
update.Trend = multiplier > 0 ? "Up" : (multiplier == 0 ? "Firm" : "Down");
update.Action = newPrice > item.SellTrigger ? "Sell" : (newPrice < item.BuyTrigger ? "Buy" : "Hold");
update.TotalValue = newPrice * item.NumberOfShares;
updates.Add(update);
// Update the data store
item.LastPrice = newPrice;
} // foreach
// Serialize the data
StringBuilder sb = new StringBuilder();
using (StringWriter wtr = new StringWriter(sb))
{
XmlSerializer serializer = new XmlSerializer(typeof(eBrokerService.UpdateCollection));
serializer.Serialize(wtr, updates);
} // using
// Ship the data back
Updates = sb.ToString();
基本上,每一次更新循环,对于每一只股票将有20%的几率被修改。假如该股票的价格将被修改,它有一半的几率会上升,有一半的几率会下降。将改变的值是:有75%的几率是当前每股价格的1%,有15%的几率是当前每股价格的2%,有7%的几率是当前每股价格的3%,有3%的几率是当前每股价格的4%。对于每一次循环,所有被监视的股票都会被更新,即使它的价格没有变化。将被发送回宿主进行显示的数据是一个XML字符串,它包含有各只的股票代码、当前价格、根据所买的该只股票数计算出来的总市值、趋势(上升还是下降)以及是否有要进行买或卖的建议。买卖建议会显示出一个醒目的标志(红或绿),你已经在图10-5中见过。
31.现在向外部事件处理程序中添加代码。首先定位到SellStock事件处理程序,添加下面的代码:
SellStock
// Reduce the number of shares for the given ticker.
try
{
// Find this ticker.
eBrokerService.Ticker item = _items[_tickerToSell];
if (item != null)
{
// Reduce the number of shares.
item.NumberOfShares = item.NumberOfShares - _sharesToSell >= 0 ?
item.NumberOfShares - _sharesToSell : 0;
}
}
catch
{
// Do nothingwe just won't have sold any.
}
32.找到BuyStock事件处理程序,添加下面的代码:
BuyStock
// Increase the number of shares for the given ticker.
try
{
// Find this ticker.
eBrokerService.Ticker item = _items[_tickerToBuy];
if (item != null)
{
// Increase the number of shares.
item.NumberOfShares += _sharesToBuy;
}
}
catch
{
// Do nothingwe just won't have purchased any.
}
33.接下来是RemoveTicker,找到它并插入下面的代码:
RemoveTicker
// Remove the given ticker from the watch.
try
{
// Deserialize
eBrokerService.Ticker ticker = null;
using (StringReader rdr = new StringReader(_tickerToRemove))
{
XmlSerializer serializer = new XmlSerializer(typeof(eBrokerService.Ticker));
ticker = (eBrokerService.Ticker)serializer.Deserialize(rdr);
}
// Find this ticker.
if (_items.ContainsKey(ticker.Symbol))
{
// Remove it.
_items.Remove(ticker.Symbol);
}
}
catch
{
// Do nothingwe just won't have removed it.
}
34.最后是AddTicker,插入下面的代码:
AddTicker
try
{
// Deserialize
eBrokerService.Ticker ticker = null;
using (StringReader rdr = new StringReader(_tickerToAdd))
{
XmlSerializer serializer = new XmlSerializer(typeof(eBrokerService.Ticker));
ticker = (eBrokerService.Ticker)serializer.Deserialize(rdr);
}
// Add the item if not already existing.
if (!_items.ContainsKey(ticker.Symbol))
{
// Add it.
_items.Add(ticker.Symbol, ticker);
}
}
catch
{
// Do nothingwe just won't have added it.
}
35.假如你现在对本工作流进行编译,不会出现编译错误。
现在,工作流完成了,我们需要回到我们关注的本地通信服务和宿主的结合上来。因为我们已经在第8章详细介绍过这方面的内容,因此我在这里不再整个进行重新介绍。假如你打开本例中相关的文件,你会看到这些代码和第8章中看过的很相似。
注意:我在第8章中提到过下面的内容,但它是一个重要的问题,您对这个问题的认识应该得到加强:如果你在工作流和宿主应用程序中对对象或者对象的集合进行了共用的话,运行中就会有风险,这牵涉到多线程数据访问的问题,因为工作流和宿主将共享对同一对象的引用。假如你的应用程序存在这个问题,当在工作流和宿主之间传递它们时,你就可考虑对这些对象进行克隆(在你的数据类中实现ICloneable接口),或者使用序列化技术。对于本应用程序,我选择了XML序列化。
但我想谈谈连接器类BrokerDataConnector中的一些代码。IWFBroker接口因为包含了事件,因此和我们在第8章中看到的示例的接口不同。因为连接器类必须实现该接口(在本例中,BrokerDataConnector实现了IWFBroker),因此该连接器也必须处理这些事件。但是,事件的实现和清单10-1中看到的一样,没有特别之处。假如你对该清单从头一直看到尾,你将看到通常的事件实现和你或许亲自写过的事件实现非常相像。
清单10-1 BrokerDataConnector.cs的完整代码
BrokerDataConnector.cs的完整代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Workflow.Activities;
using System.Workflow.Runtime;
using System.Data;
namespace eBrokerService
{
public sealed class BrokerDataConnector : IWFBroker
{
private string _dataValue = null;
private static WorkflowBrokerDataService _service = null;
private static object _syncLock = new object();
public static WorkflowBrokerDataService BrokerDataService
{
get { return _service; }
set
{
if (value != null)
{
lock (_syncLock)
{
// Re-verify the service isn't null
// now that we're locked
if (value != null)
{
_service = value;
} // if
else
{
throw new InvalidOperationException("You must provide a service instance.");
} // else
} // lock
} // if
else
{
throw new InvalidOperationException("You must provide a service instance.");
} // else
}
}
public string MarketData
{
get { return _dataValue; }
}
// Workflow to host communication method
public void MarketUpdate(string xmlMarketValues)
{
// Assign the field for later recall
_dataValue = xmlMarketValues;
// Raise the event to trigger host read
_service.RaiseMarketUpdatedEvent();
}
// Host to workflow events
public event EventHandler<TickerActionEventArgs> AddTicker;
public event EventHandler<TickerActionEventArgs> RemoveTicker;
public event EventHandler<SharesActionEventArgs> BuyStock;
public event EventHandler<SharesActionEventArgs> SellStock;
public event EventHandler<StopActionEventArgs> Stop;
public void RaiseAddTicker(Guid instanceID, string tickerXML)
{
if (AddTicker != null)
{
// Fire event
AddTicker(null, new TickerActionEventArgs(instanceID, tickerXML));
} // if
}
public void RaiseRemoveTicker(Guid instanceID, string tickerXML)
{
if (RemoveTicker != null)
{
// Fire event
RemoveTicker(null, new TickerActionEventArgs(instanceID, tickerXML));
} // if
}
public void RaiseBuyStock(Guid instanceID, string symbol, Int32 numShares)
{
if (BuyStock != null)
{
// Fire event
BuyStock(null, new SharesActionEventArgs(instanceID, symbol, numShares));
} // if
}
public void RaiseSellStock(Guid instanceID, string symbol, Int32 numShares)
{
if (SellStock != null)
{
// Fire event
SellStock(null, new SharesActionEventArgs(instanceID, symbol, numShares));
} // if
}
public void RaiseStop(Guid instanceID)
{
if (Stop != null)
{
// Fire event
Stop(null, new StopActionEventArgs(instanceID));
} // if
}
}
}
当宿主执行上面这些“raise”方法来触发基于用户输入的各种事件时,工作流就会执行连接器的MarketUpdate方法。第8章描述了该工作流用来调用MarketUpdate方法的机制。为了看看宿主怎样调用一个用来和工作流进行交互的事件(在事件参数中可根据需要携带相应的数据),我们来看看下面的代码段。这些代码用来在点击Quit按钮时退出应用程序。
cmdQuit_Click
private void cmdQuit_Click(object sender, EventArgs e)
{
// Stop the processing
// Remove from workflow
eBrokerService.BrokerDataConnector dataConnector =
(eBrokerService.BrokerDataConnector)_workflowRuntime.GetService(
typeof(eBrokerService.BrokerDataConnector));
dataConnector.RaiseStop(_workflowInstance.InstanceId);
// Just quit
Application.Exit();
}
为了触发传送数据到工作流中的这些事件,你首先需要使用工作流运行时的GetService方法获取连接器。注意该服务需要为它指明恰当的连接器类型,这样才能去使用它的那些“raise”方法。一旦得到该服务后,你就可简单地调用对应的“raise”方法,为它指定要传送的必要的数据信息去生成对应的event arguments就可以了。