一、实现目的
1、在Web中托管Workflow引擎实现工作流流转。
2、使用持久化的方式存放工作流实例状态。
二、基本原形
报销流程(网上有很成熟的基于工作流的报销流程,我还在这里显摆,唉!)
三、使用到的核心类
WorkflowRuntime、ExternalDataExchangeService、WorkflowInstance、WorkflowPersistenceService
分别在dotnet3.0的一下命名空间中:
System.Workflow.Runtime、System.Workflow.Activities、System.Workflow.Runtime.Hosting
四、工作流项目设计
1、报销流程还是沿用了网上广为流传的那个报销流程的模式(细节上也没有太大的差别),这里简单描述一下:报销者提交报销信息——〉部门经理审批——〉副总经理审批(如果数目小于1000元跳过此环节)——〉财务经理审批——〉出纳确认——〉结束,如果在除提交环节的任何一个环节审批不通过则将打回到提交者进行提交的初始状态。该例子中使用了状态机模式(个人认为状态机的Workflow可以很好的满足我以前开发中的大多数需求)。
2、使用VS2005进行Workflow开发的基本条件网上很多,这里简单的说一下:
dotnetfx3、Visual Studio 2005 Extensions for Windows Workflow Foundation (CHS)[都可以从官方下载]
3、安装好后会在项目模板中出现专门为Workflow工程开发所提供的几个模板
我个人平时喜欢建立带有控制台的,那样调试很方便,当然如果那样做了的话后来还需要再创建一个基于库的,把那些代码再粘贴过去,因为控制台的bin文件夹里只有exe文件,不能让其他项目进行引用(不过可能有其他的方法我不知道)
4、在工具箱中选择创建流程的各种活动Actives在本例中只适用到了五种State、EventDriven、HandleExternalEvent、SetSate、IfElse
其中State就是状态机中的每个状态的最大容器(当然在设计工作流的时候也可以把一些Actives放在State之外,比如EventDriven,但我还不知道放在状态之外的用处),我设计的其他Actives都是在State里面的。其实我个人使用Active时那些能放在另外一些里面都是通过将他们托放看能不能实现来判断的。
5、根据我的业务流程,在这个例子中总共使用到了6种状态,起始[InitState]和结束[EndState]好像是状态机工作流中必须的两个状态,而且好像能有多个起始状态,结束状态之有一个。此外在该例中还有部门经理审批状态[DManagerState]、副总审批状态[GManagerState]、财务经理审批状态[FManagerState]、出纳确认状态[CashierState](本人的状态分解可能在逻辑上不甚合理,请谅解)。每个状态中都有通过事件进行驱动的EventDriven活动(一个为审批通过触发的事件,一个为审批未通过),还有用来设置下一个进入某个状态的SetState活动。只有在部门经理审批状态中添加了判断报销金额再进行环节流转的Actives。包含关系大概为State活动中包含(拖放)EventDrive活动,EventDrive活动中包含(拖放)HandleExternalEvent、SetSate、IfElse活动。
(一个状态图片)
(进入EventDriven的图片)
(部门经理审批通过触发的EventDriven中判断金额进行状态选择流转的图片)
6、在工作流图形定制界面中除了将所有的Actives拖入放置好以外,还需要对每个Actives进行设置。几种类型Active的大致设置方法为:
State、EventDriven活动只需要进行名称的设置就可以了因为他们可能只是做其他事件或者操作方式的一个容器,不提供什么方法。
HandleExternalEvent活动需要设置需要处理的外部事件,这些外部事件需要在一个定义了很多事件类型的接口来选择。其实就是我们需要定义一个接口,在接口中定义需要在HandleExternalEvent中调用的事件,然后再实现这个接口,在宿主环境中调用接口的实现类来对Workflow引擎发送事件消息,HandleExternalEvent事件在得到这个消息之前是等待状态,也就是说会引发workflowRuntime的WorkflowIdled事件(我们会在这里处理工作流持久化)。HandleExternalEvent得到这个事件消息之后就会往下执行了。因此我们需要设置HandleExternalEvent活动的一下两个属性值InterfaceType和EventName,其中InterfaceType是我们定义的那个接口,EventName是我们在接口中定义的用来为特定HandleExternalEvent进行触发的事件。我们也可以通过使用Invoked来在工作流中处理一些需要的操作。
IfElse活动作为一个容器只需要设置名称,其中需要特别注意的是活动中所包含的两个分支,每个分支都可以绑定进行判断的方法。也可以只在左边的分支上进行条件判断,条件判断可以选择使用“Declarative Rule Condition”申明规则条件(本例中未使用,本人也不太清楚怎么用),还有一个“Code Condition”进行判断方法的绑定,本例中使用这种。选择后可以展开Condition属性将自己写好的条件方法代码绑定。
(IfElse活动)
(IfElse分支属性)
SetState活动只需要设置它的名称和TargetStateName属性,该属性的作用是选择下一步要流入的状态名称
7、在工作流代码文件中需要完成IfElse活动中需要的条件判断方法和一个用来接收从宿主(hosting)环境中传入的用来进行报销金额是否大于1000元的参数属性。
在工作流设计的“解决方案资源管理器”中选择 “查看代码”,加入以上代码,并最终绑定IfElse活动的Condition属性。
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
namespace Workflow
{
public sealed partial class Workflow: StateMachineWorkflowActivity
{
// 报销金额[以后有了Orcas就不需要定义这个变量了]
private string _strMoneyNum = default(string);
// 报销金额[属性]
public string MoneyNum
{
get
{
return _strMoneyNum;
}
set
{
_strMoneyNum = value;
}
}
public Workflow()
{
InitializeComponent();
}
/// <summary>
/// IfElse活动左分支的Code Condition中绑定的方法
/// </summary>
/// <param name="sender">发送对象</param>
/// <param name="e">注意这里的事件数据类型必须为ConditionalEventArgs</param>
public void GManagerOrFManager(object sender, ConditionalEventArgs e)
{
// 设置e中需要给工作流返回的bool参数
if (Convert.ToInt16(MoneyNum.Trim()) > 1000)
e.Result = true;
else
e.Result = false;
}
}
}
8、为每个需要接收事件的HandleExternalEvent活动定义一个统一的ExternalDataExchangeService类型接口,并且实现该接口,这样可以在宿主环境中使用接口实现类的实例同工作流引擎通信。
9、在整个解决方案中创建新的Windows类库项目,本例中创建名称为ExtendServiceInstance的项目
10、在解决方案中创建一个以[ExternalDataExchange]为属性的接口,本例中文件名称为ICharge
using System;
using System.Workflow.Activities;
namespace ExtendServiceInstance
{
[ExternalDataExchange]
public interface ICharge
{
#region ICharge 成员
event EventHandler<ExternalDataEventArgs> OnInitStart;
event EventHandler<ExternalDataEventArgs> OnDManagerStart;
event EventHandler<ExternalDataEventArgs> OnDManagerRStart;
event EventHandler<ExternalDataEventArgs> OnGManagerStart;
event EventHandler<ExternalDataEventArgs> OnGManagerRStart;
event EventHandler<ExternalDataEventArgs> OnFManagerStart;
event EventHandler<ExternalDataEventArgs> OnFManagerRStart;
event EventHandler<ExternalDataEventArgs> OnCashierStart;
event EventHandler<ExternalDataEventArgs> OnCashierRStart;
event EventHandler<ExternalDataEventArgs> OnEndStart;
#endregion
}
}
11、创建实现该接口的类,本例中文件名称为Charge
using System;
using System.Workflow.Activities;
namespace ExtendServiceInstance
{
[Serializable]
public class Charge : ICharge
{
#region ICharge 成员
public event EventHandler<ExternalDataEventArgs> OnInitStart;
public event EventHandler<ExternalDataEventArgs> OnDManagerStart;
public event EventHandler<ExternalDataEventArgs> OnDManagerRStart;
public event EventHandler<ExternalDataEventArgs> OnGManagerStart;
public event EventHandler<ExternalDataEventArgs> OnGManagerRStart;
public event EventHandler<ExternalDataEventArgs> OnFManagerStart;
public event EventHandler<ExternalDataEventArgs> OnFManagerRStart;
public event EventHandler<ExternalDataEventArgs> OnCashierStart;
public event EventHandler<ExternalDataEventArgs> OnCashierRStart;
public event EventHandler<ExternalDataEventArgs> OnEndStart;
#endregion
public void RaiseInitStartEvent(Guid instanceId)
{
if (OnInitStart != null)
{
OnInitStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseDManagerStartEvent(Guid instanceId)
{
if (OnDManagerStart != null)
{
OnDManagerStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseDManagerRStartEvent(Guid instanceId)
{
if (OnDManagerRStart != null)
{
OnDManagerRStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseGManagerStartEvent(Guid instanceId)
{
if (OnGManagerStart != null)
{
OnGManagerStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseGManagerRStartEvent(Guid instanceId)
{
if (OnGManagerRStart != null)
{
OnGManagerRStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseFManagerStartEvent(Guid instanceId)
{
if (OnFManagerStart != null)
{
OnFManagerStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseFManagerRStartEvent(Guid instanceId)
{
if (OnFManagerRStart != null)
{
OnFManagerRStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseCashierStartEvent(Guid instanceId)
{
if (OnCashierStart != null)
{
OnCashierStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseCashierRStartEvent(Guid instanceId)
{
if (OnCashierRStart != null)
{
OnCashierRStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
public void RaiseEndStartEvent(Guid instanceId)
{
if (OnEndStart != null)
{
OnEndStart(this, (new ExternalDataEventArgs(instanceId)));
}
}
}
}
类中的每个方法对应一个事件,目的是使用这些方法来触发所定义的事件进一步激发和工作流引擎的通信。12、在之前创建的工作流设计工程中引用我们刚才创建的类库项目
13、为之前在工作流设计中定义的所有HandleExternalEvent活动绑定接口和事件
14、编译工作流工程,如果没有编译错误,到这里工作流的设计就算完成了。
五、配置工作流持久化SQL Server数据库
本例中使用的工作流持久化方法是使用SqlWorkflowPersistenceService来对WorkflowPersistenceService服务进行实例化,因此需要在一台SQL Server数据库中配置实例化所需要的持久化库。本例以SQL Server 2005为例进行配置。
1、 配置数据库前确保<%WINDOWS%>Microsoft.NETFrameworkv3.0Windows Workflow FoundationSQLEN文件夹和其中的SqlPersistenceService_Logic.sql以及SqlPersistenceService_Schema.sql文件的存在。确保方法是安装dotnet3.0。
2、 开发SQL Server 2005的SQL Server Management Studio Express
3、 登陆后选择数据库里面的新建
4、 数据库名称可自定,在本例中名称为SqlPersistenceService
5、 选择“文件”——〉“打开”——〉“文件”。
6、 选择咱们刚才要求确保的那个目录中的sql脚本文件进行执行,顺序为先SqlPersistenceService_Schema.sql后SqlPersistenceService_Logic.sql。以SqlPersistenceService_Schema.sql为例演示:
打开SqlPersistenceService_Schema.sql文件后,需要再次登陆数据库。登陆完成后,在下拉框中选择咱们刚才新建的数据库
点击“!执行”
SqlPersistenceService_Logic.sql的执行过程同上。
7、至此持久化数据库配置完成。
六、宿主环境设计
1、在解决方案中新添加一个Web工程,本例中工程名称为WebUI,在工程中添加四个aspx页面进行项目运行时的宿主和跳转。
Default.aspx 加载报销条目的GridView页面
Info.aspx 工作流的宿主页面
Insert.aspx 添加报销信息页面
Login.aspx 登陆界面
2、因为本例核心为工作流的应用,所以我只介绍Info.aspx界面中有关宿主的核心内容。
3、在工程中“添加引用”将我们前边创建的工作流工程引用进来。
4、在Info.aspx文件的using 中需要加入以下几个引用。
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;
using System.Workflow.Activities;
using System.Collections.Generic;
using ExtendServiceInstance;
using Workflow;
之后要使用到的WorkflowRuntime、WorkflowInstance 在System.Workflow.Runtime命名空间下,Charge在ExtendServiceInstance命名空间下,ExternalDataExchangeService在System.Workflow.Activities命名空间下,WorkflowPersistenceService、SqlWorkflowPersistenceService在System.Workflow.Runtime.Hosting命名空间下,Dictionary在System.Collections.Generic命名空间下
5、使用状态机工作流的大致步骤为:
实例化工作流引擎 WorkflowRuntime——〉绑定工作流引擎中的事件——〉加载各种需要的服务——〉启动工作流引擎——〉如果是新创建工作流实例则是用WorkflowRuntime .CreateWorkflow方法,如果是从持久化SQL Server数据库中恢复工作流实例则使用workflowRuntime.GetWorkflow和WorkflowInstance.Load方法——〉使用StateMachineWorkflowInstance强制转化工作流实例来获得状态机工作流所有状态或者当前状态等——〉在WorkflowRuntime的WorkflowIdled事件所绑定的方法中调用e.WorkflowInstance.TryUnload()方法进行工作流向SQL Server数据库的写入。
6、以下是Info.aspx中核心的几段带注释的代码
实例化
/// <summary>
/// 定义工作流引擎对象,加载各种服务类、工作流实例或者创建工作流实例
/// </summary>
private static WorkflowRuntime workflowRuntime = default(WorkflowRuntime);
/// <summary>
/// 定义Charge服务类,用来和工作流实例通讯
/// </summary>
private static Charge chargeService = default(Charge);
/// <summary>
/// 定义工作流实例
/// </summary>
private static WorkflowInstance instance = default(WorkflowInstance);
/// <summary>
/// 定义扩展服务对象用来加载Charge服务类,并被工作流引擎加载
/// </summary>
private static ExternalDataExchangeService dataExchangeService = default(ExternalDataExchangeService);
/// <summary>
/// 定义持久化服务
/// </summary>
private static WorkflowPersistenceService persistenceService = default(WorkflowPersistenceService);
工作流引擎初始化、绑定服务、最后启动
if (workflowRuntime == null)
workflowRuntime = new WorkflowRuntime();
if (chargeService == null)
chargeService = new Charge();
if (persistenceService ==null)
// 实例化SqlWorkflowPersistenceService
persistenceService =
new SqlWorkflowPersistenceService(
"Data Source=10.18.100.31;Initial Catalog=SqlPersistenceService;Persist Security Info=True;
User ID=sa;Password=skyadmin;",
false,
new TimeSpan(1, 0, 0),
new TimeSpan(0, 0, 5));
if ( workflowRuntime.GetService(persistenceService.GetType()) == null )
// 绑定服务
workflowRuntime.AddService(persistenceService);
// 对工作流引擎中的各种事件进行需要的绑定
workflowRuntime.WorkflowCompleted += OnWorkflowCompleted;
workflowRuntime.WorkflowIdled += OnWorkflowIdled;
workflowRuntime.WorkflowPersisted += OnWorkflowPersisted;
workflowRuntime.WorkflowUnloaded += OnWorkflowUnloaded;
workflowRuntime.WorkflowLoaded += OnWorkflowLoaded;
workflowRuntime.WorkflowTerminated += OnWorkflowTerminated;
workflowRuntime.WorkflowAborted += OnWorkflowAborted;
// 绑定服务
dataExchangeService = new ExternalDataExchangeService();
if (workflowRuntime.GetService(dataExchangeService.GetType()) == null)
{
workflowRuntime.AddService(dataExchangeService);
if (dataExchangeService.GetService(chargeService.GetType()) == null)
dataExchangeService.AddService(chargeService);
}
// 启动引擎
workflowRuntime.StartRuntime();
新建工作流实例
// 使用Dictionary传递参数
Dictionary<string, object> parameters = new Dictionary<string,object>();
parameters.Add("MoneyNum", oleDS.Tables[0].Rows[0]["AC_MONEY"].ToString().Trim());
// 创建工作流实例
instance = workflowRuntime.CreateWorkflow(typeof(Workflow.Workflow), parameters);
// 工作流实例启动
instance.Start();
从持久化数据库加载工作流实例
// 工作流引擎被唤醒后加载工作流实例
instance = workflowRuntime.GetWorkflow(new Guid(oleDS.Tables[0].Rows[0]["AC_WORKFLOWID"].ToString().Trim()));
instance.Load();
工作流引擎挂起时进行持久化设置
private void OnWorkflowIdled(object sender, WorkflowEventArgs e)
{
// 调用SqlWorkflowPersistenceService进行持久化
e.WorkflowInstance.TryUnload();
}
工作流完成时进行自己的操作
private void OnWorkflowCompleted(object sender, WorkflowCompletedEventArgs instance)
{
// 添加工作流完成后需要的操作
}
七、设计中需要注意的问题
1、如果对状态机工作流发送的事件不是工作流当前的状态中所需要的,则会抛错。
2、因为Web不能保持程序当前的很多状态信息,因此需要自己处理对工作流引擎、实例等的信息保存。
3、在工作流引擎启动之前加载需要的服务和绑定需要工作流处理的事件。
4、同一工作流加载同一服务多次会出错。
5、工作流引擎可以不需要启动动作,而是用创建实例的启动方法来同时启动工作流引擎,但在持久化恢复时需要启动工作流引擎。
因为纯属个人练习性质的项目,该项目代码比较凌乱,且处理逻辑不够清晰,较复杂,而且是第一次写Blog,请大家谅解。谢谢!
使用本例需要改变代码中连接SQL Server数据库的字符串。