[转]WF事件驱动

本文转自:http://www.cnblogs.com/Mayvar/archive/2011/09/03/wanghonghua_201109030446.html

已经有不少朋友知道Workflow Foundation 4了。这个版本较之于以往有了一些明显的区别,开发的一些思路也大不相同了。

很多人会觉得很怀念以前3.0中的“顺序工作流”和“状态机工作流”的分类,其实大可不必。在4.0中虽然没有称之为“状态机工作流”的东西,但其实实现起来也很自然.WF 4提供了FlowChart的功能,其实就是状态机工作流。(它的不同Decision之间可以转化,而这正是状态机与顺序工作流的根本区别)

这一篇不是用来讨论状态机工作流这个话题的,提一下只是想说,大家要抓住重点,而不是表象。

专门写一篇来介绍一下所谓事件驱动的流程设计和应用,是因为

  • 首先,没有太多流程不需要用户交互,而如果需要用户交互,就得通过事件这样的机制。(无论是顺序型还是状态机)
  • 其次,没有太多文章和例子介绍这些细节,但WF4中的做法也不是很直观,一般人要理解起来还是蛮吃力的。

我们来假想这样一个场景,我们需要有一个文档审批的流程。很显然流程是不会自动启动的,它得由用户发出一个指令(通常是在界面上填写了一些数据,然后点击了一个按钮)。那么,这样的功能要怎么实现呢?

那么,就让我们开始吧

【备注】本文代码,可以通过 这里 下载

1.创建一个Activity Library.

请注意,我建议你创建Activity Library,不要为了省事就创建WCF Workflow Service Application或者Workflow Console Application.因为那样既不实用(你不可能在项目中这么做),也会隐藏很多细节。

image

将默认的那个Activity1.xaml删除掉,然后添加一个DocumentReviewWorkflow

image

image

2. 修改这个workflow的设计

在WF 4中,对Activity进行了全新的设计,3.0中的Activity几乎一个不留了。对于事件监听而言,现在是使用一个所谓的Pick的Activity

我们这个流程首先需要能够监控用户创建表单的一个行为(事件),所以,我们需要添加这样一个Pick

大致是这样的设计过程:

2.1 拖放一个Sequence到设计器中

2.2 拖放一个Pick到Sequence中,默认会有两个PickBranch,删除其中的一个

2.3 拖放一个Receive到PickBranch的Trigger里面(我们设置了这个Receive的ServiceContractName,和OperationName,你可以随便取名,没有太多限制。这里其实是使用了WCF的技术。这是WF 4的一个很大的特点:与WCF结合得很紧密)

2.4 定义一个TicketId的变量,用来保存流程编号

2.5 拖放一个Assign 和WriteLine到PickBranch的Action中。(作为响应,我们只是随机产生一个流程编号,然后输出它)

image

这个设计过程,将得到下面这样的xaml.我这里就不一一截图了。如果觉得有必要,直接将这段xaml覆盖掉你的设计即可看到效果。

<Activity mc:Ignorable="sap" x:Class="DocumentReviewLib.DocumentReviewWorkflow" sap:VirtualizedContainerService.HintSize="470,711" mva:VisualBasic.Settings="Assembly references and imported namespaces for internal implementation" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:p="http://schemas.microsoft.com/netfx/2009/xaml/servicemodel" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:s4="clr-namespace:System;assembly=System.ServiceModel" xmlns:sa="clr-namespace:System.Activities;assembly=System.Activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Sequence DisplayName="文档审批流程" sad:XamlDebuggerXmlReader.FileName="d:	empWF4EventDrivenSolutionDocumentReviewLibDocumentReviewWorkflow.xaml" sap:VirtualizedContainerService.HintSize="430,671">
    <Sequence.Variables>
      <Variable x:TypeArguments="x:Int32" Default="-1" Name="TicketId" />
    </Sequence.Variables>
    <sap:WorkflowViewStateService.ViewState>
      <scg3:Dictionary x:TypeArguments="x:String, x:Object">
        <x:Boolean x:Key="IsExpanded">True</x:Boolean>
      </scg3:Dictionary>
    </sap:WorkflowViewStateService.ViewState>
    <Pick DisplayName="文档创建" sap:VirtualizedContainerService.HintSize="408,547">
      <PickBranch DisplayName="用户提交了一个新的流程" sap:VirtualizedContainerService.HintSize="294,501">
        <PickBranch.Trigger>
          <p:Receive DisplayName="收到用户的消息" sap:VirtualizedContainerService.HintSize="264,100" OperationName="CreateTicket" ServiceContractName="IDocumentReview" />
        </PickBranch.Trigger>
        <Sequence DisplayName="事件响应" sap:VirtualizedContainerService.HintSize="264,283">
          <sap:WorkflowViewStateService.ViewState>
            <scg3:Dictionary x:TypeArguments="x:String, x:Object">
              <x:Boolean x:Key="IsExpanded">True</x:Boolean>
            </scg3:Dictionary>
          </sap:WorkflowViewStateService.ViewState>
          <Assign DisplayName="随机产生一个流程编号" sap:VirtualizedContainerService.HintSize="242,58">
            <Assign.To>
              <OutArgument x:TypeArguments="x:Int32">[TicketId]</OutArgument>
            </Assign.To>
            <Assign.Value>
              <InArgument x:TypeArguments="x:Int32">[New Random().Next()]</InArgument>
            </Assign.Value>
          </Assign>
          <WriteLine DisplayName="输出信息" sap:VirtualizedContainerService.HintSize="242,61" Text="[&quot;流程被创建,编号为:&quot; &amp; TicketId]" />
        </Sequence>
      </PickBranch>
    </Pick>
  </Sequence>
</Activity>

3. 设计一个宿主程序

流程设计好之后,我们如何将它托管起来,并且允许客户端发出流程有关的操作呢

我们需要设计一个宿主。为简便起见,我们可以用Console来做为宿主。

image

同时,我们需要添加几个程序集的引用

image

image

image

然后,修改Main方法,代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Activities;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

