Morgan Skinner
Microsoft Application Developer Consulting
September 2008
Applies to:
Windows Workflow Foundation
Microsoft Visual C#
.NET Framework Version 3.0
Visual Studio 2008
Summary: This
article constructs a custom composite activity and shows how to use that
activity within a workflow. The activity in question is an extension of the
Parallel activity shipped with the product, and provides an overview of
validation, writing a custom executor component, and styling the user interface
using designer and theme classes. (16 printed pages)
Download
the sample code
Introduction
In this article I'll delve into the details of developing
custom activities for Windows Workflow Foundation (WF) and provide a sample
implementation that includes all the parts that make up a custom activity.
First off, though, I will provide some background about activities and the
classes used to make up a complete activity.
There are a number of classes that you'll need to get to
grips with in order to create an activity - in this example I'll create an
activity called the ParallelIf, which works like a regular parallel activity
however each branch has a condition that, if true, ensures that that branch is
executed. Likewise if the condition evaluates to false, the child activities
will be excluded.
The image shown in Figure 1 below shows the classes that
I'll be using in this article.
.gif)
Figure 1. Classes
that make up an activity
I have deliberately split these into left and right sides,
as the classes on the left deal primarily with the design-time experience of
the activity, and that on the right deals with the behavior of the activity
when it's used in a workflow. The activity is the only object that is mandatory
- all others will use defaults if not explicitly defined. In the description
below the terminology "Visual" and "Behavior" is my own.
Visual Classes
There are three main classes that go to make up the visual
aspects of the activity: ToolboxItem, Designer, and Theme.
The ToolboxItem class
The first class I'll describe is the ToolboxItem class.
First off, you might expect that this defines the visual rendering of the image
inside the toolbox, and indeed it does, but it delegates the task of defining
the actual image to our old friend the ToolboxBitmap attribute (which, as you
may know, has been around since the .NET Framework 1.0). There are plenty of
things you can add to the ToolboxItem class, but here I'll concentrate on the
minimum necessary to get your activity into the toolbox.
The main work of this class is to define what happens when
your activity is dragged from the toolbox onto the workflow designer - so you
need to override the CreateComponentsCore method that returns a list of
initialized components - that is your activity and any subordinate activities
you wish to add. The elided code below shows an example toolbox item and how to
hook this to your activity.
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Runtime.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
namespace MNS.Activities
{
public class ParallelIfToolboxItem : ActivityToolboxItem
{
protected override IComponent[] CreateComponentsCore(IDesignerHost host)
{
CompositeActivity activity = new ParallelIfActivity();
activity.Activities.Add(new ParallelIfBranch());
activity.Activities.Add(new ParallelIfBranch());
activity.Activities.Add(new ParallelIfBranch());
return new IComponent[] { activity };
}
// Other methods omitted for clarity
}
[ToolboxItem(typeof(ParallelIfToolboxItem))]
[ToolboxBitmap(typeof(ParallelIfActivity),
"Resources.ParallelIfActivity.png")]
public partial class ParallelIfActivity : CompositeActivity,
IActivityEventListener < ActivityExecutionStatusChangedEventArgs >
{
...
}
}
Here I've defined the ParallelIfToolboxItem class and added
an implementation for the CreateComponentsCore method. This constructs the new
parent activity (ParallelIfActivity), and then adds three ParallelIfBranch
activities as children to the new activity (I'll define these later on in the
article). The activity is then returned to the caller. So, when you drop this
activity onto the designer, it creates the activity and constructs the child
branches within it. You could obviously set appropriate defaults for the
activity properties here too.
I've then shown the first part of the ParallelIfActivity
class, and defined the attributes that hook up the toolbox item to the activity
and also the resource for the image. There are a number of overrides for the
ToolboxBitmap attribute, and I've used this one as I usually follow the same
method that Microsoft uses internally. In my project I have a Resources
directory as shown in Figure 2.
.gif)
Figure 2. Custom
bitmap in the Resources directory
Here I've created an image and defined it as an Embedded
Resource so it is included in the assembly during compilation. The image used
for a toolbox item should be 16 X 16 pixels with a color depth up to 256 bits
per pixel.
The Designer Class
In order to alter the visible aspects of the activity when
it is drawn on the form, you need to add a custom designer class, which typically
derives from ActivityDesigner or CompositeActivityDesigner (in the case of a
composite activity). The designer is linked to the activity using the
[Designer] attribute, in the same manner as shown for the other associated
classes presented in this article.
Within the designer you can add code to provide complete
custom drawing if necessary, but in this example I'll use a custom theme that
allows me to change many of the visual aspects of the activity but does this
with the minimum of effort.
The code below shows the implementation of my designer
class, and also how this is associated with the ParallelIfActivity class by
means of the [Designer] attribute.
using System;
using System.Collections.ObjectModel;
using System.Workflow.ComponentModel.Design;
namespace MNS.Activities
{
[Designer(typeof(ParallelIfDesigner))]
public partial class ParallelIfActivity : CompositeActivity, ...
{
...
}
public class ParallelIfDesigner : ParallelActivityDesigner
{
public override bool CanInsertActivities(HitTestInfo
insertLocation, ReadOnlyCollection<Activity> activitiesToInsert)
{
return false;
}
public override bool CanMoveActivities(HitTestInfo moveLocation,
ReadOnlyCollection<Activity> activitiesToMove)
{
return true ;
}
public override bool CanRemoveActivities(ReadOnlyCollection<Activity>
activitiesToRemove)
{
return true ;
}
protected override CompositeActivity OnCreateNewBranch()
{
return new ParallelIfBranch();
}
}
}
The designer class is used to determine what happens within
the workflow designer, so here I've overridden CanInsertActivities to return
false. This method is called when an activity is dragged from the toolbox into
the ParallelIfActivity, and I'm always returning false here as the only thing
that can be a direct child of the ParallelIfActivity is the ParallelIfBranch,
which is not on the toolbox since that's added using the OnCreateNewBranch()
method. You could extend this processing to check what type of activity is
being dropped, and maybe permit some activities and deny others. It's entirely
up to you.
I've returned true from CanRemoveActivities and
CanMoveActivities as these are both supported in my designer. The default
processing for the CanRemoveActivities method on the ParallelActivityDesigner
is to check how many child branches are defined and only permit a branch to be
deleted if this will ensure that there are still at least two branches defined.
I've overridden this behavior and instead raise an error from the validator if
fewer than two branches are available.
A composite activity such as this needs some way to create
new branches where a branch is an immediate child of the ParallelIfActivity.
The OnCreateNewBranch method is used in the designer when you right-click on
the activity and then choose Add Branch, as shown in Figure 3.
.gif)
Figure 3. Adding a
new branch in the designer
You can again set appropriate properties of the branch if
necessary before returning from the OnCreateNewBranch method. The image above
also shows the output of the theme class defined next.
Each error raised from the validator (described below) can
include some custom data - this data is contained in a hashtable and should be
serializable as the validator and the designer run in separate app domains.
I've used this to add a string value indicating that there are fewer than two
branches defined. In my designer, I then override OnExecuteDesignerAction and
check for the existence of this extra data, and if found I then add in up to
two branches to the activity. This code is shown below.
private void OnAddBranch(object sender, EventArgs e)
{
CompositeActivity activity1 = this.OnCreateNewBranch();
CompositeActivity activity2 = base.Activity as CompositeActivity;
if ((activity2 != null) && (activity1 != null))
{
int num1 = this.ContainedDesigners.Count;
Activity[] activityArray1 = new Activity[] { activity1 };
CompositeActivityDesigner.InsertActivities(this, new
ConnectorHitTestInfo(this, HitTestLocations.Designer,
activity2.Activities.Count), new
List<Activity>(activityArray1).AsReadOnly(), string.Format("Adding
branch {0}", activity1.GetType().Name));
if ((this.ContainedDesigners.Count > num1) &&
(this.ContainedDesigners.Count > 0))
{
this.ContainedDesigners[this.ContainedDesigners.Count - 1].EnsureVisible();
}
}
}
protected override void OnExecuteDesignerAction(DesignerAction designerAction)
{
// Check for the existence of my user data...
if (designerAction.UserData.Contains("LESS_THAN_TWO"))
{
CompositeActivity parent = this.Activity as CompositeActivity;
if (null != parent)
{
while ( parent.Activities.Count < 2 )
OnAddBranch(this, EventArgs.Empty);
}
}
else
base.OnExecuteDesignerAction(designerAction);
}
So, if the user were to remove all activities from the
ParallelIfActivity, a warning will be displayed on the user interface which,
when clicked, will add the appropriate number of child branches into the
designer.
The Theme Class
The Theme class defines the visual representation of the
activity, and here you can define properties such as the color of the lines
used within the activity, the line caps (such as arrows, etc.), the pen used to
draw the outline of the image, and the background brush used when painting the
image.
There are many other properties of the Theme class, so have
a look to see what else you can define. Defining a theme is easy and makes your
activity stand out from the crowd - although I recommend that you don't create
garish colors as I'm sure your users won't appreciate it.
using System;
using System.Drawing.Drawing2D;
using System.Workflow.ComponentModel.Design;
namespace MNS.Activities
{
[ActivityDesignerTheme( typeof (ParallelIfTheme))]
public class ParallelIfDesigner : ParallelActivityDesigner
{
...
}
public class ParallelIfTheme : CompositeDesignerTheme
{
public ParallelIfTheme(WorkflowTheme theme)
: base(theme)
{
this.ShowDropShadow = true;
this.ConnectorStartCap = LineAnchor.None;
this.ConnectorEndCap = LineAnchor.None;
this.BorderStyle = DashStyle.Dash;
}
}
}
There are two main types of themes - those used for regular
activities and those used for composite activities. The options for rendering
composite activities are a superset of those available for regular activities.
The ActivityDesignerTheme base class is used for regular activities, whereas
here I use CompositeDesignerTheme since my ParallelIfActivity contains child
activities. I've just defined the theme to set the border style, drop shadow,
and line caps.
The theme is attached to the designer by use of the
[ActivityDesignerTheme] attribute as shown in the code above. And the designer
is attached to the activity by using the [Designer] attribute as shown in the
earlier code snippet.
Behavior
Now that we've done the visual representation of the
activity, we'll move on to the behavior. There is just one class here – the
Validator.
The Validator Class
This class is used at design time primarily to provide hints
to the user that the activity is in an inconsistent state, and is typically
used to display warnings where mandatory properties have not been defined.
Within your validator you might want to check that a
particular property is non-null, or verify that an appropriate number of child
activities have been added to the activity. This is the example I'm using in my
code - here I check that the ParallelIfActivity contains at least two branches:
using System;
using System.Drawing.Drawing2D;
using System.Workflow.ComponentModel.Design;
namespace MNS.Activities
{
[ToolboxItem(typeof(ParallelIfToolboxItem))]
[ToolboxBitmap(typeof(ParallelIf),"Resources.ParallelIf.png")]
[ActivityValidator(typeof(ParallelIfValidator))]
public class ParallelIfActivity : CompositeActivity, ...
{
}
public class ParallelIfValidator : CompositeActivityValidator
{
public override ValidationErrorCollection Validate(ValidationManager manager, object obj)
{
if (null == manager)
throw new ArgumentNullException("manager");
if (null == obj)
throw new ArgumentNullException("obj");
ParallelIfActivity pif = obj as ParallelIfActivity;
if (null == pif)
throw new ArgumentException("This validator can only be used
by the ParallelIfActivity", "obj");
ValidationErrorCollection errors = base.Validate(manager, obj);
if ( null != pif.Parent )
{
// Now actually validate the activity...
if ( pif.Activities.Count < 2 )
{
ValidationError err = new ValidationError ( "At least two
branches are required for a ParallelIf", 100, false );
err.UserData.Add("LESS_THAN_TWO", "There are less than two branches");
errors.Add(err);
}
}
return errors;
}
}
}
In my validator code I check the input arguments, and then
call the base class Validate method that returns a collection of existing
validation errors (if any). Then I check the number of child nodes of the
ParallelIfActivity and if there are fewer than two I construct a
ValidationError instance and add this to the returned errors collection. If any
errors are returned from this method the workflow designer will display an icon
next to the activity and use the error string from the ValidationError object
as what is displayed to the user.
In this example I am adding the "LESS_THAN_TWO"
marker to the validation error UserData collection, which is picked up by the
designer and used to create the appropriate number of child branches to the
activity.
When creating a validator I would always suggest calling the
base class Validate() method, as there may be other errors in a base class that
should be reported to the user in addition to any of your own.
Note that there are two main validator classes -
ActivityValidator and CompositeActivityValidator. The former is used for all
simple activities and the latter is used for all activities that include
children. The main difference is that the base class Validate() method for
CompositeActivityValidator will validate all child activities too, so you might
get a long list of errors on your parent activity that relate to everything
that is wrong with the child activities.
Compiling with a Validator
When you have associate a validator with your activity you
may find that you cannot actually compile your code as the validator also
executes during compilation. This is something that you are likely to come
across and as such I wanted to describe how to avoid this problem.
As an example, the following is the code from a validator I
originally wrote for the WriteLineActivity that is included with the code
download…
public override ValidationErrorCollection Validate(ValidationManager manager, object obj)
{
if (null == manager)
throw new ArgumentNullException("manager");
if (null == obj)
throw new ArgumentNullException("obj");
// Get the base set of errors
ValidationErrorCollection err = base.Validate(manager, obj);
WriteLine wl = obj as WriteLine;
if (null == wl)
throw new ArgumentException("The object is not an WriteLine activity", "obj");
if ( null == wl.Message )
err.Add(ValidationError.GetNotSetValidationError("Message"));
return err;
}
This checks that you have entered something for the Message
property of the WriteLine class and displays a validation error if no text has
been entered as shown below in Figure 4.
.gif)
Figure 4. Error when
compiling with a validator
This error is, to say the least, frustrating – all I’m
trying to do is compile my code (without even using the WriteLineActivity), and
I’m being thwarted. To avoid this issue there are two approaches you can take.
The first is to check in the validator if there is a parent
of the activity being validated. If so, your custom activity is being used
within a workflow and so the validation should occur. If there is however no
parent, your activity need not be validated as it is just being compiled. So,
to avoid this compile time issue, you can simply add a line of code to the
validator that checks if a parent activity exists, and only does the validation
if a parent is available…
if (null != wl.Parent)
{
if (string.IsNullOrEmpty(wl.Message))
errors.Add(ValidationError.GetNotSetValidationError("Message"));
}
This check is counterintuitive and will no doubt trip up
many developers which is why I thought to include it in the article.
The other way to avoid this compilation error is to create
your workflow activities in a regular Class Library project and not a Workflow
Activity Library project. In this case the validator isn’t run at all when
compiling your code, so you can avoid the parent check from the above code.
Personally I favour this way to avoid the validator running when compiling the
activity.
You might wonder in the code why I have checks defined as if
(null != wl.Parent) rather than if ( wl.Parent != null ). It’s a case of old
habits die hard – I used to be a C++ programmer and this order of expressions
was safer as in C++ I could inadvertently assign the value null to wl.Parent if
I was to omit the ‘!’ character. So, all of my evaluations may look the wrong
way around, but they’re there from a good habit I learned some years ago.
Custom Activities
The ParallelIfActivity
Now that the designer, validator and theme have been
defined, the code for the ParallelIfActivity is fairly easy to implement.
[ToolboxBitmap(typeof(ParallelIfActivity), "Resources.ParallelIfActivity.png")]
[ToolboxItem(typeof(ParallelIfToolboxItem))]
[Designer(typeof(ParallelIfDesigner))]
[ActivityValidator(typeof(ParallelIfValidator))]
public partial class ParallelIfActivity : CompositeActivity, IActivityEventListener < ActivityExecutionStatusChangedEventArgs >
{
private static readonly DependencyProperty IsExecutingProperty =
DependencyProperty.Register("IsExecuting",
typeof(bool),
typeof(ParallelIf));
public ParallelIf()
{
InitializeComponent();
}
/// <summary>
/// Flag used to state whether the activity is executing or not
/// </summary>
public bool IsExecuting
{
get { return (bool)base.GetValue(ParallelIf.IsExecutingProperty); }
set { base.SetValue(ParallelIf.IsExecutingProperty, value); }
}
}
This defines links to all the UI and Behavior classes
already defined, and defines a Boolean flag that is used in the executor that
holds a value indicating whether the activity is currently executing.
Executing the Activity
The Execute method is called on the activity when the
workflow is running, and its purpose is to call each child activity and execute
it. The order of execution is entirely up to you; however, it's most likely
that you will iterate through all enabled child activities (if this is a
composite activity) and execute each in turn.
In my example of a ParallelIfActivity, I have to determine whether
to execute a branch of the activity based on the condition associated with that
branch. If that condition evaluates to true then the activities of that branch
are executed.
The code below shows how I evaluate the condition on each
branch to verify whether the branch should execute or not.
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
if (null == executionContext)
throw new ArgumentNullException("executionContext");
// Set the flag to indicate that I am executing
this.IsExecuting = true;
bool hasStarted = false;
for (int child = 0; child < this.EnabledActivities.Count; child++)
{
ParallelIfBranch branch = this.EnabledActivities[child] as ParallelIfBranch;
if (null != branch)
{
// Evaluate the condition and only execute branches that return true
if (branch.Condition.Evaluate(branch, executionContext))
{
// Now register a callback to be notified when the child has closed
branch.RegisterForStatusChange(Activity.ClosedEvent, this);
// Then execute the child activity
executionContext.ExecuteActivity(branch);
// Used to determine the outcome of the execute method
hasStarted = true;
}
}
}
return hasStarted ? ActivityExecutionStatus.Executing : ActivityExecutionStatus.Closed ;
}
Here I verify the input parameter and then loop through all
executable activities - these are defined as activities that have not been
commented out within the designer. For each of these I then evaluate the
condition that, if true, queues the activity for execution using the
executionContext.ExecuteActivity() method call. I also set a flag indicating
that at least one activity is executing, which is used at the end of the method
to return the appropriate status code. If any child activities are executing I
return ActivityExecutionStatus.Executing, otherwise
ActivityExecutionStatus.Closed is returned.
The workflow runtime therefore needs some way to know that
the parallel activity has completed - this is done by adding in an event
handler that is raised when a child action completes by adding a call to
RegisterForStatusChange as shown in the code. I can then verify if all children
have completed and if so call ActivityExecutionContext.CloseActivity() as shown
in the code below.
void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent(object sender, ActivityExecutionStatusChangedEventArgs e)
{
if (null == sender)
throw new ArgumentNullException("sender");
if (null == e)
throw new ArgumentException("e");
ActivityExecutionContext context = sender as ActivityExecutionContext;
if (null == context)
throw new ArgumentException("The sender must be an ActivityExecutionContext", "sender");
// Check what state we were called from...
if (e.ExecutionStatus == ActivityExecutionStatus.Closed)
{
// We're done listening for the closed event from this activity
e.Activity.UnregisterForStatusChange(Activity.ClosedEvent, this);
// Now check if all child activities have closed...
ParallelIfActivity pif = context.Activity as ParallelIfActivity;
bool finished = true;
for (int branch = 0; branch < pif.EnabledActivities.Count; branch++)
{
Activity child = pif.EnabledActivities[branch];
if ((child.ExecutionStatus != ActivityExecutionStatus.Initialized) && (child.ExecutionStatus != ActivityExecutionStatus.Closed))
finished = false;
}
// All children are complete - so I am now complete
if (finished)
context.CloseActivity();
}
}
This just loops through each child activity and checks for
any that are neither Initialized nor Closed - in which case a child activity is
still executing so I cannot close the parent activity.
The status change code presented above may look somewhat
strange due to the use of generics, however conceptually it’s very simple. Each
activity has a set of events that are raised during its lifetime, such as
ExecutingEvent, CancelingEvent, ClosedEvent and so on. These are defined as
dependency properties on the Activity class.
When you call the activity.RegisterForStatusChange ()
method, you are passing a delegate to a particular function which will be
called when the state of an activity changes. Your delegate may need to handle
multiple status changes, and if so you can find the current state of the
activity by checking the ExecutionStatus property of the passed event argument
'e'.
Internally, a list of delegates is maintained for each of
the events that may be raised, and this list is traversed when the state
changes and each delegate is called.
In addition to the Execute and OnEvent methods presented
above, I have also provided an implementation of both the Cancel and the
OnActivityChangeAdd methods. In the Cancel method I need to cancel any
executing branches, which involves looping through any executing child
activities and calling the CancelActivity method on these.
The OnActivityChangeAdd method may be called at runtime if,
for example, your code dynamically adds nodes to the ParallelIfActivity. In
this instance I need to register the status change handler for the newly added
activity, and then execute that activity. The code for these methods is
available in the download.
The ParallelIfBranch activity
This is the child activity of the ParallelfActivity and
includes a Condition property that is evaluated by the executor to determine if
the branch should be executed or not. The full code for this activity is shown
below.
[ToolboxItem(false)]
[Designer(typeof(ParallelIfBranchDesigner))]
[ActivityValidator(typeof(ParallelIfBranchValidator))]
public class ParallelIfBranch : SequenceActivity
{
/// <summary>
/// Define the condition property as a DependencyProperty
/// </summary>
public static readonly DependencyProperty ConditionProperty =
DependencyProperty.Register("Condition",
typeof(Condition),
typeof(ParallelIfBranch),
new PropertyMetadata(DependencyPropertyOptions.Metadata));
/// <summary>
/// Get/Set the Condition
/// </summary>
/// <remarks>
/// The condition is evaluated at runtime, and if true then the
subordinate activities will be executed
/// </remarks>
public Condition Condition
{
get { return base.GetValue(ConditionProperty) as Condition; }
set { base.SetValue(ConditionProperty, value); }
}
}
In addition to this activity there is a validator that
ensures that a value has been defined for the Condition property, and a
designer that simply overrides the CanBeParentedTo method to ensure that the
ParallelIfBranch can be added only as a direct descendant to the
ParallelIfActivity.
Activities in Use
As the point of this article is to provide a new activity to
the arsenal of available activities, this section will show how the
ParallelIfActivity can be used within a workflow. In the code download I have
included the full implementation that you can use to add this activity to your
toolbox. Once there, you can drag the ParallelIfActivity onto the designer and
you will be rewarded with something like that shown in Figure 5 below.
.gif)
Figure 5. The
ParallelIfActivity within a workflow
By default the ToolboxItem class is defined to add in three
branches to the ParallelIfActivity. These show up with errors as the Condition
property of the activities has not been defined.
Select each branch in turn and define a condition - you can
use a code condition or a rule condition here. Once you have defined the
conditions for each branch you should then be able to execute the workflow.
I've included a WriteLineActivity in the code download that you can use to
output data to the console, which is useful when checking which branches of the
ParallelIf have actually been executed.
As an alternative to outputting data to the Console, you
could use the inbuilt tracking services of Windows Workflow to record extra
data as your activities execute. In order to do this you can call the TrackData
() method of the Activity class to record any custom data you wish within the
tracking store.
For More Information
There are a number of sites you can use to obtain more
information about Windows Workflow Foundation. See http://msdn.microsoft.com/workflow
for more details.
About the Author
Morgan works for Microsoft in the UK, where he is an
Application Development Consultant specializing in Windows Workflow, C#,
Windows Forms, and ASP.NET. He's been working with the .NET Framework since its
inception. Check out Morgans site at http://www.morganskinner.com for links to
other articles he has written.