Workflow Foundation 4.0 Activity Data Model (II)

In the 1st segment, I talked about high level principle for WF4’s data model design. 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);

        …

}

}

       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.

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: 

 

image

Figure 1. Argument class hierarchy

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.

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.

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#.

To be continued…

Yun Jin
Development Lead
https://blogs.msdn.com/yunjin