WF4.0实战(十四):ASP.NET结合WF4.0完整示例

简介:

有网友问如何在web中使用WF。今天我将实现一个完整的示例。这个示例将包括WF4.0的大部分知识点。包括:

1、持久化服务

2、跟踪服务

3、自定义扩展

4、WCF Workflow Service

5、WorkflowServiceHost

6、使用Interop活动去调用WF3.0工作流程

效果:

    我先描述一下这个示例的功能,然后演示一下这个示例的功能,然后进一步的说明如何去实现。

    这个示例是一个任务队列,这个示例在客户端有两个aspx页面。一个是用于用户输入请求的页面,这个请求会根据你选择的分类将这个任务分入到不同的任务队列。第二个页面用于处理这些请求。这些不同分类的队列有两种处理方式,没一个队列对应一种处理方式,一种是大家熟知的先进先出的方式。每次都是处理最先提交的请求,程序自动迁出最老的任务给你处理,第二种是,你选择这个任务队列,程序就会显示这个队列所有的任务,然后你选择一个任务进行处理。

    这个示例中一定有四个任务队列:Product,Service,Marketing,General。这些任务队列的处理方式,你可以自己设置。当你提交一个请求之后,程序会根据的你在第一个页面上选择的分类将这个请求归入不同的队列。在再第二个页面进行处理。第二个页面的处理方式有三种:

    第一种:将这个任务指定到另外一个任务队列中

    第二种:不指定给另外一个处理队列,直接处理,流程结束

    第三种:取消处理,将从任务队列中取出的任务归还回去

    当你采用第一种方式处理的时候。就将这个任务规划到另外一个队列当中。此时,你需要在另外的这个队列中将任务迁出然后进行处理,处理方式也是以上三种。如果你选择第二种,流程完成。

    这个例子有点类似工作流中的加签流程。你可以无限的加签。

    以上是简单的描述示例的功能,下面我将用截图的方式展示一下这个示例:

    登录界面:

wf4demo1

    点击导航条上的Submit,在Category下拉框中选择一项,填写Comments,点击提交,如下图:

wfdemo2

    流程启动成功,显示Guid,如下图:

wf4demo3

    在任务处理页面上,将多出一笔任务;

wfdemo4

    上面已经有三个任务队列存在任务了。任务队列General有2笔任务待处理。QC的意思是是否要进行质检。这三个队列中,Marketing队列处理的方式是列出所有的任务供你选择,其他两个队列的处理方式是先进先出。

    点击Marketing的select,将这个队列的三个任务出现在下面的列表中,供你选择其中的一个进行处理:

wfdemo5

    而点击General的select,直接将最老的任务迁出:

wfdemo6

 

 

 

 

    我们将General的任务分配给队列Seivice,如下图:

wfdemo7

    你会发现Service多出一任务:

wfdemo8

    演示到此结束。下面我将叙述如何去实现以及用到的WF4.0中的所有的知识点。

实现篇:

 

设计数据库:

wcdemo9

    数据库操作使用的是Linq,看下上面这张截图。上面说的4中队列数据存储在SubQueue中,Queue是SubQueue的父表。就存了一条数据。QueueInstance是业务逻辑的主表。QueueTrack用于存储跟踪信息,包括:start、Assign、Route、UnAssign。OperateConfig表用于存放WF3.0活动的配置信息。

你用VS2010打开附件的代码,你会发现:

wfdemo10

代码分了五个项目,为了增加代码的重用性。

1、RequestWeb用于是一个Asp.net应用程序,用于提交任务和处理任务。

2、QCPolicy是一个WF3.0的项目,这里我讲解一下。

wfdemo11

这个流程用于判断是否需要进行QC,它将用到下面三张数据表进行判断:

wfdemo12 

WF3.0这个工作流用到了ReviewPolicy活动,如果你对WF3.0也熟悉的话,应该就知道这个用这个活动设置判断的业务规则。WF4.0现在已经不采用这种方式了,设置如下图。

wfdemo13

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);
主流程:
wfdemo14 

看上图,用了一个Pick 与4个PickBranch,每一个PickBranch里面是一个或者多个ReceiveAndSendReply。QuseuStats用于返回每个任务队列的任务数量。