namespace Host
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WorkflowServiceHost(
                new DocumentReviewLib.DocumentReviewWorkflow(),
                new Uri("http://localhost:8080/DRS"));

            host.AddDefaultEndpoints();
            host.Description.Behaviors.Add(
                new ServiceMetadataBehavior() { HttpGetEnabled = true });

            host.AddServiceEndpoint(
                "IMetadataExchange",
                MetadataExchangeBindings.CreateMexHttpBinding(),
                "mex");

            host.Open();
            Console.WriteLine("Server is ready.");
            Console.Read();

        }
    }
}

这里,我们创建了一个所谓的WorkflowServiceHost(其实这是一个特殊的WCF中的ServiceHost对象),启动了在8080端口的监听

启动Host的调试(如果是Vista或Win7),请将Visual Studio run as administrator,如果没有出什么意外的话,你可以看到下面这样的一个窗口

image

然后,你在浏览器中输入下面的地址,可以看到

image

这就表示服务已经成功启动了。

根据上面页面的提示,我们可以产生一个服务代理类。注意,要使用Visual Studio Command Prompt,而不是默认的cmd

image

image

4. 设计一个客户端程序

我们接下来要设计一个Windows Forms的客户端程序,来使用该服务,发起流程的操作

image

将刚才工具所生成的两个文件添加到当前项目,并且将output.config修改为app.config

image

添加对System.ServiceModel的引用

image

修改窗口的设计如下

image

代码如下

        private void btCreate_Click(object sender, EventArgs e)
        {
            var proxy = new DocumentReviewClient();
            proxy.CreateTicket();
        }

是不是很简单呢?其实这里就是WCF调用。(有在WF3中做过流程开发的朋友一定不会陌生,以前我们也是自己定义WCF服务来实现客户端与服务器端的通讯)

那么,现在可以进行调试了。

5. 调试流程

因为既要启动服务器,又要启动客户端,所以我们可以设置一下启动项目顺序

image

image

接下来,就可以按下F5键进行调试了

image

image

然后,我们点击“创建流程”按钮。我们并没有发现什么。流程并没有被启动。这是为什么呢?

放心,没有什么大不了的,这只是我预先设计好的一个“陷阱”而已。我是想让大家明白,所有的WCF调用,默认都不会启动流程,除非有一个属性设置为true。这就是那个Receive Activity的CanCreateInstance属性,我们可以回到DocumentReviewLib中,将其设置为true。因为它默认是false。这个属性的意思是说,如果收到这个事件,那么要不要创建工作流的实例。一般在一个流程的顶部事件中,都是设置为true的

image

再次按下F5键,进行调试,然后点击“创建流程”按钮

image

这样看起来,流程确实被创建了。如果我们点击多次呢?毫无疑问,它会有多个实例产生出来

image

看起来不错,不是吗?我们从客户端发起了一个操作,它将被工作流收到消息,并且进行了相应的处理。(我们这里只是输出消息,你可以想象一下,你完全可以在这里更新数据库的记录)

那么,接下来有一个话题,一般情况下,我们一个流程都不止一个事件,例如发起流程之后,需要将这个编号返回给客户端,同时还要等待经理审批。如此这般的需求怎么实现呢?

我在下一篇先介绍一下,如何将处理结果发回给客户端。

然后,第三篇介绍如何实现审批事件。

【备注】本文代码,可以通过 这里 下载

WF事件驱动(2)

我介绍到了WF4的全新事件驱动工作流设计的第一部分。我们可以大致总结几个重点

1. WF4的事件机制与WF3有了革命性的不同。WF3是基于ExternalDataExchange服务的。而WF4是基于WCF的。这种设计相对来说,对于开发人员而言,简便了很多。

2. WF3中是使用所谓的EventDriven这样的Activity,而WF4则使用了Receive这样的Activity.其实,从WF 3.5的时候就可以看到这样的端倪。

3. 如果需要进行事件的监听,则不能使用WorkflowInvoker或者WorkflowApplication来启动流程,而是需要通过WorkflowServiceHost来启动监听即可。(这里有个根本区别,WorkflowServiceHost只是启动监听,并不立即创建Workflow的实例。)

上一篇我们讲到了如何接收客户端的请求,并且做出响应。这种响应我们仅限于在服务端打印有关的消息。

但显然这样是不够的。我们创建好流程整合,至少希望将流程的最新编号通知客户端吧。那么,如何进行这样的设计呢?

本文代码,可以通过 这里 下载

1.修改工作流设计

WF4中对这种需求已经考虑得很周全了,我们可以选择Receive这个Activity,然后在右键菜单中找到Create SendReply

image

点击该菜单,会有一个提示

image

我们可以选择将其粘贴到任何地方,例如

image

请注意,此时会多出来一个变量,叫__handler1,它的类型是CorrelationHandler,这个东西我们后面一篇会详细介绍,这里可以不关注它的细节。

粘贴过来的这个Activity,它是自动与之前的Receive这个Activity进行关联的

image

那么,我们到底要发送什么数据给用户呢?可以点击Activity上面的Content这个地方

image

将Message data绑定到我们之前的那个TicketId变量,并且设置Type为INT 32

这样,我们就将流程设计好了。重新编译一下吧

2. 重新生成客户端代码类

流程发生了此类变化,通常客户端代理类也要有所变化。我们先启动服务器程序

image

确认在浏览器中可以看到下面的结果

image

生成客户端代码文件

image

3. 修改客户端

将生成好的DocumentReviewWorkflow.cs文件,添加到客户端项目中,替换掉原先那个文件。

这里无需添加output.config,因为这个文件内容其实没有啥变化 。

修改窗口如下,添加了一个ListBox

image

修改代码如下

        private void btCreate_Click(object sender, EventArgs e)
        {
            var proxy = new DocumentReviewClient();
            var result = proxy.CreateTicket();

            lstTickets.Items.Add(result);
        }

请注意,现在CreateTicket方法是有返回值的(int?),而在上一篇,它没有返回值,是void.

4. 调试程序

按下F5键进行调试,在窗口上多次点击“创建流程”的话,会怎么样呢

image

不光是服务端有消息输出,我们在客户端也可以看到一个TicketId的列表

image

本文代码,可以通过 这里 下载

下一篇,我们将完成这个流程的例子,我们将添加经理审批的事件。通过这样一个例子,你可以理解如何设计基于事件驱动的流程。

WF事件驱动(3)

前面两篇已经实现了最简单的基于事件的工作流程,用户可以在客户端(任意类型的客户端)发出流程操作的指令,通过WCF的通讯,驱动后台的工作流工作。 

