65.9K
CodeProject 正在变化。 阅读更多。
Home

增强文档工作流审批 v2

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (5投票s)

2007年3月24日

13分钟阅读

viewsIcon

114407

downloadIcon

1262

本文支持以下功能:文件日志记录、TransactionScopeActivity、WorkflowCommitBatchService、SqlTrackingService、故障处理程序活动以及使用自定义服务的双向通信

本文目的

演示这些额外功能

• 将所有接口移动到一个程序集中,所有其他项目将引用此程序集。

• 在应用程序配置文件中注册“开箱即用和自定义服务”,以更好地维护。

• 使用“开箱即用”的系统诊断 LogToFile 工具。

• 内部使用“开箱即用”的 WorkflowCommitBatchService,并使用 TransactionScopeActivity

并准备我们的文档服务以处理批处理和事务。

• 使用故障处理程序活动来处理工作流内部错误。

• 通过自定义服务,工作流与主机进行双向通信。

• 使用“开箱即用”的 SqlTrackingService 来跟踪工作流。

• 使用 SqlTrackingService 工具来查询跟踪到的工作流。

如果您尚未阅读第一篇文章 文档审批工作流系统

我鼓励您阅读并运行该文章,因为本文是为更好的设计而增强的版本,

并且提供了更多工作流服务。

要运行此应用程序

• 您应该安装 .Net 3 正式版

http://www.microsoft.com/downloads/details.aspx?familyid=10CC340B-F857-4A14-83F5-25634C3BF043&displaylang=en

• 您应该安装 Visual Studio Windows Workflow 扩展

http://www.microsoft.com/downloads/details.aspx?familyid=e8232f93-48f0-4e74-b09d-b51f1d4231a4&displaylang=en

• 此版本的应用程序使用 SQL Server 2000

• 您应该为 SQL 持久性服务和 SQL 跟踪服务创建数据库,这些脚本位于

C:\$windows folder$\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN

• 对于 SQLPersistenceService,您可能需要从控制面板 -> 管理工具 -> 服务中选择分布式事务协调器服务并运行它(如果它没有运行)。

从控制面板 -> 管理工具 -> 服务中选择分布式事务协调器服务并运行它(如果它没有运行)

• 对于事务服务,您应该配置您的组件服务,

从控制面板 -> 管理工具 -> 组件服务,然后选择计算机 -> 我的电脑

然后右键单击选择属性,在默认属性选项卡中勾选“启用此计算机上的分布式 COM”,在 MSDTC 选项卡中勾选“使用本地协调器”。如果您运行任何使用 SQLPersistenceService 和 TransactionScopeActivity 的工作流示例,则无需进行任何上述设置,只需进行以下设置即可。

在默认属性选项卡中勾选“启用此计算机上的分布式 COM”,在 MSDTC 选项卡中勾选“使用本地协调器”。如果您运行任何使用 SQLPersistenceService 和 TransactionScopeActivity 的工作流示例,则无需进行任何上述设置,只需进行以下设置即可。

do any of the previous settings , and you may to do only the next settings

do any of the previous settings , and you may to do only the next settings

• 您应该创建下载源文件中的应用程序数据库

• 然后更改 app.config 文件以指向新的数据库名称,或者如果您选择

工作流DB作为持久化和跟踪数据库的默认数据库名称,以及DocumentDB作为应用程序数据库

• 您可能需要用一些数据填充文档数据库,以便

初始化将在客户端和管理应用程序中显示的用户和组,

因此请阅读包(zip 文件)中的 readme.txt

功能详情

• 将所有接口移动到一个程序集中,所有其他项目将引用此程序集。

这个主题在第一篇文章中已经详细提过,

思路很简单,尝试将您的工作流和所有工作流辅助类放在单独的项目中,

然后将工作流所需的所有接口分离到单独的项目中,

最后编写所有核心应用程序代码来实现和使用接口,

而不是具体的类,所以,如果您这样做

您将获得 2 个主要好处
• 您将避免循环引用,因为工作流将使用核心应用程序中的服务,

而核心应用程序将使用工作流,因此编译器不允许您这样做。

