Windows Workflow Foundation (WF)

Introduction

One of the goals of workflows in general is to make "logic", "business processes", etc more visible by having a graphical representation of these. At design time it's pretty easy to compose a workflow using the Visual Studio 2005 designer but to this extent a workflow stays pretty static. So what about adapting or modifying a workflow at runtime? In this series of posts I'll outline various methodologies to modify a running workflow. For part one of this series, take a look over here.

Modification from the outside

In the previous part we've been looking at workflow modification from the inside. One of the problems with such an approach is that the workflow has to be prepared in some way or another to allow changes. That is, code must be present inside the workflow to make the changes. For that reason it might be more interesting in some scenarios to make the change from the ouside. This is what we'll be looking at for now.

Consider the workflow from the previous part again, but now with the first set of activities disabled as shown below. Of course you can just neglect those and create a new empty workflow and add the non-disabled stuff. I just want to point out the powerful "comment out" feature in WF once more :-).

Again we stick with simple CodeActivity activities, this time with the following ExecuteCode definitions:

private void killMeFromOutsideActivity_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("I should have been dead by now!"
);
}

private void HappyEndActivity_ExecuteCode(object sender, EventArgs
e)
{
   Console.WriteLine("The happy end :-)"
);
}

In order to change the workflow from the outside we need to reach a (what I use to call) "safe point". Tol illustrate this, I'm showing you the use of a SuspendActivity. Over to the hosting code now:

using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
   AutoResetEvent waitHandle = new AutoResetEvent(false
);
workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs
e) {waitHandle.Set();};
   workflowRuntime.WorkflowTerminated +=
delegate(object sender, WorkflowTerminatedEventArgs
e)
   {
      Console
.WriteLine(e.Exception.Message);
      waitHandle.Set();
   };

   workflowRuntime.WorkflowSuspended +=
new EventHandler<WorkflowSuspendedEventArgs
>(workflowRuntime_WorkflowSuspended);

   Dictionary<string, object> arguments = new Dictionary<string, object
>();
   arguments.Add(
"AllowUpdates", true
);

   WorkflowInstance
instance = workflowRuntime.CreateWorkflow(typeof(DynamicWf.Workflow1), arguments);
   instance.Start();

   waitHandle.WaitOne();
}

Most of the code is the same as in part 1. Recall that the AllowUpdates argument is used to determine whether updates are allowed or not by using it inside the workflow's DynamicUpdateCondition Code Condition. So, you can just ignore this for now and in case you're starting from an empty workflow, just ignore this piece of code.

The most important piece of code however is the following:

   workflowRuntime.WorkflowSuspended += new EventHandler<WorkflowSuspendedEventArgs>(workflowRuntime_WorkflowSuspended);

with the following corresponding event handler:

static void workflowRuntime_WorkflowSuspended(object sender, WorkflowSuspendedEventArgs e)
{
   Console.WriteLine("I'm suspended"
);

   WorkflowInstance
workflowInstance = e.WorkflowInstance;
   Activity
wRoot = workflowInstance.GetWorkflowDefinition();

   WorkflowChanges changes = new WorkflowChanges
(wRoot);
   changes.TransientWorkflow.Activities.Remove(changes.TransientWorkflow.Activities[
"KillMeFromOutsideActivity"
]);

   try
   {
      workflowInstance.ApplyWorkflowChanges(changes);
   }
   catch (InvalidOperationException ex)
   {
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine("No update allowed - " + ex.Message);
      Console.ResetColor();
   }


   Console.WriteLine("Let's resume"
);
   e.WorkflowInstance.Resume();
}

The code displayed above should be pretty self-explanatory. Again the WorkflowChanges class is at the center of the update logic. In order to obtain a reference to the running (correction: at this point suspended) workflow we can use the WorkflowSuspendedEventArgs argument's WorkflowInstance property.

Note: An important thing is to call ApplyWorkflowChanges on the workflow object of which the root activity (GetWorkflowDefinition) was passed to the WorkflowChanges constructor. I've run into some troubles when experimenting with this and the code above is the only one which is correct.

