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

WF從入門到精通(第十三章):打造自定義活動(一)

編輯:關於.NET

學習完本章,你將掌握:

1.了解對於創建一個功能齊全的自定義工作流活動來說哪些組件是必須的

2.創建基本的自定義工作流活動

3.在基本的自定義工作流活動中應用驗證規則

4.把基本的自定義工作流活動集成到Microsoft Visual Studio的工作流視圖設計器和工具箱中

WF並不可能涵蓋到你可能在你的工作流中想要實現的各個方方面面。即使WF對於開發社區來說仍是非常新的技術,但目前已經可以獲得許多免費發布的自定義活動,可以肯定商業級的活動最終也會跟進。

在這章中,你將通過創建一個新的工作流活動來了解WF的個中奧妙,這個活動從遠程FTP服務器中檢索文件。你將看到在創建你自己的活動時哪些東西是必需的,以及其中哪些部分挺不錯。你也將更深入地了解活動是怎樣和工作流運行時交互的。

備注:只在一章中對自定義活動開發的每一個細節進行探討是不可能,這兒簡化了太多的細節。不過好消息是,對於得到一個完整功能的活動來說是容易的,這不用知道每一個細節。

關於活動的更多知識

在第四章(活動及工作流類型介紹)中,我們初步了解了一下活動並討論了像ActivityExecutionContext之類一些話題,ActivityExecutionContext用來容納一些和正執行的活動相關的一些信息,工作流運行時需要不時對這些信息進行訪問。我們這裡將對WF活動進行更深入一些的了解。

活動的虛擬方法

在創建自定義活動時首先需要了解的是基類為你提供了哪些虛擬的方法和屬性。表13-1顯示了活動中被普遍使用的可重寫的一些方法。(這裡沒有虛擬屬性。)

表13-1Activity中被普遍使用的可重寫的虛擬方法

方法 功能 Cancel 在工作流被取消時被調用。 Compensate 這個方法實際上並不來自於Activity基類,它實際上需要由ICompensatableActivity接口提供,許多活動都從該接口派生。因此,不管出於什麼目的和意圖,都把它當作Activity的方法。你將實現這個方法以便對失敗的事務進行補償。 Execute 被用來執行活動要去完成的對應的工作。 HandleFault 在活動內部代碼拋出一個未經處理的異常時被調用。注意一旦該方法被調用將沒有辦法重啟該活動。 Initialize 在活動被初始化時被調用。 OnActivityExecutionContextLoad 在活動完成了它的工作流程後被調用。當前執行上下文(current execution context)正在轉移到另一個活動。 Uninitialize 在活動要被反初始化時被調用。

在你的活動已經被加載到工作流運行時中但在執行之前的時候,假如你需要進行一些特定的處理工作,一個極好的位置是在Initializze方法中做這些事情。你或許也會在Uninitialize方法中執行一些相似的處理工作之外的事情。

OnActivityExecutionContextLoad和OnActivityExecutionContextUnload方法分別表示活動正加載到工作流運行時中和活動正從工作流運行時中移走。在OnActivityExecutionContextLoad被調用之前以及OnActivityExecutionContextUnload被調用之後,從WF的角度來看,該活動是處於卸載狀態中。它或許是被序列化到一個隊列中、保存進一個數據庫中或者甚至是在磁盤上等待被加載。但在這些方法(OnActivityExecutionContextLoad和OnActivityExecutionContextUnload方法)被調用之前或之後它並不存在於工作流運行時之中。

Cancel、HandleFault和Compensate都在顯而易見的條件(指取消、失敗和補償條件)激發的時候被調用。盡管Compensate真正用在執行你的事務補償的地方(看看第15章:工作流和事務),但它們主要的用途都是去執行一些你想去執行的額外的工作(例如日志)。牢記這些方法被調用的時候都太晚了,因為到你的活動被要求對失敗進行補償的時候,你不能對事務進行恢復;你也不能撤銷一個未經處理的異常或者終止一個取消(cancle)的請求。所有你能做的是去執行一些清理或者其它處理的請求,就Compensate來說,實際上是為失敗的事務提供補償功能。

Execute是最有可能被重寫的Activity的虛擬方法,這只不過是因為這個方法需要你重寫以去執行活動應當要去執行的工作。

活動組件

盡管毫無疑問你需要親自去寫自定義活動代碼,完整開發的WF活動都帶有一些額外的支持和工作流無關的行為的代碼,但通常在工作流可視化設計器中都為開發者提供了更豐富的開發體驗。例如,你可能想要提供一個驗證器對象以便對不適當的活動配置進行檢查並返回錯誤信息;或者你可能需要提供一個ToolboxItem或者ToolboxBitmap以便更好地和Visual Studio工具箱集成。不管你是否相信,通過使用一個專門的設計器類來修改活動的主題,你實際上能夠調整你的活動放到工作流視圖設計器中的呈現樣式。在本章中的示例實現了所有這些東西以對它們的功能和效果進行演示。

執行上下文(Execution Contexts)

你可能還記得,有兩種類型的活動:基本(單一功能)活動和組合(容器)活動。你可能會認為它們之間的主要區別是其中一個是單一的活動,而另一個能容納可嵌入活動。這毫無疑問是一個主要的區別。

但是還有其它重要的區別,尤其是活動在執行上下文(execution context)中怎樣工作這一點上。活動執行上下文在第4章中介紹過,它是WF去記載一些重要事情的一種簡單方法,就像是一個正在工作的活動來自於哪個工作流隊列一樣。但它也為活動控制提供了一個機制,為WF在那些正執行的活動之間實施規則提供了一種手段。活動執行上下文的一個有趣的地方是你的工作流實例啟動的上下文可能並不是你的自定義活動中正被使用的上下文。活動執行上下文能被克隆並傳給子活動,對於迭代(iterative)類型的活動來說總會發生這種情況。

但是對我們這裡的目的而言,可能最重要的事情是要記住創建自定義活動的時候,至少要記住活動執行上下文。活動執行上下文保存了當前的執行狀態,並且當你重寫了System.Workflow.Activity中的那些虛擬方法的時候,它只有某些狀態值是有效的。表13-2顯示了哪些執行狀態值能應用到System.Workflow.Activity中的方法的重寫中。Compensate稍微有點例外,因為它不是System.Workflow.Activity的虛擬方法,它來自於ICompensatableActivity,可它由活動實現,就返回狀態值而言這條規則仍然適用於Compensate。返回任何無效狀態值(例如從Execute中返回ActivityExecutionStatus.Faulting)其結果就是運行時拋出一個InvalidOperationException。

表13-2有效的執行狀態

可重寫的方法 有效的返回執行狀態 Cancel ActivityExecutionStatus.Canceling和ActivityExecutionStatus.Closed Compensate ActivityExecutionStatus.Compensating和ActivityExecutionStatus.Closed Execute ActivityExecutionStatus.Executing和ActivityExecutionStatus.Closed HandleFault ActivityExecutionStatus.Faulting和ActivityExecutionStatus.Closed Initialize ActivityExecutionStatus.Initialized。和其它狀態值不一樣,在此時工作流活動被初始化,並沒有任何東西去關閉它,因此ActivityExecutionStatus.Closed不是可選的。

通常,你要分別為這些虛擬方法的任務進行處理並返回ActivityExecutionStatus.Closed。返回其它另外的有效值表明需要由工作流運行時或者一個包含它的活動(指它的父活動)來采取更進一步的行動(操作)。例如,假如你的活動有子活動,當你的主活動的Execute方法完成後還有子活動沒有完成的話,主活動的Execute方法就應當返回ActivityExecutionStatus.Executing。否則,它就應該返回ActivityExecutionStatus.Closed。

活動生命周期

那麼這些方法是在什麼時候由工作流運行時執行呢?表13-1中的方法以下面的順序被執行:

1.OnActivityExecutionContextLoad
2.Initialize
3.Execute
4.Uninitialize
5.OnActivityExecutionContextUnload
6.Dispose

從工作流運行時的角度來看,OnActivityExecutionContextLoad和OnActivityExecutionContextUnload界定了活動的生命周期。OnActivityExecutionContextLoad在一個活動剛剛被加載到運行時內存中的時候被調用,而OnActivityExecutionContextUnload在一個活動從運行時中刪除的前一刻被調用。

備注:活動通常從反序列化過程創建而不是由工作流運行時直接調用構造器創建。因此,假如你需要在創建活動的時候為其分配資源的話,OnActivityContextLoad是做這件事情的最好位置,而不是在構造器中。

盡管從內存的角度來說OnActivityExecutionContextLoad和OnActivityExecutionContextUnload指示了活動的創建,但是Initialize和Uninitialize則表示活動在工作流運行時中執行的生命周期。當工作流運行時調用Initialize方法的時候,你的活動就准備就緒了。當Uninitialize被執行的時候,從工作流運行時的角度來看你的活動就已經完成了並准備從內存中移出。Dispose這個.NET對象的原型銷毀方法對於釋放靜態資源是很有用的。

當然,工作流並不能總是控制其中一些方法的執行。例如Compensate,它僅在一個可補償的事務失敗時才被調用。這些剩下的方法實際上在Execute時會被不確定地調用(不一定會被調用)。

創建一個FTP活動

為了對本章中目前為止我所描述的一些東西進行演示,我決定創建一個活動,我們當中許多寫行業處理軟件的人都希望找到的一個有用的東西:FTP活動。這個FtpGetFileActivity活動,使用.NET中基於Web的FTP類來從遠程FTP服務器中檢索文件。使用這些相同的類來把文件寫到遠程FTP資源中也是可行的,但我把這樣的活動作為練習留給你去創建。

備注:我將以你知道(並正確地配置過)FTP站點的前提下開始我的工作。為了我們此處的目的進行討論,我將使用眾所周知的IP地址127.0.0.1作為服務器的IP地址(當然,這代表的是localhost)。你也可自由地把這個IP地址替換為你喜歡的任何有效的服務器IP地址或者主機名。對於FTP安全的問題和服務器配置方面的內容超出了本章的范圍,假如你正使用的是IIS並需要了解關於FTP配置方面的更多信息的話,可看看http://msdn.microsoft.com/en-us/library/6ws081sa.aspx。

為了宿主該FTP活動,我創建了一個名稱為FileGrabber的示例應用程序(它的用戶界面如圖13-1所示。)。有了它,你就能提供出一個FTP用戶帳戶和密碼以及你想檢索的FTP資源。我將下載的資源是一個Saturn V運載火箭移到發射位置的圖像文件,我已經在本書的CD中為你提供了該圖片,你也可把它放到你的FTP服務器上。假設你的FTP服務器在你的本機上,該圖片的URL是ftp://127.0.0.1/SaturnV.jpg。假如你不使用我的圖片文件,你就需要修改你的本地服務器上所能獲取的某個文件的URL以和我所提供的地址匹配,或者另外使用任何你能下載的文件的有效URL。

圖13-1FileGrabber用戶界面

和你可能已經知道的一樣,不是所有的FTP站點都需要一個FTP用戶賬戶和密碼來進行訪問。有些允許匿名訪問,它使用“anonymous”作為用戶名,使用你的電子郵件地址作為密碼。該FTP活動也被這樣配置,假如你不想提供它們,則用戶名默認為anonymous而密碼默認為[email protected]

因為本示例應用程序是一個Windows Forms應用程序,因此在工作流檢索文件的時候我們不想讓應用程序看起來被鎖定。畢竟工作流實例在不同的線程上執行,因此我們的用戶界面應能夠繼續響應。不過,我們將會禁用某些控制,同時允許其它的一些東西保持活躍狀態。一個狀態控制將在文件傳輸正在發生的期間顯示出來,一旦文件下載完成,該狀態控制將會被隱藏。假如用戶在某個文件正在傳輸時試圖退出該應用程序,我們將在取消該工作流實例並退出應用程序之前對用戶的決定進行確定。文件下載期間應用程序用戶界面的情形如圖13-2所示。

圖13-2FileGrabber在下載某個文件時的用戶界面

為了讓你節約一些時間,該FileGrabber應用程序已經被寫出了。唯一缺少的是一點點配置工作流並讓它啟動的代碼。但是,工作流將執行的這個FTP活動本身並不存在,我們首先就來創建該FTP活動。隨著本章的進展,我們將會(逐步)向該活動中添加更多的東西,最後把它放到一個工作流中,FileGrabber能執行該工作流去下載某個文件。

創建一個新的FTP工作流活動

1.該FileGrabber應用程序再次為你提供了兩個版本:完整版本和非完整版本。你需要下載本章源代碼,打開FileGrabber文件夾中的解決方案。

2.FileGrabber解決方案只包含有一個項目(它是一個Windows Forms應用程序)。我們現在將添加第二個項目,我們將用它來創建我們的FTP活動。為此,向我們的解決方案中添加一個新項目,項目類型選擇類庫,項目名稱為FtpActivity,然後點擊確定。

3.一旦該新的FtpActivity項目添加完成後,Visual Studio會自動地打開它在本項目中創建好的Class1.cs文件。首先做一些准備工作,把“Class1.cs”文件的名稱重命名為“FtpGetFileActivity.cs”,同時Visual Studio也會自動的把類的名稱為你從Class1重命名為FtpGetFileActivity。

4.確實,我們正創建的是一個WF活動,但是卻沒有添加相應的引用,我們不會離題太遠。當我們添加WF引用的時候,我們也將為我們本章將執行的任務添加其它的引用。因此在解決方案資源管理器的FtpActivity項目上點擊鼠標右鍵,然後選擇添加引用。當打開“添加引用”對話框後,從“.NET”選項卡列表中選中下面所有程序集,然後點擊確定:

a.System.Drawing
b.System.Windows.Forms
c.System.Workflow.Activities
d.System.Workflow.ComponentModel
e.System.Workflow.Runtime

5.現在我們就可以添加我們需要的名稱空間了。添加下面的名稱空間:

using System.IO;
using System.Net;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Activities;
using System.Drawing;

6.因為我們正創建的是一個活動,因此我們需要使FtpGetFileActivity派生自一個恰當的基類。修改當前的類定義如下:

public sealed class FtpGetFileActivity : System.Workflow.ComponentModel.Activity

備注:因為我們正創建的是一個基本活動,因此該FTP活動派生自System.Workflow.ComponentModel.Activity。但是,假如你正創建的是一個組合活動的話,它應當派生自System.Workflow.ComponentModel.CompositeActivity。

7.對於本例子,FtpGetFileActivity將暴露三個屬性:FtpUrl、FtpUser和FtpPassword。活動的屬性幾乎總是依賴屬性,因此我們將添加三個依賴屬性,我們就從FtpUrl開始。在FtpGetFileActivity類的左大括號中輸入下面的代碼(此時該類沒有包含其它代碼):

FtpUrl依賴屬性

public static DependencyProperty FtpUrlProperty =
DependencyProperty.Register("FtpUrl", typeof(System.String),
typeof(FtpGetFileActivity));
[Description ("Please provide the full URL for the file to download.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Required)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpUrl
{
  get
  {
    return ((string)
      (base.GetValue(FtpGetFileActivity.FtpUrlProperty)));
  }
  set
  {
    Uri tempUri = null;
    if (Uri.TryCreate(value, UriKind.Absolute, out tempUri))
    {
      if (tempUri.Scheme == Uri.UriSchemeFtp)
      {
        base.SetValue(FtpGetFileActivity.FtpUrlProperty,
          tempUri.AbsoluteUri);
      }
    }
    else
    {
      // Not a valid FTP URI
      throw new ArgumentException("The value assigned to the" +
        " FtpUrl property is not a valid FTP URI.");
    };
  }
}

備注:完整地描述所有的設計器特性,並理解這些特性使FtpGetFileActivity在工作流的視圖設計器上怎樣呈現出來方面的內容超出了本章的范圍。不過,話雖如此,我還是要簡要的描述一下。Description特性提供了關於指定屬性的相關說明,在該屬性被選中的時候將在Visual Studio的屬性面板中顯示出對應的這些相關說明。

DesignerSerializationVisibility特性指定屬性對設計時序列化程序所具有的可見性。(在本例中,該屬性將由代碼生成器生成。)Browsable特性告知Visual Studio把所修飾的屬性以編輯框的形式顯示出來。Category特性指明了所修飾的屬性將呈現在哪種類別的屬性組中(本例中是自定義類別)。ValidationOption特性是WF所特有的,它告知工作流視圖設計器它所修飾的屬性的驗證選項。(在本例中,FTP URL是必須執行驗證的。值必須存在並將對其驗證。)稍後當我們添加一個自定義活動驗證器的時候我們將會需要這個特性。http://msdn2.microsoft.com/en-us/library/a19191fh.aspx為你提供了設計器特性和它們的使用的一些概述信息以及相關更多信息的鏈接。

8.接下來為FtpUser屬性添加代碼。把下面的代碼放到你前一步所插入的FtpUrl代碼的下面:

FtpUser依賴屬性

public static DependencyProperty FtpUserProperty =
DependencyProperty.Register("FtpUser", typeof(System.String),
typeof(FtpGetFileActivity));
[Description("Please provide the FTP user account name.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Optional)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpUser
{
  get
  {
    return ((string)(
       base.GetValue(FtpGetFileActivity.FtpUserProperty)));
  }
  set
  {
    base.SetValue(FtpGetFileActivity.FtpUserProperty, value);
  }
}

9.現在在你剛插入的FtpUser代碼的下面放入最後的一個屬性FtpPassword:

FtpPassword依賴屬性

public static DependencyProperty FtpPasswordProperty =
DependencyProperty.Register("FtpPassword", typeof(System.String),
typeof(FtpGetFileActivity));
[Description("Please provide the FTP user account password.")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[ValidationOption(ValidationOption.Optional)]
[Browsable(true)]
[Category("FTP Parameters")]
public string FtpPassword
{
  get
  {
    return ((string)(
       base.GetValue(FtpGetFileActivity.FtpPasswordProperty)));
  }
  set
  {
    base.SetValue(FtpGetFileActivity.FtpPasswordProperty, value);
  }
}

10.正像你可能知道的,一些FTP服務器允許匿名訪問。雖然許多服務器都要求用戶注冊,但也有其它的FTP站點被配置為公共的存取權限。在公共存取權限的情況下,用戶名通常是anonymous,並且用戶的電子郵件地址被作為密碼使用。我們將為FtpGetFileActivity指定一個FTP URL地址,但用戶名和密碼從應用程序的角度來看將是可選的。然而,從FTP的角度來看,我們必須提供一些東西。因此我們現在添加了一些常量字符串,以便稍後我們為FTP進行身份驗證時添加代碼的時候使用它。因此,在你剛剛添加的FtpPassword屬性的下面,添加下面這些常量字符串:

private const string AnonymousUser = "anonymous";

private const string AnonymousPassword = "[email protected]";

11.根據你想讓你的自定義活動去做的事情,你通常將重寫基類Activity所暴露的一個或多個虛擬方法。雖然嚴格意義上不是必須的,但你通常都可能想至少去對Execute進行重寫,因為在Execute中要完成的工作將得以實現。在你插入到FtpGetFileActivity源文件的常量字符串的下面,添加這些重寫Execute的代碼:

Execute方法

protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext)
{
  // Retrieve the file.
  GetFile();
  // Work complete, so close.
  return ActivityExecutionStatus.Closed;
}

12.Execute調用了GetFile方法,因此在Execute的下面添加如下這些代碼:

GetFile方法

private void GetFile()
{
  // Create the Uri. We check the validity again
  // even though we checked it in the property
  // setter since binding may have taken place.
  // Binding shoots the new value directly to the
  // dependency property, skipping our local
  // getter/setter logic. Note that if the URL
  // is very malformed, the Uri constructor will
  // throw.
  Uri requestUri = new Uri(FtpUrl);
  if (requestUri.Scheme != Uri.UriSchemeFtp)
  {
    // Not a valid FTP URI
    throw new ArgumentException("The value assigned to the" +
     "FtpUrl property is not a valid FTP URI.");
  } // if
  string fileName =
    Path.GetFileName(requestUri.AbsolutePath);
  if (String.IsNullOrEmpty(fileName))
  {
    // No file to retrieve.
    return;
  } // if
  Stream bitStream = null;
  FileStream fileStream = null;
  StreamReader reader = null;
  try
  {
    // Open the connection
    FtpWebRequest request =
      (FtpWebRequest)WebRequest.Create(requestUri);
    // Establish the authentication credentials
    if (!String.IsNullOrEmpty(FtpUser))
    {
      request.Credentials =
        new NetworkCredential(FtpUser, FtpPassword);
    } // if
    else
    {
      request.Credentials =
        new NetworkCredential(AnonymousUser,
        !String.IsNullOrEmpty(FtpPassword) ?
        FtpPassword : AnonymousPassword);
    } // else
    // Make the request and retrieve response stream
    FtpWebResponse response =
      (FtpWebResponse)request.GetResponse();
    bitStream = response.GetResponseStream();
    // Create the local file
    fileStream = File.Create(fileName);
    // Read the stream, dumping bits into local file
    byte[] buffer = new byte[1024];
    Int32 bytesRead = 0;
    while ((bytesRead = bitStream.Read(buffer, 0, buffer.Length)) > 0)
    {
      fileStream.Write(buffer, 0, bytesRead);
    } // while
  } // try
  finally
  {
    // Close the response stream
    if (reader != null) reader.Close();
    else if (bitStream != null) bitStream.Close();
    // Close the file
    if (fileStream != null) fileStream.Close();
  } // finally
}

備注:不可否認,假如我能找到能完成我所需要任務的現成代碼而不是從零開始寫的話,我會每次都這樣去做。(事實上,一位大學教授曾經告訴過我這是軟件工程的一個重大原則。)我重用的大部分代碼都來自於Microsoft的示例。我提到這些是以防你想去創建這個把文件發送到FTP服務器或者甚至可能去刪除它們的活動。(對於這些操作的代碼Microsoft的示例也已經提供了)你可以在http://msdn.microsoft.com/en-us/library/system.net.ftpwebrequest.aspx找到該例子。

本文配套源碼

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