• 使用接口编程的通用概念使您的应用程序更具动态性并支持版本控制。

注意,在某些情况下,您可以使用抽象类而不是接口,在某些情况下,使用

用于您的基本库的抽象类比接口好得多,它超出了本文的范围,或者

始终使用抽象类,然后将其包装在单独程序集中的接口中

“我经常这样做”以在客户端类中使用,

但概念是相同的,“抽象类和接口应始终远离

核心库或业务逻辑项目”。

图 1 DocumentBaseLibrary 包含所有应用程序接口、抽象类和可枚举数据类型。

图 2 DocumentBaseLibrary 中接口的类图

图 3 DocumentBaseLibrary 中的抽象类



在应用程序配置中注册“开箱即用和自定义服务”



为了更好地维护,您可以在 app.config 中注册工作流运行时,并为此部分命名,然后使用

Workflow Runtime 构造函数来使用它 app.config 示例
<configuration>
  <configSections>
    <section
       name="WfRuntime"
       type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection,
            System.Workflow.Runtime, Version=3.0.00000.0,
            Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
  </configSections>
  <connectionStrings>
    <add name="documentDB"
        connectionString="Data Source=(local);Initial Catalog=documentDB;Persist Security Info=True;User ID=sa;Password=sa"
        providerName="System.Data.SqlClient" />
    <add name="workflowDB"
        connectionString="Data Source=(local);Initial Catalog=workflowDB;Persist Security Info=True;User ID=sa;Password=sa"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.diagnostics>
        <switches>
            <add name="System.Workflow.Runtime" value="All" />
            <add name="System.Workflow.Runtime.Hosting" value="All" />
            <add name="System.Workflow.Runtime.Tracking" value="Critical" />
            <add name="System.Workflow.Activities" value="Warning" />
            <add name="System.Workflow.Activities.Rules" value="Off" />

            <add name="System.Workflow LogToFile" value="1" />
        </switches>
    </system.diagnostics>
  <WfRuntime>
    <CommonParameters>
      <add name="ConnectionString"
           value="Data Source=(local);Initial Catalog=WorkflowDB;
                     Integrated Security=true"/>
    </CommonParameters>
    <Services>
      
      <add type=
       "System.Workflow.Runtime.Hosting.DefaultWorkflowSchedulerService,
         System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
         PublicKeyToken=31bf3856ad364e35"
        maxSimultaneousWorkflows="3" />
      <add
        type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService,
              System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
              PublicKeyToken=31bf3856ad364e35" UnloadOnIdle="true" />
      <add type=
       "DocumentCore.Bll.DocumentTransactionalService,
         DocumentCore"/>
      
        <add
          type="System.Workflow.Runtime.Tracking.SqlTrackingService,
              System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
              PublicKeyToken=31bf3856ad364e35"
              
             />
      
    </Services>
  </WfRuntime>
</configuration>

最好创建一个 WorkflowHelper 类,该类是单例或静态类,只有静态成员并具有不同的

WorkflowRuntime 属性可简化工作流运行时初始化,并避免运行多个

工作流运行时实例,或从不同的配置部分加载工作流

这个类应该封装所有对工作流部分的访问,并负责启动工作流实例。

使用“开箱即用”的系统诊断 LogToFile。

LogToFlile 是可配置的诊断工具,位于 app.config 的 system.diagnostics 部分。

运行应用程序后,您将找到一个名为 WorkflowTrace.log 的日志文件

在文本文件旁边,这是此文件结果的示例