但之前的例子只有一个事件,就是“创建流程”的事件,显然这是不够的。这一篇就来把这个例子完善一下,通过这个练习之后,大家应该可以大致了解在WF4中如何设计基于事件的流程了

我们将为这个流程添加一个“审批流程”的事件。

本文代码,请通过 这里 下载

1. 修改工作流设计

很显然地,我们会在下面添加另外一个Pick Activity,然后里面也添加一个Receive来实现事件监听

image

同时,我们定义了三个变量用来接收用户传递过来的数据

image

我们在Receive上面设置了参数与这些变量之间的映射

image

一切看起来都还是挺自然的。请注意,这个Receive,因为是第二个事件,所以无需创建新的工作流实例(如果每个事件都创建新的实例,那就乱套了),也就是说CanCreateInstance不需要设置为true

image

这个工作流的xaml文件代码如下

<Activity mc:Ignorable="sap" x:Class="DocumentReviewLib.DocumentReviewWorkflow" sap:VirtualizedContainerService.HintSize="483,1245" mva:VisualBasic.Settings="Assembly references and imported namespaces for internal implementation" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:p="http://schemas.microsoft.com/netfx/2009/xaml/servicemodel" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:s4="clr-namespace:System;assembly=System.ServiceModel" xmlns:sa="clr-namespace:System.Activities;assembly=System.Activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:ssa="clr-namespace:System.ServiceModel.Activities;assembly=System.ServiceModel.Activities" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Sequence DisplayName="文档审批流程" sad:XamlDebuggerXmlReader.FileName="d:	empWF4EventDrivenSolutionDocumentReviewLibDocumentReviewWorkflow.xaml" sap:VirtualizedContainerService.HintSize="443,1205">
    <Sequence.Variables>
      <Variable x:TypeArguments="x:Int32" Default="-1" Name="TicketId" />
      <Variable x:TypeArguments="p:CorrelationHandle" Name="__handle1" />
    </Sequence.Variables>
    <sap:WorkflowViewStateService.ViewState>
      <scg3:Dictionary x:TypeArguments="x:String, x:Object">
        <x:Boolean x:Key="IsExpanded">True</x:Boolean>
      </scg3:Dictionary>
    </sap:WorkflowViewStateService.ViewState>
    <Pick DisplayName="文档创建" sap:VirtualizedContainerService.HintSize="421,677">
      <PickBranch DisplayName="用户提交了一个新的流程" sap:VirtualizedContainerService.HintSize="307,631">
        <PickBranch.Trigger>
          <p:Receive x:Name="__ReferenceID0" CanCreateInstance="True" DisplayName="收到用户的消息" sap:VirtualizedContainerService.HintSize="277,100" OperationName="CreateTicket" ServiceContractName="IDocumentReview">
            <p:Receive.CorrelationInitializers>
              <p:RequestReplyCorrelationInitializer CorrelationHandle="[__handle1]" />
            </p:Receive.CorrelationInitializers>
          </p:Receive>
        </PickBranch.Trigger>
        <Sequence DisplayName="事件响应" sap:VirtualizedContainerService.HintSize="277,413">
          <sap:WorkflowViewStateService.ViewState>
            <scg3:Dictionary x:TypeArguments="x:String, x:Object">
              <x:Boolean x:Key="IsExpanded">True</x:Boolean>
            </scg3:Dictionary>
          </sap:WorkflowViewStateService.ViewState>
          <Assign DisplayName="随机产生一个流程编号" sap:VirtualizedContainerService.HintSize="255,58">
            <Assign.To>
              <OutArgument x:TypeArguments="x:Int32">[TicketId]</OutArgument>
            </Assign.To>
            <Assign.Value>
              <InArgument x:TypeArguments="x:Int32">[New Random().Next()]</InArgument>
            </Assign.Value>
          </Assign>
          <p:SendReply Request="{x:Reference __ReferenceID0}" DisplayName="SendReplyTo收到用户的消息" sap:VirtualizedContainerService.HintSize="255,90">
            <p:SendMessageContent DeclaredMessageType="x:Int32">
              <InArgument x:TypeArguments="x:Int32">[TicketId]</InArgument>
            </p:SendMessageContent>
          </p:SendReply>
          <WriteLine DisplayName="输出信息" sap:VirtualizedContainerService.HintSize="255,61" Text="[&quot;流程被创建,编号为:&quot; &amp; TicketId]" />
        </Sequence>
      </PickBranch>
    </Pick>
    <Pick DisplayName="文档审批" sap:VirtualizedContainerService.HintSize="421,364">
      <PickBranch DisplayName="经理审批" sap:VirtualizedContainerService.HintSize="285,318">
        <PickBranch.Variables>
          <Variable x:TypeArguments="x:String" Name="action" />
          <Variable x:TypeArguments="x:String" Name="comment" />
          <Variable x:TypeArguments="x:Int32" Name="reviewId" />
        </PickBranch.Variables>
        <PickBranch.Trigger>
          <p:Receive DisplayName="经理审批" sap:VirtualizedContainerService.HintSize="255,100" OperationName="UpdateTicket" ServiceContractName="IDocumentReview">
            <p:ReceiveParametersContent>
              <OutArgument x:TypeArguments="x:String" x:Key="action">[action]</OutArgument>
              <OutArgument x:TypeArguments="x:String" x:Key="comment">[comment]</OutArgument>
              <OutArgument x:TypeArguments="x:Int32" x:Key="id">[reviewId]</OutArgument>
            </p:ReceiveParametersContent>
          </p:Receive>
        </PickBranch.Trigger>
        <WriteLine DisplayName="打印消息" sap:VirtualizedContainerService.HintSize="255,100" Text="[String.Format(&quot;Ticket update --- action : {0}, comment :{1} &quot;, action, comment)]" />
      </PickBranch>
    </Pick>
  </Sequence>
</Activity>

2. 更新客户端代码和设计

工作流已经进行了修改,相应地,客户端代码也需要有一些更新

启动宿主程序,然后通过下面的方式再次得到客户端代码文件。

image

同样,我们只需要添加那个DocumentReviewWorkflow.cs到项目中来

我们修改窗口如下,添加审批的工具按钮

image

