Tuesday, December 2, 2014

WWF - Passing arguements to Custom Activity inside a Sequence container in coded workflow

Why workflow?

According to me Workflow is kind of DSL which helps to separate the business logic from input output operations. It provides a domain which contains the objects which are involved in the system and the business logic / rules which can be easily modified. Its very much useful when the business rules are very much dynamic and putting developers for modifying the logic for ever is not practical. One of the workflow framework is Windows Workflow Foundation. We will be using WWF in rest of this post.

Why Windows Workflow Foundation?

The big advantage of WWF is its tooling. Microsoft is providing good tooling experience in their Visual Studio and the amazing factor is that the designer can be packaged in to our WPF applications easily. In other words we can easily give our users a graphical way to alter the program flow. Its really more than giving some configuration screens or files.

The WWF uses XAML to store the workflow definition. ie When we create new workflow , open it in visual studio editor, add activities (steps) and save, we get a .xaml file. We can open it in notepad and see how the logic is captured in the form of xml.

The operations what we can do with editor can be done using code as well. 

Simple coded workflow using C# .Net

As we saw above, workflow is a DSL which has all most all the features of normal programming language. It supports variables as storage locations and arguments as input/output mechanism. Refer this link to see how simple it is to invoke an workflow which have only one step/activity.

We can connect one or more activities using Sequence Activity just like how we can do from VisualStudio. Below is one example where we are connecting more than one activity using Sequence activity when we create coded workflows.

WorkflowInvoker.Invoke(new Sequence
            {
                Activities =
                        {
                                        new WriteLine {Text = "Workflow"},
                                        new WriteLine {Text = "World"}
                        }
            });

Custom Activity

WWF is really extensible. Apart from using its built-in activities we can have our own activities as well which are knows as custom activities. Development of custom activities are as simple as inheriting from Activity class. Similarly invocation of custom activity is too simple as calling the WorkflowInvoker.Invoke() method by passing the object of custom activity. It can accept arguments as well like built in activity. One example below.


    public class MyParameterizedActivity : CodeActivity
    {
        [RequiredArgument]
        public InArgument<string> Arg1 { get; set; }
        
        protected override void CacheMetadata(CodeActivityMetadata metadata)
        {
            base.CacheMetadata(metadata);
        }
        protected override void Execute(CodeActivityContext context)
        {
            string arg = context.GetValue(Arg1);
            Console.WriteLine("Param received.Value :" + arg);
        }
    }

Code to invoke goes below

WorkflowInvoker.Invoke(new MyParameterizedActivity(), new Dictionary<string, object>()
            {
                {"Arg1","argument value"}
            });

Passing arguments into Custom Activity in a coded workflow

Whatever we saw above are some background things. The real intention of this post comes now. We saw about Sequence and custom activities. If we host this custom activity inside a sequence activity whether we will get the argument value easily into the custom activity?

Below is the easiest way people think of. But this will not work

            //The below will compile but won't run as there is no arguement for Sequence Activity
            //The error message will be "'MyParameterizedActivity': Value for a required activity argument 'Arg1' was not supplied."
            Activity act = new Sequence
            {
                Activities =
                {   new WriteLine() {Text="Test" },
                    new MyParameterizedActivity()
                    {
                    }
                },
            };
            WorkflowInvoker.Invoke(act, new Dictionary<string, object>()
            {
                    {"Arg1","arg1 value"}
            });

Basically here nobody is telling that which argument /variable needs to be mapped with the Arg1 of custom activity 'MyParameterizedActivity'. So how to overcome this?

Here comes the DynamicActivity.Using that we can specify the mapping. Below is the code snippet. Hope its not much complicated :)

// There is a Property in DynamicActivity and that holds the value.
// That value is passed to MyParameterizedActivity
            InArgument<string> InArg1 = new InArgument<string>();
            DynamicActivity<int> wf = new DynamicActivity<int>
            {
                Properties =
                {
                    new DynamicActivityProperty
                    {
                        Name = "DynamicActivityArg1",
                        Type = typeof(InArgument<string>),
                        Value = InArg1
                    },
                },
                Implementation = () => new Sequence
                {
                    Activities =
                    {
                        new WriteLine() {Text ="Writing from first console activity "},
                        new MyParameterizedActivity()
                        {
                            Arg1 = new InArgument<string>((env)=> InArg1.Get(env))
                        },
                        new WriteLine() {Text ="Writing from second console activity "}
                    }
                }
            };

            WorkflowInvoker.Invoke(wf, new Dictionary<string, object>
                {
                    { "DynamicActivityArg1", "Value for arg 1" },
                });

Why this much complication to orchestrate / sequence 2 activities. Its because the WWF don't have support to pass value from one activity to other activity. So we need to use extra variable in between.

Getting return values from custom activity hosted inside DynamicActivity

We saw the things done to pass one variable to custom activity if its hosted as a step inside another activity. Below is the code for creating a custom activity which returns a value and  hosted inside DynamicActivity.
                

// Custom activity class which just multiplies and stored the result
public class MyMultiplyActivity : CodeActivity
    {
        [RequiredArgument]
        public InArgument<int> Operand1 { get; set; }
        [RequiredArgument]
        public InArgument<int> Operand2 { get; set; }

        [RequiredArgument]   
        public OutArgument<int> Product { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            int a = Operand1.Get(context);
            int b = context.GetValue(Operand2);
            int c = a * b;
            Product.Set(context, c);
            //Sum.Set(context, a + b);
        }
    }

Below is the code to invoke and print the result.


            // Dynamic activity accepts 2 arguments and pass to custom activity
            InArgument<int> Operand1 = new InArgument<int>();
            InArgument<int> Operand2 = new InArgument<int>();
            DynamicActivity<int> wf = new DynamicActivity<int>
            {
                Properties =
                {
                    new DynamicActivityProperty
                    {
                        Name = "Operand1",
                        Type = typeof(InArgument<int>),
                        Value = Operand1
                    },
                    new DynamicActivityProperty
                    {
                        Name = "Operand2",
                        Type = typeof(InArgument<int>),
                        Value = Operand2
                    }
                },
                Implementation = () => new Sequence
                {
                    Activities =
                    {
                        new MyMultiplyActivity()
                        {
                            Operand1 =new InArgument<int>((env)=>Operand1.Get(env)),
                            Operand2=new InArgument<int>((env)=>Operand2.Get(env)),
                            Product=new ArgumentReference<int>{ ArgumentName = "Result" },
                        },
                        new WriteLine() {Text="Console activity after custom activity" }
                    }
                }
            };
            int result = WorkflowInvoker.Invoke<int>(wf, new Dictionary<string, object>
                {
                    { "Operand1", 25 },
                    { "Operand2", 15 }
                });
            Console.WriteLine(result);


The 'Result' is kind of built in argument name which is mapped to the returned value of Invoke() method

We can think about more and more scenarios such as how can we return 2 values from the custom activity. Not about returning as class object, instead what about having more than 1 OutArguments and how to handle them from the invoker side?

Another interesting area is to use scripts instead of typed coding. If we are using VB / C# scripts instead of typed code we can avoid some amount of code in mapping variables to activity parameters.

No comments: