Export (0) Print
Expand All

Introducing Windows Workflow Foundation

This chapter is excerpted from Programming .NET 3.5: Build N-Tier Applications with WPF, AJAX, Silverlight, LINQ, WCF, and More by Jesse Liberty, Alex Horovitz, published by O'Reilly Media

Microsoft's Windows Workflow Foundation (WF) is a programming framework that facilitates the creation of reactive programs (described in the upcoming sidebar) designed to respond to external stimuli. It is an implementation of an important new idea that has recently found its way into programming: programmers, seeing the power of runtimes (such as the JVM and the CLR), are now starting to ask for the incorporation of design constructs as data in the same way type definitions are available as data.

Runtimes have shown the value of machine-readable representations. By way of example, most programmers almost immediately see the benefit of features such as reflection and serialization. The question naturally arises, "Why can't I model control flow, logic constructs, concurrency, and other design-time constructs as data in the same way I can model methods, fields, and classes?" The answer: there is no good reason.

Fortunately, the folks at Microsoft were thinking along the same lines, and they have given us an extensible meta-runtime in the form of WF. The meta approach taken by the architects of WF, under the leadership of Dharma Shukla, has resulted in a highly user-driven implementation (and by user, we mean you!). The WF programming model is organized around specific activities. WF is also inherently extensible, which makes it easier for you to capture the intentions of domain experts in the grammars/languages they know and understand.

In this chapter, you're going to build some simple applications. Our aim is to illustrate the core concepts of WF without specifically using the Microsoft tools. Then, after you've gained an appreciation of the heavy lifting involved, we'll take you though some of the simpler concepts involved in creating some small workflow applications using WF.

Conventional (Pre-WF) Flow Control

First, let's take a look at a couple of pre-WF examples that have one thing in common: either they deal with flow control in their own way, or they don't deal with it at all. Afterward, we'll see how WF changes the picture.

Reactive Programs

In the past, we created reactive programs to accomplish workflow-like activities. Reactive programs can be generally understood to be programs with the following characteristics:

  • They pause during execution.

  • The amount of time for which they pause is not predetermined.

  • While paused, they await further input.

This is not really anything new to the world of computing. Collaboration between programs on the same and different machines has been an important goal since the very early days of computing. Over the years, technologies have been developed to assist in the communication between programs. From sockets to web services, computer scientists continue to evolve the mechanism through which inter-application communication occurs.

A Console Application: TalkBack

To get started with this first example, open Visual Studio 2008 and select New Project from the File menu. Create a new Console Application called TalkBack, as shown in Figure 12.1, "Creating the TalkBack console application".

Figure 12.1. Creating the TalkBack console application

Creating the TalkBack console application

You will need to add the following code to Program.cs:

using System;
using System.Collections.Generic;
using System.Text;

namespace TalkBack
{
   class Program
   {
      static void Main(string[] args)
      {
         // Print an instruction
         String key = DateTime.Now.GetHashCode().ToString();

         Console.WriteLine("Enter the following key to continue: "
             + key);

         String input = Console.ReadLine();

         if (key.Equals(input))
         {
            Console.WriteLine("We have a match: " + key + " = "
                + input);
         }
         else
         {
         Console.WriteLine("Oops! " + key + " is not the same as "
             + input);

         }

         // Leave something on the screen and wait for input to exit
         Console.WriteLine("");
         Console.WriteLine("Press Enter to exit...");
         Console.ReadLine();
      }
   }
}

TalkBack is an example of a simple reactive program: it's a basic console application designed to gather input from the user, make a decision about that input, and display a result. As you can clearly see in Figure 12.2, "TalkBack: a simple reactive program", this program pauses during execution for an unknown length of time, waiting for further input.

In many ways, this is like most of the computer programs with which we are all familiar. In the real world, we encounter reactive programs all the time. When you shop on Amazon.com or make travel reservations on Orbitz.com, these reactive programs are guided by your input. Likewise, when Amazon sends data to UPS about your order, or Orbitz books your seat on a United Airlines flight, UPS and United Airlines have reactive programs that are guided by input from other programs and that transfer the relevant information to the requesting company's programs.

Figure 12.2. TalkBack: a simple reactive program

TalkBack: a simple reactive program

To further your understanding of workflow, next you'll write a simple order-status web service in ASP.NET.

An ASP.NET Web Service: OrderStatus

Create a new C#-based ASP.NET Web Service called OrderStatus in Visual Studio. Enter the following code in the Service.cs file:

using System;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
// To allow this web service to be called from script,
// using ASP.NET AJAX, uncomment the following line