System.Workflow.Runtime Start: 0
工作流运行时跟踪器已激活! System.Workflow.Runtime.Hosting Information: 0
WorkflowRuntime: 创建 WorkflowRuntime 7816a986-75e8-4383-81dc-033c17ce8055 System.Workflow.Runtime.Hosting 信息: 0 : WorkflowRuntime: 启动 WorkflowRuntime 7816a986-75e8-4383-81dc-033c17ce8055 System.Workflow.Runtime.Hosting 信息: 0 : SqlWorkflowPersistenceService(00000000-0000-0000-0000-000000000000): 正在启动, LoadInternalSeconds=120 System.Workflow.Runtime.Hosting 信息: 0
DefaultWorkflowCommitWorkBatchService:启动 System.Workflow.Runtime.Hosting 信息:0:SqlWorkflowPersistenceService 打开连接开始:2007/03/20 19:09:19 System.Workflow.Runtime.Hosting 信息:0:SqlWorkflowPersistenceService. 打开连接结束:2007/03/20 19:09:20 System.Workflow.Runtime.Hosting 信息:0:SqlWorkflowPersistenceService.RetrieveNonblockingInstanceStateIds 执行读取器开始:2007/03/20 19:09:20 比较工作流日志记录工具和工作流跟踪服务是不公平的。日志记录工具将帮助您在编程时调试工作流(跟踪服务也会这样做),结果将写入普通文件,因此很难提取任何信息,或对该文件运行查询,并且该文件应该非常大。您应该定期清除此文件,因此日志记录既不可靠也不像跟踪服务那样可定制,因此如果您仅在调试模式下启用日志记录工具,并在发布模式下使用 #if DEBUG 指令停止它,这是一个好主意。

使用“开箱即用”的 WorkflowCommitBatchService

内部使用 TransactionScopeActivity 和准备我们的文档服务

与批处理和事务协同工作。

在工作流中处理事务非常重要,尤其是在实际工作流场景中,它是必不可少的。

如果任何步骤失败并且所有相关步骤都没有回滚,您的应用程序将遭受数据不一致的困扰。

很少有程序员设计工作流并完成应用程序,然后在完成基本测试后

他们开始添加事务服务和故障处理活动。


只要您的应用程序支持事务并具有完善的“灾难恢复计划”,这不是一个坏主意。

在应用程序部署到生产服务器之前。

现在,事务问题出现在我们的工作流中,因为使用了不止一个数据库

在工作流应用程序中,至少一个数据库用于 SQLPersistanceService

另一个(或更多)用于应用程序本身(顺便说一句,您也可以让跟踪服务参与事务

也可能有一个额外的数据库)所以工作流基础提供了一个非常方便的工具

用于管理事务,而无需显式使用事务对象或分布式事务服务,

只需拖动 TransactionScopeActivity 并将您的活动放入其中即可。

只要您不放置任何使工作流空闲的活动(如长时间延迟活动或侦听器活动),

并记住您不能让事务长时间打开。

然后您的自定义服务应该实现 IPendingWork
public interface IPendingWork
    {
        void Commit(Transaction transaction, ICollection items);
        void Complete(bool succeeded, ICollection items);
        bool MustCommit(ICollection items);
    }
然后,您所有的方法都应该只保存所有应用程序请求信息(方法名称、参数)

到 Workbatch 对象,并在 Commit 方法中执行实际操作。请注意,如果您正在使用

SQL Server 2000,默认情况下,您所有的数据库连接都将加入全局事务,

所以不要使用 connection.EnlistTransaction() 方法,

它会抛出异常,因为它应该属于

只能是一个事务,如果你想检查当前的全局事务,只需在任何

方法中设置断点,并在即时窗口 System.Transactions.Transaction.Current 中测试此命令

只是为了确保您的代码在事务中运行

现在,这是文档事务服务代码的示例
[Serializable]
    public class DocumentTransactionalService : IDocumentService, IPendingWork
….
public void CreateInWorkflowDocumentForUser(IDocument document, string approvalUser, Guid workflowID)
        {
            Request req = new Request();
            req.RequestType = RequestType.CreateInWorkflowDocumentForUser;
            req.Document = document;
            req.ApprovalUser = approvalUser;
            req.WorkflowID = workflowID;
            WorkflowEnvironment.WorkBatch.Add(this, req);
        }
public void ApproveDocumentWorkflow(IDocument document, string approvalUser, Guid workflowID)
        {
            Request req = new Request();
            req.RequestType = RequestType.ApproveDocumentWorkflow;
            req.Document = document;
            req.ApprovalUser = approvalUser;
            req.WorkflowID = workflowID;
            WorkflowEnvironment.WorkBatch.Add(this, req);

        }
