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

WF從入門到精通(第十七章):關聯及本地主機通信(上)

編輯:關於.NET

學習完本章,你將掌握:

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接口的完整代碼

using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.ComponentModel;
using System.Workflow.Activities;
namespace TruckService
{
  [CorrelationParameter("truckID")]
  [ExternalDataExchange]
  public interface ITruckService
  {
    // 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;
  }
}

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。因此,你至少要把接口中的方法和事件添加到這個類中。但是在你完成這些之前,我們先添加一些需要的代碼。首先,正好在這個類的左大括號後面添加下面的字段。

protected const string KeyFormat = "{0}.Truck_{1}";
protected static Dictionary<string, string> _dataValues =
                    new Dictionary<string, string>();
protected static Dictionary<string, WorkflowTruckTrackingDataService>
  _dataServices =
   new Dictionary<string, WorkflowTruckTrackingDataService>();
private static object _syncLock = new object();

3.因為數據連接器需要與數據項保存聯系並且在工作流運行時中是一個單例模式(singleton),因此我們將添加一對靜態方法,它們用來對數據服務進行注冊並對已經注冊的數據服務進行檢索。

WorkflowTruckTrackingDataService方法和RegisterDataService方法

public static WorkflowTruckTrackingDataService
  GetRegisteredWorkflowDataService(Guid instanceID,
                  Int32 truckID)
{
  string key = String.Format(KeyFormat, instanceID, truckID);
  WorkflowTruckTrackingDataService serviceInstance = null;
  if (_dataServices.ContainsKey(key))
  {
    // Return the appropriate data service
    serviceInstance = _dataServices[key];
  }
  return serviceInstance;
}
public static void
  RegisterDataService(WorkflowTruckTrackingDataService dataService)
{
  string key = String.Format(KeyFormat,
                dataService.InstanceID.ToString(),
                dataService.TruckID);
  lock (_syncLock)
  {
    _dataServices.Add(key, dataService);
  }
}

4.當一個新的工作流實例被啟動的時候,在主應用程序中都要對數據服務進行注冊(每個工作流實例對應一個數據服務),一旦數據服務被注冊後,它就在數據連接器中保存關聯數據。我們需要有一個去檢索該數據的方式。以前,我們使用的是一個屬性,但是對於我們現在來說這已經不適用了,因為我們必須傳入工作流實例ID和關聯值(在本例中是一個卡車的標識符)二者。為了檢索該數據,在靜態的注冊方法(RegisterDataService方法)的下面添加如下這個方法:

RetrieveTruckInfo方法

public string RetrieveTruckInfo(Guid instanceID, Int32 truckID)
{
  string payload = String.Empty;
  string key = String.Format(KeyFormat, instanceID, truckID);
  if (_dataValues.ContainsKey(key))
  {
    payload = _dataValues[key];
  }
  return payload;
}

5.有了這最後的一個方法後,我們現在就去添加來自於ITruckService接口的方法。它們的位置緊跟在前一步驟所添加的檢索數據的方法下面。

繼承自ITruckService接口的方法

// Workflow to host communication methods
public void ReadyTruck(Int32 truckID, Int32 startingX, Int32 startingY)
{
  // Pull correlated service
  WorkflowTruckTrackingDataService service =
                 GetRegisteredWorkflowDataService(
                 WorkflowEnvironment.WorkflowInstanceId,
                 truckID);
  // Place data in correlated store
  UpdateTruckData(service.InstanceID, truckID, startingX, startingY);
  // Raise the event to trigger host activity
  if (service != null)
  {
    service.RaiseTruckLeavingEvent(truckID, startingX, startingY);
  }
}
public void UpdateTruck(Int32 truckID, Int32 X, Int32 Y)
{
  // Pull correlated service
  WorkflowTruckTrackingDataService service =
                 GetRegisteredWorkflowDataService(
                 WorkflowEnvironment.WorkflowInstanceId,
                 truckID);
  // Update data in correlated store
  UpdateTruckData(service.InstanceID, truckID, X, Y);
  // Raise the event to trigger host activity
  if (service != null)
  {
    service.RaiseRouteUpdatedEvent(truckID, X, Y);
  }
}
public void RemoveTruck(Int32 truckID)
{
  // Pull correlated service
  WorkflowTruckTrackingDataService service =
                 GetRegisteredWorkflowDataService(
                 WorkflowEnvironment.WorkflowInstanceId,
                 truckID);
  // Remove truck from correlated store
  string key = String.Format(KeyFormat, service.InstanceID, truckID);
  if (_dataValues.ContainsKey(key))
  {
    // Remove it
    _dataValues.Remove(key);
  }
  // Raise the event to trigger host activity
  if (service != null)
  {
    service.RaiseTruckArrivedEvent(truckID);
  }
}

6.在ITruckService中的方法的後面是一些事件,因此也要添加它們,它們的位置在你第5步所添加的方法的下面。

繼承自ITruckService接口的事件

// Host to workflow events
public event EventHandler<CancelTruckEventArgs> CancelTruck;
public void RaiseCancelTruck(Guid instanceID, Int32 truckID)
{
  if (CancelTruck != null)
  {
    // Fire event
    CancelTruck(null, new CancelTruckEventArgs(instanceID, truckID));
  }
}
public event EventHandler<AddTruckEventArgs> AddTruck;
public void RaiseAddTruck(Guid instanceID, Int32 truckID, Int32 routeID)
{
  if (AddTruck != null)
  {
    // Fire event
    AddTruck(null, new AddTruckEventArgs(instanceID, truckID, routeID));
  }
}

7.回頭看看在第5步中插入的方法,你會看到一個用來把關聯數據插入到對應的數據字典中去的方法(UpdateTruckData)。數據本身必須被轉換為XML,因此以其把下面這些代碼都擴散到上面的三個方法中,那還不如添加一個UpdateTruckData方法,然後把它們都放在UpdateTruckData方法中。現在就添加該方法,把它放在你前面添加的事件的下面:

UpdateTruckData方法

protected Truck UpdateTruckData(Guid instanceID, Int32 truckID, Int32 X, Int32 Y)
{
  string key = String.Format(KeyFormat, instanceID, truckID);
  Truck truck = null;
  if (!_dataValues.ContainsKey(key))
  {
    // Create new truck
    truck = new Truck();
    truck.ID = truckID;
  }
  else
  {
    // Pull existing truck
    string serializedTruck = _dataValues[key];
    StringReader rdr = new StringReader(serializedTruck);
    XmlSerializer serializer = new XmlSerializer(typeof(Truck));
    truck = (Truck)serializer.Deserialize(rdr);
  }
  // Update values
  truck.X = X;
  truck.Y = Y;
  // Serialize values
  StringBuilder sb = new StringBuilder();
  using (StringWriter wtr = new StringWriter(sb))
  {
    XmlSerializer serializer = new XmlSerializer(typeof(Truck));
    serializer.Serialize(wtr, truck);
  }
  // Ship the data back
  _dataValues[key] = sb.ToString();
  return truck;
}

8.保存該文件。

列表17-2中列出了TruckServiceDataConnector類的完整代碼。在強調一次,這個類的作用是保存來自各個工作流實例的關聯數據。該數據被存儲到一個Dictionary對象中,鍵值對把工作流實例標識符和卡車標識符關聯起來。該數據連接器類在工作流運行時中是一個單例服務。

備注:你可能會問:為什麼數據要轉換為XML而不直接使用數據對象本身?這是因為,當在工作流和宿主之間來回傳送數據的時候,WF並不對對象進行序列化。因此,並不會創建對象的副本(毫無疑問是為了提高性能)。對象的傳遞是通過引用實現的,由此工作流和宿主都在同一個對象上工作。假如你不想這樣(注:指對象引用),又不想使用本范例中的方法(注:指轉換為XML)的話,你就可以對對象進行序列化或者實現ICloneable來傳遞對象的副本。假如這種行為(注:指對象引用)並不影響你的設計,你就不需要再去做任何事情而直接(在工作流和宿主之間)來回通過引用的方式去傳遞你的對象。雖然如此,也要牢記你的對象將會被兩個不同的線程所共享。

列表17-2 TruckServiceDataConnector.cs的完整代碼

