从了解到深入——剖析WF4的数据流
****************************
回想2001年左右,当时在CSDN中活跃着一批钻研技术的人,从具体技术问题到求职问题到生活问题,大家争论、探索、互助、分享的气氛使CSDN成为国内一个最吸引人的技术论坛,令人怀念。然而现在的CSDN,日益走向商业化运作,网站BUG频出,更重要的是,再也看不到当年的那种良好气氛,反而充满了浮躁,我在CSDN呆了8年了,博客也写了四五年,看到原有的“老人”因失望或其他原因一个个地离开了CSDN,颇有些伤感。
在朋友的强力推荐下,我在园子里认真地逛了逛,发现在这里也聚集了一批对技术充满了热情的人,正如当年的CSDN。
我也是一名对技术有着持久热情的人,非常希望能与园子里的朋友相互交流,当前我正在撰写有关.NET 4.0的技术书籍,前段时间已经在CSDN博客上发表了12讲介绍《.NET 4.0并行计算技术》,感兴趣的朋友可以访问以下链接在线阅读:
迎接新一轮的技术进步浪潮(http://blog.csdn.net/bitfan/archive/2009/10/26/4728180.aspx)
也可以直接下载汇总了所有文章的PDF文档及示例源码(http://download.csdn.net/source/1769413)。
从今天开始,我在园子将发表几篇有关WF4的技术文章,敬请朋友们指出疏漏与不足,给与宝贵的反馈意见。
金旭亮
====================================
36.2.4 工作流中的数据流
在前面的例子中,我们掌握了如何创建和运行一个工作流的基础知识,却没涉及到如何向工作流中传入数据,又如何从工作流中取出数据的问题。事实上,这是工作流开发中的一个核心问题。
让我们从一个简单的例子开始(示例项目:AddNumberWorkflow)。
我们将开发一个简单的工作流,这个工作流从外界接收两个整数,然后,将两数之和返回给外界。
由此,我们可以绘出此工作流的数据流向图(图 36‑9)。
图 36‑9 示例项目AddNumberWorkflow中的数据流
1 开发可以接收和发送数据的Activity
当需要为工作流编写实现特定功能的代码时,典型的作法是创建一个自定义的Activity,然后将此Activity加入到一个工作流中。
Visual Studio为此提供了一个“CodeActivity”模板,供用户生成一个包容自定义功能代码的Activity:
图 36‑10 向项目中加入自定义Activity
以下是示例程序中完成两数相加功能的AddNumber Activity的功能代码:
public sealed class AddNumber : CodeActivity
{
//两个输入参数
public InArgument<int> Number1 { get; set; }
public InArgument<int> Number2 { get; set; }
//一个输出参数
public OutArgument<int> Result { get; set; }
protected override void Execute(CodeActivityContext context)
{
//读取输入参数的值
int n1 = Number1.Get(context);
int n2 = Number2.Get(context);
//设置输出参数的值
Result.Set(context,n1+n2);
}
}
上述代码非常简单,但却包容了工作流技术最重要的几个基本概念。
(1)工作流使用“参数(Argument)”这个概念来描述进出工作流的数据。依据数据流动的方向,参数分为三大类:输入(InArgument<T>)、输出(OutArugment<T>)和输入输出(InOutArgument<T>)。分别用相应的泛型类(在括号中列出的)来表示。
(2)在工作流中,参数与普通的类属性有区别,不能直接通过名字来存取参数的值,而必须调用参数泛型类的Get()和Set()方法。
(3)请注意功能代码放到了Execute()方法中,此方法是基类CodeActivity所定义的,我们的子类重写了此方法。由此可知,如果您需要编写功能代码,请自定义一个派生自CodeActivity的子类,然后,重写Execute()方法,在此方法中放置功能代码。
(4)请注意Execute()方法有一个CodeActivityContext类型的参数,此参数引用一个Activity运行上下文对象,此对象封装了Activity运行时的“周围环境”信息。每个Activity在运行时都着联着一个上下文对象,Activity内部可以通过这个对象访问工作流引擎(即Workflow Runtime:工作流运行时)所提供的功能,以及参数的实际值等“运行环境信息”。
比如,示例程序需要设置输出参数的值,除了调用OutArgument<int>的Set()方法之外,也可以通过CodeActivityContext对象达到同样的目的:
context.SetValue(Result, n1 + n2);
交叉链接:
事实上,CodeActivityContext对象能提供的信息与功能是很有限的,它仅能查询自身运行环境中的一些数据信息和生成“跟踪(Track)”记录的功能。
如果自定义的Activity需要拥有调用工作流引擎所有服务的能力,请将从NativeActivity派生,此时,自定义Activity将获得一个类型为ActivityExecutionContext的上下文对象,通过此上下文对象可以实现诸如创建书签、持久化等功能。
第37章将详细介绍这方面的内容。
总之,Activity在一个环境中运行,此环境被抽象为一个上下文对象。此对象由工作流引擎负责维护,并在Activity对象创建时关联上相应的上下文对象。
完成AddNumber Activity的编码工作之后,就可以使用它了。
在运行时向自定义Activity传入数据和取回结果
任何一名软件工程师都很清楚,虽然函数定义与函数调用时都有参数,但两者的含义是不一样的。函数定义是指定的参数称为“形参”,它只是指明这个函数可以接收什么类型的数据,并不是数据本身。真正的数据是在函数运行时通过“实参”传送给函数的。
类似地,自定义Activtiy中定义的输入或输出参数都是“形参”,而“实参”是程序运行时由工作流引擎负责生成并传送给自定义Activtiy的实例的。
在WF中,工作流引擎使用一个Dictionary<string,object>对象集合来保存将要传给Activity的数据,在使用WorkflowInvoker.Invoke()方法调用此Activity时,这一对象集合将成为Invoke()方法的第2个参数。
当Activity开始执行时,工作流引擎遍历这一“实参”集合,依据每个参数的“关键字(Key)”在Activity定义的类型为InArgument<T>或InOutArgument<T>的参数中查找相匹配的输入型参数[1],然后将实参值传给这些参数。
WorkflowInvoker.Invoke()方法返回时,工作流引擎生成一个新的Dictionary<string,object>对象集合,抽取Activity中所有类型为OutArgument<T>或InOutArgument<T>的参数值,将它们加入到新生成的对象集合中,此对象集合将成为WorkflowInvoker.Invoke()方法的返回值。
以下代码展示了如何将两个整数传送给AddNumber Actvity,并取回其处理结果:
static void Main(string[] args)
{
//接收用户输入,将其保存到参数集合中
Dictionary<string, object> argus = new Dictionary<string, object>();
Console.Write("请输入第一个整数:");
argus.Add("Number1",Convert.ToInt32(Console.ReadLine()));
Console.Write("请输入第二个整数:");
argus.Add("Number2",Convert.ToInt32(Console.ReadLine()));
//运行工作流,将参数传送给它
IDictionary<string,object> OutputData=
WorkflowInvoker.Invoke(new AddNumber(),argus);
Console.WriteLine("结果为:{0}",
Convert.ToInt32(OutputData["Result"]));
Console.ReadKey();
}
请读者务必了解工作流引擎向工作流传入和传出数据的基本方法。
对于那些仅返回一个值的自定义Activity,WF4提供了一个CodeActivity<T>泛型类,此类直接就定义好了一个名为Result的输出型参数(其类型为OutArgument<T>),并且定义了一个新的Execute()方法供子类所重写。
使用CodeActivity<T>作为自定义Activity的基类,可以简化代码:
public sealed class AddNumber2 : CodeActivity<int>
{
//两个输入参数
public InArgument<int> Number1 { get; set; }
public InArgument<int> Number2 { get; set; }
protected override int Execute(CodeActivityContext context)
{
//直接向外界返回处理结果
return Number1.Get(context) + Number2.Get(context);
}
}
使用此Activity的方法也变得更为简单:
//接收用户输入,将其保存到参数集合中
Dictionary<string, object> argus = new Dictionary<string, object>();
//……
int Result = WorkflowInvoker.Invoke(new AddNumber2(), argus);
Console.WriteLine("结果为:{0}", Result);
3 以可视化的方式使用自定义Activity
在完成自定义Activity的编写工作之后,使用Visual Studio编译一下项目,然后切换到任何一个工作流的设计视图,将会发现工具箱中会自动增加一个选项卡,编写的自定义Activity将被放置到此选项卡中,准备好了在工作流中使用。
图 36‑11 Visual Studio会自动更新工具箱
将AddNumber从工具箱中拖到一个放置了一个“空白”的工作流(示例项目中给此工作流取名“WorkflowUsedAddNumberActivity”)中,就定义好了一个拥有“计算两数之和”功能的工作流。
然而,此工作流的调用方法有所不同,如果您采用前面的方法直接调用它:
//……
IDictionary<string, object> OutputData =
WorkflowInvoker.Invoke(new WorkflowUsedAddNumberActivity(), argus);
//……
将会报告“找不到指定名字的参数”的错误。
这里面的关键之处在于:
使用可视化的工作流设计器复用自定义的Activity时,实际上自定义的Activity是作为“子”Activtiy被嵌入到顶层的Activtiy(本例为WorkflowUsedAddNumberActivity)中去的。换句话说:现在您的自定义Activity多了一个“父亲”。
而工作流引擎只负责将数据传送给最顶层的Activity打交道,因此,作为“儿子”的自定义Activity就接收不到这些数据了。
解决方法很简单:让“父亲”将数据转发给“儿子”就行了。这需要为“父亲”定义相应的参数。
请注意一下工作流设计器底部状态条上有一个“Arguments”的按钮,点击它可以直接创建参数(图 36‑12)。
图 36‑12 在工作流设计器中为Activity定义参数
注意正确设定参数的“方向(Direction)”。
“父亲”的参数设计好之后,还必须完成将工作流引擎传入的数据转发给“儿子”的过程。
图 36‑13 “儿子”从“父亲”定义的参数中接收数据
现在就可以直接调用工作流WorkflowUsedAddNumberActivity并让它完成“两数相加”的任务了。
请读者注意掌握如何在工作流设计器中以可视化的方式定义参数的方法。
4 使用可视化的方式自定义Activity
对于我们这个功能非常简单的Activity来说,其实可以不用写代码,直接基于现有的Activity设计出来。这里会用到一个WF4所提供的Assign Activity。顾名思义,此Activity的功能就是给参数赋值。
使用Visual Studio的“Activity”模板向示例项目中添加一个名为AddNumberUsePrimitivesActivity的自定义Activity,然后从工具箱的“Control Flow”选项卡中将一个“Sequence”拖到工作流设计器中,再从“Primitives”选项卡中将一个“Assign” 放到Sequence Activity内部,形成一个两层嵌套的工作流(图 36‑14)。
选中Sequence Activity,给其定义好Number1,Number2和Result三个参数。
再选中Assign Activity,在左边文本框中输入Result(这实际上就是上面所定义的输出参数),在右边文本框中输入表达式“Number1+Number2”,自定义Activity开发完成,它的使用方法与前面介绍的AddNumber Activity完全一致。
图 36‑14 以“可视化”的方式直接开发自定义Activity
5 小结
本节所介绍的内容中,最关键的是“参数”的概念。
在WF4中,参数是数据流入和流出Acitivity的地方。它的功能完成类似传统程序设计语言中的函数参数。
参数有一个名字和相应的数据类型,依据数据是流入还是流出Activity,有三种类型的参数:输入(in)、输出(out)和输入/输出(in/out)。
在自定义Activity时,使用InArgument<T>、OutArgument<T>和 InOutArgument<T>类型来定义参数。
参数拥有四个基本要素:
(1) 名字(Name):必需。工作流引擎使用名字来从参数中提取或向参数传送数据。
(2) 类型(Type):必需。指明参数所能保存的数据类型。
(3) 方向(Direction):必需。指明参数所接收的数据是流入还是流出Activity
(4) 缺省值(Default Value):可选。使用一个表达式来给定参数的默认值。
其中,“表达式(Expression)”与“变量(Variable)” 是工作流开发中另两个非常重要与基础的概念,下一小节将介绍并深入地探讨其内部实现机理。