The preceding chapter introduced you to workflows and Windows Workflow Foundation. In this chapter, you get your hands dirty and develop your first real workflow. What better way to get acclimated to Windows Workflow Foundation than through a Hello World example?
To follow along with the examples throughout the book, you need the following installed on your system:
The .NET Framework 3.0 - Because Windows Workflow Foundation is built on top of this, you need the .NET Framework 3.0 installed for runtime functionality.
Windows Vista comes with the .NET 3.0 Framework out of the box; however, you should ensure that this option is installed on your machine. If you are running an operating system other than Windows Vista, you need to install the .NET Framework 3.0. If you are running Windows XP Service Pack (SP2) or Windows 2003 Server, you are good to go; otherwise, you need to check the requirements for which operating systems the .NET 3.0 Framework supports.
Visual Studio 2005 (any edition) - Required for workflow development.
Windows Workflow Foundation extensions for Visual Studio 2005 - Available for download from Microsoft.
The main objective of this exercise is to provide a high-level view of developing workflows using Windows Workflow Foundation. The example focuses on a console application that passes someone’s first name to a workflow, which in turn generates a personalized message. This message is passed back to the calling application and displayed.
This exercise also introduces you to the development environment for Windows Workflow Foundation in Visual Studio 2005.
To get going with the example, launch Visual Studio 2005. As with any other type of Visual Studio solution, the first step is to create a new, blank project. Just like the templates for ASP.NET websites and Windows Forms projects, Windows Workflow Foundation has its own set of project types.
To create the project for this example, select File New Project. In the New Project dialog box that appears is a Workflows section under Visual C#. Don’t worry too much about studying this screen. The Visual Studio 2005 environment is discussed in detail in Chapter 4. Next, select Sequential Workflow Console Application, as shown in Figure 2-1, and name the project HelloWorld.
Figure 2-1
After you create the new project, your development environment should look similar to Figure 2-2.
Figure 2-2
If the Solution Explorer is not displayed, select View Solution Explorer so that you can examine the files within the project. The Workflow1.cs file is the workflow itself, and the Program.cs file contains the .NET code that starts the application. The following sections discuss both files.
Open the workflow by double-clicking Workflow1.cs in the Solution Explorer if it is not already open. The document is labeled Workflow1.cs [Design] in the document view area of Visual Studio.
Just as with ASP.NET web forms, you can use a code-beside model to create workflows. Code-beside is a way for developers to keep the presentation or design separate from code. The code-beside and other workflow-development models are covered in Chapter 4.
To see the .NET code that is behind the workflow, right-click Workflow1.cs in the Solution Explorer and select View Code. The following code is displayed:
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Collections; using System.Drawing; 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 HelloWorld { public sealed partial class Workflow1: SequentialWorkflowActivity { public Workflow1() { InitializeComponent(); } } }
Because you are passing a string representation of someone’s name to the workflow to create a personalized message, you need to add some fields and properties to this file. In the Workflow1 class, modify the code to look like the following:
namespace HelloWorld { public sealed partial class Workflow1: SequentialWorkflowActivity { private string firstName; private string message; public string FirstName { set { firstName = value; } } public string Message { get { return message; } } public Workflow1() { InitializeComponent(); } } }
These fields and properties pass data to and from the calling console application and the workflow itself. Notice that the FirstName property has only a set accessor. This is because you are passing the first name to the workflow. The opposite applies to the Message property; you are exposing this to the world outside the workflow, so it needs a get accessor.
Switch back to the design view for Workflow1. If the Toolbox is not currently displayed in Visual Studio, select View Toolbox. Next, drag the Code component from the Windows Workflow section of the Toolbox (see Figure 2-3) onto the design surface of the workflow. Drag it between the green circle at the top and the red octagon at the bottom. (These components, called activities, are discussed in Chapter 6.)
Figure 2-3
Next, rename the Code activity something more meaningful. Do this by selecting the Code activity with your mouse on the design surface and pressing F4 on your keyboard to display the properties available for the activity. Then change the (Name) property to createMessage. A red circle with an exclamation point in it appears in the upper-right corner of the Code activity. This indicates a problem. Click the red icon to display the Code activity error message and find out what’s wrong (see Figure 2-4).
Figure 2-4
This message tells you that an action required for the Code activity has not yet been performed. In this case, the ExecuteCode property has not been set.
To remedy this, you need to wire a handler to the ExecuteCode event (the same way you wire an event handler in ASP.NET or Windows Forms with Visual Studio). Select the Code activity so that its properties are displayed in the Properties window. Next, switch to the events view in the Properties window by clicking the lightning-bolt icon. This displays the only event for the Code activity: ExecuteCode. To wire an event handler automatically, simply double-click the empty text area to the right of the ExecuteCode event label. This creates a method in the Workflow1.cs code-beside file and wires it to the event. The events view of the Properties window should now look like Figure 2-5.
Figure 2-5
The next step is to create the message to be returned from the workflow. Modify the createMessage_ExecuteCode method to look like the following code:
private void createMessage_ExecuteCode(object sender, EventArgs e) { message = "Hello " + firstName + "!"; }
This code simply sets the class field, message, with a personalized hello greeting. Remember, the message field is exposed through the public Message property. This allows the calling application to read the value set within the workflow.
The purpose of the console application is to act as a host for the workflow. Hosting, an essential part of Windows Workflow Foundation, is discussed in Chapter 5.
Open the Program.cs file by double-clicking it in the Solution Explorer. This file contains the following code, which kicks off the console application and then starts the workflow:
using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Workflow.Runtime; using System.Workflow.Runtime.Hosting; using WorkflowConsoleApplication1.Properties; namespace HelloWorld { class Program { static void Main(string[] args) { 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(HelloWorld.Workflow1)); instance.Start(); waitHandle.WaitOne(); } } }
You need to modify this code so that the personalized greeting that was generated in the workflow can be accessed and displayed. To do this, you create a string variable called message and then modify the WorkflowCompleted event handler to retrieve the message from the WorkflowCompletedEventArgs instance.
The event handler for WorkflowCompleted is an anonymous method. Anonymous methods are features of C# that enable developers to create inline code that typically exists in a method. This feature is usually used with small amounts of code.
To learn more about the features of C#, check out Professional C# 2005 (available at www.wrox.com).
To retrieve the message from the workflow, you use the OutputParameters property in the Workflow CompletedEventArgs instance passed to the event handler . OutputParameters is a Dictionary <string, object> object, so you need to supply it with a string key. The OutputParameters property is called a generic. Like anonymous methods, generics are specific features of C# that are not covered in detail here. At a high level, generics provide a way for developers to use strong typing with collections and other types that may normally be loosely typed. This is a very powerful feature of C# 2.0.
To retrieve the message from the dictionary, use the name of the public property from the workflow as the key. In this case, the key string passed is Message. Because the dictionary returns an object, you need to cast it to a string when setting the message member, as shown in the following code:
static void Main(string[] args) { WorkflowRuntime workflowRuntime = new WorkflowRuntime(); AutoResetEvent waitHandle = new AutoResetEvent(false); // a variable to hold the message from the workflow string message = String.Empty; workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e) { // the workflow is done, get the message from // the output parameters dictionary message = (string)e.OutputParameters["Message"]; waitHandle.Set(); };
The next step is to write the code that passes a person’s first name to the workflow. To do this, you use a Dictionary<string, object> collection, just as you did with the output parameters. After you create this input parameters object, you use the Add method to add the first name to the dictionary. Because the first-name parameter is set to the workflow’s public FirstName property, you must use the same spelling and case for the key when you add it to the dictionary. Then you need to modify the line of code that creates the WorkflowInstance object to pass the parameters dictionary. Here’s how all of this works:
... workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e) { Console.WriteLine(e.Exception.Message); waitHandle.Set(); }; // create a dictionary for input parameters Dictionary<string, object> inParms = new Dictionary<string, object>(); // add a first name to the parms list inParms.Add("FirstName", "Todd"); WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(HelloWorld.Workflow1), inParms); instance.Start(); ...
The final step for this first example is to display the message in the console’s output. Add the Console.WriteLine() call, as follows:
... instance.Start(); waitHandle.WaitOne(); // write the message to the console Console.WriteLine(message);
The example is now ready to run. To test it, press F5. The code should compile, and a console window should appear with the output (see Figure 2-6).
Figure 2-6
Congratulations - you’ve just developed your first Windows Workflow Foundation workflow!
Although the previous example may have been a nice crash course in developing with Windows Workflow Foundation, it was pretty boring. This section expands on the first example and lets you do something a little more complex.
In the first example, the calling application is forced to pass a name to the workflow to receive the message. To spice things up a bit, this example uses some decision-making logic to generate the message based on whether or not a name was passed in.
To achieve this functionality, you again tap the power of the Visual Studio Toolbox. This time, locate the IfElse activity (see Figure 2-7) and drag it on to the workflow surface above the existing Code activity.
Figure 2-7
The IfElse activity can have any number of branches that are executed based on an expression that returns either true or false. The final branch on an IfElse activity does not need an expression because it can act as the else case just as in traditional programming. Figure 2-8 shows what the IfElse activity looks like before configuration.
Figure 2-8
Just as with the Code activity in the previous example, the IfElse activity is warning the developer that something is not quite right and needs to be fixed before proceeding. Again, the error is indicated by the red exclamation point. In this case, the Condition property of the first IfElse branch activity has not been set.
To fix the issue, you need to supply a condition so the IfElse activity is able to make decisions. The IfElse activity is covered in detail in Chapter 6, so there is not a detailed discussion here about how it works. However, you need to have a basic understanding of this activity to follow this example.
To provide a condition for the IfElse activity, you first need to write a method with a predefined signature in the code-beside file. Here is the method skeleton:
private void HaveFirstName(object sender, ConditionalEventArgs e) { }
Notice the method’s name is HaveFirstName because that is what the workflow checks for when making its decision.
Next, finish the HaveFirstName method by adding the following Boolean expression:
private void HaveFirstName(object sender, ConditionalEventArgs e) { if (!String.IsNullOrEmpty(this.firstName)) { e.Result = true; } }
Here, the Result property of the ConditionalEventArgs instance is being set to true only when the firstName member contains a value. (The Result property is false by default.) The IfElse activity then uses the value set in this method to determine whether or not to execute a given conditional branch.
Now switch back to the workflow’s design view. Select the first conditional branch, currently called ifElseBranchActivity1, on the left side of the IfElse activity so that its properties are displayed. Your current objective is to provide a condition to the branch. To do this, select the Condition property from the properties grid and select System.Workflow.Activities.CodeCondition from the drop-down list. Selecting this condition type enables you to provide a method with the same signature as the method you just created.
Expand the Condition property by clicking the plus symbol. A subproperty, also called Condition, displays a drop-down list from which you can select the HaveFirstName method. When this branch is evaluated during execution, it uses the logic that exists in HaveFirstName. Because this branch provides the same functionality as the first example, the existing Code activity needs to be placed in this branch. Drag and drop the createMessage Code activity from below the IfElse activity to the Drop Activities Here branch on the left.
Next, you need to place a new Code activity in the ifElseBranchActivity2 branch on the right. This activity produces a message that doesn’t depend on the existence of a name.
Rename the new Code activity to createMessage2, and wire a handler to its ExecuteCode event as you did in the previous example. The code in the event handler should look like the following:
private void createMessage2_ExecuteCode(object sender, EventArgs e) { message = "Hello world!"; }
As you can see, the message member is simply set to a static Hello World! message.
Now switch to the workflow’s design view. Performing a little cleanup on the activities’ names might be a good idea at this point to make the workflow a little more readable. Make the following naming changes:
IfElseActivity1 - HaveName
IfElseBranchActivity1 - Yes
IfElseBranchActivity2 - No
The finished workflow should look like Figure 2-9.
Figure 2-9
This workflow produces the same result as before: a personalized message. This is because the calling application is still passing in a name to the workflow. To test the new logic, comment the line of code in the console application that adds the FirstName parameter to the inParms object as follows:
// inParms.Add("FirstName", "Todd");
This produces a screen that looks like Figure 2-10.
Figure 2-10