TruckServiceDataConnector類的完整代碼

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Workflow.Activities;
using System.Workflow.Runtime;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
namespace TruckService
{
  public class TruckServiceDataConnector : ITruckService
  {
    protected const string KeyFormat = "{0}.Truck_{1}";
    protected static Dictionary<string, string> _dataValues = new Dictionary<string, string>();
    protected static Dictionary<string, WorkflowTruckTrackingDataService> _dataServices = new Dictionary<string, WorkflowTruckTrackingDataService>();
    private static object _syncLock = new object();
    public static WorkflowTruckTrackingDataService GetRegisteredWorkflowDataService(Guid instanceID, Int32 truckID)
    {
      string key = String.Format(KeyFormat, instanceID, truckID);
      WorkflowTruckTrackingDataService serviceInstance = null;
      if (_dataServices.ContainsKey(key))
      {
        // Return the appropriate data service
        serviceInstance = _dataServices[key];
      } // if
      return serviceInstance;
    }
    public static void RegisterDataService(WorkflowTruckTrackingDataService dataService)
    {
      string key = String.Format(KeyFormat, dataService.InstanceID.ToString(), dataService.TruckID);
      lock (_syncLock)
      {
        _dataServices.Add(key, dataService);
      } // lock
    }
    public string RetrieveTruckInfo(Guid instanceID, Int32 truckID)
    {
      string payload = String.Empty;
      string key = String.Format(KeyFormat, instanceID, truckID);
      if (_dataValues.ContainsKey(key))
      {
        payload = _dataValues[key];
      } // if
      return payload;
    }
    // Workflow to host communication methods
    public void ReadyTruck(Int32 truckID, Int32 startingX, Int32 startingY)
    {
      // Pull correlated service
      WorkflowTruckTrackingDataService service = GetRegisteredWorkflowDataService(WorkflowEnvironment.WorkflowInstanceId, truckID);
      // Place data in correlated store
      UpdateTruckData(service.InstanceID, truckID, startingX, startingY);
      // Raise the event to trigger host activity
      if (service != null)
      {
        service.RaiseTruckLeavingEvent(truckID, startingX, startingY);
      } // if
    }
    public void UpdateTruck(Int32 truckID, Int32 X, Int32 Y)
    {
      // Pull correlated service
      WorkflowTruckTrackingDataService service = GetRegisteredWorkflowDataService(WorkflowEnvironment.WorkflowInstanceId, truckID);
      // Update data in correlated store
      UpdateTruckData(service.InstanceID, truckID, X, Y);
      // Raise the event to trigger host activity
      if (service != null)
      {
        service.RaiseRouteUpdatedEvent(truckID, X, Y);
      } // if
    }
    public void RemoveTruck(Int32 truckID)
    {
      // Pull correlated service
      WorkflowTruckTrackingDataService service = GetRegisteredWorkflowDataService(WorkflowEnvironment.WorkflowInstanceId, truckID);
      // Remove truck from correlated store
      string key = String.Format(KeyFormat, service.InstanceID, truckID);
      if (_dataValues.ContainsKey(key))
      {
        // Remove it
        _dataValues.Remove(key);
      } // if
      // Raise the event to trigger host activity
      if (service != null)
      {
        service.RaiseTruckArrivedEvent(truckID);
      } // if
    }
    // Host to workflow events
    public event EventHandler<CancelTruckEventArgs> CancelTruck;
    public void RaiseCancelTruck(Guid instanceID, Int32 truckID)
    {
      if (CancelTruck != null)
      {
        // Fire event
        CancelTruck(null, new CancelTruckEventArgs(instanceID, truckID));
      } // if
    }
    public event EventHandler<AddTruckEventArgs> AddTruck;
    public void RaiseAddTruck(Guid instanceID, Int32 truckID, Int32 routeID)
    {
      if (AddTruck != null)
      {
        // Fire event
        AddTruck(null, new AddTruckEventArgs(instanceID, truckID, routeID));
      } // if
    }
    protected Truck UpdateTruckData(Guid instanceID, Int32 truckID, Int32 X, Int32 Y)
    {
      string key = String.Format(KeyFormat, instanceID, truckID);
      Truck truck = null;
      if (!_dataValues.ContainsKey(key))
      {
        // Create new truck
        truck = new Truck();
        truck.ID = truckID;
      } // if
      else
      {
        // Pull existing truck
        string serializedTruck = _dataValues[key];
        StringReader rdr = new StringReader(serializedTruck);
        XmlSerializer serializer = new XmlSerializer(typeof(Truck));
        truck = (Truck)serializer.Deserialize(rdr);     
      } // else
      // Update values
      truck.X = X;
      truck.Y = Y;
      // Serialize values
      StringBuilder sb = new StringBuilder();
      using (StringWriter wtr = new StringWriter(sb))
      {
        XmlSerializer serializer = new XmlSerializer(typeof(Truck));
        serializer.Serialize(wtr, truck);
      } // using
      // Ship the data back
      _dataValues[key] = sb.ToString();
      return truck;
    }
  }
}

