小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Microsoft Windows Workflow Foundation 入門:開發(fā)人員...

 青石水 2010-05-08
Microsoft Windows Workflow Foundation 入門:開發(fā)人員演練
發(fā)布日期 : 11/30/2005 | 更新日期 : 11/30/2005

Dino Esposito
Solid Quality Learning

適用于:
Microsoft Windows Workflow Foundation
Microsoft Windows Vista

摘要:對于需要為 Microsoft .NET 平臺創(chuàng)建工作流驅動應用程序的開發(fā)人員而言,本文將介紹他們感興趣的 Microsoft Windows Workflow Foundation 技術和功能。

本文撰寫的對象為 Windows Workflow Foundation beta 1。請注意,在該技術的最終版本問世之前,內容上很可能會發(fā)生更改。

本頁內容

有關向 Windows 平臺添加工作流支持的初步知識有關向 Windows 平臺添加工作流支持的初步知識
創(chuàng)建第一個工作流創(chuàng)建第一個工作流
接收和使用數(shù)據(jù)接收和使用數(shù)據(jù)
工作流運行庫 工作流運行庫
工作流和活動工作流和活動
開發(fā)自定義活動開發(fā)自定義活動
計劃更現(xiàn)實的工作流計劃更現(xiàn)實的工作流
小結小結

有關向 Windows 平臺添加工作流支持的初步知識

Microsoft Windows Workflow Foundation (WWF) 是一個可擴展框架,用于在 Windows 平臺上開發(fā)工作流解決方案。作為即將問世的 Microsoft WinFX 的組成部分,Windows Workflow Foundation 同時提供了 API 和一些工具,用于開發(fā)和執(zhí)行基于工作流的應用程序。Windows Workflow Foundation 提供單個統(tǒng)一的模型,以便創(chuàng)建跨越多個類別應用程序的端到端解決方案,包括人力工作流和系統(tǒng)工作流。

Windows Workflow Foundation 是一個廣泛且通用的工作流框架,并且從下到上、在每個級別都針對可擴展性進行了設計。基于 Windows Workflow Foundation 的解決方案,由得到 Microsoft .NET 代碼支持且在宿主應用程序中運行的互連組件組成。就像在定制的環(huán)境中以可視方式創(chuàng)建 Web 頁一樣,您需要在可視設計器中制訂特定工作流的步驟,并且添加代碼隱藏工作流組件以實現(xiàn)規(guī)則并定義業(yè)務過程。

Windows Workflow Foundation 提供一個工作流引擎、一個 .NET 托管 API、運行庫服務以及與 Microsoft Visual Studio 2005 集成的可視化設計器和調試器??墒褂?Windows Workflow Foundation 來生成并執(zhí)行同時跨越客戶端和服務器的工作流,以及可在所有類型的 .NET 應用程序內部執(zhí)行的工作流。

本文通過幾個循序漸進的示例對 Windows Workflow Foundation 的進行了流暢的簡介,并且說明它的工作方式。

工作流 是以活動示意圖形式定義的人力或系統(tǒng)過程模型。活動 是工作流中的一個步驟,并且是工作流的執(zhí)行、重用和創(chuàng)作單位?;顒邮疽鈭D表達規(guī)則、操作、狀態(tài)以及它們的關系。Windows Workflow Foundation 工作流通過安排活動而設計,然后它編譯為 .NET 程序集,且在工作流運行庫和公共語言運行庫 (CLR) 中執(zhí)行。

創(chuàng)建第一個工作流

Windows Workflow Foundation 主要由 .NET 驅動的運行庫環(huán)境組成,該環(huán)境處理在 Visual Studio 設計器中設計和實現(xiàn)的特殊對象。Microsoft .NET Framework 2.0 是支持 Windows Workflow Foundation 所必需的。單獨的安裝程序包為 Visual Studio 2005 添加了 Windows Workflow Foundation 設計器和項目模板支持。一旦安裝,就會向 Visual Studio 2005 中的標準項目列表中添加一個全新的節(jié)點,如圖 1 所示。

1. Visual Studio 2005 中的工作流項目模板

您可以在各種選項中進行選擇,其中每個選項都標識了特定類型的工作流應用程序。表 1 顯示工作流項目模板的不完全列表。

表 1. Visual Studio 2005 中的工作流項目類型

類型

說明

順序工作流控制臺應用程序 (Sequential Workflow Console Application)

創(chuàng)建用于生成工作流的項目,該工作流包含一個默認的順序工作流和一個控制臺測試宿主應用程序。

順序工作流庫 (Sequential Workflow Library)

創(chuàng)建用于以庫的形式生成順序工作流的項目。

工作流活動庫 (Workflow Activity Library)

創(chuàng)建一個用來創(chuàng)建活動的庫的項目,以后可以將其作為工作流應用程序中的構造塊重用。

狀態(tài)機控制臺應用程序 (State Machine Console Application)

創(chuàng)建用于生成狀態(tài)機工作流和控制臺宿主應用程序的項目。

狀態(tài)機工作流庫 (State Machine Workflow Library)

創(chuàng)建用于以庫的形式生成狀態(tài)機工作流的項目。

空工作流 (Empty Workflow)

創(chuàng)建可以包含工作流和活動的空項目。

Windows Workflow Foundation 支持兩種基本工作流樣式:順序工作流和狀態(tài)機工作流。

順序工作流 非常適合以下類型的操作,即該操作由依次執(zhí)行直至最后一個活動完成的步驟的管線表示。但是,順序工作流的執(zhí)行并非完全是順序的。它們仍然可以接收外部事件或者啟動并行任務,在這種情況下,確切的執(zhí)行順序可能有所不同。

狀態(tài)機工作流 由一組狀態(tài)、轉換和操作組成。首先,將一個狀態(tài)表示為起始狀態(tài),然后,基于事件執(zhí)行向另一個狀態(tài)的轉換。狀態(tài)機工作流可以具有確定工作流結束的最終狀態(tài)。

讓我們假設您選擇并創(chuàng)建了一個新的順序工作流控制臺應用程序項目。Visual Studio 2005 解決方案資源管理器將包含兩個文件 — workflow1.cs 以及最初從視圖中隱藏的 workflow1.designer.cs。這兩個文件表示創(chuàng)建的工作流。Windows Workflow Foundation 工作流包含工作流模型文件和一個代碼文件類。workflow1.cs 類是可在其中寫入您自己的工作流業(yè)務邏輯的代碼文件類。workflow1.designer.cs 類表示活動示意圖的說明。該文件由 Visual Studio 2005 按照與 Microsoft Windows Forms 項目中的窗體非常類似的方式自動管理。當向工作流中添加活動時,Visual Studio 2005 會用以編程方式生成活動示意圖的 Microsoft C# 代碼更新設計器類。要繼續(xù)使用 Windows 窗體的類比,那么工作流類似于窗體,而活動類似于控件。

我們可以為活動布局選擇另一種形式的持久性 — XML 工作流標記格式。要嘗試這一方法,可從項目中刪除 workflow1.cs 文件并添加一個新的工作流項,如圖 2 所示。

2. 用代碼分隔添加順序工作流項

現(xiàn)在,項目包含兩個文件:workflow1.xoml 和 workflow1.xoml.cs。前一個文件包含表示工作流模型的 XML 工作流標記;后一個文件是一個代碼文件類,并且包含工作流的源代碼和事件處理程序。如果雙擊 .xoml 文件,會看到處于運行狀態(tài)的可視工作流設計器(參見圖 3)。

不存在為工作流模型的序列化選擇標記或代碼的運行時暗示 — 一旦該工作流編譯為程序集,它們就是等效的。

工作流應用程序是完成工作(例如,發(fā)送或接收數(shù)據(jù))的活動和管理一組子活動的執(zhí)行的復合活動(例如,IfElseWhile)的混合體。工作流可實現(xiàn)復雜的端到端方案,例如文檔審閱、PO 批準、IT 用戶管理、合作伙伴之間的信息交換、任何種類的向導或業(yè)務線應用程序。

圖 3 顯示一個極為簡單的示例工作流,它只包含一個活動 — code1 塊。

3. Visual Studio 2005 工作流設計器

Code 塊對應于 Code 類的一個實例,并且表示工作流中的一個活動,其行為以用戶定義的代碼表示。后端代碼是通過 Visual Studio 2005 輸入的(只需雙擊設計器中的所選元素),這是 ASP.NET 應用程序和其他 Visual Studio 2005 項目為人熟悉的編程風格。

當雙擊該活動時,代碼文件會打開且提供代碼處理程序的存根。

private void code1_ExecuteCode(object sender, EventArgs e)
{
// Some code here
}

當工作流運行庫在處理工作流的過程中開始處理指定的活動塊時,在代碼處理程序中鍵入的任何語句都將執(zhí)行。以下代碼只是輸出一條歡迎消息。

private void code1_ExecuteCode(object sender, EventArgs e)
{
Code c = (Code) sender;
Console.WriteLine("Hello, from '{0}'.\nI'm an instance of the {1} class.",
c.ID, c.ToString());
}

除了可視化布局以外,工作流還包含 workflow1.xoml.cs 文件中保存的以下代碼。

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
namespace HelloWorldWorkflow
{
public partial class Workflow1 : SequentialWorkflow
{
private void code1_ExecuteCode(object sender, EventArgs e)
{
Code c = (Code) sender;
Console.WriteLine("Hello, from '{0}'.\nI'm an instance of the {1} class.",
c.ID, c.ToString());
}
}
}

partial 屬性涉及不完全類 — 這是 .NET Framework 2.0 中的一個新概念。不完全類 是這樣的一種類:它的定義可能覆蓋不同的源文件。每個源文件都包含一個有頭有尾的普通類定義,只不過該定義是不完整的,并且沒有窮盡該類所需的邏輯。編譯器將把不完全的類定義合并到該類可以編譯的完整定義中。不完全類與面向對象沒有任何關系;它們是用來擴展項目中類行為的源代碼級別和受到程序集限制的方式。在 .NET Framework 2.0 中,不完全類是用于防止 Visual Studio 2005 在代碼文件內部注入自動生成代碼的手段。原始類中缺少的任何綁定代碼都將由運行庫通過添加不完全類進行添加。

工作流只能由 Windows Workflow Foundation 工作流運行庫執(zhí)行,并且工作流運行庫需要外部應用程序按照若干個規(guī)則來承載它。出于測試目的,Visual Studio 2005 還向項目中添加了一個 program.cs 文件。該文件是一個簡單的控制臺應用程序,如下所示。

class Program
{
static AutoResetEvent waitHandle = new AutoResetEvent(false);
static void Main(string[] args)
{
WorkflowRuntime workflowRuntime = new WorkflowRuntime();
workflowRuntime.StartRuntime();
workflowRuntime.WorkflowCompleted += OnWorkflowCompleted;
Type type = typeof(HelloWorldWorkflow.Workflow1);
workflowRuntime.StartWorkflow(type);
waitHandle.WaitOne();
workflowRuntime.StopRuntime();
// A bit of feedback to the user
Console.WriteLine("");
Console.WriteLine("");
Console.WriteLine("==========================");
Console.WriteLine("Press any key to exit.");
Console.WriteLine("==========================");
Console.ReadLine();
}
static void OnWorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
{
waitHandle.Set();
}
}

正如您在上述代碼中的粗體行中看到的那樣,出于簡單性的目的,Visual Studio 2005 在該控制臺應用程序中硬編碼工作流類的名稱。要防止控制臺應用程序在完成之后立即退出,您可能需要在 Main 方法的結尾添加 Console.ReadLine 調用。此刻,您已經(jīng)做好生成和測試該工作流的所有準備:請按 F5 并執(zhí)行。如果一切順利,那么會看到圖 4 中顯示的輸出。

4. 控制臺宿主應用程序執(zhí)行的示例工作流

調試工作流應用程序也很容易。實際上,需要做的所有工作就是放置斷點。您可以在工作流代碼文件類中的任何位置放置斷點(就像通常對 C# 代碼所做的那樣),或者直接在設計器視圖中放置斷點(這確實非常有趣)。需要選擇希望調試器介入的活動,按 F9 設置斷點,如圖 5 所示。

5. 工作流設計器視圖中放置的斷點

一旦代碼流到達設置了斷點的活動,Visual Studio 2005 就會將控制權移交給工作流調試器(參見圖 6)。從這里開始,您可以按照預期的那樣,在可視化設計器中按 F11 單步執(zhí)行代碼和活動。

6. 處于調試會話狀態(tài)的工作流應用程序

接收和使用數(shù)據(jù)

讓我們繼續(xù)分析并修改該工作流,以使其在實例化以后接收和使用數(shù)據(jù)。有兩種在實例化工作流以后使其接收數(shù)據(jù)的常規(guī)方法:參數(shù)和事件。如果選擇使用參數(shù),則需要在可視化設計器中手動定義參數(shù)名稱和類型的列表。如果選擇使用事件,則需要創(chuàng)建并添加一個自定義活動(該活動充當在工作流模型中的某個位置介入的外部源),并且傳入一些數(shù)據(jù)。我們將在下文中說明基于事件的方法;現(xiàn)在,讓我們重點說明一下參數(shù)。

如圖 7 所示,Workflow1 的 Properties 窗格顯示一個 Parameters 集合,您將在設計時用名稱/值對進行填充。

7. 向工作流中添加參數(shù)

圖 8 顯示正在工作的參數(shù)編輯器。您可以為所需的每個參數(shù)創(chuàng)建一個新項并指明它的名稱、類型和方向。

8. 添加的 FirstName LastName 字符串參數(shù)

參數(shù)的類型可手動鍵入,也可從定制的對象瀏覽器中選擇。在關閉 Workflow Parameters Editor 對話框后,立即修改代碼文件以合并剛剛定義的參數(shù)。您通常添加兩個公共屬性,并且使它們公開 Parameters 集合的內容,如下面的代碼所示。

public partial class Workflow1 : SequentialWorkflow
{
public string UserFirstName
{
get { return (string) Parameters["FirstName"].Value; }
set { Parameters["FirstName"].Value = value; }
}
public string UserLastName
{
get { return (string) Parameters["LastName"].Value; }
set { Parameters["LastName"].Value = value; }
}
:
}

使用公共屬性確實是一種良好的編程做法,可使代碼變得更加簡潔明了。它絕不是使用參數(shù)數(shù)據(jù)的要求。直接對工作流的 Parameters 集合進行調用也是完全可行的。如果用公共屬性包裝參數(shù),則請隨意選擇屬性的名稱。然而,請記住,C# 中的參數(shù)名是區(qū)分大小寫的。

那么是誰實際上通過這些參數(shù)提供輸入數(shù)據(jù)呢?完成這一任務的是宿主應用程序。然而,需要注意的是,宿主在初始化時(此時,工作流被加載以便執(zhí)行到運行庫容器中)設置任何參數(shù)。為了稍微詳細地說明這一點,讓我們編寫一個基于 Windows 窗體的示例宿主應用程序。該示例應用程序將提供幾個文本框,以便用戶輸入名字和姓氏(參見圖 9),并且將它們傳遞給工作流代碼處理程序。為了使用這些參數(shù),讓我們重新編寫代碼處理程序,如下所示。

private void code1_ExecuteCode(object sender, EventArgs e)
{
MessageBox.Show("Welcome, " + UserFirstName + " " + UserLastName);
}

該示例 Windows 窗體宿主應用程序中的關鍵內容是附加到 Start Workflow 按鈕的單擊處理程序。

9. 工作流宿主 Windows 窗體應用程序

窗體的代碼隱藏類的全部源代碼如下所示。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
namespace WinFormHost
{
public partial class Form1 : Form
{
private WorkflowRuntime _wr = null;
private string _workflowAssembly = "";
private string _workflowTypeName = "";
public Form1()
{
InitializeComponent();
_workflowAssembly = "WorkflowWithParams";
_workflowTypeName = "WorkflowWithParams.Workflow1";
_wr = new WorkflowRuntime();
_wr.StartRuntime();
}
private void btnStartWorkflow_Click(object sender, EventArgs e)
{
string assemblyName = _workflowAssembly;
string typeName = _workflowTypeName;
// Attempt to get type by fully-qualified name
Assembly assembly = Assembly.Load(assemblyName);
Type workflowType = assembly.GetType(typeName);
Dictionary parameters = new Dictionary();
parameters.Add("FirstName", txtFirstName.Text);
parameters.Add("LastName", txtLastName.Text);
// Start the workflow
Guid instanceID = Guid.NewGuid();
_wr.StartWorkflow(workflowType, instanceID, parameters);
}
}
}

要填充參數(shù)集合,需要使用由字符串和對象組成的基于泛型的字典。項的名稱是字符串,而所包含的值被配置為對象。需要向該字典中添加像工作流模型中的靜態(tài)參數(shù)一樣多的項 — 在此情況下,請?zhí)砑?FirstNameLastName。這兩個參數(shù)獲得在 UI 文本框中鍵入的內容。

最后,在創(chuàng)建指定模型的實例的同時將執(zhí)行工作流。運行庫對象上的 StartWorkflow 方法具有多個重載。代碼中使用的版本接受工作流類型、輸入?yún)?shù)的集合以及系統(tǒng)生成的全局唯一標識符 (GUID)。

對于每個進程,只需要一個工作流運行庫實例;對于每個 AppDomain,不能有一個以上的實例。這里,最需要做的事就是直接在窗體的構造函數(shù)中創(chuàng)建所需的實例。同一個運行庫對象可以照看多種工作流實例。運行庫基于實例的 GUID 來區(qū)分它們,并且為每個特定實例接收私有數(shù)據(jù)。

10. 正在工作的參數(shù)化工作流(由 Windows 窗體應用程序承載)

出于純粹的教學目的,讓我們在這一開發(fā)階段迅速觀察一下設計器和工作流標記代碼。下面是 workflow1.designer.cs 源代碼文件。

public sealed partial class Workflow1 : SequentialWorkflow
{
private void InitializeComponent()
{
ParameterDeclaration FirstName = new ParameterDeclaration();
ParameterDeclaration LastName = new ParameterDeclaration();
this.code1 = new System.Workflow.Activities.Code();
//
// code1
//
this.code1.ID = "code1";
this.code1.ExecuteCode += new System.EventHandler(this.code1_ExecuteCode);
//
// Workflow1
//
this.Activities.Add(this.code1);
this.DynamicUpdateCondition = null;
this.ID = "Workflow1";
FirstName.Direction = System.Workflow.ComponentModel.ParameterDirection.In;
FirstName.Name = "FirstName";
FirstName.Type = typeof(string);
FirstName.Value = null;
LastName.Direction = System.Workflow.ComponentModel.ParameterDirection.In;
LastName.Name = "LastName";
LastName.Type = typeof(string);
LastName.Value = null;
this.Parameters.Add(FirstName);
this.Parameters.Add(LastName);
}
private Code code1;
}

下面是相應的工作流標記內容。

<?Mapping XmlNamespace="ComponentModel"
ClrNamespace="System.Workflow.ComponentModel"
Assembly="System.Workflow.ComponentModel" ?>
<?Mapping XmlNamespace="Compiler" ClrNamespace="System.Workflow.ComponentModel.Compiler"
Assembly="System.Workflow.ComponentModel" ?>
<?Mapping XmlNamespace="Activities" ClrNamespace="System.Workflow.Activities"
Assembly="System.Workflow.Activities" ?>
<?Mapping XmlNamespace="RuleConditions" ClrNamespace="System.Workflow.Activities.Rules"
Assembly="System.Workflow.Activities.Rules" ?>
<SequentialWorkflow x:Class="WorkflowWithParams.Workflow1"
x:CompileWith="Workflow1.xoml.cs"
ID="Workflow1"
xmlns:x="Definition" xmlns="Activities">
<SequentialWorkflow.Parameters>
<wcm:ParameterDeclaration Name="FirstName" Type="System.String" Direction="In"
xmlns:wcm="ComponentModel" />
<wcm:ParameterDeclaration Name="LastName" Type="System.String" Direction="In"
xmlns:wcm="ComponentModel" />
</SequentialWorkflow.Parameters>
<Code ExecuteCode="code1_ExecuteCode" ID="code1" />
</SequentialWorkflow>

請注意,在創(chuàng)建該工作流的實例以便執(zhí)行時,必須顯式初始化并傳入 Parameters 集合中靜態(tài)定義的所有參數(shù)。

工作流運行庫

宿主通過 WorkflowRuntime 類與 Windows Workflow Foundation 交互。請不要被如上所示示例宿主表面上的簡單性所蒙騙,以至于您注意不到這一要點??梢宰屗拗髫撠熖幚砗芏喔郊印㈥P鍵的方面,例如:創(chuàng)建一個或多個進程以及一個或多個 AppDomain;按照需要封送 AppDomain 之間的調用;設置隔離機制。出于可伸縮性的原因,宿主可能需要創(chuàng)建多個進程來利用一臺計算機中的多個 CPU,或者在一個計算機場中運行大量工作流實例。

宿主還可以做其他事情。例如,它可以控制在工作流需要長久等待時應用的策略,偵聽特定事件并將它們傳達給用戶或管理員,設置超時并重試每個工作流,公開性能計數(shù)器,以及寫入日志信息以用于調試和診斷。

主機通過在啟動時向容器注冊的預定義和自定義服務來完成大多數(shù)附加任務。示例宿主沒有做上述任何一件事情,并且僅限于啟動工作流實例。這在很多常見情況下是可以接受的。

工作流和活動

讓我們后退一步,并且在工作流項目處于活動狀態(tài)時分析一下 Visual Studio 2005 工具箱。圖 11 中顯示的工具箱列出了可用來設計步驟的順序及其相互關系,以便形成工作流模型的活動。

圖 11. Windows Workflow Foundation 工作流的構造塊

表 2 提供每個活動的簡短說明,以及這些活動適用于哪些方案。

表 2. Windows Workflow Foundation 構造塊

活動

說明

Code

使您能夠向工作流中添加 Microsoft Visual Basic .NET 或 C# 代碼以執(zhí)行自定義操作。但是,這些代碼不應該用對 Web 服務等外部資源的依賴性來阻塞工作流。

Compensate

使您能夠在發(fā)生錯誤時調用代碼來撤消或者補償已經(jīng)由工作流執(zhí)行的操作。通常,對于現(xiàn)在已被取消的操作,您可能希望向先前已經(jīng)獲得成功通知的用戶發(fā)送電子郵件。

ConditionedActivityGroup (CAG)

使您的工作流能夠基于特定于每個活動的準則有條件地執(zhí)行一組子活動,直到針對 CAG 整體滿足完成條件。子活動相互獨立并可能并行執(zhí)行。

Delay

使您能夠控制工作流的定時以及將延遲內置到工作流。您可以在 Delay 活動上提供超時,以便工作流在恢復執(zhí)行之前暫停。

EventDriven

代表一系列其執(zhí)行由事件觸發(fā)的活動。第一個子活動必須能夠等待外部事件??尚械氖滓踊顒邮?EventSink 和 Delay。在這種情況下,Delay 用作超時。

EventSink

在向 WorkflowRuntime 注冊的數(shù)據(jù)交換服務引發(fā)指定事件時,使工作流能夠從該服務接收數(shù)據(jù)。

ExceptionHandler

使您能夠處理指定類型的異常。ExceptionHandler 活動是其他活動的包裝,在指定的異常發(fā)生時,這些活動實際執(zhí)行所需的任何工作??筛鶕?jù)情況指定一個用于存儲異常的本地變量,并且使其可以在代碼隱藏中使用。

IfElse

使您的工作流能夠有條件地執(zhí)行多個可供選擇的分支之一??稍诿總€分支上放置一個條件,而條件為真的第一個分支將執(zhí)行。無需在最后一個分支上放置條件,因為它被視為“else”分支。

InvokeMethod

使您的工作流能夠調用接口上的方法,以便將消息從工作流發(fā)送到向 WorkflowRuntime 注冊的數(shù)據(jù)交換服務。

InvokeWebService

使您的工作流能夠調用 Web 服務方法。您需要指定要使用的代理類(使用 WSDL),以及您想要調用的方法的名稱。同步和異步調用都受到支持。

InvokeWorkflow

使您的工作流能夠調用或啟動另一個工作流(可達到任意深度)。例如,被調用的工作流可以調用第三個工作流,該工作流又可以調用第四個工作流,等等。遞歸調用不受支持。受支持的調用模型是發(fā)后不理。

Listen

使工作流能夠等待(可能存在的)多個事件之一,或者在指定的超時間隔之后停止等待,并且基于結果分支??上蛎總€分支中添加一個或多個由事件驅動的活動。只有第一個滿足條件的分支被執(zhí)行;其他分支都不會運行。

Parallel

使您的工作流能夠相互獨立地執(zhí)行兩個或更多個操作。該活動在繼續(xù)執(zhí)行之前會等待這些操作終止。

Policy

使您能夠表示或執(zhí)行規(guī)則集合。該活動不在工具箱中;要訪問它的功能,必須創(chuàng)建自定義活動并使用派生。

Replicator

使您的工作流能夠創(chuàng)建給定活動的任意多個實例,并且順序或同時執(zhí)行它們。

SelectData

使您的工作流能夠通過在外部數(shù)據(jù)源對象上定義的方法查詢外部數(shù)據(jù)。當觸發(fā) SelectData 活動時,關聯(lián)的方法將在宿主線程內部執(zhí)行。該方法返回的值被傳遞給工作流。

Sequence

使您能夠協(xié)調一組子活動的連續(xù)執(zhí)行。該序列在最后一個子活動完成之后完成。

SetState

使您的狀態(tài)機工作流能夠指定向新狀態(tài)的轉換。

State

表示狀態(tài)機工作流中的狀態(tài)。

StateInitialization

在 State 活動中,用作在狀態(tài)轉換時執(zhí)行的子活動的容器。

Suspend

掛起工作流的操作,以便能夠在發(fā)生某個錯誤條件時進行干預。當工作流實例掛起時,將記錄錯誤??芍付ㄒ粋€消息字符串來幫助管理員診斷發(fā)生了什么事情。與當前實例關聯(lián)的所有狀態(tài)信息都被保存,并且這些信息會在管理員繼續(xù)執(zhí)行時恢復。

Terminate

使您能夠在發(fā)生任何異常情況時立即結束工作流的操作。如果是在 Parallel 活動內部調用,則所有分支都被突然終止,而無論它們的當前狀態(tài)如何。當工作流終止時,會記錄錯誤,并提供一個消息以幫助管理員弄清楚發(fā)生了什么事情。

Throw

使您能夠引發(fā)指定類型的異常。使用該活動等效于在用戶代碼中引發(fā)異常的代碼處理程序。該活動是引發(fā) .NET 異常的聲明性方式。

TransactionalContext

事務上下文 是用于對活動進行分組的塊。該活動主要用于事務性執(zhí)行、補償和異常處理,可以根據(jù)情況進行同步。通過同步事務性上下文,可確保對活動中共享數(shù)據(jù)的任何訪問都將正確地序列化。

UpdateData

使您的工作流能夠通過在外部數(shù)據(jù)源對象上定義的方法更新數(shù)據(jù)存儲區(qū)。當 UpdateData 活動被觸發(fā)時,關聯(lián)的方法將在宿主線程內部執(zhí)行。

WaitForData

使您的工作流能夠從外部數(shù)據(jù)源對象接收信息。當傳入的數(shù)據(jù)修改綁定數(shù)據(jù)源的狀態(tài)時,該活動被觸發(fā)。傳入的數(shù)據(jù)是通過綁定數(shù)據(jù)源服務接收的。

WaitForQuery

使外部應用程序能夠在您的工作流中查詢數(shù)據(jù)。該活動將在從宿主收到查詢之前一直等待。來自外部應用程序的查詢使用綁定數(shù)據(jù)源服務上的方法提交給工作流。

WebServiceReceive

使作為 Web 服務本身公開的工作流能夠接收 Web 服務請求。

WebServiceResponse

使作為 Web 服務本身公開的工作流能夠響應 Web 服務請求。

While

使您的工作流能夠在一個條件被滿足時執(zhí)行一個或多個活動。在每次迭代之前,都評估該條件。如果為真,則所有子活動都會執(zhí)行;否則,該活動完成??芍付暶餍詶l件或代碼條件。

活動表示使用 Windows Workflow Foundation 進行工作流編程的聲明性方法。使用活動,可在設計時創(chuàng)作工作流模型并將值分配給每個活動的屬性。如果選擇帶有代碼分隔功能的工作流項,則最后的結果會作為 XML 標記保存到具有 .xoml 擴展名的工作流標記文件中。否則,創(chuàng)作的模型將作為對工作流對象模型的一系列調用持久保存在設計器生成的 C# 或 Visual Basic .NET 類文件中。前一種方法類似于 ASP.NET 頁,而后一種方法類似于 Windows 窗體應用程序所采用的方法。

Visual Studio 2005 隱藏了這兩種方法之間的大多數(shù)差異。您總是以可視方式設計工作流,并且 Visual Studio 2005 透明地將您的工作持久保存為兩種不同格式中的一種。如果您選擇采用“僅代碼”解決方案(沒有 XOML 和代碼分隔),則可調整設計器代碼以使其變得更加靈活一些。例如,可讓它從配置文件或數(shù)據(jù)庫中讀取參數(shù)的默認值。如果選擇采用工作流標記和代碼分隔,則在工作流的代碼及其模型之間產(chǎn)生巧妙的分隔。

是否可以用編程方式修改工作流模型?在設計時,可在 Visual Studio 中以編程方式對工作流做您可以做的所有事情。在運行時,對活動集合進行動態(tài)更新也是可以的,而這為您提供了對正在運行的工作流實例進行更改的能力。動態(tài)更改由在設計時未知的業(yè)務更改激發(fā),或由首先修改然后完成業(yè)務過程的業(yè)務邏輯需要激發(fā)。在任何情況下,它都應該只涉及有限的更改 — 完善而不是重新設計。

動態(tài)更新適用于應用程序上下文中的單個工作流實例。同一工作流類型的將來實例將不會受到更改的影響。對工作流實例的動態(tài)更新可以從工作流實例本身中進行,也可以從您的應用程序代碼外部進行。

Windows Workflow Foundation 框架支持 Web 服務互操作性,這包括能夠將工作流作為 Web 服務向 ASP.NET 客戶端和其他工作流公開。Windows Workflow Foundation 支持將工作流作為在 Microsoft IIS 6.0 上運行 ASP.NET 的 Web 服務器或服務器場上的 ASP.NET Web 服務公開。

Windows Workflow Foundation 框架活動集包含 WebServiceReceiveWebServiceResponse 活動,這使工作流能用作 Web 服務終結點。

要想作為 Web 服務公開,工作流必須包含 WebServiceReceive 活動,以便從客戶端獲得傳入的調用。快捷菜單命令將工作流作為 Web 服務發(fā)布,如圖 12 所示。

圖 12. 將工作流作為 Web 服務發(fā)布

開發(fā)自定義活動

Windows Workflow Foundation 中可擴展性的要點是創(chuàng)建自定義活動,因為這使您可以擴展用于生成工作流模型的構造塊集。

讓我們研究一下活動的內部體系結構,方法是開發(fā)一個自定義活動來發(fā)送電子郵件。Windows Workflow Foundation 為自定義活動提供一個現(xiàn)成的 Visual Studio 2005 模板。它的名稱為 Workflow Activity Library。該模板創(chuàng)建一個可任意重命名的 C# 文件 — 例如,可將其重命名為 SendMailActivity?;顒邮菑母割惱^承的普通類。可從任何現(xiàn)有活動(無論它是內置的活動,還是您自己創(chuàng)建或從第三方供應商購買的活動)派生您的活動。顯然,父類向新的組件中添加了預定義的行為。要完全從頭開始生成活動,請讓其從 Activity 派生。下面的代碼示例顯示新類的主干。

public partial class SendMailActivity : System.Workflow.ComponentModel.Activity
{
public SendMailActivity()
{
InitializeComponent();
}
protected override Status Execute(ActivityExecutionContext context)
{
:
}
}

正如您可以猜到的那樣,Execute 方法是該組件的核心 — 即完成該組件的核心任務的位置。

在開發(fā)之后,活動就被放到工具箱中,以供拖放操作將其拖放到新的工作流應用程序中。盡管屬性列表不是必需的,但不帶屬性的活動幾乎沒有任何用處。要添加屬性,您需要在設計器中選擇正在開發(fā)的活動,然后單擊 Properties 窗格上的 Activity Properties 項(參見圖 13)。

圖 13. 向自定義活動中添加屬性

向活動中添加屬性與向工作流中添加參數(shù)并無太大的不同。必須做的工作就是為每個需要的屬性配置名稱和屬性。圖 14 顯示如何向 SendMail 活動中添加 To 屬性。

圖 14. 添加到 SendMail 活動中的 To 屬性

為了完成所有工作,我們添加了其他屬性(如 From、Subject、BodyHost),以便用戶可以完整地配置要發(fā)送的電子郵件。當您添加屬性時,向導會修改包含活動的邏輯在內的 C# 代碼隱藏文件。

最后一個步驟是使 Execute 方法變得充實一些,以指示它在執(zhí)行該活動時發(fā)送電子郵件。

protected override Status Execute(ActivityExecutionContext context)
{
MailAddress toAddress = new MailAddress(To);
MailAddress fromAddress = new MailAddress(From);
MailAddressCollection addresses = new MailAddressCollection();
addresses.Add(toAddress);
MailMessage msg = new MailMessage(fromAddress, toAddress);
msg.Subject = Subject;
msg.Body = Body;
SmtpClient mail = new SmtpClient(Host);
mail.Send(msg);
return Status.Closed;
}

如果在工作流解決方案的內部開發(fā)活動項目,則工作流文檔將自動查找工具箱中列出的新活動,如圖 15 所示。否則,必須通過右鍵單擊工具箱來添加它。

圖 15. SendMail 活動顯示在工具箱中

圖 16 說明 SendMail 活動確實有效。

圖 16. 正在工作的 SendMail 活動

計劃更現(xiàn)實的工作流

讓我們看一下如何組合表 2 中列出的一些活動,從而解決一項更為現(xiàn)實的任務。假設有這樣一個業(yè)務應用程序,其中,訂單在完成之前可能要經(jīng)歷多個狀態(tài)。在典型的方案中,有一些根據(jù)當前狀態(tài)指示訂單中可能發(fā)生某些事件的規(guī)則。例如,可以處理或更新未完成的訂單,但不能將其取消或發(fā)送。

當事件發(fā)生時,狀態(tài)機工作流將轉換訂單的狀態(tài)。例如,當訂單未完成并且 BeingProcessed 事件發(fā)生時,狀態(tài)機工作流會將訂單轉換到正確的狀態(tài)。圖 17 顯示示例訂單狀態(tài)機工作流的關系圖。

圖 17. 管理訂單的狀態(tài)機的示例架構

讓我們首先創(chuàng)建一個狀態(tài)機工作流。您將使用 State 活動對訂單的可能狀態(tài)進行建模。然后,通過使用 EventDriven 活動指定可以從每個狀態(tài)發(fā)生的事件。通過自定義服務產(chǎn)生的外部事件將轉換訂單的狀態(tài)。要執(zhí)行轉換,需要使用 SetState 活動。創(chuàng)作工作流后,需要使用 Windows 窗體宿主應用程序來檢驗它。

工作流通過專門為此目的建立的服務與外部世界通信。該服務會引發(fā)工作流內的事件驅動活動將掛鉤到的事件。同樣,該服務公開了供該工作流調用的公共方法并向主機發(fā)送數(shù)據(jù)。方法和事件在接口中定義。該接口也稱為數(shù)據(jù)交換服務。每當工作流與外部組件交互(進行輸入和輸出)時,您都需要該服務。

數(shù)據(jù)交換服務是常規(guī)的 .NET 類庫,它最起碼包含一個接口定義以及一個實現(xiàn)該接口的類。該接口是為您希望表示的任務定制的。在此情況下,狀態(tài)機表示訂單的生存期,該接口包含五個事件。

[DataExchangeService]
public interface IOrderService
{
event EventHandler OrderCreated;
event EventHandler OrderShipped;
event EventHandler OrderUpdated;
event EventHandler OrderProcessed;
event EventHandler OrderCanceled;
}

[DataExchangeService] 屬性將 IOrderService 標記為數(shù)據(jù)交換服務接口,以便工作流運行庫知道它將用來與工作流實例交換數(shù)據(jù)。在此情況下,宿主將向一串 EventDriven 活動引發(fā)事件,從而向工作流實例發(fā)送數(shù)據(jù)。如果需要,可通過 InvokeMethod 活動從工作流實例內部調用 IOrderService 接口中的方法。

該接口中的事件聲明使用泛型,這是 .NET Framework 2.0 中的一項非常熱門的新功能。EventHandler 類是一個委托,它表示用于處理事件的函數(shù)的原型。在 .NET Framework 1.x 中,EventHandler 按如下方式定義。

void EventHandler(object sender, EventArgs e)

要使該事件傳遞自定義數(shù)據(jù)結構(例如,OrderEventArgs),必須創(chuàng)建一個新的委托并使用它來替代 EventHandler。下面是一個示例。

delegate void OrderEventHandler(object sender, OrderEventArgs e)

該模式在 .NET Framework 2.0 中仍然有效。然而,.NET Framework 2.0 中泛型的出現(xiàn)使您無需顯式定義(和實例化)新的委托類即可獲得相同的結果。您將使用 EventHandler 委托的泛型版本,其中,事件數(shù)據(jù)的類型是參數(shù)。

事件傳遞 OrderEventArgs 類型的客戶端數(shù)據(jù),該類型是一個從 Windows Workflow Foundation WorkflowMessageEventArgs 類派生的自定義類,后者在同一個程序集中按如下方式定義。

[Serializable]
public class OrderEventArgs : WorkflowMessageEventArgs
{
private string _orderId;
public OrderEventArgs(Guid instanceId, string orderId) : base(instanceId)
{
_orderId = orderId;
}
public string OrderId
{
get { return _orderId; }
set { _orderId = value; }
}
}

下一步,需要定義一個實現(xiàn)該接口的類。該類所引發(fā)的公共方法與接口中引發(fā)的事件一樣多。

public class OrderService : IOrderService
{
public OrderService()
{
}
public void RaiseOrderCreatedEvent(string orderId, Guid instanceId)
{
if (OrderCreated != null)
OrderCreated(null, new OrderEventArgs(instanceId, orderId));
}
public void RaiseOrderShippedEvent(string orderId, Guid instanceId)
{
if (OrderShipped != null)
OrderShipped(null, new OrderEventArgs(instanceId, orderId));
}
public void RaiseOrderUpdatedEvent(string orderId, Guid instanceId)
{
if (OrderUpdated != null)
OrderUpdated(null, new OrderEventArgs(instanceId, orderId));
}
public void RaiseOrderProcessedEvent(string orderId, Guid instanceId)
{
if (OrderProcessed != null)
OrderProcessed(null, new OrderEventArgs(instanceId, orderId));
}
public void RaiseOrderCanceledEvent(string orderId, Guid instanceId)
{
if (OrderCanceled != null)
OrderCanceled(null, new OrderEventArgs(instanceId, orderId));
}
public event EventHandler OrderCreated;
public event EventHandler OrderShipped;
public event EventHandler OrderUpdated;
public event EventHandler OrderProcessed;
public event EventHandler OrderCanceled;
}

現(xiàn)在,需要用訂單服務編譯該程序集,并重新切換到狀態(tài)機工作流項目。在該工作流項目中,首先需要添加對新創(chuàng)建程序集的引用。接下來,需要添加四個 State 活動并且按如下方式命名它們: WaitingForOrderState、OrderOpenStateOrderProcessedState、OrderCompletedState。

表 3 表示工作流的狀態(tài)關系圖。每個狀態(tài)都有一些能夠導致向另一個狀態(tài)進行轉換的事件。

表 3. 訂單的示例狀態(tài)機

狀態(tài)

受支持的事件

轉換到

WaitingForOrderState

OrderCreated

OrderOpenState

OrderOpenState

OrderUpdated

OrderOpenState

 

OrderProcessed

OrderProcessedState

OrderProcessedState

OrderUpdated

OrderOpenState

 

OrderCanceled

Terminate 活動

 

OrderShipped

OrderCompletedState

OrderCompletedState

   

要實現(xiàn)該關系圖,需要向每個 State 活動中添加與該表中受支持事件相同數(shù)目的 EventDriven 塊。例如,名為 WaitingForOrderStateState 活動將包含單個 EventDriven 活動(該活動的名稱可以是任意的,例如 OrderCreatedEvent)。如圖 18 所示,EventDriven 活動嵌入一個 EventSink 活動和一個 SetState 活動,以便捕獲外部事件并轉換到新的狀態(tài)。

圖 18. OrderCreatedEvent EventDriven 活動的內部視圖

EventSink 活動的 Properties 窗格上,可選擇自己喜歡的數(shù)據(jù)交換服務(在此情況下為 IOrderService 接口)以及要預訂的事件名稱。如果單擊 EventSink 活動 Properties 窗格上的 InterfaceType 項,則 Visual Studio 2005 將提供該項目可用的數(shù)據(jù)交換服務列表。選擇服務后,EventName 屬性反映由該服務所公開事件的列表??蛇x擇自己感興趣的事件并繼續(xù)。對于 OrderCreatedEvent 活動,可選擇 OrderCreated 事件。

SetState 活動將狀態(tài)機轉換到由其 TargetState 屬性指示的新狀態(tài)。圖 18 中的 SetState 活動設置為 OrderOpenState。

可對表 3 中的所有狀態(tài)和事件接收器重復執(zhí)行上述操作。最后,您的工作流應該如圖 19 所示。

圖 19. 最終完成的訂單狀態(tài)機

最后一個步驟涉及到生成 Windows 窗體應用程序以測試該工作流。用戶界面包含一個用于跟蹤所有未完成訂單的列表視圖,以及用于創(chuàng)建新訂單的文本框和按鈕。其他按鈕將用來更新、處理和終止該訂單。

狀態(tài)機工作流在 Form_Load 事件中初始化。狀態(tài)機工作流的初始化要比順序工作流復雜一些,尤其是當您希望能夠跟蹤狀態(tài)更改的時候。下面的代碼示例顯示如何初始化工作流運行庫。

private void StartWorkflowRuntime()
{
// Create a new Workflow Runtime for this application
_runtime = new WorkflowRuntime();
// Register event handlers for the WorkflowRuntime object
_runtime.WorkflowTerminated += new
EventHandler(WorkflowRuntime_WorkflowTerminated);
_runtime.WorkflowCompleted += new
EventHandler(WorkflowRuntime_WorkflowCompleted);
// Create a new instance of the StateMachineTrackingService class
_stateMachineTrackingService = new StateMachineTrackingService(_runtime);
// Start the workflow runtime
_runtime.StartRuntime();
// Add a new instance of the OrderService to the runtime
_orderService = new OrderService();
_runtime.AddService(_orderService);
}

StateMachineTrackingService 在運行庫之上工作,并且用跟蹤工作流中狀態(tài)更改的功能來擴展它。數(shù)據(jù)交換服務的實例還被添加到運行庫中。

當用戶單擊以創(chuàng)建新訂單時,將執(zhí)行下面的代碼。

private Guid StartOrderWorkflow(string orderID)
{
// Create a new GUID for the WorkflowInstanceId
Guid instanceID = Guid.NewGuid();
// Load the OrderWorkflows assembly
Assembly asm = Assembly.Load("OrderWorkflows");
// Get a type reference to the OrderWorkflows.Workflow1 class
Type workflowType = asm.GetType("OrderWorkflows.Workflow1");
// Start a new instance of the state machine with state tracking support
StateMachineInstance stateMachine =
_stateMachineTrackingService.RegisterInstance(workflowType, instanceID);
stateMachine.StateChanged += new
EventHandler(StateMachine_StateChanged);
stateMachine.StartWorkflow();
_stateMachineInstances.Add(instanceID.ToString(), stateMachine);
// Return the workflow GUID
return instanceID;
}

首先,代碼實例化工作流實例并注冊狀態(tài)更改的事件處理程序。請注意,使用 .NET Reflection 來獲得類型信息并不是絕對需要的,但這可以大大提高靈活性。普通的舊運算符 typeof 也可以很好地將工作流實例的類型傳遞給工作流運行庫。

圖 20 顯示正在工作的示例應用程序。按鈕是基于所選工作流實例的狀態(tài)而啟用的。

圖 20. Windows 窗體應用程序中承載的狀態(tài)機工作流

當用戶單擊給定按鈕時,通信接口上的相應事件將被引發(fā)并被工作流的事件接收器捕獲。例如,對處于掛起狀態(tài)的工作流實例而言,其 Order Processed 按鈕的單擊事件將按如下方式處理。

private void btnOrderEvent_Click(object sender, EventArgs e)
{
// Get the name of the clicked button
string buttonName = ((Button)sender).Name;
// Get the GUID of the selected order
Guid instanceID = GetSelectedWorkflowInstanceID();
// Get the ID of the selected order
string orderID = GetSelectedOrderID();
// Disable buttons before proceeding
DisableButtons();
// Determines what to do based on the name of the clicked button
switch(buttonName)
{
// Raise an OrderShipped event using the Order Local Service
case "btnOrderShipped":
_orderService.RaiseOrderShippedEvent(orderID, instanceID);
break;
// Raise an OrderUpdated event using the Order Local Service
case "btnOrderUpdated":
_orderService.RaiseOrderUpdatedEvent(orderID, instanceID);
break;
// Raise an OrderCanceled event using the Order Local Service
case "btnOrderCanceled":
_orderService.RaiseOrderCanceledEvent(orderID, instanceID);
break;
// Raise an OrderProcessed event using the Order Local Service
case "btnOrderProcessed":
_orderService.RaiseOrderProcessedEvent(orderID, instanceID);
break;
}
}

該工作流中引發(fā)的事件由圖 21 中所示的 EventDriven 活動捕獲。

圖 21. 狀態(tài)機用于處理 Order Processed 事件的 EventDriven 塊

EventSink 活動捕獲該事件,并且通過轉換到 SetState 活動所設置的狀態(tài)來處理它。工作流中的狀態(tài)更改由附加的狀態(tài)跟蹤服務檢測,并通過 StateChanged 事件報告給宿主,如上述代碼清單所示。

您可以在 http://msdn.microsoft.com/workflow 找到本文中討論的全部示例的完整源代碼,以及更多的工作流內容。

小結

Windows Workflow Foundation 旨在成為新的和現(xiàn)有的 Microsoft 產(chǎn)品的工作流框架,它向所有需要為 .NET 平臺創(chuàng)建工作流驅動應用程序的開發(fā)人員提供了 WinFX 的強大功能和 Visual Studio 2005 的易用性。

Windows Workflow Foundation 為工作臺帶來的主要好處是統(tǒng)一的工作流模型和一組能夠取代很多專用庫的工具。在這方面,Windows Workflow Foundation 對于目前工作流產(chǎn)品的供應商也具有重要意義,因為采用 Windows Workflow Foundation 則意味著他們不必再維護其低級別的代碼,并且可以集中力量去完成更高級別的任務。

Windows Workflow Foundation 是一種面向多種特定應用程序和需要的工作流技術。Windows Workflow Foundation 因而成為一種廣泛的框架,它是為提高每個級別的可擴展性而設計的。這種形式的可擴展性的最佳示例是自定義活動和可插接的運行庫服務。自定義活動使您可擴展可用來創(chuàng)作工作流的構造塊集??筛某志眯源鎯透櫟冗\行庫服務以適應應用程序的環(huán)境,并可使應用程序將數(shù)據(jù)持久存儲到 Microsoft SQL Server 或其他供應商的數(shù)據(jù)庫中。

Windows Workflow Foundation 的 Visual Studio 2005 擴展將允許對工作流進行可視化建模和直接代碼訪問。

還可以在其他設計環(huán)境中承載可視化設計器,從而使設計器提供商可以將可視化建模功能嵌入到其自己的環(huán)境中,并且提供應用程序用戶所熟悉的用戶體驗。

本文僅僅討論了所有 Windows Workflow Foundation 技術和功能中的一些粗淺知識,提供了有關其工作方式、內部原理的概述和一些有代表性的示例代碼。

關于作者

Dino Esposito 是一位居住于意大利羅馬的培訓師和顧問。作為 Wintellect 團隊的成員,Dino 專門研究 ASP.NET 和 ADO.NET,并且花費大部分時間在歐洲和美國進行教學和咨詢活動。值得一提的是,Dino 為 Wintellect 管理 ADO.NET 課件,同時為 MSDN Magazine 撰寫 Cutting Edge 專欄。

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多