public void Commit(System.Transactions.Transaction transaction, System.Collections.ICollection items)
        {
            foreach (Request request in items)
            {
                switch (request.RequestType)
                {
                    case RequestType.ApproveDocumentWorkflow:
                        ApproveDocumentWorkflowInTransaction(request.Document, request.ApprovalUser, request.WorkflowID);
                        break;
                    case RequestType.CreateInWorkflowDocumentForGroup:
                        CreateInWorkflowDocumentForGroupInTransaction(request.Document, request.ApprovalGroup, request.WorkflowID);
                        break;
                    case RequestType.CreateInWorkflowDocumentForUser:
                        CreateInWorkflowDocumentForUserInTransaction(request.Document, request.ApprovalUser, request.WorkflowID);
                        break;
                    case RequestType.RejectDocumentWorkflow:
                        RejectDocumentWorkflowInTransaction(request.Document, request.ApprovalUser, request.WorkflowID);
                        break;
                    default: throw new NotSupportedException(request.RequestType.ToString());
                }
            }
        }

        public void Complete(bool succeeded, System.Collections.ICollection items)
        {
            // nothing to clean
        }

        public bool MustCommit(System.Collections.ICollection items)
        {
            return true;
        }

测试事务服务:证明您的工作流是事务性的最简单方法,

只需在任何对数据库进行更改的活动之后抛出一个未处理的异常,

并注册 workflowRuntime 的 WorkflowTerminated 事件,双重检查此异常。

然后您会看到所有更改都回滚了。

在我们的示例中,采购分支有一个未处理的抛出活动,该活动抛出一个 System.InvalidOperationException 类型的异常,尽管我放置了故障处理程序来处理此错误,但我再次抛出了它,

尽管我放置了故障处理程序来处理此错误,但我再次抛出了它,

然后您会看到文档如何回滚到开放状态,尽管在采购组中启动工作流的第一个活动发生了更改。

在采购组中启动工作流的第一个活动发生了更改。


图 4 Throw 活动 1 将暂停工作流,但是工作流将

回滚通过 ApproveFromPurchaseGroup 分支中的活动所做的所有工作

使用故障处理程序活动来处理工作流内部错误。

故障处理程序与简单 C# 应用程序中的捕获和处理异常完全相同,

工作流设计器中唯一的高级功能就是故障处理程序设计器,否则都相同。

因此,您在 C# 代码中了解的捕获异常的所有知识都可以应用于故障处理,

这是使用故障处理程序的简单指南

• 在没有完善处理计划的情况下,切勿捕获异常,最好将其留下,让主机处理它,

否则将作为未处理的异常。

• 避免捕获泛型异常,如 System.Exception,始终使用多个 catch 块,

并按从最具体到最泛型异常的顺序排列,如这段代码所示
try
            {
            }
            catch (OperationCanceledException oEx)
            {
                // code to handle this specific exception
            }
            catch (NotSupportedException nEx)
            {
                // code to handle this specific exception
            }
            // ---
            // ---
            catch (Exception ex)
            { 
            // generic exception that all code will come here if it is not fell down in any other exceptions
}

异常中还有许多其他设计指南,例如用自定义异常包装异常,

并记录技术异常并将友好消息添加到应用程序异常,

所有这些细节都超出了本文的范围。

现在您可以将工作流故障处理程序与之前的异常代码映射

首先,您应该选择故障处理程序设计器,如图 5 所示。

图 5 故障处理程序设计器



请注意,并非所有活动都支持故障处理程序,只有条件分支和其他活动

支持故障处理程序,然后您应该将故障处理程序活动拖到故障处理程序设计中

(您可以添加多个默认处理程序,每个处理程序可以处理“捕获”不同的异常并从故事板中选择一个活动处理程序)

图 6

您可以从故事板中选择 1 个 FaultHandlerActivity,并添加您的活动来处理这个特定的异常(故障)

第二步,您应该从 FaultHndlerActivity 中选择您的处理程序将处理的异常。

图 7 FaltType 代表特定的异常类型


如果您选择 System.Exception,您将处理所有异常,但这并不

是个好主意,您应该尽可能为特定故障设置处理程序。

事务和故障处理

