作为MS推出的第一个SAAS官方demo,LitwareHR成为笔者近期关注的重点。笔者在一个月之前还完全没有接触过.Net3.0中的主要内容(WCF,WF等),所以对LitwareHR的研究也比较吃力。本文大概梳理了工作流技术在LitwareHR中的应用场景和实现方式,适用于对WF和LitwareHR都稍有了解的tx阅读。其实本文还很不成熟,但目前园子里关于LitwareHR的文章太少了,希望能看到有更多的同学对SAAS和LitwareHR给与关注。后面笔者还会逐渐把对LitwareHR的分析发上来,大家一起学习,一起讨论。
1.WF应用的业务场景
由于只是一个示例应用,所以LitwareHR中只给出了一个业务应用:招聘流程。在这个业务应用中包括两种工作流类型,一个是主招聘流程,这是一个状态机工作流,用来发布招聘项目、启动招聘进程等;另一个是简历审核流程,用来具体对一个提交简历的个人进行一系列业务处理,如面试等,这是一个顺序工作流。主招聘流程是系统预定义的,不能被配置;而简历审核流程可以做简单配置。在招聘方(如contoso)发布了一个职位之后,就会创建一个招聘流程。任何在public site注册的人都可以去申请这个职位,一旦申请被提交,简历审核流程就会被创建。根据申请中的数据的不同(比如来自不同地区的申请),可以触发不同的简历审核流程(这是在后台的Business Process配置菜单下进行配置的)。你可以创建新的简历审核流程,也可以设定在何种条件下触发何种简历审核流程。另外,在对一个具体的简历审核流程的编辑上,由于Litware 1.0是个纯的Web程序,不包含Smart Client,所以也就没有包含一个Workflow Designer(WD),因此仅支持一些很简单的工作流编辑。首先它仅仅是一个顺序工作流,不支持状态机工作流;其次,工作流中的Activity完全是线性、顺序排列,你只能改变Activity的执行顺序,而无法添加更多的逻辑判断规则(即对rule/condition的支持较差)。另外,由于没有WD,我们甚至无法自定义活动的类型,只能从下拉列表中选择系统已经编程实现的Activity类型,下面这段代码就是用来初始化活动类型下拉框的,可以看到所有的可选活动类型都是写死在代码里的:
1
public Dictionary<string, string> GetStepTypes()
2![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
3
Dictionary<string, string> types = new Dictionary<string, string>();
4
types.Add(typeof(InterviewActivity).ToString(), Constants.Ruleset.InterviewStep);
5
types.Add(typeof(NegotiationActivity).ToString(), Constants.Ruleset.NegotiationStep);
6
types.Add(typeof(ResultActivity).ToString(), Constants.Ruleset.ResultStep);
7
return types;
8
}
另外,LitwareHR中WF是和WCF集成在一起使用,也就是说,对工作流的管理和操作都作为服务封装在WCF的服务端。我们又已经知道,LitwareHR中的WCF是寄宿在IIS上的,那在这种情况下,WF runtime会被Host到哪个进程上呢?应该就是w3wp.exe吧。
2.主要的处理流程及对应代码
下面我们就以 单位发布招聘计划(初始化招聘工作流)--〉应聘者提交简历(初始化简历处理工作流)--〉单位进行面试等工作(运行简历处理工作流实例)--〉同意或拒绝录取(结束简历处理工作流) 为主线,具体介绍一下LitwareHR中WF技术的应用。当然,这个主线偏重于对工作流运行时的介绍,如果您比较关注工作流的编辑和工作流规则的制定(这些内容在LitwareHR中体现得比较薄弱,相对来说也比较简单),可以从Private Site中的Business Logic的配置部分入手,本文就不赘述了。
(1)发布招聘计划(open position)
在下面的代码段里,不仅有招聘工作流的初始化过程,还包括了整个工作流运行时的初始化过程。
![](/Images/OutliningIndicators/ContractedBlock.gif)
发布招聘计划
1![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
/**//*在运行时的构造过程里,我们看到RecruitingMoniker和EvaluationMoniker这两个本地通信服务被加入了运行时,
2
*他们将作为工作流和运行环境交换信息的通道,我们在后面会看到。
3
*/
4
static public WorkflowRuntime Runtime
5![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
6
get
7![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
8
if (_runtime == null)
9![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
10
//根据WorkRuntime配置节点的配置信息来生成运行时实例。
11
//这给了我们不用重新编译代码就能改变运行时特性的能力。
12
_runtime = new WorkflowRuntime("WorkflowRuntime");
13
if (_runtime.GetService<ExternalDataExchangeService>() != null)
14![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
15![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*RecruitingMoniker实现了IRecruitingMoniker接口(此接口是一个被ExternalDataExchangeAttribute属性修饰)
16
*的类型。它提供了招聘开始、招聘结束、简历提交三个事件,运行时通过注册这三个事件与工作流实例交互。
17
*/
18
_runtime.GetService<ExternalDataExchangeService>().AddService(new RecruitingMoniker());
19![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*EvaluationMoniker实现了IEvaluationMoniker接口(此接口是一个被ExternalDataExchangeAttribute属性修饰)
20
*的类型。它提供了面试结束、商议结束、录取、拒绝录取四个事件,运行时通过注册这四个事件与工作流实例交互。
21
*/
22
_runtime.GetService<ExternalDataExchangeService>().AddService(new EvaluationMoniker());
23
}
24
_runtime.AddService(new WorkflowFactoryRuntimeService());
25
_runtime.StartRuntime();
26
}
27
return _runtime;
28
}
29
}
30![](/Images/OutliningIndicators/None.gif)
31![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
/**//*
32
*可以看到,每当新的招聘职位发布的时候,会有一个类型为RecruitingProcess的工作流实例并创建并开始在运行时运行。
33
*那就让我们来关注一下RecruitingProcess的工作流定义是怎么样的。
34
*
35
*/
36
static public Guid OpenRecruitingProcess(Position position)
37![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
38
Dictionary<string, object> parameters = new Dictionary<string, object>();
39
parameters.Add("TenantId", position.TenantId);
40
parameters.Add("PositionId", position.Id);
41
parameters.Add("Position", position);
42
WorkflowInstance workflowInstance = Runtime.CreateWorkflow(typeof(RecruitingProcess), parameters);
43
workflowInstance.Start();
44
RunWorkflowInScheduler(workflowInstance.InstanceId);
45![](/Images/OutliningIndicators/InBlock.gif)
46
return workflowInstance.InstanceId;
47
}
48![](/Images/OutliningIndicators/None.gif)
49![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
/**//*
50
*下面这段xml就是RecruitingProcess的工作流定义的xoml文件。里面值得注意的内容有:
51
*(1)初始状态为RPOpened
52
*(2)当RPOpened接收到ResumeSubmitted事件时,会触发其中的名为ResumeSubmitted的EventDrivenActivity。
53
*(3)ResumeSubmitted状态内部定义了3个需要顺序执行的活动:HandleExternalEventActivity,TenantPolicyActivity,
54
* TenantPolicyActivity。
55
*/
56
<StateMachineWorkflowActivity x:Class="LitwareHR.Recruiting.Workflows.RecruitingProcess" InitialStateName="RPOpened"
57
x:Name="RecruitingProcess" DynamicUpdateCondition="{x:Null}" CompletedStateName="{x:Null}"
58
xmlns:ns0="clr-namespace:LitwareHR.Recruiting.Workflows.Activities" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
59
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
60
<StateActivity x:Name="RPOpened">
61
<EventDrivenActivity x:Name="ResumeSubmitted">
62
<HandleExternalEventActivity Invoked="OnResumeSubmitted" x:Name="handleExternalEventActivity1"
63
EventName="ResumeSubmitted" InterfaceType="{x:Type LitwareHR.Recruiting.Workflows.IRecruitingMoniker}" />
64
<ns0:TenantPolicyActivity TenantId="{ActivityBind RecruitingProcess,Path=TenantId}" x:Name="RecruitingRules" />
65
<ns0:WorkflowFactoryActivity Invoking="SetNewWorkflowParameters" x:Name="workflowFactoryActivity1"
66
TargetWorkflow="{ActivityBind RecruitingProcess,Path=TargetEvaluationProcess}"
67
InstanceId="00000000-0000-0000-0000-000000000000" />
68
</EventDrivenActivity>
69
<EventDrivenActivity x:Name="Close">
70
<HandleExternalEventActivity x:Name="handleExternalEventActivity2" EventName="RecruitingProcessClosed"
71
InterfaceType="{x:Type LitwareHR.Recruiting.Workflows.IRecruitingMoniker}" />
72
<SetStateActivity x:Name="setStateActivity2" TargetStateName="RPClosed" />
73
</EventDrivenActivity>
74
</StateActivity>
75
<StateActivity x:Name="RPClosed">
76
<EventDrivenActivity x:Name="Open">
77
<HandleExternalEventActivity x:Name="handleExternalEventActivity3" EventName="RecruitingProcessOpened"
78
InterfaceType="{x:Type LitwareHR.Recruiting.Workflows.IRecruitingMoniker}" />
79
<SetStateActivity x:Name="setStateActivity1" TargetStateName="RPOpened" />
80
</EventDrivenActivity>
81
</StateActivity>
82
</StateMachineWorkflowActivity>
到这里问题还没探讨完,我们的工作流经常会像这里所展示的招聘工作流一样,要等待一个事件才能继续向下进行(本例中的事件是ResumeSubmitted)。在等待事件的过程中,运行时不会一直把工作流实例加载到内存中,而是会通过“钝化”操作将工作流实例当前的执行状态保存到数据库里,待事件发生后再继续加载执行(当然,这里牵扯到一些配置和算法,比如说运行时会在等待多长时间之后进行钝化操作,笔者目前尚未对此进行更深入地研究)。
(2)应聘者提交简历(submit resume)
在上面的段落中我们看到,一个招聘流程是一个状态机工作流,它的初始状态会被ResumeSubmitted事件触发,而这个事件的触发过程将在这个段落中被描述。先简单看一看工作流宿主环境中的方法调用过程:
1![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
/**//*
2
*当用户提交简历后,ProcessLogic中的这个方法将首先被调用。
3
*/
4
static public void SubmitResume(Resume resume, Upn upn)
5![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
6
RecruitingMoniker service = Runtime.GetService<RecruitingMoniker>();
7
Guid recruitingProcessId = GetRecruitingProcessByPositionId(resume.PositionId);
8![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*通过本地通信服务的SubmitResume方法来触发简历提交事件*/
9
service.SubmitResume(recruitingProcessId, resume, upn);
10![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*
11
*在等待建立提交的漫长时间内,招聘工作流可能早已被“钝化”,其状态也已被persistence和tracking服务保存到了数据库里。
12
*现在发生了简历提交事件,工作流需要继续运行了。
13
*/
14
RunWorkflowInScheduler(recruitingProcessId);
15
}
16![](/Images/OutliningIndicators/None.gif)
17![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
/**//*RecruitingMoniker.SubmitResume()方法引发了ResumeSubmitted事件,这将启动
18
状态及工作流的事件驱动活动(EventDrivenActivity)。*/
19
public void SubmitResume(Guid recruitingProcessId, Resume resume, Upn upn)
20![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
21
ResumeSubmittedEventArgs args = new ResumeSubmittedEventArgs(recruitingProcessId, resume);
22
args.Identity = upn.ToString();
23![](/Images/OutliningIndicators/InBlock.gif)
24
EventHandler<ResumeSubmittedEventArgs> evh = ResumeSubmitted;
25
if (evh != null)
26![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
27
evh(null, args);
28
}
29
}
既然事件启动,我们就重新关注一下在“发布招聘计划”中提到的名为ResumeSubmitted的EventDrivenActivity的定义吧,这个活动一共有三个子活动,第一个活动用来注册监听以及处理事件,在这个活动之后还有两个活动要被执行,分别是TenantPolicyActivity和WorkflowFactoryActivity。大家应该能想到的是,简历提交了,招聘工作流要继续进行,但我们在BusinessLogic的配置界面里可以配置多个Workflow的定义,需要根据预先定义的规则集(RuleSet)来确定究竟选用哪个。而这,正是TenantPolicyActivity所要做的。看一下这个活动的执行代码:
1
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
2![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
3![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*这里返回的是代表整个招聘流程的工作流。*/
4
Activity targetActivity = this.GetRootWorkflow(this.Parent);
5![](/Images/OutliningIndicators/InBlock.gif)
6![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*根据tenantID获得相应用户的规则集定义。*/
7
string tmp = WorkflowLogic.GetRuleSetDef(TenantId);
8
WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer();
9
using (StringReader strReader = new StringReader(tmp))
10![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
11
using (XmlReader xmlReader = XmlReader.Create(strReader))
12![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
13
RuleSet ruleSet = serializer.Deserialize(xmlReader) as RuleSet;
14
if (ruleSet != null)
15![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
16
RuleValidation validation = new RuleValidation(targetActivity.GetType(), null);
17
RuleExecution execution = new RuleExecution(validation, targetActivity, executionContext);
18![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*执行规则集中的规则。大家看一看保存在RuleSetDef表中的数据就知道,其实这里就是给
19
*工作流的TargetEvaluationProcess属性赋值,而这个赋值决定了后面将构造怎样的一个工作流来完成
20
*后面的招聘工作*/
21
ruleSet.Execute(execution);
22
}
23
return base.Execute(executionContext);
24
}
25
}
26
}
TenantPolicyActivity活动通过RuleSet来指定了要创建的子工作流名称,接下来的WorkflowFactoryActivity就是根据名称来创建这个工作流了,看一看它的执行代码:
1
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
2![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
{
3![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*有兴趣的话大家可以看一看和这里的事件处理有关的代码,是一个典型的工作流向其子活动传递消息(提供服务)
4
*的应用。*/
5
this.RaiseEvent(InvokingEvent, this, new EventArgs());
6![](/Images/OutliningIndicators/InBlock.gif)
7
if (TargetWorkflow == null || TargetWorkflow.Equals(Guid.Empty))
8![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
{
9
throw new InvalidOperationException("TargetWorkflow property must be set to a valid Type that derives from Activity.");
10
}
11![](/Images/OutliningIndicators/InBlock.gif)
12
string str = WorkflowLogic.GetWorkflowDef(TargetWorkflow).Xoml;
13
TextReader sr = new StringReader(str);
14
XmlReader xomlReader = XmlReader.Create(sr);
15![](/Images/OutliningIndicators/InBlock.gif)
16
WorkflowFactoryRuntimeService startWorkflowService = executionContext.GetService<WorkflowFactoryRuntimeService>();
17![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
/**//*根据tenant用户对工作流的定义,创建了一个新的简历处理工作流。值得注意的是,简历处理工作流
18
*并未作为招聘工作流的子流程存在,而是一个单独的根节点工作流。。*/
19
Guid instanceId = startWorkflowService.StartWorkflow(xomlReader, null, this.Parameters);
20![](/Images/OutliningIndicators/InBlock.gif)
21
base.SetValue(InstanceIdProperty, instanceId);
22
this.RaiseEvent(InvokedEvent, this, new EventArgs());
23![](/Images/OutliningIndicators/InBlock.gif)
24
return ActivityExecutionStatus.Closed;
25
}
(3)简历的处理(EvaluationProcess)
看一看通过用户定义的工作流配置,你会发现处理简历的工作流就是一个很简单的顺序工作流(这是由litware1.0中的操作界面比较简单导致的,难以设计更复杂的工作流出来):
![](/Images/OutliningIndicators/ContractedBlock.gif)
保存在数据库中的工作流定义
<ns0:EvaluationProcess x:Name="test1" Completed="onCompleted" 其他属性省略>
<ns1:InterviewActivity Positive="False" x:Name="int">
<ns1:InterviewActivity.WorkflowRoles>
<WorkflowRoleCollection>
<ns0:CustomRole Name="Administrator" />
</WorkflowRoleCollection>
</ns1:InterviewActivity.WorkflowRoles>
</ns1:InterviewActivity>
<ns1:ResultActivity Positive="False" x:Name="res1">
<ns1:ResultActivity.WorkflowRoles>
<WorkflowRoleCollection>
<ns0:CustomRole Name="Administrator" />
</WorkflowRoleCollection>
</ns1:ResultActivity.WorkflowRoles>
</ns1:ResultActivity>
<ns1:ResultActivity Positive="False" x:Name="res2">
<ns1:ResultActivity.WorkflowRoles>
<WorkflowRoleCollection>
<ns0:CustomRole Name="Administrator" />
</WorkflowRoleCollection>
</ns1:ResultActivity.WorkflowRoles>
</ns1:ResultActivity>
</ns0:EvaluationProcess>
下图是出现在处理简历工作流中的活动类型的类图:
![](/images/cnblogs_com/xingyukun/cd.jpg)
因为这三个活动在结构和处理方式上是类似的,我们就把注意力集中到其中一个活动上,以InterviewActivity为例进行说明。下面给出这个活动的设计图:
![](/images/cnblogs_com/xingyukun/activity.gif)
可以看到这个活动内部包含一个条件判断,条件成立的情况下触发一个HandlerExternalEventActivity。条件定义在InterviewActivity.Rules里面,非常简单,就是整个工作流的Positive属性为true。而这个HandlerExternalEventActivity的创建过程为:
1
this.handleExternalEventActivity1.EventName = "InterviewSubmitted";
2
this.handleExternalEventActivity1.InterfaceType = typeof(LitwareHR.Recruiting.Workflows.IEvaluationMoniker);
3
this.handleExternalEventActivity1.Name = "handleExternalEventActivity1";
4
this.handleExternalEventActivity1.Invoked +=
5
new System.EventHandler<System.Workflow.Activities.ExternalDataEventArgs>(this.InterviewActivityInvoked);
6
this.handleExternalEventActivity1.SetBinding(System.Workflow.Activities.HandleExternalEventActivity.RolesProperty,
7
((System.Workflow.ComponentModel.ActivityBind)(activitybind1)));
所以,它等待的是本地通信服务EvaludationMoniker的InterviewSubmitted事件。而拥有权限的人在Public Site上对处于面试过程的简历进行处理并提交后,InterviewSubmitted事件就会被触发。
(4)结束工作流
结束工作流是非常简单的。特别是对于简历审批工作流,是一个非常简单的顺序工作流,最后一个Activity运行完了,它也就运行完了,这里就不再多说了。