有关代码如下

        private void btApproval_Click(object sender, EventArgs e)
        {
            //同意某个流程
            var action = "approval";
            UpdateTicket(action);

        }

        private void UpdateTicket(string action)
        {
            if (lstTickets.SelectedIndex > -1)
            {
                var id = int.Parse(lstTickets.SelectedItem.ToString());
                var comment = txtComment.Text;

                var proxy = new DocumentReviewClient();
                proxy.UpdateTicket(action, comment, id);

            }
        }

        private void btReject_Click(object sender, EventArgs e)
        {
            var action = "Reject";
            UpdateTicket(action);
        }

代码也是很好理解的,几乎无需特别的解释。

3. 调试程序

imageimage

我几乎迫不及待地想要看一下效果了。选择某个Ticket之后,点击“同意”按钮,激动人心的时刻到了。晕,又是什么也没有发生。Angry smile

你猜对了,这又是我预先埋下的一个“陷阱”。放心吧,如果那么容易做出来,我是不会专门写一篇来介绍的。

4.修改流程设计

有点沮丧吗?其实大可不必。淡定,淡定

静下心来想一下,为什么它不能工作呢?很显然,客户端的请求是发送到了服务端的,那么就是说服务端不知道该怎么处理?这样解释是合理的吧。

那么服务端为什么不知道该怎么处理呢?你可能会说,我都告诉他TicketId了,为什么它不会找到那个Instance,然后触发里面的事件呢?

症结点就在于这里,你光是传递TicketId过来是不够的,还需要在工作流中将这个Id标识为一个上下文事件关联的依据。

还记得第二篇提到的CorrelationHandler吗?既然事件需要被定位到,那么它就需要有一定的机制可以区分。不同实例之间就是通过不同的CorrelationHandler来区别的。

那么,我们既然想让UpdateTicket事件与相应的CreateTicket事件所创建的那个实例关联起来,就需要定义一个特殊的Handler。

image

注意,这个__handler是我们定义的,而上面的那个__handler1是之前第二个练习时自动创建的

接下来,我们需要对这个handler进行初始化。这个可以通过一个InitializeCorrelation的Activity来完成

image

我们让这个初始化好的handler具有一个属性,Id,保存当前这个TicketId

image

然后,我们修改”UpdateTicket”这个事件

设置CorrelatesWith属性

image

设置CorrelatesOn属性

image

这个设置,也就是说,让UpdateTicket方法传入的id参数,自动作为查找CorrelationHandler的依据

【注意】这一步至关重要

保存流程,编译即可。因为这种更改,不涉及到与客户端的合约更改,所以无需重新生成客户端代码。这也是用WF来做流程设计的好处之一。

最终版xaml如下

<Activity mc:Ignorable="sap" x:Class="DocumentReviewLib.DocumentReviewWorkflow" sap:VirtualizedContainerService.HintSize="483,1378" mva:VisualBasic.Settings="Assembly references and imported namespaces for internal implementation" xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System" xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities" xmlns:p="http://schemas.microsoft.com/netfx/2009/xaml/servicemodel" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:s1="clr-namespace:System;assembly=System" xmlns:s2="clr-namespace:System;assembly=System.Xml" xmlns:s3="clr-namespace:System;assembly=System.Core" xmlns:s4="clr-namespace:System;assembly=System.ServiceModel" xmlns:sa="clr-namespace:System.Activities;assembly=System.Activities" xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities" xmlns:sap="http://schemas.microsoft.com/netfx/2009/xaml/activities/presentation" xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System" xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel" xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core" xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib" xmlns:sd="clr-namespace:System.Data;assembly=System.Data" xmlns:sl="clr-namespace:System.Linq;assembly=System.Core" xmlns:ssa="clr-namespace:System.ServiceModel.Activities;assembly=System.ServiceModel.Activities" xmlns:ssx="clr-namespace:System.ServiceModel.XamlIntegration;assembly=System.ServiceModel" xmlns:st="clr-namespace:System.Text;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Sequence DisplayName="文档审批流程" sad:XamlDebuggerXmlReader.FileName="d:	empWF4EventDrivenSolutionDocumentReviewLibDocumentReviewWorkflow.xaml" sap:VirtualizedContainerService.HintSize="443,1338">
    <Sequence.Variables>
      <Variable x:TypeArguments="x:Int32" Default="-1" Name="TicketId" />
      <Variable x:TypeArguments="p:CorrelationHandle" Name="__handle1" />
      <Variable x:TypeArguments="p:CorrelationHandle" Name="__handler" />
    </Sequence.Variables>
    <sap:WorkflowViewStateService.ViewState>
      <scg3:Dictionary x:TypeArguments="x:String, x:Object">
        <x:Boolean x:Key="IsExpanded">True</x:Boolean>
      </scg3:Dictionary>
    </sap:WorkflowViewStateService.ViewState>
    <Pick DisplayName="文档创建" sap:VirtualizedContainerService.HintSize="421,810">
      <PickBranch DisplayName="用户提交了一个新的流程" sap:VirtualizedContainerService.HintSize="307,764">
        <PickBranch.Trigger>
          <p:Receive x:Name="__ReferenceID0" CanCreateInstance="True" DisplayName="收到用户的消息" sap:VirtualizedContainerService.HintSize="277,100" OperationName="CreateTicket" ServiceContractName="IDocumentReview">
            <p:Receive.CorrelationInitializers>
              <p:RequestReplyCorrelationInitializer CorrelationHandle="[__handle1]" />
            </p:Receive.CorrelationInitializers>
          </p:Receive>
        </PickBranch.Trigger>
        <Sequence DisplayName="事件响应" sap:VirtualizedContainerService.HintSize="277,546">
          <sap:WorkflowViewStateService.ViewState>
            <scg3:Dictionary x:TypeArguments="x:String, x:Object">
              <x:Boolean x:Key="IsExpanded">True</x:Boolean>
            </scg3:Dictionary>
          </sap:WorkflowViewStateService.ViewState>
          <Assign DisplayName="随机产生一个流程编号" sap:VirtualizedContainerService.HintSize="255,58">
            <Assign.To>
              <OutArgument x:TypeArguments="x:Int32">[TicketId]</OutArgument>
            </Assign.To>
            <Assign.Value>
              <InArgument x:TypeArguments="x:Int32">[New Random().Next()]</InArgument>
            </Assign.Value>
          </Assign>
          <p:InitializeCorrelation Correlation="[__handler]" sap:VirtualizedContainerService.HintSize="255,93">
            <InArgument x:TypeArguments="x:String" x:Key="Id">[TicketId.ToString()]</InArgument>
          </p:InitializeCorrelation>
          <p:SendReply Request="{x:Reference __ReferenceID0}" DisplayName="SendReplyTo收到用户的消息" sap:VirtualizedContainerService.HintSize="255,90">
            <p:SendMessageContent DeclaredMessageType="x:Int32">
              <InArgument x:TypeArguments="x:Int32">[TicketId]</InArgument>
            </p:SendMessageContent>
          </p:SendReply>
          <WriteLine DisplayName="输出信息" sap:VirtualizedContainerService.HintSize="255,61" Text="[&quot;流程被创建,编号为:&quot; &amp; TicketId]" />
        </Sequence>
      </PickBranch>
    </Pick>
    <Pick DisplayName="文档审批" sap:VirtualizedContainerService.HintSize="421,364">
      <PickBranch DisplayName="经理审批" sap:VirtualizedContainerService.HintSize="285,318">
        <PickBranch.Variables>
          <Variable x:TypeArguments="x:String" Name="action" />
          <Variable x:TypeArguments="x:String" Name="comment" />
          <Variable x:TypeArguments="x:Int32" Name="reviewId" />
        </PickBranch.Variables>
        <PickBranch.Trigger>
          <p:Receive CorrelatesWith="[__handler]" DisplayName="经理审批" sap:VirtualizedContainerService.HintSize="255,100" OperationName="UpdateTicket" ServiceContractName="IDocumentReview">
            <p:Receive.CorrelatesOn>
              <p:XPathMessageQuery x:Key="Id">
                <p:XPathMessageQuery.Namespaces>
                  <ssx:XPathMessageContextMarkup>
                    <x:String x:Key="xgSc">http://tempuri.org/</x:String>
                  </ssx:XPathMessageContextMarkup>
                </p:XPathMessageQuery.Namespaces>sm:body()/xgSc:UpdateTicket/xgSc:id</p:XPathMessageQuery>
            </p:Receive.CorrelatesOn>
            <p:ReceiveParametersContent>
              <OutArgument x:TypeArguments="x:String" x:Key="action">[action]</OutArgument>
              <OutArgument x:TypeArguments="x:String" x:Key="comment">[comment]</OutArgument>
              <OutArgument x:TypeArguments="x:Int32" x:Key="id">[reviewId]</OutArgument>
            </p:ReceiveParametersContent>
          </p:Receive>
        </PickBranch.Trigger>
        <WriteLine DisplayName="打印消息" sap:VirtualizedContainerService.HintSize="255,100" Text="[String.Format(&quot;Ticket update --- Ticketid :{0} , action : {1}, comment :{2} &quot;, reviewId, action, comment)]" />
      </PickBranch>
    </Pick>
  </Sequence>
</Activity>

5.测试程序

发起流程后,分别选择不同的编号,进行审批。结果如下

image

大功告成,收工

总结:

利用WF 4的全新框架,可以快速设计基于事件驱动的工作流。它提高了开发效率,简化了开发流程,而更重要的是,这对于广大开发人员的身心健康有着很有益地帮助

WF事件驱动(4)

前面三篇,我介绍到了如何在WF 4中设计简单的审批流程,没有什么特别出奇的技术,只不过WF4对于事件机制有了不小的改进吧。

这一篇要来谈谈更加深入一点的话题:如果我们的流程需要长时间才能完成(这是很常见的),那么如何在这些流程空闲(例如等待经理审批)的时候,更好地管理它们呢?

我们都知道,默认情况下,所有流程实例都是在内存中被创建的一个对象。那么这里提到的管理,有两个层面的意思:

  1. 如果某些实例处于空闲状态,那么他们所占用的内存可能是浪费的。
  2. 由于可能因意外情况导致的宕机(例如停电,或者被某个恶作剧者按下了重启按钮),所以放在内存中的实例是很不保险的

所以,为了达到上面的两个目的,WF 提供了所谓的“持久化”的功能。就是支持我们将工作流实例通过一定的方式保存起来,等需要的时候再取出来即可。

WF3就开始支持这种特性,那时候称之为“持久化服务”。WF4对此做了进一步的改进和完善。本文主要就是讨论WF4下面如何做持久化。

完整代码,请通过 这里 下载

1. 准备持久化数据库

WF的持久化功能默认是用一个SQL Server的数据库来保存数据的。当然,在此基础上我们可以扩展。但通常使用默认的这个数据库是明智的选择。

WF4提供了两个脚本,可以让我们来生成这个数据库。这两个脚本通常在下面的目录

C:WindowsMicrosoft.NETFramework64v4.0.30319SQLen

【注意】如果你不是x64的系统,则可能是C:WindowsMicrosoft.NETFrameworkv4.0.30319SQLen

image

我们可以手工先在SQL Server Management Studio中创建一个数据库,例如叫WF4

image

然后,将数据库上下文切换到WF4,依次运行两个脚本

SqlWorkflowInstanceStoreSchema.sql

SqlWorkflowInstanceStoreLogic.sql

这个数据库的结构如果有兴趣,可以研究一下。这里就不过多展开了

image

2. 修改宿主程序,添加持久化服务的功能

数据库准备好之后,我们需要对宿主稍做修改,就可以完成持久化功能的配置

首先,我们需要在宿主程序中添加两个程序集引用

image

修改Main方法代码如下 (请注意红色字体)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Activities;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

using System.Activities.DurableInstancing;
using System.Runtime.DurableInstancing;
using System.Activities.Persistence;
using System.ServiceModel.Activities.Description;
using System.Xml.Linq;

namespace Host
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WorkflowServiceHost(
                new DocumentReviewLib.DocumentReviewWorkflow(),
                new Uri("http://localhost:8080/DRS"));

            host.AddDefaultEndpoints();//这个方法是添加了一些标准的端点

            host.Description.Behaviors.Add(
                new ServiceMetadataBehavior() { HttpGetEnabled = true });

            host.AddServiceEndpoint(
                "IMetadataExchange",
                MetadataExchangeBindings.CreateMexHttpBinding(),
                "mex");


            var store = new SqlWorkflowInstanceStore(
                "server=(local)\sqlexpress;database=WF4;integrated security=true");
            
            host.DurableInstancingOptions.InstanceStore = store;

            host.Open();
            Console.WriteLine("Server is ready.");
            Console.Read();

        }
    }
}