如果您在事务范围内捕获了异常但没有再次抛出它,

事务将部分提交,因此,您将最终得到不一致的数据。因此,在处理异常时要小心,

在事务的情况下。在代码示例中,我创建了 2 个故障分支,并通过抛出异常模拟了错误,

两者都在全局事务下,其中一个被处理并再次抛出,另一个被处理并

吸收,因此在第二种情况下,我们最终导致数据不一致,

所以你应该回滚事务或再次抛出它

通过自定义服务,工作流与主机进行双向通信。

工作流应该以任何方式与主机通信,但我们知道有一些基本步骤

我们期望如果对象 A 将与对象 B 通信。

首先,我们必须创建通道或队列来传递(队列)数据,

然后我们必须通知客户端数据已准备好或可用。其中一种通信

技术是使用 WorkflowQueuingService,它是一个现成的内置服务,但本文我将重点介绍

自定义服务,以及如何将其用作双向通信。您可以让客户端通知工作流

任何更改并发送工作流想要的数据,您也可以让工作流通知

客户端任何更改并提供客户端所需的任何数据。

让我们 بررسی 这两种情况


• 通知工作流客户端的更改
您可以通过使用 DataExchangeService 来实现,只需用 [ExternalDataExchange] 属性修饰您的服务接口即可,如下所示。
在下面的示例中
[ExternalDataExchange]
    public interface IDocumentService
    {

        #region events to fire methods for workflow
        event EventHandler<externalexternaldocumenteventargument __designer:dtid="562949953421586"> RaiseApproveDocumentWorkflowEvent;
        event EventHandler<externalexternaldocumenteventargument> RaiseRejectDocumentWorkflowEvent;
        
        #endregion
/// other code
  }
</externalexternaldocumenteventargument></externalexternaldocumenteventargument>

并使用继承自 ExternalDataEventArgs 的泛型类中的特殊事件。
[Serializable]
    public class ExternalExternalDocumentEventArgument:ExternalDataEventArgs
    {
/// other code
}

然后您可以在工作流中添加 ListenActivity 并等待客户端引发事件,

您必须通过 HandleExternalEventActivity 为这些特定事件注册工作流。

图 8 监听器


每个 HandleExternalEventActivity 都将使用一个包含 ExternalDataEventArgs 类的事件,

对于任何用 [ExternalDataExchange] 装饰的接口。

• 从工作流通知客户端更改

工作流服务的美妙之处在于,只要您可以访问 WorkflowRuntime 类,您就可以随时访问此服务,

所以你可以使用 GetService<>() 方法来获取服务的引用,

然后您将像使用任何已经使用过的对象一样使用该服务。例如,

我创建了方法和事件,这些方法和事件将在实现 IDocumentService 的类内部引发。
[ExternalDataExchange]
    public interface IDocumentService
    {

// code

        #region events to used in the host, to track what is happening in workflow
        event EventHandler<documenteventarguments __designer:dtid="562949953421612"> OnCreated;
        event EventHandler<documenteventarguments> OnUpdated;
        event EventHandler<documenteventarguments> OnArchived;
        event EventHandler<documenteventarguments> OnDeleted;
        event EventHandler<documenteventarguments> OnSentToWorkflow;
        event EventHandler<documenteventarguments> OnApproved;
        event EventHandler<documenteventarguments> OnRejected;
        event EventHandler<documenteventarguments> OnError;
        #endregion
</documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments></documenteventarguments>

然后我在 DocumentTransactionalService protected void 内部触发了这些事件。
CreateInWorkflowDocumentForUserInTransaction(IDocument document, string approvalUser, Guid workflowID)
        {
            DocumentDalc.CreateInWorkflowDocumentRecordForUser(document, workflowID, approvalUser);
            if (this.OnSentToWorkflow != null) OnSentToWorkflow.Invoke(this, new DocumentEventArguments(document, "InWorkflow"));

        }
因此,您可以让客户端注册此事件,以便在文档发送到工作流时通知他,

所以,您可以在自定义服务中尽可能多地创建事件,并让客户端

收到任何更改通知。您可能需要传递工作流 ID 以用于跟踪工作流实例,

您可以根据需要自定义事件参数
_service = WorkflowHelper.DocumentService as DocumentTransactionalService;
            _service.OnCreated += ReceiverEvents.ServiceEvents;
            _service.OnSentToWorkflow += ReceiverEvents.ServiceEvents;
            _service.OnError += new EventHandler<documenteventarguments __designer:dtid="562949953421622">(_service_OnError);
            return wr;
</documenteventarguments>

使用“开箱即用”的 SqlTrackingService 跟踪工作流

跟踪服务是工作流中最重要的服务之一,原因很简单,工作流状态

以二进制数据形式保存在数据库中,并且在工作流实例完成后,它将从 InstanceState 表中删除。

数据库中的表,并且数据库中没有 InstanceState 数据的日志或数据历史记录。所以,你需要

在工作流实例完成前后跟踪工作流事件。您可以使用“开箱即用”的

SqlTrackingService 并且它已准备好使用,只需创建数据库(您可以选择与

示例中相同的 workflowDB)您可以使用配置文件来跟踪特定事件,或者保留默认配置文件,这样您就可以跟踪所有事件。

您可以使您的跟踪服务具有事务性。本文的目的是让您快速了解

跟踪服务,通过在 app.config 文件中注册 SqlTrackingService。


<add
          type="System.Workflow.Runtime.Tracking.SqlTrackingService,
                  System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral,
                  PublicKeyToken=31bf3856ad364e35"
              
             />
This service is using SQL Server database, by default it will use the database in common parameter inside the WorkflowRuntime section
<CommonParameters>
          <add name="ConnectionString"
           value="Data Source=(local);Initial Catalog=WorkflowDB;
                     Integrated Security=true"/>
</CommonParameters>
 


跟踪服务主要跟踪 3 个主要事件

• 用户事件。

• 工作流事件。

• 活动事件。

很明显,数据库会增长得非常快,不像持久化服务那样在每个工作流结束后清除每个工作流。

工作流结束后。这个问题通过跟踪服务中的表分区属性解决了,

您可以配置跟踪服务,使其每月或每年或您希望的任何时期进行分区。

在应用程序示例中,我使用了最少的 SQLTrackingService 配置,只是为了运行查询

针对跟踪服务,并体验这项服务的美妙之处。

现在要运行跟踪服务报告,首先运行管理应用程序并从菜单中选择

这是运行跟踪查询的代码
    private IList<UserTrackingRecord> GetUserEvents(Guid instanceID)
        {
            SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(WorkflowHelper.TrackingConnectionString);
            SqlTrackingQueryOptions ops = new SqlTrackingQueryOptions();
            SqlTrackingWorkflowInstance wftrackInstance;

            sqlTrackingQuery.TryGetWorkflow(instanceID,out wftrackInstance);
            return wftrackInstance.UserEvents; 
        }
        private IList<ActivityTrackingRecord> GetActivityEvents(Guid instanceID)
        {
            SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(WorkflowHelper.TrackingConnectionString);
            SqlTrackingQueryOptions ops = new SqlTrackingQueryOptions();
            SqlTrackingWorkflowInstance wftrackInstance;

            sqlTrackingQuery.TryGetWorkflow(instanceID, out wftrackInstance);
            return wftrackInstance.ActivityEvents;
        }
        private IList<WorkflowTrackingRecord> GetWorkflowEvents(Guid instanceID)
        {
            SqlTrackingQuery sqlTrackingQuery = new SqlTrackingQuery(WorkflowHelper.TrackingConnectionString);
            SqlTrackingQueryOptions ops = new SqlTrackingQueryOptions();
            SqlTrackingWorkflowInstance wftrackInstance;

            sqlTrackingQuery.TryGetWorkflow(instanceID, out wftrackInstance);
            return wftrackInstance.WorkflowEvents;
        }

结论

要在实际应用程序中使用 Windows Workflow Foundation,您可以使用许多“开箱即用”的

由 Windows Workflow Foundation 框架引入的服务,以减轻编写

事务性数据库命令和跟踪数据库,或其它有用服务的负担。
© . All rights reserved.