[转]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.因为那样既不实用(你不可能在项目中这么做),也会隐藏很多细节。
将默认的那个Activity1.xaml删除掉,然后添加一个DocumentReviewWorkflow
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中。(作为响应,我们只是随机产生一个流程编号,然后输出它)
这个设计过程,将得到下面这样的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:\temp\WF4EventDrivenSolution\DocumentReviewLib\DocumentReviewWorkflow.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="["流程被创建,编号为:" & TicketId]" />
</Sequence>
</PickBranch>
</Pick>
</Sequence>
</Activity>
3. 设计一个宿主程序
流程设计好之后,我们如何将它托管起来,并且允许客户端发出流程有关的操作呢
我们需要设计一个宿主。为简便起见,我们可以用Console来做为宿主。
同时,我们需要添加几个程序集的引用
然后,修改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,如果没有出什么意外的话,你可以看到下面这样的一个窗口
然后,你在浏览器中输入下面的地址,可以看到
这就表示服务已经成功启动了。
根据上面页面的提示,我们可以产生一个服务代理类。注意,要使用Visual Studio Command Prompt,而不是默认的cmd
4. 设计一个客户端程序
我们接下来要设计一个Windows Forms的客户端程序,来使用该服务,发起流程的操作
将刚才工具所生成的两个文件添加到当前项目,并且将output.config修改为app.config
添加对System.ServiceModel的引用
修改窗口的设计如下
代码如下
private void btCreate_Click(object sender, EventArgs e)
{
var proxy = new DocumentReviewClient();
proxy.CreateTicket();
}
是不是很简单呢?其实这里就是WCF调用。(有在WF3中做过流程开发的朋友一定不会陌生,以前我们也是自己定义WCF服务来实现客户端与服务器端的通讯)
那么,现在可以进行调试了。
5. 调试流程
因为既要启动服务器,又要启动客户端,所以我们可以设置一下启动项目顺序
接下来,就可以按下F5键进行调试了
然后,我们点击“创建流程”按钮。我们并没有发现什么。流程并没有被启动。这是为什么呢?
放心,没有什么大不了的,这只是我预先设计好的一个“陷阱”而已。我是想让大家明白,所有的WCF调用,默认都不会启动流程,除非有一个属性设置为true。这就是那个Receive Activity的CanCreateInstance属性,我们可以回到DocumentReviewLib中,将其设置为true。因为它默认是false。这个属性的意思是说,如果收到这个事件,那么要不要创建工作流的实例。一般在一个流程的顶部事件中,都是设置为true的
再次按下F5键,进行调试,然后点击“创建流程”按钮
这样看起来,流程确实被创建了。如果我们点击多次呢?毫无疑问,它会有多个实例产生出来
看起来不错,不是吗?我们从客户端发起了一个操作,它将被工作流收到消息,并且进行了相应的处理。(我们这里只是输出消息,你可以想象一下,你完全可以在这里更新数据库的记录)
那么,接下来有一个话题,一般情况下,我们一个流程都不止一个事件,例如发起流程之后,需要将这个编号返回给客户端,同时还要等待经理审批。如此这般的需求怎么实现呢?
我在下一篇先介绍一下,如何将处理结果发回给客户端。
然后,第三篇介绍如何实现审批事件。
【备注】本文代码,可以通过 这里 下载
我介绍到了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
点击该菜单,会有一个提示
我们可以选择将其粘贴到任何地方,例如
请注意,此时会多出来一个变量,叫__handler1,它的类型是CorrelationHandler,这个东西我们后面一篇会详细介绍,这里可以不关注它的细节。
粘贴过来的这个Activity,它是自动与之前的Receive这个Activity进行关联的
那么,我们到底要发送什么数据给用户呢?可以点击Activity上面的Content这个地方
将Message data绑定到我们之前的那个TicketId变量,并且设置Type为INT 32
这样,我们就将流程设计好了。重新编译一下吧
2. 重新生成客户端代码类
流程发生了此类变化,通常客户端代理类也要有所变化。我们先启动服务器程序
确认在浏览器中可以看到下面的结果
生成客户端代码文件
3. 修改客户端
将生成好的DocumentReviewWorkflow.cs文件,添加到客户端项目中,替换掉原先那个文件。
这里无需添加output.config,因为这个文件内容其实没有啥变化 。
修改窗口如下,添加了一个ListBox
修改代码如下
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键进行调试,在窗口上多次点击“创建流程”的话,会怎么样呢
不光是服务端有消息输出,我们在客户端也可以看到一个TicketId的列表
本文代码,可以通过 这里 下载
下一篇,我们将完成这个流程的例子,我们将添加经理审批的事件。通过这样一个例子,你可以理解如何设计基于事件驱动的流程。
前面两篇已经实现了最简单的基于事件的工作流程,用户可以在客户端(任意类型的客户端)发出流程操作的指令,通过WCF的通讯,驱动后台的工作流工作。
但之前的例子只有一个事件,就是“创建流程”的事件,显然这是不够的。这一篇就来把这个例子完善一下,通过这个练习之后,大家应该可以大致了解在WF4中如何设计基于事件的流程了
我们将为这个流程添加一个“审批流程”的事件。
本文代码,请通过 这里 下载
1. 修改工作流设计
很显然地,我们会在下面添加另外一个Pick Activity,然后里面也添加一个Receive来实现事件监听
同时,我们定义了三个变量用来接收用户传递过来的数据
我们在Receive上面设置了参数与这些变量之间的映射
一切看起来都还是挺自然的。请注意,这个Receive,因为是第二个事件,所以无需创建新的工作流实例(如果每个事件都创建新的实例,那就乱套了),也就是说CanCreateInstance不需要设置为true
这个工作流的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:\temp\WF4EventDrivenSolution\DocumentReviewLib\DocumentReviewWorkflow.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="["流程被创建,编号为:" & 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("Ticket update --- action : {0}, comment :{1} ", action, comment)]" />
</PickBranch>
</Pick>
</Sequence>
</Activity>
2. 更新客户端代码和设计
工作流已经进行了修改,相应地,客户端代码也需要有一些更新
启动宿主程序,然后通过下面的方式再次得到客户端代码文件。
同样,我们只需要添加那个DocumentReviewWorkflow.cs到项目中来
我们修改窗口如下,添加审批的工具按钮
有关代码如下
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. 调试程序
我几乎迫不及待地想要看一下效果了。选择某个Ticket之后,点击“同意”按钮,激动人心的时刻到了。晕,又是什么也没有发生。
你猜对了,这又是我预先埋下的一个“陷阱”。放心吧,如果那么容易做出来,我是不会专门写一篇来介绍的。
4.修改流程设计
有点沮丧吗?其实大可不必。淡定,淡定
静下心来想一下,为什么它不能工作呢?很显然,客户端的请求是发送到了服务端的,那么就是说服务端不知道该怎么处理?这样解释是合理的吧。
那么服务端为什么不知道该怎么处理呢?你可能会说,我都告诉他TicketId了,为什么它不会找到那个Instance,然后触发里面的事件呢?
症结点就在于这里,你光是传递TicketId过来是不够的,还需要在工作流中将这个Id标识为一个上下文事件关联的依据。
还记得第二篇提到的CorrelationHandler吗?既然事件需要被定位到,那么它就需要有一定的机制可以区分。不同实例之间就是通过不同的CorrelationHandler来区别的。
那么,我们既然想让UpdateTicket事件与相应的CreateTicket事件所创建的那个实例关联起来,就需要定义一个特殊的Handler。
注意,这个__handler是我们定义的,而上面的那个__handler1是之前第二个练习时自动创建的
接下来,我们需要对这个handler进行初始化。这个可以通过一个InitializeCorrelation的Activity来完成
我们让这个初始化好的handler具有一个属性,Id,保存当前这个TicketId
然后,我们修改”UpdateTicket”这个事件
设置CorrelatesWith属性
设置CorrelatesOn属性
这个设置,也就是说,让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:\temp\WF4EventDrivenSolution\DocumentReviewLib\DocumentReviewWorkflow.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="["流程被创建,编号为:" & 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("Ticket update --- Ticketid :{0} , action : {1}, comment :{2} ", reviewId, action, comment)]" />
</PickBranch>
</Pick>
</Sequence>
</Activity>
5.测试程序
发起流程后,分别选择不同的编号,进行审批。结果如下
大功告成,收工
总结:
利用WF 4的全新框架,可以快速设计基于事件驱动的工作流。它提高了开发效率,简化了开发流程,而更重要的是,这对于广大开发人员的身心健康有着很有益地帮助
前面三篇,我介绍到了如何在WF 4中设计简单的审批流程,没有什么特别出奇的技术,只不过WF4对于事件机制有了不小的改进吧。
这一篇要来谈谈更加深入一点的话题:如果我们的流程需要长时间才能完成(这是很常见的),那么如何在这些流程空闲(例如等待经理审批)的时候,更好地管理它们呢?
我们都知道,默认情况下,所有流程实例都是在内存中被创建的一个对象。那么这里提到的管理,有两个层面的意思:
- 如果某些实例处于空闲状态,那么他们所占用的内存可能是浪费的。
- 由于可能因意外情况导致的宕机(例如停电,或者被某个恶作剧者按下了重启按钮),所以放在内存中的实例是很不保险的
所以,为了达到上面的两个目的,WF 提供了所谓的“持久化”的功能。就是支持我们将工作流实例通过一定的方式保存起来,等需要的时候再取出来即可。
WF3就开始支持这种特性,那时候称之为“持久化服务”。WF4对此做了进一步的改进和完善。本文主要就是讨论WF4下面如何做持久化。
完整代码,请通过 这里 下载
1. 准备持久化数据库
WF的持久化功能默认是用一个SQL Server的数据库来保存数据的。当然,在此基础上我们可以扩展。但通常使用默认的这个数据库是明智的选择。
WF4提供了两个脚本,可以让我们来生成这个数据库。这两个脚本通常在下面的目录
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SQL\en
【注意】如果你不是x64的系统,则可能是C:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en
我们可以手工先在SQL Server Management Studio中创建一个数据库,例如叫WF4
然后,将数据库上下文切换到WF4,依次运行两个脚本
SqlWorkflowInstanceStoreSchema.sql
SqlWorkflowInstanceStoreLogic.sql
这个数据库的结构如果有兴趣,可以研究一下。这里就不过多展开了
2. 修改宿主程序,添加持久化服务的功能
数据库准备好之后,我们需要对宿主稍做修改,就可以完成持久化功能的配置
首先,我们需要在宿主程序中添加两个程序集引用
修改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进行调试
我们回到数据库看一下情况,请注意看那个InstancesTable
也就是说,它已经把这四个实例保存到了数据库中。
然后,我们接下来对流程进行审批
很显然,236738261这个流程已经结束了,我们再来看一下数据库中的记录
我们看到,现在数据库中的记录数也变为了3条。
4.如何加载已经保存好的实例
既然有这样一个数据库保存好了我们的实例,那么就可以放心大胆地将宿主程序关闭掉。
我们来看,数据库中的记录仍然是在的。请注意,我因为做了其他一些测试,所以现在实例有5个
看起来很不错,不是吗?
但是有一个问题随之而来,当我们再次打开应用程序的时候,我们可能希望宿主程序能自动地加载这些实例的信息,或者说我们仍然能够对这些实例进行操作。这要如何来完成呢?
请大家按照我的步骤来做练习
4.1 将宿主程序开起来
4.2 将客户端开起来
点击“创建流程”按钮,可以多点几次
4.3 将宿主程序关闭掉
这样做的目的,是模拟一下服务器突然停电了或者类似这样的情况。
请不要将客户端关闭
4.4 重新开启宿主程序
这样做就模拟服务器重启的场景。那么,问题就是,此时客户端还能继续处理那些未完成的流程吗?
我们可以选择一个编号之后,还是和以前那样,点击“同意”或者“拒绝”按钮
我们发现,这个流程还是可以继续处理的。而且,我们并不需要在服务端做任何特殊的设计。
所以,我们可以这么总结一下:当一个流程的请求被发送到服务端,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("\n"+e.Message+"\n");
};
host.Open(); Console.WriteLine("Server is ready.");
Console.Read(); } }
}
调试的时候,可以对一个编号连续点击两次“同意”,则第二次会被视为不合法的请求。如下图所示
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;
} }
}
这里提到一个特殊的表:InstancePromotedPropertiesTable(就是在持久化那个数据库中,本例为WF4),大家如果有时间可以看一下结构。它有66个字段。
同时,在这个项目中,我们还添加一个自定义的Activity,来实现真正的数据保存
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 在宿主中使用该扩展,并且设定要保存的信息
代码也要做相应的修改,请注意红色部分
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("\n"+e.Message+"\n");
}; 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中添加了如下三个引用
将自定义的Activity拖放咋合适位置,并且让它的属性TicketId绑定到变量
5.4 调试程序
启动服务器和客户端,点击多次后,到SSMS中查看 [WF4].[System.Activities.DurableInstancing].[InstancePromotedPropertiesTable]这个表的数据
那么,怎么查询这些数据呢?
其实也不难,我们一般推荐在数据库中做一个视图,如
USE WF4
GO CREATE VIEW DocumentReviewTask
AS
SELECT [SurrogateInstanceId]
,[PromotionName]
,[Value1] AS TicketId
FROM [WF4].[System.Activities.DurableInstancing].[InstancePromotedPropertiesTable]
查询这个视图的结果如下
5.5 在宿主程序中通过一个特殊的服务,提供这个列表给客户端
因为涉及到数据访问,我们这里用一个LINQ to SQL Class来简化开发
从数据库中将那个视图托拽到设计器中
将宿主代码修改如下,请注意红色部分
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("\n"+e.Message+"\n");
}; 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 调试程序
总结:我用了四篇文章介绍了基于WF4实现审批流程的一个例子,通过实例可以帮助大家更好地理解有关的技术。
完整代码,请通过 这里 下载
之前,我通过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("\n" + e.Message + "\n");
}; 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>
这个案例的最终代码范例,请通过 这里 下载
[转]WF事件驱动的更多相关文章
- 【转】WF事件驱动
转自:http://www.cnblogs.com/Mayvar/category/315963.html 这系统的教程有代码可以下载 WF事件驱动(5) 摘要: 之前,我通过4篇文章介绍了在WF4中 ...
- js有关事件驱动
事件驱动 /* 什么是事件? 1.事件发生了 2.我要对这个事件做对应的处理 ...
- 风尘浪子 只要肯努力,梦想总有一天会实现 WF工作流与Web服务的相互调用 —— 通过Web服务调用Workflow工作流(开发持久化工作流) _转
如果你曾经负责开发企业ERP系统或者OA系统,工作流对你来说一定并不陌生.工作流(Workflow)是对工作流程及其各操作步骤之间业务规则 的抽象.概括.描述.工作流要解决的主要问题是:为实现某个业务 ...
- WF中的Bookmark
最近在学习WF, 把自己的一些学习心得放上来 Bookmark允许你以事件驱动的方式与一个Workflow内的Activity进行交互. 如果把启动一个workflow比做开始读一本书, 在Workf ...
- [WF4.0 实战] 事件驱动应用
看到题目或许非常多人都会疑问,为什么要使用事件监听呢? 眼下的认识: 1,使用事件监听能够将工作流的结点返回值返回到client 2,能够实现等待与重新启动,相当于之前的WaitActivity创建B ...
- C++ 事件驱动型银行排队模拟
最近重拾之前半途而废的C++,恰好看到了<C++ 实现银行排队服务模拟>,但是没有实验楼的会员,看不到具体的实现,正好用来作为练习. 模拟的是银行的排队叫号系统,所有顾客以先来后到的顺序在 ...
- Node学习笔记(二):事件驱动
接触Node,提得最多的可能就是回调,异步非阻塞处理,思前想后,JavaScript从前端语言过渡到服务器端,最大的劣势可能就是线程,当然这方面的不足现在也被慢慢弥补起来了(很多第三方的npm包可供下 ...
- 基本术语表【WF】
术语 定义 activity(活动) Windows Workflow Foundation 中的程序行为单元. 可将单个活动组合在一起,形成更复杂的活动. activity action(活动操作) ...
- IDDD 实现领域驱动设计-CQRS(命令查询职责分离)和 EDA(事件驱动架构)
上一篇:<IDDD 实现领域驱动设计-SOA.REST 和六边形架构> 阅读目录: CQRS-命令查询职责分离 EDA-事件驱动架构 Domin Event-领域事件 Long-Runni ...
随机推荐
- INTERSECT(交集)集合运算
在集合论中,两个集合(记为集合A和B)的交集是由既属于A,也属于B的所有元素组成的集合. 在T-SQL 中,INTERSECT 集合运算对两个输入查询的结果集取其交集,只返回在两个查询结果集中都出现的 ...
- 用CSS画小猪佩奇,你就是下一个社会人! js将“I am a coder”反转成 “coder a am I”,不许用split,join,subString,reverse;求解方法三
用CSS画小猪佩奇,你就是下一个社会人! 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 作者:江志耿 | 腾讯TEG网络工程师 我是佩奇,哼,这是我的弟弟乔治,呱呱,这是我的妈妈,嚯 ...
- ZrcListView
https://github.com/zarics/ZrcListView
- Android:内存控制及OOM处理
1. OOM(内存溢出)和Memory Leak(内存泄露)有什么关系? OOM可能是因为Memory Leak,也可能是你的应用本身就比较耗内存(比如图片浏览型的).所以,出现OOM不一定是Me ...
- busybox相关的工具
1 mdev busybox里面的类似于udev的工具,学名micro udev. mdev -s扫描/sys目录,如果是设备的话,就会为之在/dev目录下创建设备结点. 2 busybox执行不同的 ...
- redirect和rewrite
1 服务器端重定向 客户端想要访问的内容不在该服务器上,该服务器自己去另外的服务器请求到该内容,然后还是由该服务器将内容返回给客户端.称为rewrite. 2 客户端重定向 客户端想要访问的内容不在该 ...
- Common non-standard response fields
https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#cite_note-52 Common non-standard response f ...
- aapt的常用命令
1. 列出apk包的内容 aapt l[ist] [-v] [-a] file.{zip,jar,apk} -v 以table形式列出来 -a 详细列出内容 例如:aapt l <你的apk文件 ...
- Macaca框架
收藏 http://www.cnblogs.com/jinjiangongzuoshi/p/6537795.html
- hdu 2476 (string painter) ( 字符串刷子 区间DP)
String painter Time Limit: 5000/2000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)To ...