Introduction to Windows Workflow 4.0
Note: I apologize in advance if this first post seems unorganized. There is a lot of material to cover and I couldn’t find a really good way to order the topics. Hang in there and hopefully it will start making sense.
In order to help everyone get up to speed with build customization, I wanted to take a few posts to share our collective knowledge on the Workflow framework in .NET 4.0. In this first post I plan to tackle the different base classes that may be used when designing your own custom activities, along with typical usage scenarios. Other topics, such as extensions, tracking, and more advanced customization will be covered later. I will also be keeping an eye on comments to help point me in the right direction. So without further adieu, on with the code!
The Core Activity Classes
There are 4 base classes that you should become very familiar with in the framework. Each class has a corresponding generic version, which informs the workflow runtime that it returns a result of the specified type (more on this later). I have listed out the 4 base classes below, along with general information on when you might choose to utilize each one.
System.Activities.Activity
This class serves as the base class for all of the other activities in included in the framework, as well as the class you will be extending for activities composed in code. Most of our activities in TFS 2010 are derived from this activity or Activity<T> , which is simply an activity that returns a result of type T.
System.Activities.CodeActivity
CodeActivity, along with CodeActivity<T> , provide a simple mechanism for providing the implementation of an activity as .NET code. Classes that utilize this as a base class should not perform long running operations as there is no way to cancel the execution of this activity.
System.Activities.AsyncCodeActivity
AsyncCodeActivity and AsyncCodeActivity<T> are very similar to the CodeActivity classes but are intended to perform long running operations. If you need perform I/O or would like to provide the ability to cancel the execution of your activity, you should derive from the asynchronous variety. Most of the code activities in TFS 2010 utilize the AsyncCodeActivity classes so a build may be stopped cleanly during an operation.
System.Activities.NativeActivity
This particular activity, in conjunction with NativeActivity<T> , are not intended for heavy use. In fact, the workflow framework even makes somewhat limited use of this activity as a base class. Typically you should only use this activity if you would like to define your own scheduling behavior or need to interact with bookmarks and external services.
The Core Data Classes
System.Activities.Variable
A variable in represents the same concept as it does in code when you declare an integer or string. The purpose of variables is to act as a data storage mechanism within a particular visibility scope. Much like you cannot declare two variables of the name ‘foo’ in C# without a compiler error, you cannot successfully run a workflow that attempts to define two variables in the same scope with the same name.
System.Activities.Argument
An argument represents the flow of data, and does not actually have the ability to store anything (the actual storage mechanism for an argument is typically a variable or top-level input to the workflow). There are 3 important sub-classes of argument that you will need to become familiar with:
-
- System.Activities.InArgument<T>
- The value is meant to flow into the method. Much like a pointer in C, the pointer itself cannot be changed but if a complex object is passed as an argument then fields of that object may be changed.
- System.Activities.OutArgument<T>
- The value is meant to flow out of the method. This argument type is synonymous to the ‘out’ keyword in C#.
- System.Activities.InOutArgument<T>
- The value is meant to flow in and possibly be modified as an output. This argument type is synonymous to the ‘ref’ keyword in C#.
- System.Activities.InArgument<T>
Designing Your First Activity
For simplicity, we’re going to start with the age-old example of “Hello, World!” but accomplish the task with workflow. For this introduction we just need to load up visual studio and create a new C# ‘Console Application’ project. Normally you would select a ‘Workflow Console Application’ for the project type, but I’d rather go through the steps manually to hopefully aid in your understanding of the framework. Once you have the project created, add a reference to System.Activities using the ‘.NET’ tab. Now we need to create our first activity for interacting with the console for output. Create a new C# class called ‘WriteLine’ and paste the following code into the file.
using System;
using System.Activities;
using System.ComponentModel;
using System.IO;
namespace HelloWorkflow
{
public sealed class WriteLine : CodeActivity
{
[Browsable(true)]
[RequiredArgument]
[DefaultValue(null)]
public InArgument<TextWriter> TextWriter
{
get;
set;
}
[Browsable(true)]
[RequiredArgument]
[DefaultValue(null)]
public InArgument<String> Text
{
get;
set;
}
protected override void Execute(CodeActivityContext context)
{
String text = Text.Get(context);
TextWriter textWriter = TextWriter.Get(context);
if (textWriter == null)
{
throw new ArgumentException("The provided text writer is invalid");
}
textWriter.WriteLine(text);
}
}
}
What we have done is created an activity, called WriteLine, that takes a TextWriter and some Text as arguments and writes the text to the writer. I know this may seem overly simple but it works pretty well for explaining the general concepts of data flow. If you think about this a little bit, you may notice that this mimics code quite a bit! For instance, we could write this using C# instead in the following way:
public static void WriteLine(TextWriter textWriter, String text)
{
if (textWriter == null)
{
throw new ArgumentNullException("textWriter");
}
textWriter.WriteLine(text);
}
Taking a moment to analyze the correlation between these two approaches, we notice a couple of similarities. First, the CodeActivity.Execute(CodeActivityContext) method is nothing more than the private implementation of the C# method. Second, the input arguments are nothing more than properties on the activity of type System.Activities.InArgument<T> . Although there isn’t always a direct 1-to-1 mapping as illustrated by this simple example, you should approach your development of activities in much the same way. Activities are meant to be modular, reusable pieces of code that may be composed into more complex functionality, exactly like functions/methods in written code.
Now that we have our activity, let’s see how we can use it. Go back to your Program.cs file, or where your Main method exists, and make it look similar to the following.
static void Main(String[] args)
{
WorkflowInvoker invoker = new WorkflowInvoker(new WriteLine());
Dictionary<String, Object> inputs = new Dictionary<String, Object>();
inputs.Add("TextWriter", Console.Out);
inputs.Add("Text", "Hello, Workflow!");
invoker.Invoke(inputs);
}
If you now run this program you should see the text “Hello, Workflow!” output to the standard output stream of the console window! Ok, I know that wasn’t all that exciting, but you always need to start somewhere to understand the basic concepts before we can move on to the better stuff. Let’s take a second to go through the main method and explain what it does. First we allocate a WorkflowInvoker object, which is very useful for running simple workflows (most hosting environments, including TFS Build 2010, will most likely utilize the WorkflowApplication class instead), specifying the activity definition we would like to use. Next, we set up some inputs which map to the argument names on our activity, which define the execution environment for the runtime. Last, we invoke the workflow which will execute our activity with the provided input environment.
Activity Definition vs. Activity Instance
One particularly interesting area I would like to point out is the separation of the activity definition from the activity instance. When you load a workflow for execution and provide an instance of a C# class, in this case the WriteLine activity, you are actually supplying what is known as the activity definition. The definition defines the inputs, outputs, control flow, and everything else you would expect that remains static, much like a class definition in code. Each time the activity is executed it reuses the same C# class instance of the definition – once again, each time the activity is executed it reuses the same C# class instance of the definition. Why did I say this twice? I want to make it extremely clear why it’s important to differentiate between properties that belong to the definition and properties that belong to the individual execution instance. Instance-level storage is accomplished with the mechanisms provided by the workflow runtime (Variable<T>, InArgument<T>, InOutArgument<T>, etc), while definition-level storage is accomplished via standard class properties. If you take a look back at our WriteLine activity, the execute method obtains the values for the inputs using the provided CodeActivityContext. Along with providing mechanisms to interact with the tracking participant and the ability to get extensions, the activity context also provides the storage for the current instance environment visible to the activity. So, if you want to retrieve the value for one of the inputs to the activity you must have access to the activity context to retrieve the value. If you would like to experiment with this more, try adding an integer as a regular field or property to the class and increment it each time the workflow is run.
Wrapping It Up
Reading this post should introduce you to basic concepts of workflow, how data flows in a workflow, and how to get started writing your own activity. I plan on continuing this series with regular updates on progressively more advanced and cumulative topics, so if you’re looking to make use of this framework in your own programs or are simply attempting to extend your build process keep an eye out! My current plan is to cover AsyncCodeActivity and composition through Activity in the following posts, but feel free to steer me in a different direction.