// [System.Web.Script.Services.ScriptService]
public class Service : System.Web.Services.WebService
{
   public Service()
   {
      // Uncomment the following line if using designed components
      // InitializeComponent();
   }

   [WebMethod(EnableSession = true)]
   public string WelcomeInstructions()
   {
      String orderNumber = "W123456";
      Session["orderNumber"] = orderNumber;
      return "Please enter your order number: "
          + orderNumber + "\n\n";
   }

   [WebMethod(EnableSession = true)]
   public string Get OrderStatusForOrderNumber(String s)
   {

      if (Session["orderNumber"].Equals(s))
      {
         return "Your order is being prepared for shipment";
      }
      else
      {
         return "Invalid order number...";
      }

   }
}

This is a very simple reactive program implemented as a web service. The two methods are easy enough to understand, but there's no sense of the application flow; that is, there is nothing in the methods to prevent them from being called out of order. You'll need to implement the application flow by hand.

The first thing you'll need to do is add some flow control to the methods. As you'll see, it's fairly easy to write flow control into your code. In the code just shown, you saved the user's order number in an ASP Session variable. Next, you'll test the value of this variable to monitor the order in which the methods are called. Consider these additions (in bold) to the original code:

public string WelcomeInstructions()
{
   bool orderNumberNotNull = (Session["orderNumber"] != null);

   if (orderNumberNotNull)
   {
      throw new InvalidOperationException();
   }
   else
   {
      String orderNumber = "W123456";
      Session["orderNumber"] = orderNumber;
      return "Please enter your order number: " + orderNumber + "\n\n";
   }
}

public string GetOrderStatusForOrderNumber(String s)
{
   bool orderNumberIsNull = (Session["orderNumber"] == null);
   bool retrievedStatus = (Session["retrievedStatus"] != null);

   if (orderNumberIsNull)
   {
      throw new InvalidOperationException();
   }

   else
   {
      if (retrievedStatus)
      {
         throw new InvalidOperationException();
      }
      else
      {
         if (Session["orderNumber"].Equals(s))
         {
            Session["retrievedStatus"] = true;
            return "Your order is being prepared for shipment";
         }
         else
         {
            Session["retrievedStatus"] = true;
            return "Invalid order number...";
         }
      }
   }
}

These additions have returned flow control to your web service. If you compile it now it will run, and you should see a screen that describes the service inside your browser window (Figure 12.3, "OrderStatus as a web service").

Figure 12.3. OrderStatus as a web service

OrderStatus as a web service

You've taken advantage of ASP.NET's scalability in order to create and maintain state for a large number of sessions, but while doing so you have also introduced some serious problems.

For starters, to manage flow control, you are depending on a set of runtime checks that are hidden from the consumer of the service. Also, in this example the order number is shared by both operations (WelcomeInstructions and GetOrderStatusForOrderNumber) and is manipulated as a key/value pair with nonspecific (weak) typing. If that were not enough, the order of operation is determined by testing to see whether the information needed to continue with the request is in place. All in all, this is no way to be writing reliable software.

To make matters worse, you haven't yet dealt with considerations such as threading or process agility. You'll need to be able to resume a workflow after it's been halted for an arbitrary period of time. That means you'll need a listener and a general-purpose runtime that can deal with resumption. Also, you haven't done any work to allow the program to be declared as data in a database or XAML.

And, you know what? Nowadays, that won't be necessary-WF will do the heavy lifting for you. In the rest of this chapter, we'll take a high-level view of the WF tools and toolkit, to provide you with an introduction to what WF can do for you.

Activities

Activities are the fundamental building blocks of WF workflows. As building blocks, they represent the basic steps within a workflow. In essence, a workflow is developed as a tree of activities, where a specific activity makes up an individual unit of execution. You will likely develop your WF solutions by assembling specific activities, which, as a result of their nature as reusable objects, can themselves be compositions of more than one activity.

The two types of WF activities are known as basic activities and composite activities. As its name suggests, a basic activity is custom-coded to provide its function set. It follows, then, that a composite activity is built out of other existing activities (both basic and composite).

A Simple Workflow Application: HelloWorkflow

Let's begin by creating a simple workflow. Open Visual Studio 2008 and choose New Project from the File menu. Select Sequential Workflow Console Application from the list of installed templates, and name the project (of all things) HelloWorkflow (see Figure 12.4, "Creating the HelloWorkflow project").

Having successfully created your project, you should see an empty Sequential Workflow design pane like the one shown in Figure 12.5, "The Sequential Workflow design pane".