这样就可以完成服务的配置了。当然,我们这里仅仅是为了直观期间,用了代码的方式(而且用的是最简单的做法)。在生产环境下,我们可能会倾向于用配置文件的方式,而不是代码。

事实上,我个人认为WCF,WF中一个很大的亮点就是减少了对代码的依赖度,确实还是做得不错的。

3. 调试程序

按下F5进行调试

image

image

我们回到数据库看一下情况,请注意看那个InstancesTable

image

也就是说,它已经把这四个实例保存到了数据库中。

然后,我们接下来对流程进行审批

image

很显然,236738261这个流程已经结束了,我们再来看一下数据库中的记录

image

我们看到,现在数据库中的记录数也变为了3条。

4.如何加载已经保存好的实例

既然有这样一个数据库保存好了我们的实例,那么就可以放心大胆地将宿主程序关闭掉。

我们来看,数据库中的记录仍然是在的。请注意,我因为做了其他一些测试,所以现在实例有5个

image

看起来很不错,不是吗?

但是有一个问题随之而来,当我们再次打开应用程序的时候,我们可能希望宿主程序能自动地加载这些实例的信息,或者说我们仍然能够对这些实例进行操作。这要如何来完成呢?

请大家按照我的步骤来做练习

4.1 将宿主程序开起来

image

4.2 将客户端开起来

image

点击“创建流程”按钮,可以多点几次

image

image

4.3 将宿主程序关闭掉

这样做的目的,是模拟一下服务器突然停电了或者类似这样的情况。

请不要将客户端关闭

4.4 重新开启宿主程序

这样做就模拟服务器重启的场景。那么,问题就是,此时客户端还能继续处理那些未完成的流程吗?

image

我们可以选择一个编号之后,还是和以前那样,点击“同意”或者“拒绝”按钮

image

我们发现,这个流程还是可以继续处理的。而且,我们并不需要在服务端做任何特殊的设计。

所以,我们可以这么总结一下:当一个流程的请求被发送到服务端,WorkflowServiceHost会收到,它先在内存中查找看是否有合适的实例,如果没有,则会尝试查看数据库中是否有合适的实例,如果有,则会加载它

那么,如果在内存和数据库都没有实例的话,会怎么样呢?(例如某个流程已经被处理完了,你还是硬要继续审批)。这种情况下,WorkflowServiceHost会将这个请求列为所谓的错误的消息。

为了证明这一点,我们对服务器代码稍作修改(请注意红色部分)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Activities;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

using System.Activities.DurableInstancing;
using System.Runtime.DurableInstancing;
using System.Activities.Persistence;
using System.ServiceModel.Activities.Description;
using System.Xml.Linq;

namespace Host
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WorkflowServiceHost(
                new DocumentReviewLib.DocumentReviewWorkflow(),
                new Uri("http://localhost:8080/DRS"));

            host.AddDefaultEndpoints();//这个方法是添加了一些标准的端点

            host.Description.Behaviors.Add(
                new ServiceMetadataBehavior() { HttpGetEnabled = true });

            host.AddServiceEndpoint(
                "IMetadataExchange",
                MetadataExchangeBindings.CreateMexHttpBinding(),
                "mex");


            var store = new SqlWorkflowInstanceStore("server=(local)\sqlexpress;database=WF4;integrated security=true");
            host.DurableInstancingOptions.InstanceStore = store;

            host.UnknownMessageReceived += (o, e) =>
            {
                Console.WriteLine("
"+e.Message+"
");
            };
            host.Open();



            Console.WriteLine("Server is ready.");
            Console.Read();

        }


    }
}

调试的时候,可以对一个编号连续点击两次“同意”,则第二次会被视为不合法的请求。如下图所示

image

5. 如何在客户端获取待处理任务列表

在上一个练习中,我们为了测试服务器端会自动检索那些未完成的流程实例,我们将宿主程序关闭后再打开了,但是我也特别提醒大家,不要将客户端程序关闭。

为什么呢?因为如果你关闭了,那些编号就没有了,而我们UpdateTicket操作是要根据TicketId进行操作的。

那么,就引申出来一个更大的问题,客户端不可能永远开着的,那么这些未完成流程的TicketId要保存在哪里?而客户端又如何能获取到这个列表呢?

有的朋友可能会说,我们可以单独搞一个数据库吧,用一个表来保存这些信息好了。那当然是可以的,但并不见得是很好的一个做法。

在WF4所提供的持久化功能中,考虑到了这种问题。它可以用一个特殊的表保存我们流程运转期间的一些数据。这里姑且称为“流程数据”吧

为了实现这样的功能,需要对持久化进行必要的扩展,请大家按照我下面的步骤来操作。

5.1 创建一个PersistenceParticipant

这是所谓的持久化参与者。它将在持久化的过程中起一定的作用。

为了便于复用,我们单独创建了一个ClassLibrary,取名为Extensions

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities.Persistence;
using System.Xml.Linq;

namespace Extensions
{
    public class MyInstanceStoreParticpant : PersistenceParticipant
    {

        public int TicketId { get; set; }
        XNamespace xNS = XNamespace.Get("http://xizhang.com/DocumentReview");
       
        /// <summary>
        /// 这个方法会在工作流实例被持久化的时候自动调用
        /// 这些数据是会被保存到InstancePromotedPropertiesTable这个表的
        /// </summary>
        /// <param name="readWriteValues"></param>
        /// <param name="writeOnlyValues"></param>
        protected override void CollectValues(out IDictionary<XName, object> readWriteValues, out IDictionary<XName, object> writeOnlyValues)
        {
            readWriteValues = new Dictionary<XName, object>();
            readWriteValues.Add(xNS.GetName("TicketId"), this.TicketId);

            writeOnlyValues = null;
        }

    }
}

image

这里提到一个特殊的表:InstancePromotedPropertiesTable(就是在持久化那个数据库中,本例为WF4),大家如果有时间可以看一下结构。它有66个字段。

