Simplify Workflow with Custom Activities
The number of built-in Activities that you can use to create a service that handles a long running service is small. Fortunately, it's easy to add additional Activities that wrap up business logic.
In my article
on using Workflow to support long running operations in an service-oriented kind of way, I mentioned in passing that you have the option to create custom activities for your workflows. Here's how to do that.
In a workflow, you can call any method on any class by using the InvokeMethod Activity. It's a general-purpose tool; however, that doesn't provide much support for the specific activity you're performing. That's especially obvious when you start passing parameters to the Activity. You don't get any IntelliSense support, just some error messages that will, at best, tell you if you have the right number of parameters of the right type. Creating your own custom Activities gives you an Activity dedicated to whatever task you want to perform with named, data-typed properties you can set reliably.
A custom Activity can be a simple wrapper around some existing method you'd otherwise call using the InvokeMethod activity, or your Activity can hold the actual code you want to execute.
To create a custom Activity, first add a class to your project and have it inherit from System.Activities.CodeActivity (under Add New Item, select the Activity template in the Workflow section):
Public NotInheritable Class SendEmails
If you're only going to be using your custom Activity in one workflow, add it to the Workflow project. If you think your activity will be useful in multiple Workflows, create the class in a separate Class Library project.
Your Activity must override the Execute method of CodeActivity class (this is the method called by Windows Workflow when your Activity is processed). Workflow will pass that method a CodeActivityContext object, which gives you access to data in (and about) the Workflow:
Protected Overrides Sub Execute(ByVal context As CodeActivityContext)
You next step is to define the properties a developer will use to pass values to your Activity. These properties must be declared using the generic InArgument<T>, specifying the data type you want the parameter to have. For any required parameter, you can decorate the property with the System.Activites.RequiredFieldAttribute to ensure that a developer using your Activity realizes that a value must be provided.
These two lines define two input values, one as a string and one as a Customer object (the CustomerParameter is required):
Property Body() As InArgument(Of String)
Property CustomerToEmail() As InArgument(Of Customer)
Within your Execute method, you can retrieve the value of the parameter with the GetValue method of the CodeActivityContext object passed to the method, passing a reference to the Property you want to retrieve:
cust = context.GetValue(Me.Cust)
There's a corresponding SaveValue method you could use to return a value through one of these properties. To use that, declare the property as either an OutArgument or an InOutArgument. However, if you do need to return a value, it's a better practice to inherit from the CodeActivity<T> class, specifying the data type you'll return. The CodeActivity<T> class still includes an Execute method, but now it's a function that returns a value of the type you specified when you declared the class.
This example, for instance, creates an Activity that returns a Boolean value:
Public NotInheritable Class SendEmail
Inherits CodeActivity(Of Boolean)
Protected Overrides Function Execute(
context As System.Activities.CodeActivityContext) As Boolean
After you've finished coding your class, rebuild your application and you'll find that your Toolbox includes a new icon for the Activity you've just defined—you just have to drag it into your workflow and set its properties to have it execute in your workflow.
In addition to accessing the properties set on your Activity, it's also possible to access “Workflow-level” variables from within your Execute method. Like using an OutArgument, it's not a best practice (you're tying your CustomActivity to what are, effectively, global variables) but, in case you need it, here's the code to retrieve and set a Workflow variable:
Dim props As System.ComponentModel.PropertyDescriptorCollection
props = context.DataContext.GetProperties()
cust = CType(props("CurrentCustomer").
cust.LastName = "Irvine"
This is just the simplest custom Activity you can create. If, for instance, you have a long-running method, rather than pause the Workflow while you wait for the method to complete, inherit from ASyncCodeActivity (it has both a BeginExecute and EndExecute method); if you need something more sophisticated than a class with a single method, inherit from the NativeActivity class; if you want to coordinate the actions of other Activities, you can extend the Activity class itself. Really, whatever your need is there's an Activity object waiting for you to extend.
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.