GetRequest用于返回任务列表。LoadRequest用于返回具体的某项任务数据。

主要的逻辑是在Submit中。双击进入Submit,看Submit的第一部分:

wfdemo15

上图这部分用于将客户端请求的数据保存到数据库中,并创建一个Queus实例。

wfdemo18

    这是一个while循环。这样就能无限制的将任务划分到其他任务队列下面。

wfdemo19

    上图是第三部分。它也在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,如需转载请自行联系原作者

相关文章
|
7月前
|
开发框架 前端开发 .NET
七天.NET 8操作SQLite入门到实战 - (1)第七天BootstrapBlazor UI组件库引入
七天.NET 8操作SQLite入门到实战 - (1)第七天BootstrapBlazor UI组件库引入
|
4月前
|
API
【Azure 媒体服务】Media Service的编码示例 -- 创建缩略图子画面的.NET代码调试问题
【Azure 媒体服务】Media Service的编码示例 -- 创建缩略图子画面的.NET代码调试问题
|
1天前
|
开发框架 搜索推荐 算法
一个包含了 50+ C#/.NET编程技巧实战练习教程
一个包含了 50+ C#/.NET编程技巧实战练习教程
46 18
|
1月前
|
消息中间件 开发框架 .NET
.NET 8 强大功能 IHostedService 与 BackgroundService 实战
【11月更文挑战第7天】本文介绍了 ASP.NET Core 中的 `IHostedService` 和 `BackgroundService` 接口及其用途。`IHostedService` 定义了 `StartAsync` 和 `StopAsync` 方法,用于在应用启动和停止时执行异步操作,适用于资源初始化和清理等任务。`BackgroundService` 是 `IHostedService` 的抽象实现,简化了后台任务的编写,通过 `ExecuteAsync` 方法实现长时间运行的任务逻辑。文章还提供了创建和注册这两个服务的实战步骤,帮助开发者在实际项目中应用这些功能。
|
2月前
|
开发框架 NoSQL MongoDB
C#/.NET/.NET Core开发实战教程集合
C#/.NET/.NET Core开发实战教程集合
|
7月前
|
开发框架 .NET 中间件
七天.NET 8操作SQLite入门到实战 - (2)第七天Blazor班级管理页面编写和接口对接
七天.NET 8操作SQLite入门到实战 - (2)第七天Blazor班级管理页面编写和接口对接
163 7
|
3月前
|
SQL 关系型数据库 数据库
七天.NET 8操作SQLite入门到实战详细教程(选型、开发、发布、部署)
七天.NET 8操作SQLite入门到实战详细教程(选型、开发、发布、部署)
|
4月前
|
测试技术 API 开发者
.NET单元测试框架大比拼:MSTest、xUnit与NUnit的实战较量与选择指南
【8月更文挑战第28天】单元测试是软件开发中不可或缺的一环,它能够确保代码的质量和稳定性。在.NET生态系统中,MSTest、xUnit和NUnit是最为流行的单元测试框架。本文将对这三种测试框架进行全面解析,并通过示例代码展示它们的基本用法和特点。
442 8
|
4月前
|
开发框架 缓存 前端开发
实战.NET Framework 迁移到 .NET 5/6
从.NET Framework 迁移到.NET 5/6 是一次重要的技术革新,涵盖开发环境与应用架构的全面升级。本文通过具体案例详细解析迁移流程,包括评估现有应用、利用.NET Portability Analyzer 工具识别可移植代码、创建新项目、逐步迁移代码及处理依赖项更新等关键步骤。特别关注命名空间调整、JSON 序列化工具更换及数据库访问层重构等内容,旨在帮助开发者掌握最佳实践,确保迁移过程平稳高效,同时提升应用性能与可维护性。
160 2
|
4月前
|
XML API 图形学
【Azure Developer】.Net 简单示例 "文字动图显示" Typing to SVG
【Azure Developer】.Net 简单示例 "文字动图显示" Typing to SVG
【Azure Developer】.Net 简单示例 "文字动图显示" Typing to SVG