Export (0) Print
Expand All
Dazzling Graphics: Top Ten UI Development Breakthroughs In Windows Presentation Foundation
Distributed .NET: Learn The ABCs Of Programming Windows Communication Foundation
A First Look at InfoCard
Talking Windows: Exploring New Speech Recognition And Synthesis APIs In Windows Vista
Windows Workflow Foundation
Windows Workflow Foundation, Part 2
WinFX Workflow: Simplify Development With The Declarative Model Of Windows Workflow Foundation
XPS Documents: A First Look at APIs For Creating XML Paper Specification Documents
Expand Minimize

Windows Workflow Foundation: Creating a Custom Composite Activity

 

Morgan Skinner
Microsoft Premier Support for Developers

Updated January 2006 for beta 2

Applies to:
   Windows Workflow Foundation beta 2
   Microsoft Visual C# Version 2.0
   Visual Studio 2005

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. (19 printed pages)

Note   This article was written using beta 2 and in the future some changes may need to be made to get this to work on later versions of Windows Workflow Foundation.

Download the code sample, Windows Workflow Sample - ParallelIf.msi.


Contents

Introduction
Visual Classes
Behavior
Custom Activities
Activities in Use
For More Information
About the Author

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, 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.

Aa480200.parallelif01(en-us,MSDN.10).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 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.

Aa480200.parallelif02(en-us,MSDN.10).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 permit a branch to be deleted only 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—here 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.

Aa480200.parallelif03(en-us,MSDN.10).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, and especially if you have upgraded from beta 1 of Windows Workflow, you may find that you cannot actually compile your code as the validator now executes during compilation. This is something new for Beta 2 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.

Aa480200.parallelif04(en-us,MSDN.10).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 twarted. It's something new in beta 2 and, in my opinion, something that needs to change before the final release of Windows Workflow. 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 favor 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.

Aa480200.parallelif05(en-us,MSDN.10).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 and http://www.windowsworkflow.net for more details.

There is also a good introductory book on Windows Workflow available online that was written by several key members of the Microsoft team. See Presenting Windows Workflow Foundation for details.

 

About the Author

Morgan works for Microsoft in the UK, where he is an Application Development Consultant specializing in C#, Windows Forms, and ASP.NET. He's been working with the .NET Framework since its inception. Check out the Premier Support for Developers (PSfD) team blog at http://blogs.msdn.com/psfd and Morgan's site http://www.morganskinner.com.

Show:
© 2014 Microsoft