The result should look like this:

Again, when making the workflow's DynamicUpdateCondition evaluate to false (see previous episode), as shown below, the update will fail:

   Dictionary<string, object> arguments = new Dictionary<string, object>();
   arguments.Add(
"AllowUpdates", false);

Happy WF-ing once again!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

One of the goals of workflows in general is to make "logic", "business processes", etc more visible by having a graphical representation of these. At design time it's pretty easy to compose a workflow using the Visual Studio 2005 designer but to this extent a workflow stays pretty static. So what about adapting or modifying a workflow at runtime? In this series of posts I'll outline various methodologies to modify a running workflow.

Modification from the inside

One of the options to change a workflow is doing it from the inside. Basically this means that the "basic" logic of the workflow foresees that something might have to be changed and that the workflow modifies itself (from the inside) when certain criteria are met. Assume the following workflow but ignore the disabled activities (marked with a green background) - we'll examine these in another blog post because these illustrate modifying a workflow from the outside:

We have - for illustration purposes and to keep things simple - four CodeActivity activities and one DynamicSequenceActivity. Under normal (non-modification) circumstances the system would execute these one after another in a sequential order; Our goal is to add and remove activities in this workflow.

Modification 1 - Adding another activity

Assume we want to add another activity dynamically in between the SelfModificationActivity and the AnotherModificationActivity (you could choose another position too however). An ideal place to do that is inside the SelfModificationActivity, in a real situation based on some decision logic. In our case, we're just going to make the change under any circumstance. Take a look at the ExecuteCode handler logic for the SelfModificationActivity:

private void SelfModificationActivity_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("This is the SelfModificationActivity speaking"
);

   WorkflowChanges wc = new WorkflowChanges(this
);
   MyActivity ma = new MyActivity("With greetings from SelfModificationActivity"
);
   wc.TransientWorkflow.Activities.Insert(1, ma);

   try
   {
      this
.ApplyWorkflowChanges(wc);
   }
   catch (InvalidOperationException
ex)
   {
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine("No update allowed - "
+ ex.Message);
      Console
.ResetColor();
   }
}

The most important class to notice in here is the WorkflowChanges class. As the API describes this class is the wrapper around a set of proposed changes to a workflow. Therefore applying the changes using the ApplyWorkflowChanges method can throw an exception - something I'll illustrate later on. In this piece of code I'm just adding another activity on position 1 of the workflow (that is, immediately after the SelfModificationActivity). This index-based modification might not be the most ideal way of working, but it's a nice mind-setting example to start with.

The code displayed above inserts an activity of type MyActivity. What's going on in there isn't very important for the sake of the demo, but here is a possible definition for our demo:

public partial class MyActivity: SequenceActivity
{
   private string
message;

   public
MyActivity()
   {
      InitializeComponent();
   }

   public MyActivity(string message) : this
()
   {
      this
.message = message;
   }

   private void MainActivity_ExecuteCode(object sender, EventArgs
e)
   {
     
Console.WriteLine("This is the MyActivity speaking - "
+ message);
   }
}

Modification 2 - Deleting an activity

Deleting an activity can be an interesting option to skip certain activities under some circumstances where logic isn't expressed in the workflow itself (you might consider an if-else structure in the workflow to get a similar and designer-visible effect). Nevertheless, let's assume you want to remove an activity from the running workflow instance at runtime based on some conditions (of which the logic might be loaded at runtime, e.g. using reflection). More specifically I want to kill the DeadManWalkingAcitivity:

private void DeadManWalkingActivity_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("If you can see this, you are God :o"
);
}

The code to remove this activity will be added to the AnotherModificationActivity's ExecuteCode handler:

private void AnotherModificationActivity_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("Let's kill the dead man"
);

   WorkflowChanges wc = new WorkflowChanges(this
);
   Activity deadman = wc.TransientWorkflow.GetActivityByName("DeadManWalkingActivity"
);
   wc.TransientWorkflow.Activities.Remove(deadman);

   try
   {
      this
.ApplyWorkflowChanges(wc);
   }
   catch (InvalidOperationException
ex)
   {
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine("No update allowed - "
+ ex.Message);
      Console
.ResetColor();
   }
}

Again, the general approach is the same. Use a WorkflowChanges object, manipulate the TransientWorkflow's Activities collection (in this case by calling Remove) and call ApplyWorkflowChanges passing in the WorkflowChanges object with proposed changes. In this piece of code however, the activity to be removed is retrieved by name, not by some index number using GetActivityByName.

Modification 3 - Using the SequenceActivity

A third possibility is to foresee a placeholder in which activities will be loaded dynamically at runtime. Instead of filling out the "Drop activities here" at design time, we'll add activities in there at runtime. For demo purposes, the code to do this will be added to the DynamicSequenceActivity CodeActivity:

private void DynamicSequenceActivity_ExecuteCode(object sender, EventArgs e)
{
   if (dynamicActivityType == null
)
      return
;

   Type t = Type
.GetType(dynamicActivityType);

   WorkflowChanges wc = new WorkflowChanges(this
);
   Activity a = t.Assembly.CreateInstance(t.FullName) as Activity
;
   ((
SequenceActivity)wc.TransientWorkflow.GetActivityByName("DynamicSequence"
)).Activities.Add(a);

   try
   {
      this
.ApplyWorkflowChanges(wc);
   }
   catch (InvalidOperationException
ex)
   {
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine("No update allowed - "
+ ex.Message);
      Console
.ResetColor();
   }
}

Instead of just statically adding some activity, we're using reflection in the demo above to load an activity from the type specified as "dynamicActivityType". Again for demo purposes, this variable is just a simple property in the workflow class:

private string dynamicActivityType;

public string
DynamicActivityType
{
   get { return
dynamicActivityType; }
   set { dynamicActivityType = value
; }
}

This property is set when starting the workflow (e.g. upon calling a webservice method) but one can imagine various sources to get this property value from (e.g. a database). To illustrate setting this property, look at the following piece of demo code:

static void Main(string[] args)
{
   using(WorkflowRuntime workflowRuntime = new WorkflowRuntime
())
   {
      AutoResetEvent waitHandle = new AutoResetEvent(false
);
      workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs
e) {waitHandle.Set();};
      workflowRuntime.WorkflowTerminated +=
delegate(object sender, WorkflowTerminatedEventArgs
e)
      {
         Console
.WriteLine(e.Exception.Message);
         waitHandle.Set();
      };

      Dictionary<string, object> arguments = new Dictionary<string, object
>();
     
arguments.Add("DynamicActivityType", "DynamicWf.DynamicallyLoadedActivity, DynamicWf"
);

      WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(DynamicWf.Workflow1
), arguments);
      instance.Start();

      waitHandle.WaitOne();
   }
}

In this case, I'm just referring to "DynamicWf.DynamicallyLoadedActivity, DynamicWf" in the same assembly but you can make it much more flexible of course. You can think of some "DynamicallyLoadedActivity" yourself, just define a custom activity of your choice. An example is (how original again :$) a one-CodeActivity-wrapping custom activity with the CodeActivity's ExecuteCode set so:

private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
   Console.ForegroundColor = ConsoleColor.Green;
   Console.WriteLine("Greetings from a dynamic friend!"
);
   Console
.ResetColor();
}

The result

Executing the workflow constructed above (ignore the disabled activities for a while) yields the following result:

This is exactly the result we expected: the MyActivity was inserted on the right position; the "dead man activity" wasn't executed and another activity (DynamicallyLoadedActivity) was - as the name implies - loaded dynamically using reflection.

Changes allowed?

You might be concerned about the fact that a workflow seems to allow dynamic updates at all times, especially when we'll be looking (in a next post) at modifications from the outside. The good news is that there is a way for a workflow to decide on whether it allows updates to be made or not. This is done by setting the DynamicUpdateCondition property of the defined workflow. You can use a "Declarative Rule Condition" or a "Code Condition". Let's choose the latter option for now:

The corresponding code looks as follows:

private void CanBeUpdated(object sender, ConditionalEventArgs e)
{
   e.Result = allowUpdates;
}

Making the decision whether updates are allowed or not can be a complex piece of code of course, but let's stick to simplicity again and just use a boolean property for this purpose:

private bool allowUpdates;

public bool
AllowUpdates
{
   get { return
allowUpdates; }
   set { allowUpdates = value
; }
}

The decision is then communicated back using the ConditionalEventArgs's Result property. To illustrate the result in case of dynamic update denial, consider the following host code that sets the AllowUpdates property at startup of the workflow instance:

static void Main(string[] args)
{
   using(WorkflowRuntime workflowRuntime = new WorkflowRuntime
())
   {
      AutoResetEvent waitHandle = new AutoResetEvent(false
);
      workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs
e) {waitHandle.Set();};
      workflowRuntime.WorkflowTerminated +=
delegate(object sender, WorkflowTerminatedEventArgs
e)
      {
         Console
.WriteLine(e.Exception.Message);
         waitHandle.Set();
      };

      Dictionary<string, object> arguments = new Dictionary<string, object
>();
      arguments.Add("AllowUpdates", false);

     
arguments.Add("DynamicActivityType", "DynamicWf.DynamicallyLoadedActivity, DynamicWf"
);

      WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(DynamicWf.Workflow1
), arguments);
      instance.Start();

      waitHandle.WaitOne();
   }
}

When executing the code now, you'll see the following:

As you can see, every time the ApplyWorkflowChanges method is called, the dynamic update condition is evaluated. In case it evaluates to false, an InvalidOperationException is thrown, which gets caught by our update-code that foresees this possibility:

   try
   {
      this
.ApplyWorkflowChanges(wc);
   }
   catch (InvalidOperationException
ex)
   {
      Console.ForegroundColor = ConsoleColor.Red;
      Console.WriteLine("No update allowed - "
+ ex.Message);
      Console
.ResetColor();
   }

Happy WF-ing!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Windows Workflow Foundation (WF) is one of the key pillars of the .NET Framework 3.0 (formerly known as WinFX), next to Windows Presentation Foundation (WPF), Windows Communication Foundation (WCF) and Windows CardSpace (WCS). If you haven't done so, download the July CTP now.

A personal opportunity

Now, why am I telling you this? Historically, I've been avoiding BizTalk-related stuff just to be able to concentrate on the basic stuff I'm usually spending time with, such as CLR, C# language features, the BCL, SQL Server, web services, etc. Learning to work with another server product like BizTalk implies yet another learning curve and time was rather limited. But enough boring excuses.

Workflow is more widely accepted than a few years ago and is growing up to become (if it isn't already) a mainstream paradigm in software development. Combine this with the opportunity to write a university thesis on workflow (at UGent) and guess what Bart said to himself a couple of months ago? Indeed, time to enter the world of Windows Workflow Foundation. The research subject is entitled "Dynamic and Generic Workflows with WF", quite a broad definition with a lot of freedom. One of the things that will be researched is how to adapt workflows dynamically under various (stress) conditions. So, stay tuned for additional information on WF-related experiments.

Reading resources

In the meantime I recommend the following book, which was handed out at the PDC 05 last year. It's already quite outdated due to "CTP madness" (well, I should say "evolution", we're talking about technologies which are still under development after all, although the end is near, very near) but it's the only book I know of so far that's publicly available:

Other books that will appear in the next couple of months include:

Hello, Workflow!

Because I want to stress the "getting started" nature of this blog post, let's do a trivial thing. To all WF newbies: welcome to your first WF exposure.

Before you read on, make sure to have downloaded and installed the .NET Framework 3.0, the Windows SDK and the Visual Studio 2005 Extensions for WF. More information can be found on http://msdn.microsoft.com/windowsvista/downloads/products/getthebeta/default.aspx.

Step 1 - A new workflow project

Open Visual Studio 2005 and create a new project. Choose for a Sequential Workflow Console Application:

Let's call it HelloWorkflow to illustrate how big our imagination really is :$. In the solution explorer, delete Workflow1.cs. Although this is not necessary it's interesting to create the workflow in another way, using "code separation" with XOML (which, according to Wikipedia, stands for eXtensible Object Markup Language).

Step 2 - Add a workflow with code separation

Add a new item to the project and choose for Sequential Workflow (with code separation) and call it HelloWorkflow. As you can see, it has the extension .xoml:

Step 3 - Defining the workflow

Workflows consist of activities that are executed in a well-defined order by the workflow runtime engine. These activities can be found in the Visual Studio 2005 toolbox, but it's also possible to create your own activities e.g. by composition (just as you can create a new Windows Forms control by defining a User Control consisting of other "orchestrated" controls). The toolbox looks as follows:

For the sake of the demo, drag and drop a Code(Activity) to the "Drop Activities to create a Sequential Workflow" part in the designer. Using the properties grid, change the name of the new activity from codeActivity1 to sayHello. The result should look like this:

The red exclamation mark tells you there's still something wrong with the activity's configuration. More specifically, the designer tells you "Property 'ExecuteCode' is not set.". Basically, ExecuteCode is an event handler which can be created by double-clicking on the code activity. (Note: for other activities too, always use the red exclamation mark assistance to configure an activity properly). The code for our activity will consist of a simple Console.WriteLine call:

namespace HelloWorkflow
{
   public partial class HelloWorkflow :
SequentialWorkflowActivity
   {
      private void sayHello_ExecuteCode(object sender, EventArgs
e)
      {
         Console.WriteLine("Hello, Workflow!"
);
      }
   }
}

Notice the use of a partial class. The other part of the HelloWorkflow definition lives in the .xoml file. Basically XOML (and XAML too) is just some chunk of XML describing a series of object instantiations, property setting stuff, nesting, etc. Tip: right-click the HelloWorkflow.xoml file in the Solution Explorer, choose Open With... and select the XML Editor:

<SequentialWorkflowActivity x:Class="HelloWorkflow.HelloWorkflow" x:Name="HelloWorkflow" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
   <
CodeActivity x:Name="sayHello" ExecuteCode="sayHello_ExecuteCode"
/>
</
SequentialWorkflowActivity>

Basically this code comes down to:

namespace HelloWorkflow
{
   public HelloWorkflow : SequentialWorkflowActivity
   {
      public HelloWorkflow()
      {
         CodeActivity sayHello = new CodeActivity();
         sayHello.ExecuteCode += new EventHandler(this.sayHello_ExecuteCode);

         this.Activities.Add(sayHello);
      }
   }
}

Step 4 - Hosting the workflow engine

In order to execute a workflow, one needs to host the workflow engine. However, no worries: Visual Studio 2005 has generated the code for us in Program.cs. It needs a little modification however because we did remove Workflow1.cs and replaced it by HelloWorkflow.cs. Here's the modified Main code (change indicated in bold):

static void Main(string[] args)
{
   using(WorkflowRuntime workflowRuntime = new WorkflowRuntime
())
   {
      AutoResetEvent waitHandle = new AutoResetEvent(false
);
      workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs
e) {waitHandle.Set();};
      workflowRuntime.WorkflowTerminated +=
delegate(object sender, WorkflowTerminatedEventArgs
e)
      {
         Console
.WriteLine(e.Exception.Message);
         waitHandle.Set();
      };

      WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(HelloWorkflow
));
      instance.Start();

      waitHandle.WaitOne();
   }
}

This code initializes the WorkflowRuntime and starts the workflow by calling the CreateWorkflow method. The anonymous method plumbing keeps the console application alive till the workflow has executed and also reports exceptions that terminated the workflow.

Step 5 - Run run run

Time to hit CTRL-F5 and to see Windows Workflow Foundation come alive:

What's coming up next?

In a next WF episode I'll tell you more about making dynamic changes to a running workflow. Other topics that will be covered include workflow persistence (dehydration and rehydration of a workflow), combining workflow with web services and combining WCF with WF.

Stay tuned!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

More Posts « Previous page