程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WF從入門到精通(第十章):事件活動

WF從入門到精通(第十章):事件活動

編輯:關於.NET

學習完本章,你將掌握:

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)方法,它用來把本事件參數中的值和你工作流中的字段或依賴屬性進行綁定。重寫該方法(或者處理它所觸發的事件)是檢索來自於宿主並被保存到事件參數中的數據一個主要的機制,通常,你會創建一個自定義的事件參數來把數據嵌入進參數對象自身中。

盡管你能直接從Visual Studio的工具箱中使用HandleExternalEvent活動,但更普遍的情形是使用你在第8章中看過的wca.exe工具來為你正使用的通信接口創建一個派生自HandleExternalEvent的自定義類。例如,假如在你的接口中定義了一個名稱為SendDataToHost的事件,wca.exe將會生成一個稱作SendDataToHost的新活動(它派生自HandleExternalEvent),並為你指定了EventName和InterfaceType,而且通過你創建的事件參數也為你和SendDataToHost事件進行了數據綁定。在本章晚些時候我將提供一個例子。

 

使用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 "...\eBroker\eBrokerService\bin\Debug"。

4.就如第8章中做過的一樣,在命令行提示符中輸入下面的命令(包含有雙引號):"<%Program Files%>\Microsoft SDKs\Windows\v6.0A\bin\wca.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就可以了。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved