有網友問如何在web中使用WF。今天我將實現一個完整的示例。這個示例將包括WF4.0的大部分知識點。包括:
1、持久化服務
2、跟蹤服務
3、自定義擴展
4、WCF Workflow Service
5、WorkflowServiceHost
6、使用Interop活動去調用WF3.0工作流程
效果:
我先描述一下這個示例的功能,然後演示一下這個示例的功能,然後進一步的說明如何去實現。
這個示例是一個任務隊列,這個示例在客戶端有兩個aspx頁面。一個是用於用戶輸入請求的頁面,這個請求會根據你選擇的分類將這個 任務分入到不同的任務隊列。第二個頁面用於處理這些請求。這些不同分類的隊列有兩種處理方式,沒一個隊列對應一種處理方式,一種是 大家熟知的先進先出的方式。每次都是處理最先提交的請求,程序自動遷出最老的任務給你處理,第二種是,你選擇這個任務隊列,程序就 會顯示這個隊列所有的任務,然後你選擇一個任務進行處理。
這個示例中一定有四個任務隊列:Product,Service,Marketing,General。這些任務隊列的處理方式,你可以自己設置。當你提交一 個請求之後,程序會根據的你在第一個頁面上選擇的分類將這個請求歸入不同的隊列。在再第二個頁面進行處理。第二個頁面的處理方式有 三種:
第一種:將這個任務指定到另外一個任務隊列中
第二種:不指定給另外一個處理隊列,直接處理,流程結束
第三種:取消處理,將從任務隊列中取出的任務歸還回去
當你采用第一種方式處理的時候。就將這個任務規劃到另外一個隊列當中。此時,你需要在另外的這個隊列中將任務遷出然後進行處理 ,處理方式也是以上三種。如果你選擇第二種,流程完成。
這個例子有點類似工作流中的加簽流程。你可以無限的加簽。
以上是簡單的描述示例的功能,下面我將用截圖的方式展示一下這個示例:
登錄界面:
點擊導航條上的Submit,在Category下拉框中選擇一項,填寫Comments,點擊提交,如下圖:
流程啟動成功,顯示Guid,如下圖:
在任務處理頁面上,將多出一筆任務;
上面已經有三個任務隊列存在任務了。任務隊列General有2筆任務待處理。QC的意思是是否要進行質檢。這三個隊列中,Marketing隊列 處理的方式是列出所有的任務供你選擇,其他兩個隊列的處理方式是先進先出。
點擊Marketing的select,將這個隊列的三個任務出現在下面的列表中,供你選擇其中的一個進行處理:
而點擊General的select,直接將最老的任務遷出:
我們將General的任務分配給隊列Seivice,如下圖:
你會發現Service多出一任務:
演示到此結束。下面我將敘述如何去實現以及用到的WF4.0中的所有的知識點。
實現篇:
設計數據庫:
數據庫操作使用的是Linq,看下上面這張截圖。上面說的4中隊列數據存儲在SubQueue中,Queue是SubQueue的父表。就存了一條數據。 QueueInstance是業務邏輯的主表。QueueTrack用於存儲跟蹤信息,包括:start、Assign、Route、UnAssign。 OperateConfig表用於存放 WF3.0活動的配置信息。
你用VS2010打開附件的代碼,你會發現:
代碼分了五個項目,為了增加代碼的重用性。
1、RequestWeb用於是一個Asp.net應用程序,用於提交任務和處理任務。
2、QCPolicy是一個WF3.0的項目,這裡我講解一下。
這個流程用於判斷是否需要進行QC,它將用到下面三張數據表進行判斷:
WF3.0這個工作流用到了ReviewPolicy活動,如果你對WF3.0也熟悉的話,應該就知道這個用這個活動設置判斷的業務規則。WF4.0現在已 經不采用這種方式了,設置如下圖。
3、TestQC是一個測試項目,測試QCPolicy。
4、UserTasks定義了一些工作流活動。
5、ServiceLayer是一個webservice項目。
持久化服務
持久化服務能將運行的工作流程保存到數據庫中。這個例子的持久化服務是在WorkflowServiceHost中配置的。用了微軟持久化服務,在 數據庫中運行SqlWorkflowInstanceStoreSchema.sql和SqlWorkflowInstanceStoreLogic.sql兩個腳本,創建持久化數據表。
web.config配置:
<behavior>
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="True"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="True"/>
<!-- This line configures the persistence service -->
<sqlWorkflowInstanceStore
connectionStringName="Request"
instanceCompletionAction="DeleteAll"
instanceLockedExceptionAction="NoRetry"
instanceEncodingOption="GZip"
hostLockRenewalPeriod="00:00:30" />
<workflowIdle
timeToUnload="00:00:10"
timeToPersist="00:00:05" />
<!-- Configure the connection string for the persistence extensions-->
<dbConnection connectionStringName="Request"/>
<persistRequest connectionStringName="Request"/>
<persistQueueInstance connectionStringName="Request"/>
<tracking connectionStringName="Request"/>
</behavior>
看上面的代碼,connectionStringName="Request"指定持久化的連接字符串。
instanceCompletionAction="DeleteAll"指定工作流完成之後刪除持久化數據。
自定義擴展。
使用自定義擴展,需要先定義擴展,然後在將這個擴展服務添加到運行時中。這個例子中一共定義了四個自定義擴展。
以最簡單的為例:DBConnection。這個用於在工作流內能取到連接字符串。
定義擴展,分三個類:
/*****************************************************/
// The extension class is used to define the behavior
/*****************************************************/
public class DBConnectionExtension : BehaviorExtensionElement
{
public DBConnectionExtension()
{
Console.WriteLine("Behavior extension started");
}
[ConfigurationProperty("connectionStringName", DefaultValue = "",
IsKey = false, IsRequired = true)]
public string ConnectionStringName
{
get { return (string)this["connectionStringName"]; }
set { this["connectionStringName"] = value; }
}
public string ConnectionString
{
get
{
ConnectionStringSettingsCollection connectionStrings =
WebConfigurationManager.ConnectionStrings;
if (connectionStrings == null) return null;
string connectionString = null;
if (connectionStrings[ConnectionStringName] != null)
{
connectionString =
connectionStrings[ConnectionStringName].ConnectionString;
}
if (connectionString == null)
{
throw new ConfigurationErrorsException
("Connection string is required");
}
return connectionString;
}
}
public override Type BehaviorType
{
get { return typeof(DBConnectionBehavior); }
}
protected override object CreateBehavior()
{
return new DBConnectionBehavior(ConnectionString);
}
}
/*****************************************************/
// The behavior class is used to create an extension
// for each new instance
/*****************************************************/
public class DBConnectionBehavior : IServiceBehavior
{
string _connectionString;
public DBConnectionBehavior(string connectionString)
{
this._connectionString = connectionString;
}
public virtual void ApplyDispatchBehavior
(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
WorkflowServiceHost workflowServiceHost
= serviceHostBase as WorkflowServiceHost;
if (null != workflowServiceHost)
{
string workflowDisplayName
= workflowServiceHost.Activity.DisplayName;
workflowServiceHost.WorkflowExtensions.Add(()
=> new DBConnection(_connectionString));
}
}
public virtual void AddBindingParameters
(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase,
Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters)
{
}
public virtual void Validate
(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
}
}
/*****************************************************/
// This is the actual extension class
/*****************************************************/
public class DBConnection
{
private string _connectionString = "";
public DBConnection(string connectionString)
{
_connectionString = connectionString;
}
public string ConnectionString { get { return _connectionString; } }
}
在web.config中進行配置來添加擴展:
<extensions>
<behaviorExtensions>
<add name="dbConnection" type="UserTasks.Extensions.DBConnectionExtension, UserTasks, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
如何使用這個擴展,看下面的例子:
DBConnection ext = context.GetExtension<DBConnection>();
if (ext == null)
throw new InvalidProgramException("No connection string available");
RequestDataContext dc = new RequestDataContext(ext.ConnectionString);
跟蹤服務:
跟蹤服務其實就是一個自定義的擴展,先看定義也分三個類:
/*****************************************************/
// The extension class is used to define the behavior
/*****************************************************/
public class QueueTrackingExtension : BehaviorExtensionElement
{
public QueueTrackingExtension()
{
Console.WriteLine("Behavior extension started");
}
[ConfigurationProperty("connectionStringName", DefaultValue = "", IsKey = false, IsRequired = true)]
public string ConnectionStringName
{
get { return (string)this["connectionStringName"]; }
set { this["connectionStringName"] = value; }
}
public string ConnectionString
{
get
{
ConnectionStringSettingsCollection connectionStrings = WebConfigurationManager.ConnectionStrings;
if (connectionStrings == null) return null;
string connectionString = null;
if (connectionStrings[ConnectionStringName] != null)
{
connectionString = connectionStrings[ConnectionStringName].ConnectionString;
}
if (connectionString == null)
{
throw new ConfigurationErrorsException("Connection string is required");
}
return connectionString;
}
}
public override Type BehaviorType { get { return typeof(QueueTrackingBehavior); } }
protected override object CreateBehavior() { return new QueueTrackingBehavior(ConnectionString); }
}
/*****************************************************/
// The behavior class is used to create an exention for
// each new instance
/*****************************************************/
public class QueueTrackingBehavior : IServiceBehavior
{
string _connectionString;
public QueueTrackingBehavior(string connectionString)
{
this._connectionString = connectionString;
}
public virtual void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
WorkflowServiceHost workflowServiceHost = serviceHostBase as WorkflowServiceHost;
if (null != workflowServiceHost)
{
string workflowDisplayName = workflowServiceHost.Activity.DisplayName;
workflowServiceHost.WorkflowExtensions.Add(()
=> new QueueTracking(_connectionString));
}
}
public virtual void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { }
public virtual void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
}
/*****************************************************/
// This is the actual extension class
/*****************************************************/
public class QueueTracking : TrackingParticipant
{
private string _connectionString = "";
public QueueTracking(string connectionString)
{
_connectionString = connectionString;
}
protected override void Track(TrackingRecord record, TimeSpan timeout)
{
CustomTrackingRecord customTrackingRecord =
record as CustomTrackingRecord;
if (customTrackingRecord != null)
{
if (customTrackingRecord.Name == "Start" ||
customTrackingRecord.Name == "Route" ||
customTrackingRecord.Name == "Assign" ||
customTrackingRecord.Name == "UnAssign" ||
customTrackingRecord.Name == "QC")
{
QueueTrack t = new QueueTrack();
// Extract all the user data
if ((customTrackingRecord != null) &&
(customTrackingRecord.Data.Count > 0))
{
foreach (string key in customTrackingRecord.Data.Keys)
{
switch (key)
{
case "QueueInstanceKey":
if (customTrackingRecord.Data[key] != null)
t.QueueInstanceKey = (Guid)customTrackingRecord.Data[key];
break;
case "SubQueueID":
if (customTrackingRecord.Data[key] != null)
t.SubQueueID = (int)customTrackingRecord.Data[key];
break;
case "QC":
if (customTrackingRecord.Data[key] != null)
t.QC = (bool)customTrackingRecord.Data[key];
break;
case "OperatorKey":
if (customTrackingRecord.Data[key] != null)
t.OperatorKey = (Guid)customTrackingRecord.Data[key];
break;
}
}
}
if (t.SubQueueID != null && t.QC == null)
t.QC = false;
t.EventType = customTrackingRecord.Name;
t.EventDate = DateTime.UtcNow;
// Insert a record into the TrackUser table
UserTasksDataContext dc =
new UserTasksDataContext(_connectionString);
dc.QueueTracks.InsertOnSubmit(t);
dc.SubmitChanges();
}
}
}
}
web.config中配置:
<add name="tracking" type="UserTasks.Extensions.QueueTrackingExtension, UserTasks, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
使用:
// Add a custom track record
看上圖,用了一個Pick 與4個PickBranch,每一個PickBranch裡面是一個或者多個ReceiveAndSendReply。QuseuStats用於返回每個 任務隊列的任務數量。
CustomTrackingRecord userRecord = new CustomTrackingRecord("Assign")
{
Data =
{
{"QueueInstanceKey", qi.QueueInstanceKey},
{"OperatorKey", OperatorKey.Get(context)},
{"SubQueueID", qi.CurrentSubQueueID},
{"QC", qi.QC}
}
};
// Emit the custom tracking record
context.Track(userRecord);
主流 程:
GetRequest用於返回任務列表。LoadRequest用於返回具體的某項任務數據。
主要的邏輯是在Submit中。雙擊進入Submit,看 Submit的第一部分:
上圖這部分用於將客戶端請求的數據保存到數據庫中,並創建一個Queus實例。
這是一個while循環。這樣就能無限制的將任務劃分到其他任務隊列下面。
上圖是第三部分。它也在while循環之中。Complete Request對應asp.net應用程序中的處理請求頁面的Complete按鈕。Unassign Request對應Cancel按鈕。Timeout時間設置為5分鐘,如果5分鐘不處理,就持久化到數據庫中。
以上的定義的工作流用到了UserTasks和ServiceLayer中的自定義活動,這些自定義活動都是CodeAcitivity類型的。
以一個自定義活動CreateRequest為例,代碼如下:
public sealed class CreateRequest : CodeActivity
{
public InArgument<string> RequestType { get; set; }
public InArgument<string> UserName { get; set; }
public InArgument<string> UserEmail { get; set; }
public InArgument<string> Comment { get; set; }
public InArgument<Guid> QueueInstanceKey { get; set; }
public InArgument<Guid> RequestKey { get; set; }
protected override void Execute(CodeActivityContext context)
{
// Get the connection string
DBConnection ext = context.GetExtension<DBConnection>();
if (ext == null)
throw new InvalidProgramException("No connection string available");
RequestDataContext dc = new RequestDataContext(ext.ConnectionString);
// Create and initialize a Request object
Request r = new Request();
r.UserName = UserName.Get(context);
r.UserEmail = UserEmail.Get(context);
r.RequestType = RequestType.Get(context);
r.Comment = Comment.Get(context);
r.CreateDate = DateTime.UtcNow;
r.RequestKey = RequestKey.Get(context);
r.QueueInstanceKey = QueueInstanceKey.Get(context);
// Insert the Request record
PersistRequest persist = context.GetExtension<PersistRequest>();
persist.AddRequest(r);
}
}
總結:這是一個完整的工作流的例子,用到了WF4.0的大部分功能。其他的具體看代碼吧,寫得很累,有任何問題可以給我留言。
PS:這個例子是Beginning WF: Windows Workflow in .NET 4.0最後一章的例子。我看了很久才看懂,寫下一篇文章。
本文配套源碼