You should also see a toolbox pane containing several stock activities. You're going to use some of these activities to create a very simple workflow application. This application will use two Code activities (activities where the workflow will execute some user-provided code) and two Delay activities (activities where the workflow will be suspended for a period of time).

Figure 12.4. Creating the HelloWorkflow project

Creating the HelloWorkflow project

Figure 12.5. The Sequential Workflow design pane

The Sequential Workflow design pane

Adding activities

Drag a Code activity onto the design surface, followed by a Delay activity. Repeat this process one more time, and you will have a sequential workflow that looks like the one in Figure 12.6, "Simple workflow". That was easy enough!

Figure 12.6. Simple workflow

Simple workflow

Implementing the first Code activity

Now you need to implement the first Code activity. See those little exclamation points next to the Code activities? These indicate that there is nothing bound to their ExecuteCode events. To fix this, you need to implement the Code activities. Let's do the first one now.

Double-clicking on codeActivity1 automatically creates a stub method in your Workflow1.cs file and takes you to that method. Add a line to send output to the console. When you are done, the method will look like this:

private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("Hello Workflow!");
}

Next, double-click on codeActivity2, but just leave the method that gets created empty:

private void codeActivity2_ExecuteCode(object sender, EventArgs e)
{

}

At this point, you can run the application. But make sure you are watching very carefully!

What you may (or may not) have seen was a console application come into existence, quickly spit out the message "Hello Workflow," and then quickly disappear into inexistence. No worries-you can fix that by manipulating the Delay activities.

Adjusting the Delay activity's properties

Using the Properties inspector, adjust the TimeoutDuration property for delayActivity1 (see Figure 12.7, "Setting the Delay activity's properties"). You can set it to any amount of time you like, but we have found five seconds to be sufficient. You might like something less, but you probably won't enjoy very much more.

Figure 12.7. Setting the Delay activity's properties

Setting the Delay activity's properties

Completing the workflow

Now, back in Workflow1.cs, add a Console.WriteLine() statement to the existing codeActivity2_ExecuteCode() method:

private void codeActivity2_ExecuteCode(object sender, EventArgs e)
{
    Console.WriteLine("Neat, it waited...");
}

Then, in the Properties inspector, set the TimeoutDuration of delayActivity2 to the same value you used for delayActivity1.

To review, in this simple workflow you have two Code activities and two (probably five-second) delays. Now when you compile and run the application, you should see a console application that looks similar to the one in Figure 12.8, "Simple workflow in action". Et voilà! A simple workflow.

Figure 12.8. Simple workflow in action

Simple workflow in action

A More Sophisticated Workflow Application: WFOrderStatus

In the preceding example, you used some very simple activities from the base activity library that ships with WF. As you begin to explore the library in more detail, you will discover that there are activities for transaction management, local communication, flow control, web services, external event handlers, and a great deal more. In the next application, we will expand our tour of the base library.

Go ahead and create another Sequential Workflow Console Application, and call it WFOrderStatus. In this project you're going to utilize the IfElse activity, in addition to the Code and Delay activities introduced previously, to accomplish what you did programmatically at the beginning of this chapter when you created the OrderStatus web service.

To get started, you need some way of capturing the user's order number. To enable this, drag and drop a Code activity from the toolbox as the first activity in the sequential workflow. Double-click on the resulting codeActivity1 to take you to the code-behind. Here you will implement the following:

private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("Please enter your order tracking number: ");
   OrderNumber = Console.ReadLine();
}

You will also need to add a String called orderNumber inside the Workflow1 class:

public String orderNumber;

Adding the IfElse activity

Returning to the design view, add an IfElse activity to the second position in the workflow. You should now have a sequential workflow that looks very much like the one in Figure 12.9, "IfElse activity added".

Figure 12.9. IfElse activity added

IfElse activity added

The IfElse activity itself is comprised of one or more IfElseBranch activities. These branches will be evaluated from left to right through the branches' Condition properties. You are required to set the Condition property for all but the last branch.

The first branch with a true condition will be the branch that executes. This means that if none of the branches has a true condition, nothing will execute. The one exception to this rule is when the last branch has no Condition property; in this case, it will execute by default.

Adding Code activities for the IfElseBranches

At this point, add two more Code activities, one to each branch of the IfElse activity. In addition, add a five-second Delay below the IfElse activity. Now for some "programming" by pointing and right-clicking.

Declarative rule conditions

Click on ifElseBranchActivity1 (the one on the left side), and go to the Properties window. Here, you will set the Condition property to be a declarative rule condition. After you do that, a little plus sign will appear just to the left of the Condition property. Click on it to expand the property values.

Selecting the ConditionName subproperty and then clicking on the ellipsis ("…") opens up a Select Condition panel. Click on "New" to open the Rule Condition Editor, as seen in Figure 12.10, "The Rule Condition Editor".

Figure 12.10. The Rule Condition Editor

The Rule Condition Editor

Inside the editor, create a constraint that will constitute a rule condition. In this case, you want to see whether the order number provided is the same as the predetermined order number. Therefore, the constraint is this.orderNumber == "W12345".

As you click through the OK sequence to close out these dialogs, you will notice that the condition becomes known as Condition1, and it is previewed for you in the Condition Preview section of the Select Condition pane. Clicking OK here drops you back to the Properties inspector for ifElseBranchActivity1, where you can see that ConditionName is now set to Condition1.

If this IfElseBranch activity is true, it will execute codeActivity2's ExecuteCode() method. Because this is the condition where the user has supplied the correct order number, you want the console application to respond accordingly. Double-click on codeActivity2 and enter the following:

private void codeActivity2_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine(
       "Your order: " +
       orderNumber +
       "is being packaged for shipping!"
   );
}

ifElseBranchActivity2 is the default, so you don't need to set its Condition property. However, you still must go back and double-click on codeActivity3 to add an appropriate message for the hapless customer who enters an invalid order number. The method should look like this:

private void codeActivity3_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine(
       "We're Sorry! Your order: " + OrderNumber +
       " was not found in the system!"
   );
}

Add a Delay activity and set the TimeoutDuration to 00:00:05. Now, running the application should result in a console application that takes input. Provide it with the correct order number, and you will get the expected result. Provide it with an invalid number, and you should get a console screen like the one in Figure 12.11, "Good workflow, bad result!".

Looping with the While activity

What if you wanted to make this a loop, so that customers can enter more than one order number? An easy way to handle this workflow scenario is to add in a While activity. The While activity works in a manner similar to the IfElse activity: it too has a Condition property, which can be set through either a declarative rule or a code condition. A While activity will evaluate this condition prior to each iteration and will continue to run as long as the condition returns true.

Figure 12.11. Good workflow, bad result!

Good workflow, bad result!

To see this in action, drag a While activity into the Sequential Workflow design pane and place it between codeActivity1 and ifElseActivity1. Then drag ifElseActivity1 inside the newly created whileActivity1. You should now have a sequential workflow that looks like Figure 12.12, "IfElse inside a While activity".

Next, add the following bool variable to the top of your partial class in the Workflow1.cs code-behind:

bool keepGoing = true;

This variable will allow you to continue the While activity until it is no longer necessary. Also, since you know that codeActivity2's ExecuteCode() method will be executed when the user enters the right order number, you can use that method to set keepGoing to false as follows:

private void codeActivity2_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("Your order: " + orderNumber +
       " is being packaged for shipping!");
   keepGoing = false;
}

If the user enters an invalid order number, you'll need to let her know that she must re-enter the order number. Thus, you'll also need to modify the code-behind for codeActivity3's ExecuteCode() method, as shown here:

private void codeActivity3_ExecuteCode(object sender, EventArgs e)
{
   Console.WriteLine("We're Sorry! Your order: " + orderNumber +
       " was not found in the system!");
   Console.WriteLine("Please re-enter your order tracking number: ");
   orderNumber = Console.ReadLine();
}

Figure 12.12. IfElse inside a While activity

IfElse inside a While activity

The last thing you need to do is set whileActivity1's Condition property. You'll do that the same way you set the IfElseBranchActivity's Condition properties: simply set the declarative rule condition to keepGoing.

The complete listing of Workflow1.cs should be as follows:

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;

using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;

namespace WFOrderStatus
{
   public sealed partial class Workflow1: SequentialWorkflowActivity
   {
      public String OrderNumber;
      bool keepGoing = true;

      public Workflow1()
      {
         InitializeComponent();
      }

      private void codeActivity1_ExecuteCode(object sender, EventArgs e)
      {
         Console.WriteLine("Please enter your order tracking number: ");
         OrderNumber = Console.ReadLine();
      }

      private void codeActivity2_ExecuteCode(object sender, EventArgs e)
      {
         Console.WriteLine("Your order: " +OrderNumber +
             "is being packaged for shipping!");
         keepGoing = false;
      }

      private void codeActivity3_ExecuteCode(object sender, EventArgs e)
      {
         Console.WriteLine("We're Sorry! Your order: " + OrderNumber +
             " was not found in the system!");
         Console.WriteLine("Please re-enter your order tracking number: ");
         OrderNumber = Console.ReadLine();
      }
   }
}

