有网友问如何在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 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); 主流程:
看上图,用了一个Pick 与4个PickBranch,每一个PickBranch里面是一个或者多个ReceiveAndSendReply。QuseuStats用于返回每个任务队列的任务数量。
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的大部分功能。其他的具体看代码吧,写得很累,有任何问题可以给我留言。
本文转自麒麟博客园博客,原文链接:http://www.cnblogs.com/zhuqil/archive/2010/05/08/WFQueueList.html,如需转载请自行联系原作者