同时,在这个项目中,我们还添加一个自定义的Activity,来实现真正的数据保存

image

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;

namespace Extensions
{

    public sealed class SetTicket : CodeActivity
    {
        public InArgument<int> TicketId { get; set; }
        protected override void Execute(CodeActivityContext context)
        {
            var extension = context.GetExtension<MyInstanceStoreParticpant>();
            extension.TicketId = TicketId.Get(context);
        }
    }
}

5.2 在宿主中使用该扩展,并且设定要保存的信息

image

代码也要做相应的修改,请注意红色部分

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Activities;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

using System.Activities.DurableInstancing;
using System.Runtime.DurableInstancing;
using System.Activities.Persistence;
using System.ServiceModel.Activities.Description;
using System.Xml.Linq;

namespace Host
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WorkflowServiceHost(
                new DocumentReviewLib.DocumentReviewWorkflow(),
                new Uri("http://localhost:8080/DRS"));

            host.AddDefaultEndpoints();//这个方法是添加了一些标准的端点

            host.Description.Behaviors.Add(
                new ServiceMetadataBehavior() { HttpGetEnabled = true });

            var store = new SqlWorkflowInstanceStore("server=(local)\sqlexpress;database=WF4;integrated security=true");

            host.UnknownMessageReceived += (o, e) =>
            {
                Console.WriteLine("
"+e.Message+"
");
            };


            host.Description.Behaviors.Add(
                new WorkflowIdleBehavior()
                {
                    TimeToPersist = TimeSpan.FromSeconds(0)
                });

            XNamespace xNS = XNamespace.Get("http://xizhang.com/DocumentReview");
            store.Promote("DocumentReview",
                new List<XName>() { xNS.GetName("TicketId") },
                null);


            host.WorkflowExtensions.Add(new Extensions.MyInstanceStoreParticpant());


            host.DurableInstancingOptions.InstanceStore = store;
            host.Open();



            Console.WriteLine("Server is ready.");
            Console.Read();

        }


    }
}

5.3 使用工作流设计,使用自定义的Activity

请确保在DocumentReviewLib中添加了如下三个引用

image

将自定义的Activity拖放咋合适位置,并且让它的属性TicketId绑定到变量

image

5.4 调试程序

启动服务器和客户端,点击多次后,到SSMS中查看 [WF4].[System.Activities.DurableInstancing].[InstancePromotedPropertiesTable]这个表的数据

image

那么,怎么查询这些数据呢?

其实也不难,我们一般推荐在数据库中做一个视图,如

USE WF4
GO

CREATE VIEW DocumentReviewTask
AS
SELECT [SurrogateInstanceId]
      ,[PromotionName]
      ,[Value1] AS TicketId
FROM [WF4].[System.Activities.DurableInstancing].[InstancePromotedPropertiesTable]

查询这个视图的结果如下

image

5.5 在宿主程序中通过一个特殊的服务,提供这个列表给客户端

因为涉及到数据访问,我们这里用一个LINQ to SQL Class来简化开发

image

从数据库中将那个视图托拽到设计器中

image

将宿主代码修改如下,请注意红色部分

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Activities;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

using System.Activities.DurableInstancing;
using System.Runtime.DurableInstancing;
using System.Activities.Persistence;
using System.ServiceModel.Activities.Description;
using System.Xml.Linq;

namespace Host
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WorkflowServiceHost(
                new DocumentReviewLib.DocumentReviewWorkflow(),
                new Uri("http://localhost:8080/DRS"));

            host.AddDefaultEndpoints();//这个方法是添加了一些标准的端点

            host.Description.Behaviors.Add(
                new ServiceMetadataBehavior() { HttpGetEnabled = true });

            var store = new SqlWorkflowInstanceStore("server=(local)\sqlexpress;database=WF4;integrated security=true");

            host.UnknownMessageReceived += (o, e) =>
            {
                Console.WriteLine("
"+e.Message+"
");
            };


            host.Description.Behaviors.Add(
                new WorkflowIdleBehavior()
                {
                    TimeToPersist = TimeSpan.FromSeconds(0)
                });

            XNamespace xNS = XNamespace.Get("http://xizhang.com/DocumentReview");
            store.Promote("DocumentReview",
                new List<XName>() { xNS.GetName("TicketId") },
                null);


            host.WorkflowExtensions.Add(new Extensions.MyInstanceStoreParticpant());


            host.DurableInstancingOptions.InstanceStore = store;
            host.Open();

            var common = new ServiceHost(
                typeof(CommonService),
                new Uri("http://localhost:8080/Common"));

            common.AddServiceEndpoint(
                typeof(ICommonService).FullName,
                new BasicHttpBinding(),
                "");

            common.Open();

 Console.WriteLine("Server is ready."); Console.Read(); } }  [ServiceContract] public interface ICommonService { [OperationContract] int[] GetTicketIds(); } public class CommonService : ICommonService { public int[] GetTicketIds() { var ctx = new InstanceStoreDataContext(); return ctx.DocumentReviewTasks.Select(r => (int)r.TicketId).ToArray(); } } } 

5.6 修改客户端,使用该服务

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel.Activities;
using System.ServiceModel;


namespace Client
{
    [ServiceContract]
    public interface ICommonService
    {
        [OperationContract]
        int[] GetTicketIds();
    }


    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            Load += new EventHandler(Form1_Load);
        }




        void Form1_Load(object sender, EventArgs e)
        {
            LoadTaskList();

        }

        private void LoadTaskList()
        {
            //加载所有没有完成的流程
            var factory = new ChannelFactory<ICommonService>(
                new BasicHttpBinding(), new EndpointAddress("http://localhost:8080/Common"));


            var proxy = factory.CreateChannel();

            var ids = proxy.GetTicketIds();
            foreach (var item in ids)
            {
                lstTickets.Items.Add(item);
            }
        }

        private void btCreate_Click(object sender, EventArgs e)
        {
            var proxy = new DocumentReviewClient();
            var result = proxy.CreateTicket();

            lstTickets.Items.Add(result);
        }

        private void btApproval_Click(object sender, EventArgs e)
        {
            //同意某个流程
            var action = "approval";
            UpdateTicket(action);

        }

        private void UpdateTicket(string action)
        {
            if (lstTickets.SelectedIndex > -1)
            {
                var id = int.Parse(lstTickets.SelectedItem.ToString());
                var comment = txtComment.Text;
                var proxy = new DocumentReviewClient();
                proxy.UpdateTicket(action, comment, id);

            }
        }

        private void btReject_Click(object sender, EventArgs e)
        {
            var action = "Reject";
            UpdateTicket(action);
        }

    }
}

