Workflow Foundation 4.0 Activity Data Model (III)

We talked about WF4 Activity overview and WF parameters in particular in previous posts. In this installment, I'm going to focus on WF variables.

Variable

Variables are modeled by Variable class.

    namespace System.Activities

    {

    public abstract class Variable : LocationReference

    {

        public ActivityWithResult Default { get; set; }

    public string Name { get; set; }

        …

    public object Get(ActivityContext context);

            public void Set(ActivityContext context, object value);

    }

}

    public sealed class Variable<T> : Variable

    {

        public Activity<T> Default { get; set; }

        …

        public T Get(ActivityContext context);

        public void Set(ActivityContext context, T value);

    }

         Code 15. Variable class definition

The most basic attributes of a variable should be name and type. Name is defined as a property in Variable abstract class; type is captured by generic type argument of Variable<T>, concrete subclass of Variable. Similar to Argument, Variable also has Get()method to get runtime value from ActivityContext, and Set() method for the other way around.

Public and Implementation Variable

So far we could always map WF4’s data model back to similar concept of typical procedural language like C#. There is one exception though when it comes to variables.

In C#, variables are used to share data among statements within a scope (e.g, a method). Those statements are considered private to that scope, so are the variable. It’s natural to think an Activity is a method in WF, and its child Activities as statements of the method. Along this line of thinking, any variable should also be private to its defining Activity and accessible by all child Activities.

But in WF, a lot of Activities are to model control flows and their child Activities are actually given by users of those Activities. For example, Sequence is used to execute a list of child Activities, but those child Activities are not defined by Sequence itself, but injected from outside. The closest concept in C# for this is delegate, so let me try to simulate Sequence Activity with a C# method using delegate:

        static void Sequence(params Action[] statements)

        {

            foreach (Action statement in statements)

            {

                statement();

            }

        }

                  Code 16. simulate Sequence Activity using a C# method

One would call Sequence method like this:

        Sequence(

                ()=>Console.WriteLine("hello"),

                ()=>Console.WriteLine("world!")

                );

                Code 17. calling the C# Sequence method with delegates

As illustrated, from Sequence’s point of view, it’s children activities are actually from outside. In WF term, we call this type of children “public children”, versus “implementation children” which is purely defined by the parent activity itself. When an Activity declares its children Activities in CacheMetadata, it needs to say whether a child is “public” or “implementation” (personally I don’t like the word “public” in this context because it reminds people for the “public” keyword in OO languages but means something totally different).

What if those public child Activities want to share some data? Naturally they should use variables. But those variables could not be defined by the parent Activity since it wouldn’t even know what the public children are, let alone the data they want to share. Again, I could illustrate the idea with C# code:

       static void Sequence(params Action[] statements)

        {

            //tries to access the variable defined outside the method

            //this does not compile!

            //outside = 5;

            int inside = 5;

            foreach (Action statement in statements)

            {

                statement();

            }

        }

 

               //to call the sequence with statements sharing a variable

        int outside = 0;

        Sequence(

                ()=>Console.WriteLine (outside++),

                () => Console.WriteLine(outside++),

                () => Console.WriteLine(outside++),

                () => Console.WriteLine(outside++)

                //tries to access a variable defined inside the method,

                //this does not compile!

                //,() => Console.WriteLine(inside)

                );

                   Code 18. illustrate how "public" children share data using C# code

As we could see, variables used by “public” statements have to be defined outside of method, and not visible to implementation of the method. WF programming model allow such variables to be declared on the parent Activity itself as “public variables”. For example, class Sequence is defined this way:

    public sealed class Sequence : NativeActivity

    {

        public Collection<Activity> Activities { get; }

        public Collection<Variable> Variables { get; }

        …

    }

        Code 19. Sequence Activity interface

The Variables collection holds a list of “public” variables which could be defined by users of Sequence and accessible by its “public” child Activities.

If an Activity needs some bookkeeping data for its own execution, it could declare it as an “implementation” variable and no need to expose it as a public property on the class in general.

Here is an example to demo how to declare public and implementation variables plus difference between them:

    public class MySquence : NativeActivity

    {

        //this is an implementation variable, no need to expose to users

        Variable<int> index = new Variable<int>();

       

        //a collection of public child activities for users to fill

        Collection<Activity> activities = new Collection<Activity>();

        public Collection<Activity> Activities

        {

            get { return this.activities; }

        }

        //a collectoin of public variables for users to fill

        Collection<Variable> variables = new Collection<Variable>();

        public Collection<Variable> Variables

        {

            get { return this.variables; }

        }

        protected override void CacheMetadata(NativeActivityMetadata metadata)

        {

            //base.CacheMetadata(metadata);

            foreach (Activity child in this.Activities)

            {

                //declare a public child Activity

                metadata.AddChild(child);

            }

            foreach (Variable variable in this.Variables)

            {

                //declare a public variable

                metadata.AddVariable(variable);

            }

            //declare a implementation variable

            metadata.AddImplementationVariable(this.index);

        }

        protected override void Execute(NativeActivityContext context)

        {

            //store data in the implementation variable

            this.index.Set(context, 0);

            ScheduleNextChild(context);

        }

        void ScheduleNextChild(NativeActivityContext context)

        {

   //access data in the implementation variable

            int nextChildIndex = this.index.Get(context);

            if (nextChildIndex < this.Activities.Count)

            {

                context.ScheduleActivity(this.Activities[nextChildIndex], onChildCompleted);

                this.index.Set(context, nextChildIndex + 1);

            }

        }

        void onChildCompleted(NativeActivityContext context, ActivityInstance instance)

        {

            ScheduleNextChild(context);

        }

    }

         Code 20. A simple implementation of Sequence Activity with both public and implementation variables.

This Activity has a collection of variables for its users to fill and it declare it as public by using “metadata.AddVariable” in CacheMetadata method; it also has a variable to keep track of execution progression, it’s never exposed to the Activity’s users and is declared as implementation by using “metadata.AddImplementationVariable”.

Note that, if base.CacheMatadata is called in CacheMetadata method, the 2 foreach loops are not necessary anymore. Because the base implementation of CacheMetadata reflects all the public properties of the Activity, and automatically declare any public Activity, including Activity in public collections, as its public child; it does similar thing for variables.

Data scoping

With “public” and “implementation” defined, it’s easier to define scoping. In WF4, variables and arguments are all scoped with those rules:

· An Activity’s arguments and implementation variables could only be accessed by its implementation child Activities

· An Activity’s public variables could only be accessed by its public child Activities

Hopefully those rules are pretty clear after the explanation in the previous section.

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