Windows Workflow Foundation 4.0 (WF4) is a complete rewrite of the workflow infrastructure introduced with .NET 3.0 in 2006. This rewrite changes the way that many features are used and exposed – from data flow to deployment, from execution to persistence. One of the big changes is the way that you build custom activities and that is the focus of this article.
Activities in WF4
Activities are the units of work that are composed to produce workflow based functionality. In WF4, activities explicitly declare their inputs and outputs via arguments. There are three classes for declaring arguments: InArgument<T>, OutArgument<T> and InOutArgument<T>. Data is then mapped between activity arguments by storing it on variables on parent activities.
The Standard Activity Library (those activities that are in the toolbox by default) is quite different from version 3.5. Often used activities, such as the CodeActivity are gone whereas a number of low level activities are introduced. These low level activities include
-
Assign: that allows you to map data between arguments and variables or variable to variable using potentially complex expressions
-
AddToCollection, RemoveFromCollection, ClearCollection: these and others are for manipulating collections of items (including iteration through the collection)
-
InvokeMethod: allows the execution of an arbitrary instance or static CLR method
Custom Composite Activities
Composite activities have been part of workflow since its inception. In WF 3.5 they were a tool for creating reusable pieces of workflow that would be run synchronously with the parent workflow. The building blocks for them were fairly coarse and so prevented their use as the primary mechanism for creating custom activities.
In WF4 the main mode of building workflows is declarative – using XAML. This allows many interesting deployment and management scenarios. This means we generally attempt to avoid writing code if possible. The new low level standard activities allow the creation of custom activities as composite activities authored in XAML. This is the design view of a custom composite. You can see the declaration of inputs and outputs in the argument window as we will as the flowchart based flow through a decision and two Assign activities.
However, this only works if all the necessary building block activities are available; what if you need to create a new building block? For this we have to fall back to code.
Building Custom Activities in Code
There are two ways to build custom activities in code: a streamlined simplified model and mode that gives you full access to the underlying workflow infrastructure. Which you use depends on your requirements.
Deriving from CodeActivity
The base class CodeActivity (not to be confused with the CodeActivity from WF 3.5) is for creating simple synchronous custom activities, i.e. those that start running and execute to completion without entering a wait state. Consider this example
public class GetProcessesActivity : CodeActivity
{
public OutArgument<List<Process>> Processes { get; set; }
protected override void Execute(CodeActivityContext ctx)
{
Processes.Set(ctx, Process.GetProcesses().ToList());
}
}
The custom activity derives from the CodeActivity base class. It then declares its inputs and outputs as arguments (in this case there are only one out argument). Finally it overrides Execute to do its work.
Notice that the argument value is set by referencing through the context (in this case a CodeActivtityContext). This acts as a look up to the state of this currently executing workflow.
This kind of custom activity has two main use cases: simple synchronous activities and as a migration path for people to move code that was previously encoded in the WF 3.5 CodeActivity (I suspect the naming of the base class to the same name as the 3.5 activity was not a coincidence).
Deriving from NativeActivity
Sometimes the simplified model exposed by CodeActivity is not enough. For this reason there is another base class you can derive from called NativeActivity. NativeActivity exposes all of the functionality of the WF4 runtime infrastructure. In particular it provides access to the bookmark infrastructure for creating asynchronous activities. This is hugely important for creating activities that fit with workflow’s long running model. Activities that can go into a wait state allow the workflow to be unloaded by the host.
Consider the example of an activity that is waiting for a file to arrive in a specific directory.
public class FileDropActivity : NativeActivity
{
public InArgument<string> WatchDirectory { get; set; }
public InArgument<string> FileMask { get; set; }
public OutArgument<string> FileName { get; set; }
string bookmarkName = Guid.NewGuid().ToString();
protected override void Execute(ActivityExecutionContext context)
{
context.CreateNamedBookmark(bookmarkName, OnFileCreated);
FileDropExtension fde = context.GetExtension<FileDropExtension>();
fde.WaitForFile(bookmarkName,
WatchDirectory.Get(context),
FileMask.Get(context));
}
void OnFileCreated(ActivityExecutionContext context,
Bookmark bookmark,
object value)
{
FileName.Set(context, (string)value);
}
}
Here the activity is passed the directory and file mask to wait for as in arguments and sets the arrived file name as its output.
The activity now overrides Execute. Notice that the return type of Execute is void – this contrasts with WF 3.5 where the activity had to pass back its execution status from Execute. In 4.0, WF knows whether you are waiting for something to happen based on whether you create a bookmark or not. In this case we see the creation of a bookmark stating that the OnFileCreated method should be called when the bookmark is resumed. The activity then hands off to a custom extension (extensions were known as workflow services in 3.5) calling its WaitForFile method. Internally this method sets up a FileSystemWatcher and an event handler for when the file arrives. When the file is created in the directory it resumes the bookmark. Here’s the code for the extension
class FileDropExtension
{
WorkflowInstance instance;
FileSystemWatcher fsw;
string bookmarkName;
public FileDropExtension(WorkflowInstance instance)
{
this.instance = instance;
}
public void WaitForFile(string bookmarkName, string dir, string filter)
{
this.bookmarkName = bookmarkName;
fsw = new FileSystemWatcher(dir, filter);
fsw.Created += new FileSystemEventHandler(FileCreated);
fsw.EnableRaisingEvents = true;
}
void FileCreated(object sender, FileSystemEventArgs e)
{
instance.ResumeBookmark(bookmarkName, e.FullPath);
fsw.Dispose();
}
}
As you can see, the bookmark is resumed via the WorkflowInstance under which the activity is executing.
When the bookmark is resumed, the activity’s OnFileCreated method is fired and it sets its output argument to the created file name (passed to the OnFileCreated method in its value parameter).
If you contrast this to the incredibly complex job of creating robust asynchronous activities in WF 3.5 you will see that the WF4 model is relatively simple.
Conclusion
The approach that WF4 takes to creating custom activities is a far simpler one to that in WF 3.5. The composite approach caters for many scenarios while for most building block activities using the CodeActivity as base class provides a simple solution. However, if you do need the full power of the workflow infrastructure it is available via the NativeActivity.