Workflow Services are effectively Windows Communication Foundation (WCF) services with operation implementation provided by workflow activities. .NET Framework 4 ships with numerous generic activities that you can use to implement your Workflow Services, but ultimately you will need a place where you can put your own business logic. Custom activities provide that reusable encapsulation of your business logic. In this article, I'll explore the fundamentals of building custom activities for use in Workflow Services.
Flavors of Custom Activities
Custom activities let you recombine existing activities into activities functioning at higher levels of abstraction. They also let you write code to implement domain logic or utilize other code libraries. Custom activities are defined in declarative .xaml files or imperative code files (C# or Visual Basic), which are compiled into assemblies that can be reused by future workflow definitions (with the custom activities defined being accessible from the Toolbox).
There are two types of custom activities to consider. Atomic custom activities are self-contained activities that don't rely on other activities to implement. This type of custom activity is always defined in code inherited from CodeActivity (see Figure 1), where the activity’s execution logic contains your business logic. Composite custom activities are activities with implementation sequences of one or more child activities. These can be defined in code (deriving from NativeActivity) or in XAML files (deriving from Activity).
Out of the box, Windows Workflow Foundation (WF) includes one activity that will help you execute methods defined in non-workflow types: the InvokeMethod activity. With the InvokeMethod activity, you can call instance or static methods on a type available to your workflow definition. Also you can pass in parameters and retrieve the return value from function calls. However, this approach is not as reusable as custom activities.
Defining Composite Custom Activities in XAML
The simplest form of custom activity is one that takes existing activities and combines them to define a new activity that encapsulates the combination. This activity can expose input and output arguments that the child activities it encompasses can use for their own input and output. This type of custom activity, for example, is what results when using the Add Service Reference on a WCF Workflow Services project within Visual Studio.
To begin creating a composite custom activity in XAML, start a new solution and choose the Activity Library template. If you have an existing library project, you can choose that by right-clicking the project, choosing Add New Item, and selecting the Activity template. Regardless of your starting point, this will result in a new .xaml file, which Visual Studio 2010 will open automatically with the Workflow Designer, as Figure 2 shows.
Behind the scenes, you have the XAML shown below—note the x:Class="ActivityLibrary1.Activity1" attribute on the Activity element. This is how XAML defines a new class (Activity1) that is deriving from Activity. Also notice the absence of a code beside this fully declarative custom activity implementation.
xmlns="http://schemas.microsoft.com/netfx/2009/xaml/activities" ...other namespaces elided for clarity...>
With the designer open, you can drag and drop activities from the Toolbox to define the child activities. The Activity type only supports the addition of a single child activity. To add more, consider using another flow-control composite activity such as a Sequence or Flowchart. For example, we can define a custom activity that repeats an input string a specified number of times and returns the string result by adding a While activity, then dropping a Sequence containing two Assign activities into the body of the While.
To get data in and out of the composition, as well as provide values to the child activities, we need to define Arguments. This is done in two steps. First, we define the arguments by clicking the Arguments button at the bottom of the designer and adding the desired argument—setting the name, direction, type, and optionally a default value. Second, we use the argument’s name within the properties of a child activity to pass the value down to that activity. Figure 3 show the result in the Workflow Designer. Not shown in the figure is that in addition to the NumberOfRepeats, OutputString, and RepeatString arguments, we have also defined a variable at the While scope for the CurrentIteration.
Once defined, this activity will automatically appear in the Toolbox for any WCF Workflow Service project that holds a project reference to the project or has had the activity added to the Toolbox manually by right-clicking the toolbox (when a workflow is open), selecting Choose Items, then clicking the Workflow Elements tab, which is shown in Figure 4. Click Browse, browse to the assembly, choose the activities to add from that assembly, and click OK. Now the activity can be used simply by dragging it from the Toolbox on to the Workflow Designer surface of the Workflow Service.
Defining Atomic Custom Activities in Code
When there is a need to define new logic or use logic defined in other type libraries that are not described by activities, an atomic custom activity deriving from CodeActivity should be used.
To create an atomic custom activity, start with an Activity Library project and add a CodeActivity item to it. This will result in a new *.cs or *.vb file being added to the project. A CodeActivity has no declarative form, so the code file will completely define the custom activity. Figure 5 shows the default CodeActivity template.
The code for the work you want to perform in the activity needs to be defined in the Execute method. The context parameter to the Execute method gives you access to a subset of workflow runtime features that are useful to an atomic activity: getting and setting the runtime value of the activity’s arguments, the workflow instance ID, access to the activity instance (which provides state details), access to extensions registered with the runtime (such as tracking participants or persistence providers), and a method for emitting custom tracking records.
Data exchange with a CodeActivity is accomplished by defining public argument properties of the desired concrete type. These properties must provide both a getter and setter. You define InArgument<T> properties (where T is the desired concrete type) to pass data into the activity, OutArgument<T> to return a value, and InOutArgument<T> to pass in a value that will be modified. Figure 6 provides a listing showing how to build an activity that replaces all the whitespace characters in an input string with underscores and returns both the updated string and the number of replacements made. Notice the use of the InOutArgument<string> for handling the string which is modified and how context.GetValue and context.SetValue are used to get and set the runtime values of the arguments.
The CodeActivity base class is used exclusively for performing work synchronously on the workflow runtime’s thread that is executing the activity. In some situations, it is desirable to release this and perform the custom activity’s processing on a background thread. The AsyncCodeActivity is provided as the base class for custom activities that will be performing asynchronous work on a non-workflow runtime thread. Note that in this case, asynchronous doesn't mean the workflow continues executing other activities beyond the async activity while it waits for it to finish—it’s simply asynchronous with respect to the workflow runtime thread. From the perspective of the workflow runtime, while an async activity is executing the workflow it is not idle and will not be unloaded. In fact, the runtime takes special precautions to ensure that the workflow instance stays loaded in memory while the async call is active so that when it finishes, the call-back references the same in-memory object. Figure 7 shows the implementation of a sample that demonstrates this behavior: The thread ID is printed out in the Action’s lambda method, and within the EndExecute method will be that of the background thread.
Because return values are so common in custom activities, workflow provides base types that expose a single output argument named “Result” of a generic type. The procedure for implementing such an atomic activity is the same as described for CodeActivity or AsyncCodeActivity, except that one derives from CodeActivity<TResult> or AsyncCodeActivity<TResult> and the Execute method actually expects a return value of type TResult (instead of void). When an activity defined this way is dragged on to the design surface, a dialog appears allowing the user to choose the return value’s type (see Figure 8). This enables writing activities that are agnostic of the type of their return value, without sacrificing strong typing at design time.
Defining Composite Custom Activities in Code
NativeActivity should be used in scenarios where you want to define execution control flow over any activity a user might add to the custom composite activity at design time. .NET 4 includes many examples of this: Sequence, Parallel, ForEach, and others. In addition to control flow, NativeActivity is useful for composite activities that require an idle point because they are waiting for an event or need to define custom logic when the activity is aborted or cancelled.
There is no item template for creating a NativeActivity. An easy way to get started is to add a CodeActivity item to your library project, change the base class to NativeActivity, and add the signature of the Execute method.
protected override void Execute(NativeActivityContext context)
The NativeActivityContext parameter of the Execute method provides full access to the workflow runtime. In addition to providing the features exposed by the CodeActivityContext, the NativeActivityContext provides methods for enumerating, aborting, and cancelling child activities and creating, resuming, and removing bookmarks. It also lets you schedule delegates, actions, and activities. In addition to using the context parameter in the Execute method, most implementations will override the CacheMetaData and optionally the Abort and Cancel methods defined by NativeActivity.
Figure 9 shows the implementation of a simplified version of the Sequence activity. Note that to actually be able to drag and drop child activities into the body of this custom activity within the Workflow Designer, one would need to register an activity designer for this activity (explaining how to do so is beyond the scope of this article). The Execute method simply schedules the first child in the collection of child activities, and when that activity is completed the ExecuteNextChild method is run by the workflow runtime. This method checks whether there are more activities in the collection to execute and schedules the next child activity if there is one. The CacheMetadata override is primarily used to notify the designer infrastructure of arguments, variables (both public and private), and activities added by the user to the composite activity.
Empower Workflow Services with Logic
I've shown you the three main approaches to creating custom activities to encapsulate your business logic for reuse within Workflow Service definitions: using Activity for declarative composite activities, CodeActivity for imperative atomic activities, and NativeActivity for defining imperative composite activities. By building custom activities you can reuse and modify your own business logic in workflow-based programs.