WF4 Activity Data Model---Argument
To build any meaningful Activity, developers also need to the activity’s state and data flow. In this post, I’m going to talk about WF4’s Activity data model.
Red words in this article is coming from my personal opinion. if have some error . pls kindly let my know . thanks a lot .
Argument and Variable 101
WF is just another higher level programming language that share a lot of similarity with popular imperative procedural programming languages. So let’s first take a look at data model of a typical procedural language. Here is a C# code snippet.
static void Main(string[] args)
{
string answer = Prompt("1+1=?");
Console.WriteLine("Answer is {0}", answer);
Console.ReadLine();
}
static string Prompt(string question)
{
Console.WriteLine("Please enter your name:");
string name = Console.ReadLine();
Console.WriteLine("{0}, {1}", name, question);
return Console.ReadLine();
}
Code 1
As we learnt from the first programming class, functions/methods are basic execution unit for procedural languages and the basic mechanism to model data is arguments and variables:
Arguments are to model data flow in/out of a function, like “question” for method Prompt; variables are used to store states which could be shared among multiple statements, like “name” in method Prompt and “answer” in method Main; both arguments and variables are defined by name and type; arguments are always private to a method; variables are visible to the scope where they’re declared (the closest brace which enclose the declaration).
Not surprisingly, WF4 also chooses Argument and Variable to model data flow and internal states for Activities. And WF’s Arguments/Variables share a lot of those basic characteristics as their C# counterparts. So far the concept of arguments and variables seems to be very simple, now I need to touch something more subtle before talking about WF4’s implementations.
Separation of data and program
Code1 only describes how to manipulate data like “question” and “name”, but doesn’t talk about where the data is stored. In my 32-bit machine with .Net 4.0, the C# code is compiled into this piece of X86 assembly code at runtime:
00000000 push ebp
00000001 mov ebp,esp
…
//copy argument “question”from register ecx to stack location [ebp-4]
00000006 mov dword ptr [ebp-4],ecx
…
//call Console.ReadLine()
00000027 call 5053D5A0
//copy return value of ReadLine from register eax to stack location [ebp-c] as variable “name”
0000002c mov dword ptr [ebp-0Ch],eax
//load argument “question”from stack location [ebp-4] and push it to stack
//as the 3rd argument to WriteLine method call
00000035 push dword ptr [ebp-4]
//load literal string “{0},{1}”from constant memory location to ecx
//as the 1st argument to WriteLine
00000038 mov ecx,dword ptr ds:[03292094h]
//load variable “name”from stack and store in edx as the 2nd argument to WriteLine
0000003e mov edx,dword ptr [ebp-8]
//call Console.WriteLine
00000041 call 5053DBE0
…
Code 2
As I annotated, variables and arguments are saved in stack or registers in a running process. The program does not know location of those variables at compile time, it just reference them with a relative address like “ebp-4 “ (an address calculated by subtracting 4 from value of register “ebp”). At runtime, register “ebp” will be populated with frame address for this method so variables will automatically be saved into the right place. Abstracting stack and register away, we could say arguments and variables are just names for some locations to a runtime environment; the program does not need to concern the real storage, it just needs to reference those names as if they were the real runtime data.
as we known, the design to separating program and data is basic yet critical to today’s programming systems. However, WF 3.X has an architecture where program and data are mixed together. For example, this is a simple activity defined in WF3.X model:
public class WriteLineActivity : SequenceActivity
{
public static DependencyProperty TextProperty = DependencyProperty.Register("Text",typeof(string), typeof(WriteLineActivity));
public string Text
{
get { return (string)base.GetValue(TextProperty); }
set { base.SetValue(TextProperty, value); }
}
protected override ActivityExecutionStatus Execute(ActivityExecutionContextexecutionContext)
{
Console.WriteLine(this.Text);
return ActivityExecutionStatus.Closed;
}
}
Code 3
The activity defines a property “Text” for its input. There are several problems with this design, including:
· Data is stored in the program itself. A WriteLineActivity instance is not only execution logic for the activity, but also storage for the activity’s state. That means multiple running instances of the activity could not share the same WriteLineActivity object instance. Actually in WF3, a clone of WriteLineActivity object is created for every WhileActivity iteration and every ParallelActivity branch.
· There are no way to express intention and scope of the data. It’s hard for users of WriteLineActivity to reason whether “Text” is meant for input, output, or shared state for the activity’s own children. This of course leads to confusion and spaghetti code. Also, the runtime system has hard time to figure out data dependency between the Activities and sometimes could not even cleanup activities when it could otherwise.
In WF4, Activity data model is redesigned, and the number one goal is to create a separation between data and program.
Workflow program, workflow instance, and ActivityContext
In previous section, we analyzed how a C# program code is detached from its running state. The key is to separate the concept of variable/argument as names and their real location at runtime. WF4 does exactly that. Here is a WF4 version of WriteLine:
public class MyWriteLine : CodeActivity
{
public InArgument<string> Text
{
get;
set;
}
protected override void Execute(CodeActivityContext context)
{
string text = this.Text.Get(context);
Console.WriteLine(text);
}
}
Code 4
Arguments and variables are modeled by Argument and Variable classes. But the Argument class does not store the argument value itself, same as Variable. When the “Text” argument is defined on MyWriteLine, it only tells users that this Activity has a string input named “Text”, so that users could bind data to this name and the Activity itself could use this name in its internal expressions. But the real data is saved in an ActivityContext class. Every running workflow instance has its own ActivityContext just like every running method has its own frame in call stack. To access its runtime data, the activity needs to use Argument.Get(ActivityContext) to fetch the data out of the context. That way, different running instances could share the same MyWriteLine object instance yet have their own copy of runtime state.
With the separation of program and running instance, we often call an object instance of an Activity class a "Workflow program", similar to a copy of C# program in code. It’s a static description of logic detached with runtime environment; on the other hand, when the Activity object is scheduled by the WF runtime, it creates a "workflow instance", which is more of an abstract concept like the process for a C# program. It encapsulates the program’s runtime state. One WF program could have multiple running WF instances, similiar to relationship of C# program and processes. Workflow runtime use ActivityContext class to keep all states of a running WF instance. Through ActivityContext, an Activity could get its arguments and variables. As we introduced previously, there are different flavor of Activity modeling style with different Activity base classes. Accordingly, WF has defined different flavor of ActivityContext classes which expose different level of runtime functionalities.
next , I’m going to talk about WF Argument in particular in this post.
Argument
We’ve briefly touched this before: in WF4, arguments are modeled by Argument class.
namespace System.Activities
{
public abstract class Argument
{
public Type ArgumentType { get; internal set; }
public ArgumentDirection Direction { get; internal set; }
public ActivityWithResult Expression { get; set; }
…
public object Get(ActivityContext context);
public void Set(ActivityContext context, object value);
…
public ActivityWithResult Expression { get; set; }
}
}
Code 5. Argument class definition
This class has some pretty straightforward properties you’d expect to define an argument like ArgumentType and Direction (In, Out, InOut). Expression is a formula given by Activity caller to tell WF runtime how to evaluate an Argument’s value at execution time, which we will discuss in details later. The most important methods on Argument is Get() and Set(). Get() is to fetch runtime value of an input argument from ActivityContext; similarly, Set() is used to set runtime value of an output argument to ActivityContext. the following is a code example . let 's check it out .
public sealed class AddNumber : CodeActivity
{
//these two Arguments for data input
public InArgument<int> Number1 { get; set; }
public InArgument<int> Number2 { get; set; }
//one Argument for data output
public OutArgument<int> Result { get; set; }
protected override void Execute(CodeActivityContext context)
{
//get the Number1 and Number2 argument from context
int n1 = Number1.Get(context);
int n2 = Number2.Get(context);
//set the Argument Result value.
Result.Set(context,n1+n2);
}
}
Look closely, you will find Argument is just an abstract class, so you could not instantiate this class directly. All it does is to provide a common interface for different type of arguments. This is the class diagram for Argument type tree:
In the tree, there are 3 more abstract classes with predefined ArgumentDirection: InArgument, OutArgument, and InOutArgument. And then there are 3 concrete classes which allow user to specify ArgumentType using generic type parameter: InArgument<T>, OutArgument<T>, and InOutArgument<T>. Eventually users of an Activity need to assign an argument to instance of one of those classes. that is to say . in wf4 all the arguments used in our artivity are InArgument<T>, OutArgument<T>, and InOutArgument<T> type.
Argument declaration
In C#, developers enclose arguments in a pair of parentheses after method name to “declare” those arguments to C# compiler. In WF, an Activity tree is executed by WF runtime, how would the runtime knows which Activity has defined what arguments? The answer is that Activity class needs to report its metadata, including arguments and variables, to the runtime using a virtual method CacheMetadata. Here is an example about how this method could be used to declare an argument:
public class MyWriteLine : CodeActivity
{
public InArgument<string> Text
{
get;
set;
}
protected override void CacheMetadata(CodeActivityMetadata metadata)
{
RuntimeArgument textArgument = new RuntimeArgument("Text", typeof(string),ArgumentDirection.In);
metadata.Bind(this.Text, textArgument);
metadata.AddArgument(textArgument);
}
…
}
Code 6. How to declare one argument on an Activity
What WF runtime needs is really a RuntimeArgument object, the Argument object on Activity is only used to providing binding for the RuntimeArgument.
You may find interesting that MyWrite activity has worked before without overriding CacheMetadata method. The reason is that the method’s default implementation actually queries all public properties of an Activity using .Net reflection. For any property with type Argument, it automatically creates a RuntimeArgument with correct binding. So for most of common cases, an Activity would just work even without declaring arguments in CacheMetadata. However for developers who are sensitive to performance, it might be a good idea to override CacheMetadata to avoid cost of reflection.
here comes a code snippet of System.activities.Activity Class
public abstract class Activity {
...
protected virtual void CacheMetadata(ActivityMetadata metadata)
A common scenario where explicit argument declaration is required is dynamic argument: an Activity could take a list of arguments whose number and name are not predetermined by the Activity author. For example, if MyWriteLine wants to support formatted output similar to “Console.WriteLine(string format, params Object[] arg)”, it needs to be written this way:
public class MyWriteLine : CodeActivity
{
Collection<InArgument> args = new Collection<InArgument>();
public InArgument<string> Format
{
get;
set;
}
public Collection<InArgument> Args
{
get { return this.args; }
}
protected override void CacheMetadata(CodeActivityMetadata metadata)
{
RuntimeArgument formatArgument = new RuntimeArgument("Format", typeof(string),ArgumentDirection.In);
metadata.Bind(this.Format, formatArgument);
metadata.AddArgument(formatArgument);
for (int i = 0; i < this.Args.Count; i++)
{
RuntimeArgument argArgument = new RuntimeArgument("Arg" + i,this.Args[i].ArgumentType, ArgumentDirection.In);
metadata.Bind(this.Args[i], argArgument);
metadata.AddArgument(argArgument);
}
}
protected override void Execute(CodeActivityContext context)
{
string format = this.Format.Get(context);
object[] arguments = new object[this.Args.Count];
for (int i = 0; i < this.Args.Count; i++)
{
arguments[i] = this.Args[i].Get(context);
}
Console.WriteLine(format, arguments);
}
}
Code 7. How to declare and use dynamic arguments
Base implementation of CacheMetadata would not know how to handle the Args collection, so overriding the method is necessary.
There is a special case for argument definition. If a C# method only has one output, there is no need to declare an out argument. Generally people would just use the return result, which is really a implicit and unnamed output argument. WF4 uses the same paradigm to simplify programming experience. If an Activity is derived from base class Activity<T>, it will inherit an out argument called “Result”. There is no need to explicitly declare this argument unless CacheMetadata is overridden.
Argument binding and evaluation
In C#, caller of a method could use an expression to define runtime value of an argument to the method. The expression could be a constant value, a variable, a math expression, or any programming structure with a value. For example:
static void Foo(int x);
…
int a = …;
Foo(5);
Foo(a);
Foo(new Random().Next());
Code 8. Argument binding in C#
Similarly, in WF, users of an Activity need to provide expressions for its arguments. Activity is the basic unit of WF program. So an expression in WF is just an Activity which has a return value, a subclass of Activity<T>. WF also defines a base class for Activity<T>, called ActivityWithResult:
public abstract class ActivityWithResult : Activity
{
public OutArgument Result { get; set; }
public Type ResultType { get; }
}
Code 9. ActivityWithResult
As showed before, Argument.Expression is defined as an ActivityWithResult. So client of an Activity needs to bind arguments to some Activities with return result. WF has defined some Activities for common expressions.
· Literal<T>: models literal value. T could only be string and value types. Sample usage:
MyWriteLine writeLine = new MyWriteLine
{
Format = new InArgument<string> { Expression = new Literal<string> ("{0}, {1}")}
};
Code 10. Sample usage of Literal<T>
Argument has constructor to take literal value and create Literal<T> internally, so the code could be simplified as:
MyWriteLine writeLine = new MyWriteLine
{
Format = new InArgument<string>("{0}, {1}")
};
Code 11. Sample usage of Literal<T>
· VariableValue<T>: models accessing value of a variable. Sample usage:
MyWriteLine writeLine = new MyWriteLine
{
Format = new InArgument<string> { Expression = new VariableValue<T>(a)} //a is a WF variable, of type Variable
};
Code 12. Sample usage of VariableValue<T>
Argument also has constructor to take Variable and create VariableValue<T> internally, so the code could be simplified as:
MyWriteLine writeLine = new MyWriteLine
{
Format = new InArgument<string>(a) //a is a WF variable, of type Variable
};
Code 13. Sample usage of VariableValue<T>
To bind a variable to an OutArgument as L-value, one could use VariableReference<T>.
· VisualBasicValue<T>: models an expression written in VB string. Symbols like WF variable and argument name could be used in the expression string. Sample usage:
MyWriteLine writeLine = new MyWriteLine
{
Format = new InArgument<string> { Expression = new VisualBasicValue<string>("a + b")} //a and b are both WF variables
};
Code 14. Sample usage of VisualBasicValue<T>
There is no syntax sugar in Argument class to simplify the code. But WF visual designers have direct support of VisualBasicValue<T> so it’s easy to write expressions as VB language directly in designer.
To bind a VB expression to an OutArgument as L-value, one could use VisualBasicReference<T>.
Developers could always implement their own version of Activity<T> for their own argument binding logic.
When an Activity is scheduled, WF runtime will needs to evaluate expressions of all arguments of this activity before execute the activity. Evaluation process includes scheduling the ActivityWithResult, and getting the out argument Result after it completes. One thing worth mentioning is that runtime schedule all expressions at the same time as if they are in a Parallel Activity, rather than a predefined order like C#.
next , let 's see how to get these input data and set output in some real application scenario .
supposed we are designing a simple activity function. which just simply get two input argument of integer type. and return the sum of two integer to the caller.
firstly. let 's create a workflow console app project .then add the above custom activity named AddNumber.
type the following code in it .
public sealed class AddNumber : CodeActivity
{
//these two Arguments for data input
public InArgument<int> Number1 { get; set; }
public InArgument<int> Number2 { get; set; }
//one Argument for data output
public OutArgument<int> Result { get; set; }
protected override void Execute(CodeActivityContext context)
{
//get the Number1 and Number2 argument from context
int n1 = Number1.Get(context);
int n2 = Number2.Get(context);
//set the Argument Result value.
Result.Set(context,n1+n2);// this also can be replaced with context.SetValue(Result, n1 + n2);
}
}
up to now. we already finished the custom activity simply. we can calling it for using.
by the way . WF4 can use Dictionary<string,object> as the second parameter for using WorkflowInvoker.Invoke() .
the following is the caller in main method.
static void Main(string[] args)
{
//input argument collection
Dictionary<string, object> argus = new Dictionary<string, object>();
Console.Write("请输入第一个整数:");
//attention here . the key text of the dictionary item is case sensitive
argus.Add("Number1",Convert.ToInt32(Console.ReadLine()));
Console.Write("请输入第二个整数:");
argus.Add("Number2",Convert.ToInt32(Console.ReadLine()));
//pass the input argument to workflow of activity.
IDictionary<string,object> OutputData=
WorkflowInvoker.Invoke(new AddNumber(),argus);
Console.WriteLine("结果为:{0}",
//get the result output argument.
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);
RequiredArgumentAttribute
默认情况下,在使用Activity时不需要对每一个Argument赋值,但是你可以为你的 Argument加上RequiredArgumentAttribute,用于表示必须在定义Workflow时必须为这个Argument赋初值。Runtime在创建Workflow实例时会对所有Argument进行检测,如果某个RequiredArgument没有赋值,将会抛出ValidationException。
可视化的方式使用自定义Activity
在完成自定义Activity的编写工作之后,使用Visual Studio编译一下项目,然后切换到任何一个工作流的设计视图,将会发现工具箱中会自动增加一个选项卡,编写的自定义Activity将被放置到此选项卡中,准备好了在工作流中使用。
将AddNumber从工具箱中拖到一个放置了一个“空白”的工作流(示例项目中给此工作流取名“WorkflowUsedAddNumberActivity”)中,就定义好了一个拥有“计算两数之和”功能的工作流。
然而,此工作流的调用方法有所不同,如果您采用前面的方法直接调用它:
//……
IDictionary<string, object> OutputData =
WorkflowInvoker.Invoke(new WorkflowUsedAddNumberActivity(), argus);
//……
将会报告“找不到指定名字的参数”的错误。
这里面的关键之处在于:
使用可视化的工作流设计器复用自定义的Activity时,实际上自定义的Activity是作为“子”Activtiy被嵌入到顶层的Activtiy(本例为WorkflowUsedAddNumberActivity)中去的。换句话说:现在您的自定义Activity多了一个“父亲”。
而工作流引擎只负责将数据传送给最顶层的Activity打交道,因此,作为“儿子”的自定义Activity就接收不到这些数据了。
解决方法很简单:让“父亲”将数据转发给“儿子”就行了。这需要为“父亲”定义相应的参数。
请注意一下工作流设计器底部状态条上有一个“Arguments”的按钮,点击它可以直接创建参数。
注意正确设定参数的“方向(Direction)”。“父亲”的参数设计好之后,还必须完成将工作流引擎传入的数据转发给“儿子”的过程。这时,请选中已放置在工作流中的AddNumber Actvity,在其属性窗口中定义它从“父亲”处接收数据:
现在就可以直接调用工作流WorkflowUsedAddNumberActivity并让它完成“两数相加”的任务了。 请读者注意掌握如何在工作流设计器中以可视化的方式定义参数的方法。
使用可视化的方式自定义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完全一致。
参考:http://www.cnblogs.com/bitfan/archive/2009/11/06/1597148.html
posted on 2012-06-14 23:30 malaikuangren 阅读(844) 评论(0) 编辑 收藏 举报