With these simple changes, you have created an application that will continue prompting the user indefinitely until the correct order number is provided. When run, it should look like the application in Figure 12.13, "Application running with the While activity".

As you have just seen, the primary building block of any workflow solution is the activity. The workflow is defined by the activities in it, and by the steps and tasks included in the activities. WF ships with many more stock activities than we have included in our examples so far; we'll introduce many of these activities in the next chapter.

Figure 12.13. Application running with the While activity

Application running with the While activity

Custom Activities

If you've been developing software for a long time, you probably already know that it's not usually possible to find a complete out-of-the-box solution that meets all the needs of a particular domain. Fortunately, WF allows you to develop custom activities that extend the functionality of the base activity classes. Even better, because the custom activities you write all derive (ultimately) from the base Activity class, Microsoft's workflow engine will make no distinction between your custom activities and the base class activities.

A powerful application of custom activities might be using them to create domain-specific languages for constructing workflow solutions. This is consistent with Microsoft's goal of creating an environment where the domain expert can assemble a solution using workflow activities without having to know a great deal about programming. The ability to create meaningful activities with domain-specific names should make communications between software engineers and business experts much more robust.

Imagine a scenario where a developer for a Human Resources department is assembling a workflow solution with her manager. Having an HR Manager deal with building blocks like BeginOnlineInterview and SendOnlineInterviewResultsToHiringManagers as opposed to WebServiceInput and WebServiceOutput will make things a lot easier when design conversations are ongoing. Activity names that make sense to the nontechnical domain expert and the software solutions expert allow for better collaboration and more productive results.

All running workflow instances are created and maintained by an in-process runtime engine commonly referred to as the workflow runtime engine. Accordingly, you might have several workflow runtime engines within an application domain, and each instance of the runtime engine can support multiple workflow instances, all running concurrently.

After a workflow model is compiled, it can be executed inside any Windows process (from console applications to web services). The workflow is hosted in-process, so it can easily communicate with its host application. As you can see in Figure 12.14, "The host process", workflows, activities, and the runtime engine are all hosted inside a process on an application host.

Figure 12.14. The host process

The host process

WF includes classes to provide some important services, such as making workflows executable, schedulable, transactional, and persistent. We'll explore some of these services in greater detail in Chapter 13, Applying WF: Building a State Machine; for now, this section will provide a quick overview.

As discussed earlier, in order for a workflow to be executable it needs a runtime. Runtime services are provided by the WorkflowRuntime class. You can initialize a runtime by calling new WorkflowRuntime(). Through WorkflowRuntime's AddService() method, you can make one or more services available to the runtime.

Once you have a new instance of the WorkflowRuntime and you have called StartRuntime(), you begin the process that allows you to execute your workflow activities. The call to CreateWorkflow() returns an instantiated WorkflowInstance. You call that object's Start() method to begin the execution of the activities in your workflow, which continues until either the workflow is complete or an exception occurs. In both cases termination of the workflow is the end result, as depicted in Figure 12.15, "Windows Workflow in action".

Figure 12.15. Windows Workflow in action

Windows Workflow in action

When it comes to scheduling services, you have two out-of-the-box options: the DefaultWorkflowSchedulerService class asynchronously creates the new threads necessary to execute workflows without blocking any application threads, and the ManualWorkflowSchedulerService class is available when you can spare some threads from the host application and you are not worried about synchronous execution on a single thread (or the reduction in scalability this can cause). As always, you can create and define your own scheduling service if these built-in mechanisms do not suit your needs.

If you have a requirement to maintain the internal state of a workflow, you might turn to the transaction services provided by the DefaultWorkflowTransactionService class. The DefaultWorkflowTransactionService class allows you to maintain the internal state in a durable store like SQL Server or some other relational database. As you might expect, the activities running inside a workflow instance, as well as the services connected to the same instance, will be able to share the same context for the transactions.

Persistence services are accomplished through the SQLWorkflowPersistenceService class. These services allow you to save the state of the workflow in a SQL Server database. If you have a long-running workflow, persistence will clearly be a requirement. Obviously, it isn't the optimal strategy to have a workflow dependent on persisting in memory for more than a few hours. Persistent storage allows you to pick up where you left off at any point in the future.

Monitoring and recording information about a given workflow is accomplished through the SQLTrackingService class. Tracking services utilize a tracking profile to tell the runtime about relevant information with respect to the workflow. Once the service has initiated a profile, it can open the tracking channel to receive data and events. Although the runtime does not start a tracking service as default behavior, you can configure a tracking service to help monitor service activity programmatically or through application configuration.

Show:
© 2014 Microsoft