你可以回憶一下第8章,本地通信服務也有一個服務組件,它被工作流和接口間的數據連接器使用。事實上,這個WorkflowTruckTrackingDataService服務就是用我們剛剛創建的連接器來注冊的。該服務主要實現了一些基礎的有用的方法去觸發宿主邊的一些事件,例如數據可用的事件和關聯正使用的事件等,它有助於直接保持值的相關性。

完成相關數據服務(correlated data service)

1.在TruckService項目中,你會找到一個名稱為WorkflowTruckTrackingDataService.cs的源文件。你需要打開它准備進行編輯。

2.第一件事是添加該服務執行它的任務時所需要的私有字段。在WorkflowTruckTrackingDataService類的左大括號的後面添加下面這些代碼:

private static WorkflowRuntime _workflowRuntime = null;
private static ExternalDataExchangeService _dataExchangeService = null;
private static TruckServiceDataConnector _dataConnector = null;
private static object _syncRoot = new object();

3.在這些私有字段的下面是該服務將觸發的事件。這些事件被觸發的結果就是工作流實例將調用一個CallExternalMethod活動(你可在TruckServiceDataConnector類中看到它):

public event EventHandler<TruckActivityEventArgs> TruckLeaving;
public event EventHandler<TruckActivityEventArgs> RouteUpdated;
public event EventHandler<TruckActivityEventArgs> TruckArrived;

4.接下來添加兩個字段以及對應的屬性,你需要它們去對相關的服務實例進行識別:

private Guid _instanceID = Guid.Empty;
public Guid InstanceID
{
  get { return _instanceID; }
  set { _instanceID = value; }
}
private Int32 _truckID = -1;
public Int32 TruckID
{
  get { return _truckID; }
  set { _truckID = value; }
}

5.現在我們需要添加兩個靜態方法:一個是在工作流運行時中注冊該服務並對它進行配置,另一個是去對已經注冊的服務實例進行檢索:

CreateDataService靜態方法和GetRegisteredWorkflowDataService靜態方法

public static WorkflowTruckTrackingDataService
  CreateDataService(Guid instanceID,
           WorkflowRuntime workflowRuntime,
           Int32 truckID)
{
  lock (_syncRoot)
  {
    // If we're just starting, save a copy of the workflow.
    // runtime reference
    if (_workflowRuntime == null)
    {
      _workflowRuntime = workflowRuntime;
    }
    // If we're just starting, plug in ExternalDataExchange service.
    if (_dataExchangeService == null)
    {
      _dataExchangeService = new ExternalDataExchangeService();
      _workflowRuntime.AddService(_dataExchangeService);
    }
    // Check to see if we have already added this data
    // exchange service.
    TruckServiceDataConnector dataConnector =
       (TruckServiceDataConnector)workflowRuntime.GetService(
                    typeof(TruckServiceDataConnector));
    if (dataConnector == null)
    {
      _dataConnector = new TruckServiceDataConnector();
      _dataExchangeService.AddService(_dataConnector);
    }
    else
    {
      _dataConnector = dataConnector;
    }
    // Pull the service instance we registered with the connection
    // object.
    return WorkflowTruckTrackingDataService.
          GetRegisteredWorkflowDataService(instanceID, truckID);
  }
}
public static WorkflowTruckTrackingDataService
  GetRegisteredWorkflowDataService(Guid instanceID,
                  Int32 truckID)
{
  lock (_syncRoot)
  {
    WorkflowTruckTrackingDataService workflowDataService =
       TruckServiceDataConnector.GetRegisteredWorkflowDataService(
                           instanceID, truckID);
    if (workflowDataService == null)
    {
      workflowDataService =
         new WorkflowTruckTrackingDataService(instanceID, truckID);
      TruckServiceDataConnector.RegisterDataService(
                           workflowDataService);
    }
    return workflowDataService;
  }
}

6.現在添加構造器和析構器:

類的構造器和析構器代碼

private WorkflowTruckTrackingDataService(Guid instanceID, Int32 truckID)
{
  this._instanceID = instanceID;
  this._truckID = truckID;
}
~WorkflowTruckTrackingDataService()
{
  // Clean up
  _workflowRuntime = null;
  _dataExchangeService = null;
  _dataConnector = null;
}

備注:你可以回憶一下第8章,構造器的作用是防止在服務和連接器類之間出現循環鏈接引用。而對於析構器的作用來說,只是實現IDisposable並不能完成相應的任務,因為當從工作流運行時中移除該服務的時候並不調用Dispose方法。

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