5.7 调试程序

image

 
image
 

总结:我用了四篇文章介绍了基于WF4实现审批流程的一个例子,通过实例可以帮助大家更好地理解有关的技术。

完整代码,请通过 这里 下载


WF事件驱动(5)

之前,我通过4篇文章介绍了在WF4中开发基于事件的工作流的范例。请参考下面的链接。 

这一篇是这个系列的最后一篇,介绍如何通过配置文件,而不是代码的方式启动宿主。这在现实工作中是相当有用的,请大家参考下面的实例。

【注意】有朋友也问到单独用数据库存储业务方面的数据,那是没有错的。一般可以通过自定义的Activity去完成这些操作,都是标准的ADO.NET的数据访问操作。这里就不做展开了。

这个案例的最终代码范例,请通过 这里 下载

1.修改之前的Host代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Activities;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

using System.Activities.DurableInstancing;
using System.Runtime.DurableInstancing;
using System.Activities.Persistence;
using System.ServiceModel.Activities.Description;
using System.Xml.Linq;

namespace Host
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WorkflowServiceHost(
                new DocumentReviewLib.DocumentReviewWorkflow(),
                new Uri("http://localhost:8080/DRS"));

            host.AddDefaultEndpoints();//这个方法是添加了一些标准的端点

            host.Description.Behaviors.Add(
                new ServiceMetadataBehavior() { HttpGetEnabled = true });

            var store = new SqlWorkflowInstanceStore("server=(local)\sqlexpress;database=WF4;integrated security=true");

            host.UnknownMessageReceived += (o, e) =>
            {
                Console.WriteLine("
" + e.Message + "
");
            };


            host.Description.Behaviors.Add(
                new WorkflowIdleBehavior()
                {
                    TimeToPersist = TimeSpan.FromSeconds(0)
                });

            XNamespace xNS = XNamespace.Get("http://xizhang.com/DocumentReview");
            store.Promote("DocumentReview",
                new List<XName>() { xNS.GetName("TicketId") },
                null);


            host.WorkflowExtensions.Add(new Extensions.MyInstanceStoreParticpant());


            host.DurableInstancingOptions.InstanceStore = store;
            host.Open();

            var common = new ServiceHost(
                typeof(CommonService),
                new Uri("http://localhost:8080/Common"));

            common.AddServiceEndpoint(
                typeof(ICommonService).FullName,
                new BasicHttpBinding(),
                "");

            common.Open();


            Console.WriteLine("Server is ready.");
            Console.Read();

        }


    }


    [ServiceContract]
    public interface ICommonService
    {
        [OperationContract]
        int[] GetTicketIds();
    }


    public class CommonService : ICommonService
    {

        public int[] GetTicketIds()
        {
            var ctx = new InstanceStoreDataContext();
            return ctx.DocumentReviewTasks.Select(r => (int)r.TicketId).ToArray();
        }
    }

}

2. 修改之后的Host代码(请大家比较一下有何区别)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Activities;
using System.ServiceModel.Activities;
using System.ServiceModel.Description;

using System.Activities.DurableInstancing;
using System.Runtime.DurableInstancing;
using System.Activities.Persistence;
using System.ServiceModel.Activities.Description;
using System.Xml.Linq;

namespace Host
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new WorkflowServiceHost(
                new WorkflowService() { 
                    ConfigurationName = "DocumentReviewLib.DocumentReviewWorkflow",
                    Body = new DocumentReviewLib.DocumentReviewWorkflow()
                });


            //这里可以通过进一步的Behavior定制来简化。此处略            
            XNamespace xNS = XNamespace.Get("http://xizhang.com/DocumentReview");
            var store = (SqlWorkflowInstanceStoreBehavior)host.Description.Behaviors.FirstOrDefault(b => b.GetType() == typeof(SqlWorkflowInstanceStoreBehavior));

            store.Promote("DocumentReview",
                new List<XName>() { xNS.GetName("TicketId") },
                null);



            //这里可以通过进一步的Behavior定制来简化。此处略
            host.WorkflowExtensions.Add(new Extensions.MyInstanceStoreParticpant());

            host.Open();

            var common = new ServiceHost(typeof(CommonService));

            common.Open();


            Console.WriteLine("Server is ready.");
            Console.Read();

        }


    }


    [ServiceContract]
    public interface ICommonService
    {
        [OperationContract]
        int[] GetTicketIds();
    }


    public class CommonService : ICommonService
    {

        public int[] GetTicketIds()
        {
            var ctx = new InstanceStoreDataContext();
            return ctx.DocumentReviewTasks.Select(r => (int)r.TicketId).ToArray();
        }
    }

}

3.添加的app.config文件内容

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
    </configSections>
    <connectionStrings>
        <add name="Host.Properties.Settings.WF4ConnectionString" connectionString="Data Source=.sqlexpress;Initial Catalog=WF4;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
  <system.serviceModel>

    
    
    <behaviors>
      <serviceBehaviors>
        <behavior name="WorkflowService">
          <sqlWorkflowInstanceStore connectionStringName="Host.Properties.Settings.WF4ConnectionString"/>
          <workflowIdle timeToPersist="0" timeToUnload="0"/>
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    
    <services>
      <service name="DocumentReviewLib.DocumentReviewWorkflow" behaviorConfiguration="WorkflowService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8080/DRS"/>
          </baseAddresses>
        </host>
        <endpoint contract="IDocumentReview" address="" binding="basicHttpBinding"></endpoint>
      </service>

      <service name="Host.CommonService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8080/Common"/>
          </baseAddresses>
        </host>

        <endpoint contract="Host.ICommonService" binding="basicHttpBinding" address=""></endpoint>
      </service>
    </services>
    
    
    
  </system.serviceModel>
  
</configuration>
 
这个案例的最终代码范例,请通过 这里 下载
原文地址:https://www.cnblogs.com/freeliver54/p/3953905.html