Windows Workflow Tutorial

  • May 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Windows Workflow Tutorial as PDF for free.

More details

  • Words: 34,162
  • Pages: 117
Windows Workflow Tutorial: Introduction to State Machine Workflows Introduction

Workflows model business processes. When you design a workflow, your first task is to identify the steps that occur during the business process. That is true whether the business process is the processing of an order, the calculation of a bonus payment or the processing of a loan application. The business process consists of steps and your job is to define those. Once you have the steps defined, you can use Windows Workflow Foundation (WF) to build a workflow that models the business process. You can build two types of workflows with WF: sequential and state machine workflows. A sequential workflow provides a structured series of steps in which one activity leads to another, and steps generally occur immediately one after another. A step might wait for some event (an email to arrive, for example), but sequential workflows are often used to model processes that operate without human intervention. A state machine workflow provides a set of states. The workflow begins in an initial state and ends when it reaches the completed state. Transitions between the states define the behavior. In general, state machine workflows react to events. The occurrence of an event causes the workflow to transition to another state. Whether you build a workflow as a sequential or state machine workflow depends on how the business manages the process. For example, suppose you need to build a workflow that models processing an order. You determine the following steps are involved: •

The business receives the order, including all necessary information (customer, product, quantity, credit card information, shipping address, etc).



The business checks if the customer has sufficient credit. If so, the workflow continues. If not, the order is canceled and the workflow ends.



The business checks if there is sufficient inventory to fulfill the order. If so, the process continues. If not, the order is canceled and the process ends.



The business asks the credit card company for payment. If the credit card company approves, the process continues. If not, the order is canceled and the process ends.



The item ships to the customer. The order is marked as fulfilled and the process ends.

A sequential workflow is likely the appropriate type of workflow here. The workflow starts when the company receives an order. It then continues through a number of steps until the order is either canceled as fulfilled. The workflow runs from start to finish with no delays. As an alternative, suppose you determine the following steps are involved in processing an order: •

The business receives the order, including all necessary information (customer, product, quantity, credit card information, shipping address, etc).



The business checks if the customer has sufficient credit. If so, the process continues. If not, the order is canceled and the workflow ends.



The business checks if there is sufficient inventory to fulfill the order. If so, the process continues. If not, the order is canceled and the process ends.



If the product is in stock, the process waits for shipping to ship the product.



Just before shipping the product, the business asks the credit card company for payment. If the credit card company approves, the process continues. If not, the order is canceled and the process ends.



After the product ships, the process waits for the customer to acknowledge receiving it. If the product arrives, the process ends. If the customer does not receive the product, the process waits for shipping to resend it. At this point, either the business or the customer can cancel the order, ending the process.

You can successfully implement this business process either as a sequential workflow or as a state machine workflow. To decide, think about what makes it different from the previous scenario. There are several places in the business process where the process needs to pause and wait for some other process to begin. The pause may be short. It may also be long. It could take weeks or months for new inventory to arrive if the product is out of stock. It hopefully only takes days, not weeks, for shipping to send the product. It will take anywhere from one 1

day to ten days for the product to arrive to the customer. During these periods, there is nothing for the business process to do except wait. One of the main benefits of the state machine workflow is the ability to define states and to define how the workflow moves from state to state. You can define multiple paths through the workflow. If the product is in stock, the workflow can take the following path: Waiting For Shipping -> Waiting For Acknowledgement -> Completed

If the product does not arrive, the workflow can take the following path: Waiting For Shipping -> Waiting For Acknowledgement -> Waiting For Shipping -> Waiting For Acknowledgement -> Completed

You can easily include looping and re-execution of states in a state machine workflow. It is difficult, and potentially not possible, to do this in a sequential workflow. A sequential workflow moves to the next activity when it is finished executing the previous activity. A state machine workflow typically moves to a different state when an external action occurs. This external action can be the host application raising an event handled by the workflow. The action can also be the host application programmatically setting the next state. You can also use the SetState activity in the workflow to move to a new state. Create a State Machine Workflow

To get started, in Visual Studio 2008 select File | New | Project to display the New Project dialog box. In the list of project types, select Workflow, displaying the list of templates shown in Figure 1.

Figure 1. Visual Studio 2008 provides these workflow templates. For this demonstration, select the State Machine Workflow Console Application template, name your application StateMachineDemo1, and select an appropriate folder for the project. Click OK to create the project. At this point, the workflow contains a single activity, named Workflow1InitialState, as shown in Figure 2.

2

Figure 2. A new state machine workflow contains a single activity. A state machine workflow contains workflow activities, as does a sequential workflow. You will find that you use workflow activities such as Code, While, IfElse and Sequence in the same manner regardless of the type of workflow you build. There are four activities unique to state machine workflows. These are the State, SetState, StateInitialization and StateFinalization activities (see Figure 3).

Figure 3. These activities are specific to state machine workflows. The State activity is the main activity you will use in a state machine workflow. Each State activity in a workflow represents a point where the business process waits for something to happen. WF requires that your state machine workflows have one state marked as the initial state. Although it is not required, you should also mark one state as the completed state. When the workflow starts, the workflow starts with the state marked as the initial state. You can visually identify the initial state by a green circle in the 3

activity’s title bar. When the workflow reaches the completed state, the workflow will end. You can visually identify the completed state by a red circle in the activity’s title bar. Define the Workflow States

Select the Workflow1InitialState activity and change its name to Started. Notice that the green circle disappears from the title bar. To mark this activity as the initial state, right click on it and select Set as Initial State. The green circle reappears. Note that you can also set the InitialStateName property of the workflow to Started. To add another state to the workflow, drag a State activity from the toolbox onto the design surface. Name the activity Working. Now add the final state to the workflow, naming it Finished. To mark it as the completed state, right click on it and select Set as Completed State. You should now see a red circle in the activity’s title bar. Note that you can also set the CompletedStateName property of the workflow to Finished. The workflow designer now looks like Figure 4. (The Started activity has been widened to see all of the text.)

Figure 4. The state machine workflow contains an initial state, a completed state and an additional state. The next step in building this workflow is to specify, for each state, what state or states the workflow can move to next and what happens while the workflow is in that state, including what happens to move it to another state. State Transitions: Moving from State to State

If this were a sequential workflow, the workflow would execute the activity at the top of the workflow (Started) and then move to the next activity (Working) and then move to the final activity (Finished) and then end. Sequential workflows execute from top to bottom. You set Started as the initial state so when this workflow starts, it will start in the Started state. You set Finished as the completed state so when the workflow gets to the Finished state it will end.

4

How does the workflow move off the Started state? Does it go to Working or Finished next? If it goes to the Working state, how does it move off Started? When it moves off Working, can it go back to Started or can it only go to Finished? To move the workflow to a new state, you use the SetState activity. To specify the next state, you set the TargetStateName property of the SetState activity to that state. Define the Activities that Occur in Each State

State machine workflows move from state to state. While they are in each state (except for the completed state), they can perform actions, wait for an external event to occur and transition to another state. You can add four activities to a State activity: •

The StateInitialization activity, if it exists, is the first activity the workflow executes when it enters a state. This is a Sequence activity, so you can add to it all of the activities you want to execute when the workflow enters that state. You might log the date and time. You might query a database. You could add a SetState activity to a StateInitialization activity if you want the workflow to move to a new state after performing the logging and querying.



In a state machine workflow, the workflow will enter a state and stay there until something happens. This is the essence of a state machine workflow. That something could be internal to the workflow or it could be an external event. You handle external events using an EventDriven activity. It is also a Sequence activity, so you can add to it all of the activities you want to execute when the host raises the event. One of these activities can be a SetState activity.



The StateFinalization activity, if it exists, is the last activity the workflow executes as it leaves a state. It is also a Sequence activity, so you can add to it all of the activities you want to execute before the workflow moves to a new state. You might log the date and time, query a database or clean up resources.



You can add a State activity within a State activity. This creates a hierarchical state machine workflow. You might do this if you had a number of states and each of them reacted to the same events. Rather than set up the events for each state, you could set up the event for one state and then add the other states to that state. The child states would inherit the event driven behavior of the parent state. This is beyond the scope of this tutorial.

Complete the Workflow

You will now finish this workflow. You will define what happens in each state and how the workflow moves from state to state. From the toolbox, drag a StateInitialization activity into the Started activity. stateInitializationActivity1 is a Sequence activity. To add activities to it, double click on it. The workflow designer looks like Figure 5.

Figure 5. The StateInitialization activity is a Sequence activity. Notice the navigation list, or “breadcrumb” at the top left in the designer. This will enable you to return to the main workflow view when you are finished adding activities to the StateInitialization activity.

5

Drag a Code activity from the toolbox into stateInitializationActivity1. Double-click codeActivity1, creating the activity’s ExecuteCode event handler. In the codeActivity1_ExecuteCode method, add the following code to display the order details: Visual Basic

Console.WriteLine("The workflow has started" & vbCrLf) C#

Console.WriteLine("The workflow has started\n");

Select View | Designer to return to the workflow designer. Drag a SetState activity from the toolbox into stateInitializationActivity1 below codeActivity1. Select Working from the drop-down list associated with the TargetStateName property. The workflow now looks like Figure 6.

Figure 6. The SetState activity transitions the workflow to another state. Select the link for Started or Workflow1 in the navigation menu. This returns you to the main workflow view (see Figure 7). Notice that there is now a line connecting Started to Working. This shows you the flow of the workflow from the initial state to the next state.

6

Figure 7. The line indicates the flow of the workflow from state to state. Handle External Events

So far, this workflow executes in a relatively sequential fashion. The workflow starts and then, after displaying a message, moves to the next state. That scenario doesn’t appear very stateful, does it? As mentioned previously, the typical scenario for a state machine workflow it enters a state and waits for the host application to raise an event. You handle external events using an EventDriven activity. You will then decide what actions the workflow takes, including what state it moves to next. From the toolbox, drag an EventDriven activity into Working. Select eventDrivenActivity1. Hover the mouse over the middle square on the left or right border of the activity. The cursor should change to a crosshair (see Figure 8). Hold down the mouse button. Drag the crosshair and drop it on Finished (see Figure 9). There will then be a line connecting the two states.

Figure 8. Hover the mouse over the activity and wait for the cursor to change to a crosshair.

7

Figure 9. Drag an EventDriven activity from one state to another to define a transition. Double click eventDrivenActivity1. You can see that it is a container activity and already includes a SetState activity. When you dragged the crosshair from Working to Finished, Visual Studio added a SetState activity to eventDrivenActivity1 and set its TargetStateName property to Finished (see Figure 10).

Figure 10. The EventDriven activity contains a SetState activity. At this point, the EventDriven activity indicates an error. The first activity in an EventDriven activity must implement the IEventActivity interface. This interface enables activities to subscribe to events. The HandleExternalEvent and Delay activities implement this interface. Use the HandleExternalEvent activity when the host application raises an event. To do this, you first need to create an interface that defines the events the host can raise. You also need to create a class that defines the information the host will pass to the workflow when it raises events. You then associate the HandleExternalEvent activity with an event. This is too advanced for this tutorial, but it will be covered in a later tutorial. For this workflow, you will use a Delay activity and simply pause the workflow for a few seconds. Although this is not a realistic scenario, it serves the purpose here. From the toolbox, drag a Delay activity into eventDrivenActivity1 above setStateActivity2. Set the TimeoutDuration property of delayActivity1 to five seconds. 8

Drag a Code activity from the toolbox into eventDrivenActivity1 between the two activities currently in there. Double-click codeActivity2 and add the following code to the activity’s ExecuteCode event handler: Visual Basic

Console.WriteLine("Moving on now" & vbCrLf) C#

Console.WriteLine("Moving on now\n");

Select View | Designer to return to the workflow designer. Select the link for Workflow1 or Working in the upper left of the workflow designer. From the toolbox, drag a StateFinalization activity into Working below eventDrivenActivity1. To add activities to stateFinalizationActivity1, double click on it. Drag a Code activity from the toolbox into stateFinalizationActivity1. Double-click codeActivity3, creating the activity’s ExecuteCode event handler. In the codeActivity3_ExecuteCode method, add the following code: Visual Basic

Console.WriteLine("Transitioning to Finished" & vbCrLf) C#

Console.WriteLine("Transitioning to Finished\n");

Select View | Designer to return to the workflow designer. Select the link for Workflow1 or Working in the upper left of the workflow designer. The workflow should look like Figure 11.

Figure 11. The workflow with three states and the transitions between them. Call the Workflow from the Console Application

To review the code that actually does the work starting up your workflow, in the Solution Explorer window, double-click Module1.vb or Program.cs. There, you’ll find the following code: Visual Basic

Using workflowRuntime As New WorkflowRuntime()

9

' Code removed here… Dim workflowInstance As WorkflowInstance   workflowInstance = _     workflowRuntime.CreateWorkflow(GetType(Workflow1))   workflowInstance.Start()   ' Code removed here… End Using C#

using (WorkflowRuntime workflowRuntime = new WorkflowRuntime()) {   // Code removed here…   WorkflowInstance instance = workflowRuntime.CreateWorkflow(     typeof(StateMachineDemo1.Workflow1));   instance.Start();   // Code removed here… }

This code, which runs as your application loads, starts by creating a new instance of the WorkflowRuntime class. This class provides an execution environment for workflows. The WorkflowRuntime instance creates an instance of the state machine workflow. Save and press Ctrl + F5 to run your project. If you’ve followed the directions carefully, you should first see the following output: Output

The workflow has started There should then be a five second delay before you see the following output: Moving on now Transitioning to Finished Press any key to continue... Press any key to exit the application. Build a More Useful Workflow

This workflow demonstrates the basics of building a state machine workflow, but it doesn’t really do much. For the remainder of this tutorial, you’ll modify the workflow to model an order processing workflow. When the business receives an order, the host application will start the workflow, passing to it the order information. The workflow will check if there is sufficient inventory to process the order. If not, the order is canceled. If the item is in stock, the workflow will wait for the order to ship. After the order ships, the workflow ends. Change the name of the Working state to WaitingForShipping. The workflow enters this state and waits for something, so why not identify what it is waiting for in the name? Next, you will add properties to the workflow so it can receive order information. Select View | Code. Outside the methods, but inside the Workflow1 class, add the following properties. You’ll use these to keep track of order details, as well as the status of the inventory check: Visual Basic

10

Private productIDValue As Integer Public Property ProductID() As Integer   Get     Return productIDValue   End Get   Set(ByVal value As Integer)     productIDValue = value   End Set End Property Private quantityValue As Integer Public Property Quantity() As Integer   Get     Return quantityValue   End Get   Set(ByVal value As Integer)     quantityValue = value   End Set End Property Public inStock As Boolean = False C#

public int ProductID { get; set; } public int Quantity { get; set; } public bool inStock = false;

Change the code in the codeActivity1_ExecuteCode method to display the order details and check if the item is in stock: Visual Basic

Console.WriteLine("Order received") Console.WriteLine("

Product ID: {0}", Me.ProductID)

Console.WriteLine("

Quantity: {0}" & vbCrLf, Me.Quantity)

inStock = Me.Quantity <= 10 C#

Console.WriteLine("Order received"); Console.WriteLine("

Product ID: {0}", this.ProductID);

Console.WriteLine("

Quantity: {0}\n", this.Quantity);

inStock = (this.Quantity <= 10);

In a production workflow, you would likely query a database to see if the item is in stock. For this tutorial, a quantity of greater than ten will exceed the amount of the item in stock. Select View | Designer to return to the workflow designer. Double click stateInitializationActivity1 to add additional activities. 11

Notice that setStateActivity1 indicates an error. This is because you changed the name of the state to which it transitions. To fix this, change the TargetStateName property to WaitingForShipping. Drag an IfElse activity from the toolbox into the stateInitializationActivity1activity below codeActivity1 but above setStateActivity1. The workflow should now look like Figure 12.

Figure 12. The stateInitializationActivity1activity should now contain these activities. The IfElse activity enables branching in a workflow. It contains one or more IfElseBranch activities. At runtime, the workflow evaluates the Condition property of the first branch. If the condition evaluates to true, the activities in that branch execute. If the condition evaluates to false, the workflow evaluates the condition of the next branch, if there is one. If none of the branch conditions evaluates to true, the activities in the final branch execute (unless that activity has a condition that evaluates to false). At this point, the first IfElseBranch activity indicates an error. You haven’t told it yet what condition to evaluate. To do that, select ifElseBranchActivity1. In the Properties window, find the Condition property, select the dropdown arrow to the right of the property’s value, and select Declarative Rule Condition (see Figure 13). You have the option of either defining a rule in the Properties window (Declarative Rule Condition) or creating a condition in code (Code Condition). For this tutorial, you’ll create the condition in the Properties window. 12

Figure 13. Choose the type of condition you want to use. Click the “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to itemIsInStock. (Naming the condition allows you to use the same condition in a different place within your workflow.) Click the Expression property and then click the ellipsis to the right of that property. Enter the condition shown in Figure 14. Click OK to close the editor.

Figure 14. Create the IfElseBranch activity’s rule condition. Drag setStateActivity1 into ifElseBranchActivity1. You have now set up the workflow to transition to the WaitingForShipping state if the item is in stock. You will next tell the workflow what to do if the item is not in stock. Drag a Code activity from the toolbox into the ifElseBranchActivity2 activity. Double-click codeActivity4 and add the following code to the activity’s ExecuteCode event handler: Visual Basic

Console.WriteLine("The item is not in stock") Console.WriteLine("The order is canceled" & vbCrLf) C#

Console.WriteLine("The item is not in stock"); Console.WriteLine("The order is canceled\n");

13

Select View | Designer to return to the workflow designer. Drag a SetState activity from the toolbox into ifElseBranchActivity2 below codeActivity4. Set the TargetStateName property of setStateActivity3 to Finished. The workflow should now look like Figure 15.

Figure 15. The workflow executes these activities when it starts. To recap, if the item is in stock, the workflow transitions to the WaitingForShipping state. If the item is not in stock, the workflow transitions to the Finished state and ends. Select the link for Workflow1 or Started in the upper left of the workflow designer. Notice that there is now a line from Started to Finished (see Figure 16). The workflow takes this path if the item is not in stock.

14

Figure 16. The lines indicate the updated flow of the workflow from state to state. Next, change the messages the workflow displays. Select View | Code. Change the code in the codeActivity2_ExecuteCode method to indicate the workflow is waiting for the item to ship: Visual Basic

Console.WriteLine("The item has shipped" & vbCrLf) C#

Console.WriteLine("The item has shipped\n");

Change the code in the codeActivity3_ExecuteCode method to indicate the item has shipped: Visual Basic

Console.WriteLine("The order is complete" & vbCrLf) C#

Console.WriteLine("The order is complete\n");

Your final step will be to modify the console host application to pass order information to the workflow. In the Solution Explorer, double click Module1.vb or Program.cs. Add the following code to the Main method, replacing the existing line of code that calls the CreateWorkflow method: Visual Basic

Dim parameters As New Dictionary(Of String, Object) parameters.Add("ProductID", 1) parameters.Add("Quantity", 10) workflowInstance = workflowRuntime.CreateWorkflow( _   GetType(Workflow1), parameters) C#

var parameters = new Dictionary<string, object>();

15

parameters.Add("ProductID", 1); parameters.Add("Quantity", 10); WorkflowInstance instance = workflowRuntime.CreateWorkflow( typeof(StateMachineDemo1.Workflow1), parameters);

Save and press Ctrl + F5 to run your project. You should first see the following output: Output

Order received Product ID: 1 Quantity: 10

There should then be a five second delay before you see the following output: Output

The item has shipped The order is complete Press any key to continue... Press any key to exit the application.

To see what happens if the item is not in stock, make the following modification to the Main method: Visual Basic

parameters.Add("Quantity", 15) C#

parameters.Add("Quantity", 15);

Save and press Ctrl + F5 to run your project. You should see the following output: Output

Order received Product ID: 1 Quantity: 15 The item is not in stock The order is canceled Press any key to continue... Press any key to exit the application. Conclusion

In this tutorial, you learned the basic concepts involved in creating and executing a state machine workflow. You saw that the first step in designing a workflow is to get an accurate view of the business process you are modeling. You can then decide whether to build a sequential or state machine workflow. Can you identify discrete states, places where the workflow waits for an action it does not control? If so, you will likely create a state machine workflow. The State, SetState, StateInitialization and StateFinalization activites are specific to state machine workflows. As you saw, you can use those and other activities to build a state machine workflow. Your use of these other activities will usually be the same regardless of the type of workflow you build. In this tutorial, you did not see how a host application raises events. That is obviously an important thing to learn, and you are encouraged to learn about this in a later tutorial in this series.

16

Windows Workflow Tutorial: Introduction to Sequential Workflows Introduction

Windows Workflow Foundation (WF), originally introduced as part of the .NET Framework 3.0 with extensions for Visual Studio 2005’s designers, has continued to be enhanced for the .NET Framework 3.5 and Visual Studio 2008. WF makes it possible, and for many workflow scenarios, even easy to create robust, manageable workflow-based applications. WF is actually many things: It’s a programming model, and runtime engine, and a set of tools that help you create workflow-enabled applications hosted by Windows. (For more information on WF, drop by the portal site). In this series of tutorials, you’ll work through several different examples, showing off various important features of WF, and the corresponding tools and techniques using Visual Studio 2008. Because workflows generally deal with specific business-oriented processes, and these tutorials can’t begin to emulate your specific processes, you’ll find that the specific examples shown here tend to focus on either scenarios that you can control in order to demonstrate the specific workflow features (such as working with files in the file system), or they focus on business processes with “holes” left for your imagination to insert real-world behaviors. This first tutorial introduces the basics of creating Workflow applications, using a console application as the workflow’s host. Along the way, you’ll investigate the various workflow-focused templates that Visual Studio 2008 provides, and you’ll learn what happens when you create and run a workflow application. You’ll try out a few of the many different workflow activities, as well. (These tutorials assume that you have Visual Studio 2008 installed, along with the .NET Framework 3.5. You can choose to work in either Visual Basic or C#--the steps listed here call out specific differences between the languages, when necessary. There’s a lot to cover in this first tutorial, so fire up Visual Studio 2008, and get started! Investigate the Workflow Templates

To get started, in Visual Studio 2008 select File | New | Project to display the New Project dialog box. In the list of project types, select Workflow, displaying the list of templates shown in Figure 1.

17

Figure 1. Visual Studio 2008 provides these workflow templates. In general, WF allows you to create two different types of workflows: sequential, and state machine. Sequential workflows provide a structured series of steps in which one activity leads to another, and steps generally occur immediately one after another. A step might wait for some event (an email to arrive, for example), but sequential workflows are often appropriate for tasks that operate without human intervention. State machine workflows provide a set of states; transitions between the states define the behavior. In general, state machine workflows react to events which move the workflow’s current activity from one state to another. Each workflow that you design is simply a class that inherits from one or the other of the System.Workflow.Activities.SequentialWorkflowActivity or System.Workflow.Activities.StateMachineWorkflowActivity classes, and most of the Visual Studio 2008 workflow project templates create a class that inherits from one or the other of these base classes for you. (In addition, each of these templates includes assembly references to the various assemblies required in order to design and run workflows.) If you want to create a sequential workflow, you can select any of the following templates: •

Sequential Workflow Console Application, which includes a Console application host application that helps run your workflow. You’ll investigate this specific template in this tutorial.



Sequential Workflow Library, which creates a .NET library containing only a sequential workflow class. This project template does not include a host application, so you will need to supply your own in order to execute a workflow.



Sharepoint 2007 Sequential Workflow, which creates a sequential workflow suitable for use with SharePoint 2007. (For more information on SharePoint 2007 and workflows, visit this site.)

If you want to create a state machine workflow (the topic of another tutorial in this series), you can select any of the following templates, which correspond to the similarly named templates previously listed: •

SharePoint 2007 State Machine Workflow



State Machine Workflow Console Application



State Machine Workflow Library

In addition, you can select the Empty Workflow Project template to create an empty project, with just the necessary references set; or you can select the Workflow Activity Library template to create a library in which you could create custom workflow activities. Create a Workflow Project

For this demonstration, select the Sequential Workflow Console Application template, name your application WorkflowDemo1, and select an appropriate folder for the project. Click OK to create the project. (Note that every workflow must be hosted by some application—something has to create an instance of the workflow runtime, which, in turn, creates an instance of the workflow you want to execute. In this example, you’ll create a Console application that hosts the workflow.) At this point, the workflow project contains a single workflow class, named Workflow1. This blank template should be opened for you (if not, double-click the corresponding code file in the Solution Explorer window to open it), and the workflow designer is ready for you to add your own activities (see Figure 2).

18

Figure 2. The workflow designer is ready for you to add activities. In the Solution Explorer window, right-click the Workflow1 file, and select View Code from the context menu. You’ll find code like the following, clearly pointing out that the workflow that you’re designing is really just a class that inherits from the SequentialWorkflowActivity class: Visual Basic

Public class Workflow1     Inherits SequentialWorkflowActivity End Class C#

public sealed partial class Workflow1: SequentialWorkflowActivity {   public Workflow1()   {   InitializeComponent();   } }

Just as a Windows Form that you design within Visual Studio becomes an instance of a class that inherits from the System.Windows.Forms.Form class when you run the application, the workflow that you design within Visual Studio becomes an instance of a class that inherits from either the SequentialWorkflowActivity or StateMachineWorkflowActivity class when you run the application. Later, you’ll add code to this partial class in order to add behavior to your workflow. (Actually, there’s more of a similarity than just that—in Visual Studio, when you load a file that contains a class that inherits from the Form class, Visual Studio displays the form using the Windows Forms designer. The same thing happens with a workflow class—when you ask Visual Studio to load a file that contains a class that inherits from one of the two base activity classes, it displays the class using the appropriate workflow designer.) Select View | Designer to return to design view. Look in the Toolbox window (if it’s not visible, select View | Toolbox to display it). You’ll see two tabs that deal with Workflow: the tab labeled Windows Workflow v3.0 contains most of the built-in workflow activities you’ll use, and the tab labeled Windows Workflow v3.5 contains the new workflow activities added in the .NET 19

Framework 3.5 (this tab contains only two activities dealing with WCF and workflow, and you won’t need these items for these tutorials). For now, select the Windows Workflow 3.0 tab, and expand it (see Figure 3).

Figure 3. The Windows Workflow v3.0 tab in the Toolbox window contains most of the built-in workflow activities. Some of the activities you see in the Toolbox window have obvious behavior—the Code activity allows you to execute code, the IfElse activity allows you to make choices, and the While activity executes an activity while some condition remains true. Other activities, such as the SynchronizationScope activity, require a bit more study to determine their purpose. In this tutorial, you’ll work with the simple Code and IfElse activities—later tutorials will walk you through using some of the more complex activities, such as the Replicator, While, Listen, and HandleExternalEvent activities. Add Activities to the Workflow

To get started, from the toolbox, drag a Code activity onto the design surface, so that the designer looks like Figure 4 just before you drop the activity. Once you drop the activity, the designer looks like Figure 5. (Note that as you drag activities onto the designer, you see one or more green “dots” indicating where, on the workflow, you can drop your activity.) 20

Figure 4. Before dropping the activity, you see an icon representing where it will finally appear within the workflow.

Figure 5. After dropping the activity, it appears within the workflow design. At this point, the Code activity indicates an error—you haven’t yet told it what to do when the workflow attempts to execute it! You’ll add the code soon. (Although this tutorial doesn’t walk you through the steps, you should get in the habit of providing meaningful names for your workflow activities—otherwise, it gets difficult to manage all the activities in your workflow. Setting the Name property for each activity is a good habit to get into, but it’s not necessary for this simple workflow.) At runtime, the sequential workflow starts at the green arrow, and executes activities, one at a time, in turn, until it reaches the red circle at the conclusion of the workflow. In your sample workflow, you have included only a single Code activity. Executing the workflow will cause it to run any code that you have placed in the activity’s ExecuteCode event handler. To create the code for your activity, double-click CodeActivity1. This creates an associated procedure, ready for you to edit in the code editor. Modify the procedure, adding the following code: Visual Basic

Console.WriteLine("Hello, World!") C#

Console.WriteLine("Hello, World!");

Select View | Designer, and verify that the error indicator disappears, because you have provided code for the Code activity to run. Select the Code activity, and examine the Properties window (see Figure 6). You’ll see that by adding the event handler, you’ve set the activity’s ExecuteCode property, so that the activity “knows” what to do when it becomes the active activity within the workflow. 21

Figure 6. Adding code for the Code activity sets the activity’s ExecuteCode property. Just to verify that you’ve created a working workflow, save and press Ctrl+F5 to run your project. (If you use any other means to run the project, the console window will disappear immediately, once the message appears on the screen.) After a few seconds, the console window displays your message awaits a key press. So far, you’ve created the world’s simplest working workflow, and it really only proved that workflow works at all—clearly, you would never use WF for a simple application like this. Next, you’ll investigate how WF loads and runs your workflow design. Debugging a Workflow

Although this simple application certainly doesn’t require any debugging capabilities, you’ll often need to step through your workflows. As you might expect, you can easily place a breakpoint in the ExecuteCode handler for a Code activity, and step through the code. But what if you want to debug at a higher level, stepping through activities as they execute? You can, and Visual Studio makes the process feel much like debugging at the code level. In the workflow designer, right-click CodeActivity1. From the context menu, select BreakPoint | Insert Breakpoint. At this point, the designer places a red dot on the activity, indicating that the workflow will drop into Debug mode when it begins to execute the activity (see Figure 7).

22

Figure 7. Set a breakpoint on an activity. Press F5 to start running the application. When the workflow reaches codeActivity1, it pauses execution and highlights the activity in yellow, as shown in Figure 8.

Figure 8. At runtime, Visual Studio stops at the activity’s breakpoint. Select Debug | Step Over (or the corresponding keystroke), and Visual Studio steps to the next activity, executing the previous Code activity’s ExecuteCode procedure. Select Debug | Step Into, and Visual Studio steps into the second Code activity’s ExecuteCode procedure, as you might expect. Visual Studio 2008 makes debugging a workflow no more difficult than debugging any other type of application. (In order to step through your workflow, the startup project within Visual Studio must be a workflow project. That is certainly the case in this simple example, but might not be the case in a “real-world” scenario in which the workflow exists in a library, and the host project is separate. In that case, you’ll need to configure Visual Studio so that it starts the host application when you start debugging.) Investigate the Startup Code

The project template created a simple console application for you, and the project automatically loaded and ran your workflow. It’s important to understand exactly how the project does its work, because you’ll often want to 23

host workflows from other types of applications (from Windows or Web applications, or perhaps from a Windows Service)—in those cases, you’ll need to provide your own code to get the workflow running. To investigate the code that actually does the work starting up your workflow, in the Solution Explorer window, double-click Module1.vb or Program.cs. There, you’ll find the following code: Visual Basic

Class Program Shared WaitHandle As New AutoResetEvent(False)   Shared Sub Main()     Using workflowRuntime As New WorkflowRuntime()       AddHandler workflowRuntime.WorkflowCompleted, _         AddressOf OnWorkflowCompleted       AddHandler workflowRuntime.WorkflowTerminated, _         AddressOf OnWorkflowTerminated       Dim workflowInstance As WorkflowInstance       workflowInstance = _         workflowRuntime.CreateWorkflow(GetType(Workflow1))       workflowInstance.Start()       WaitHandle.WaitOne()     End Using   End Sub   Shared Sub OnWorkflowCompleted(ByVal sender As Object, _     ByVal e As WorkflowCompletedEventArgs)     WaitHandle.Set()   End Sub   Shared Sub OnWorkflowTerminated(ByVal sender As Object, _     ByVal e As WorkflowTerminatedEventArgs)     Console.WriteLine(e.Exception.Message)     WaitHandle.Set()   End Sub End Class C#

class Program {   static void Main(string[] args)   {     using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())

24

{ 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(WorkflowDemo1.Workflow1));       instance.Start();       waitHandle.WaitOne();     }   } }

This code, which runs as your application loads, starts by creating a new instance of the WorkflowRuntime class, which provides a execution environment for workflows. Every application you create that hosts one or more workflows must create and instantiate an instance of this class. The WorkflowRuntime instance later creates workflow instances: Visual Basic

Using workflowRuntime As New WorkflowRuntime()   ' Code removed here… End Using C#

using (WorkflowRuntime workflowRuntime =   new WorkflowRuntime()) { // Code removed here… }

On its own, a Console application simply quits when the code in its Main procedure completes. In this case, however, you must keep the application “alive” as long as the workflow is still running. The project template includes an AutoResetEvent variable that helps keep that application running until the workflow has completed: Visual Basic

Shared WaitHandle As New AutoResetEvent(False)

25

C#

AutoResetEvent waitHandle = new AutoResetEvent(false);

The AutoResetEvent class allows threads to communicate with each other by signaling. In this case, the Console application runs in one thread, and the workflow runs in a separate thread. The main thread—in this case, the console application—calls the wait handle’s WaitOne method, which blocks the console application’s thread until the workflow’s thread calls the wait handle’s Set method, which allows the main thread to complete. How does this all happen? Given the WorkflowRuntime instance, the code adds event handlers for the runtime’s WorkflowCompleted and WorkflowTerminated events. The WorkflowCompleted event occurs when the workflow completes normally, and the WorkflowTerminated event occurs if the workflow completes abnormally (because of an unhandled exception, for example): Visual Basic

AddHandler workflowRuntime.WorkflowCompleted, _   AddressOf OnWorkflowCompleted AddHandler workflowRuntime.WorkflowTerminated, _   AddressOf OnWorkflowTerminated ' Later in the class: Shared Sub OnWorkflowCompleted(ByVal sender As Object, _   ByVal e As WorkflowCompletedEventArgs)   WaitHandle.Set() End Sub Shared Sub OnWorkflowTerminated(ByVal sender As Object, _   ByVal e As WorkflowTerminatedEventArgs)   Console.WriteLine(e.Exception.Message)   WaitHandle.Set() End Sub C#

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

When either event occurs, the code calls the wait handle’s Set method, which allows the console application’s thread to continue running (in effect, completing the application and allowing the console window to close). If the workflow terminates abnormally, the WorkflowTerminated event handler writes the exception to the console window before calling the Set method. (This is, as you can probably surmise, not terribly helpful—because the 26

message appears in the window immediately before the window closes, you’ll never actually see the message if the workflow throws an unhandled exception!) You’ve investigated all the code provided in the project template, except the code that actually starts your workflow running. This code appears between the code that sets up the event handlers, and the code that calls the wait handle’s WaitOne method: Visual Basic

Dim workflowInstance As WorkflowInstance workflowInstance = _   workflowRuntime.CreateWorkflow(GetType(Workflow1)) workflowInstance.Start() C#

WorkflowInstance instance = workflowRuntime.CreateWorkflow(   typeof(WorkflowDemo1.Workflow1)); instance.Start();

This code starts by creating a WorkflowInstance object, assigning to it the return value of calling the WorkflowRuntime object’s CreateWorkflow method. By passing the type of the workflow to be created as a parameter to the CreateWorkflow method, the workflow runtime can determine exactly which type of workflow to create. Finally, the code calls the Start method of the workflow instance, which begins executing the workflow at its first activity. (Sequential workflows have an initial activity; state machine workflows have an initial state. It’s a subtly different concept, and you’ll understand it better once you try out a state machine workflow.) Although you’ll probably want to spend the most time fixating on the mechanics of the AutoResetEvent object and the wait handle, please don’t—these features are only necessary for workflows hosted by a Console application, to keep the application running as long as the workflow hasn’t completed. When you host a workflow in any other type of application (a Windows Forms application, for example), you needn’t worry about keeping the application alive. Build a Slightly More Useful Workflow

Although it’s important to work through the obligatory “Hello, World” example as you’ve done, the workflow you built really didn’t do much. For the remainder of this tutorial, you’ll build a workflow application with the following features for backing up files in a folder: •

When the workflow starts up, it creates the required backup folder, if necessary, using a Code activity.



Using a While activity, it loops through all the files in the “from” folder.



Within the activity inside the While activity, a Code activity performs the file copy.



The workflow receives its “from” and “to” folders passed as parameters from the host application.

In setting up this simple workflow, you’ll learn how to use the While activity, and also how to pass parameters to a workflow from the host. In Visual Studio 2008, create a new project, again selecting Sequential Workflow Console Application as the template. Name the project BackupWorkflow. In the workflow designer, drag a Code activity, then a While activity, and finally, another Code activity into the designer. When you’re done, the designer should look like Figure 9. Note that each of the activities displays an 27

error condition: the two Code activities require code to execute, and the While activity requires you to supply a condition so that it can determine when to stop executing.

Figure 9. Create this simple workflow. The While activity allows you to drop a single activity within it (note the “Drop and Activity” prompt inside the activity). What if you want to execute multiple activities in a loop? Although you cannot drop multiple activities inside the While activity, for this very purpose, WF supplies a Sequence activity. The Sequence activity acts as a container for other activities, and as far as the While activity is concerned, it contains only a single activity once you place a Sequence activity inside it. Although this workflow only requires a single activity within the While activity, iif you were to add more than a single activity, you would need the Sequence activity. On the other hand, adding the Sequence activity adds significant overhead to the workflow’s execution—therefore, only add a Sequence activity if you need multiple activities within the While activity. As a “best practice”, consider minimizing the number of activities within the While activity, if at all possible.. Drag a Sequence activity inside the While activity. When you’re done, the workflow should resemble Figure 10.

28

Figure 11. The completed layout should look like this. Configure Code Activities

Double-click codeActivity1, creating the activity’s ExecuteCode handler. At the top of the code file, add the following statement: Visual Basic

Imports System.IO C#

using System.IO;

Outside the procedure you just created, but inside the Workflow1 class, add the following declarations. You’ll use these declarations to keep track of the “from” and “to” folders, as well as the current file and total number of files as you’re copying files: Visual Basic

Private currentFile As Integer Private files As FileInfo() Private _totalFiles As Integer Public Property TotalFiles() As Integer   Get     Return _totalFiles

29

End Get Set(ByVal value As Integer)     _totalFiles = value   End Set End Property Private _toFolder As String Public Property toFolder() As String   Get     Return _toFolder   End Get   Set(ByVal value As String)     _toFolder = value   End Set End Property Private _fromFolder As String Public Property fromFolder() As String   Get     Return _fromFolder   End Get   Set(ByVal value As String)     _fromFolder = value   End Set End Property C#

public string toFolder { get; set; } public string fromFolder { get; set; } public int totalFiles ( get; set; } private int currentFile; private FileInfo[] files;

In the codeActivity1_ExecuteCode procedure, add the following code, which initializes the variables: Visual Basic

currentFile = 0 files = New DirectoryInfo(fromFolder).GetFiles totalFiles = files.Count ' Create the backup folder. Directory.CreateDirectory(toFolder)

30

C#

currentFile = 0; files = new DirectoryInfo(fromFolder).GetFiles(); totalFiles = new DirectoryInfo(fromFolder).GetFiles().Count(); // Create the backup folder: Directory.CreateDirectory(toFolder);

Select View | Designer to switch back to the workflow designer. Double-click codeActivity2, and add the following code to the activity’s ExecuteCode handler. This code retrieves the name of the current file to be copied, copies it to the “to” folder (overwriting existing files), and increments the current file counter: Visual Basic

Dim currentFileName As String = _   Path.GetFileName(files(currentFile).Name) files(currentFile).CopyTo( _   Path.Combine(toFolder, currentFileName), True) currentFile += 1 C#

string currentFileName =   Path.GetFileName(files[currentFile].Name); files[currentFile].CopyTo(   Path.Combine(toFolder, currentFileName), true); currentFile++;

At this point, you’ve set up the Code activities and their ExecuteCode handlers, but you’re not done: You still need to configure the While activity, and you need to pass the fromFolder and toFolder values from the host application. In addition, you need to add code in the host that reports on the results of executing the workflow. Configure the While Activity

The While activity in this workflow needs to execute as many times as there are files in the “from” folder. The code includes the currentFile and totalFiles variables—you simply need to expose that information to the While activity. Select View | Designer. In the designer, select the While activity. In the Properties window, find the Condition property, select the drop-down arrow to the right of the property’s value, and select Declarative Rule Condition (see Figure 11). You have the option of either defining a rule in the Properties window (Declarative Rule Condition) or creating a condition in code (Code Condition). For this demonstration, you’ll create the condition in the Properties window.

31

Figure 12. Set up the declarative rule condition. Click the tiny “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to filesLeft. (Naming the condition allows you to use the same condition in a different place within your workflow.) Select the Expression property, and then select the ellipsis to the right of the property. Enter the condition shown in Figure 12. As you type, note the IntelliSense support. Clearly, the Rule Condition Editor window is able to retrieve information about the properties exposed by your workflow as you type. Although Visual Basic developers can enter the expression using Visual Basic syntax (using the Me keyword instead of this), the editor converts the syntax to C# syntax before it closes. Click OK to close the editor.

Figure 13. Create the While activity’s rule condition. The rule condition specifies that your workflow should loop inside the While activity as long as the currentFile value is less than the total number of files. In effect, you’ve created a simple For loop using the While activity and a little bit of code. (You might be wondering if there’s some way to create a For Each loop using an activity. There is, in fact. The Replicator activity can accomplish this same goal, with less code. You can learn more about that activity in a later tutorial.) Configuring Input Parameters

32

You still need to be able to specify the fromFolder and toFolder values from the host application. Because you’ll often need to pass parameters to a workflow, as you start it up, WF provides a standardized mechanism for passing parameters from the host to a workflow. As you did earlier, open the Module1.vb or Program.cs file in the code editor. Currently, the Main procedure creates the workflow instance using the following code: Visual Basic

workflowInstance = workflowRuntime.CreateWorkflow(GetType(Workflow1)) C#

WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(BackupWorkflow.Workflow1));

In order to pass parameters to the workflow, you must create a generic dictionary, using a String type as the key, and an Object type as the value. Add property name and value pairs to the dictionary, and pass it as a second parameter in the call to the CreateWorkflow method. The workflow runtime engine passes the parameters by name to the workflow as it creates the instance. Because the workflow you created exposes public fromFolder and toFolder properties, you can easily pass these parameters from the host application. To finish your workflow, add the following code to the Main procedure, replacing the existing line of code that calls the CreateWorkflow method (feel free to alter the specific folders to match your own situation.): Visual Basic

Dim parameters As New Dictionary(Of String, Object) parameters.Add("fromFolder", "C:\test") parameters.Add("toFolder", "C:\backup") workflowInstance =   workflowRuntime.CreateWorkflow(GetType(Workflow1), parameters) C#

var parameters = new Dictionary<string, object>(); parameters.Add("fromFolder", @"C:\test"); parameters.Add("toFolder", @"C:\backup"); WorkflowInstance instance =   workflowRuntime.CreateWorkflow(   typeof(BackupWorkflow.Workflow1), parameters);

This technique has its own set of tricks. First of all, the parameter names must match public properties (not fields) in the workflow’s class. If you add an invalid property name to the dictionary, you won’t get a compiletime error—instead, you’ll find out at runtime that your property name is incorrect. In addition, the property names are case-sensitive, even when you’re writing code in Visual Basic. Display Workflow Results

As you’ve seen earlier, the workflow runtime’s WorkflowCompleted executes once the workflow completes successfully. In addition, you’ve seen how to pass parameters to the workflow, using a custom dictionary. You can also retrieve values back from the workflow, in the WorkflowCompleted event handler: the 33

WorkflowCompletedEventArgs that the .NET Runtime passes to this event handler exposes its OutputParameters collection. You can use the name of any public property within the workflow as a string index into this collection to retrieve the value of the property. To verify this behavior, in Module1.vb or Program.cs, modify the OnWorkflowCompleted event handler so that it includes the following code (in C#, the event handler appears at the end of a long line of code, which hooks up the WorkflowCompleted event handler using an anonymous method. You’ll find it easier to add this code if you reformat the existing code so that it looks similar to the WorkflowTerminated event handler): Visual Basic

Console.WriteLine("Copied {0} file(s)", _ e.OutputParameters("TotalFiles")) Console.WriteLine("Press any key to continue...") Console.ReadKey() WaitHandle.Set() C#

Console.WriteLine("Copied {0} file(s)", currentFile); Console.WriteLine("Press any key to continue..."); Console.ReadKey(); waitHandle.Set();

Save and run your project. If you’ve followed the directions carefully, the workflow should copy all the files from the “from” folder to the “to” folder, and you should see the results of the workflow in the Console window. Conclusion

In this tutorial, you’ve learned many of the basic concepts involved in creating and executing workflows, using the Windows Workflow Foundation. Of course, the workflows you created are contrived, and exist simply to demonstrate specific features of WF—you’ll need to consider, as you begin thinking about incorporating WF into your own environment, what kinds of tasks lend themselves to running as workflows. Consider this: because WF supports features like persistence, which allows the workflow to persist its state to a data store (SQL Server, by default), WF is best suited for applications in which you have a long-running task that must survive even if the host machine needs to be rebooted during the task’s execution. For simple applications like you’ve seen here, WF is truly overkill. For enterprise solutions in which you have tasks that might not complete for days, or months, WF provides a perfect solution. Now that you’ve gotten a taste for what you can do using WF, take the time to try out the remaining tutorials in this series, and then start building your own workflows. You’ll be amazed at the power in this rich framework.

Windows Workflow Tutorial: Using Basic Flow Control (IfElse, While, Parallel) for Windows Workflow Foundation Introduction

As you have seen in the previous tutorials in this series, workflows model business processes. As you build a workflow, you are answering several questions: •

What happens?

34



How does it happen?



When does it happen?

Activities and the custom code associated with them are the “what” and “how” of a workflow. To address the “when” of a workflow, the order the activities appear in a sequential workflow and the transitions between states in a state machine workflow are one part, and the activities that control the flow of the workflow are the other. Windows Workflow Foundation (WF) provides a number of flow control activities, and you’ll spend the next few tutorials in this series exploring how to use them. In this tutorial, you will investigate the three workflow flow control basic building blocks: the IfElse, While and Parallel activities. IfElse provides branching. While provides repeated execution. Parallel provides a “roundrobin” execution of multiple child activities. Introducing the IfElse Activity

The IfElse activity is the most basic of the flow control activities – it enables you to control whether one or more activities executes. As an example, in an inventory-checking system, you might want one set of activities to execute if an item is in stock and a different set of activities to execute if the item is not in stock – the IfElse activity enables you to do that. The IfElse activity contains one or more IfElseBranch activities. At runtime, the workflow evaluates the Condition property of the first branch. If the condition evaluates to true, the activities in that branch execute. If the condition evaluates to false, the workflow skips that branch and evaluates the condition of the next branch, if there is one. Each branch except the last branch must have a condition. If the last branch has a condition, the activities in it will execute if that condition evaluates to true. If it does not have a condition, the activities in it will execute if none of the other branches has a condition that evaluates to true. In this scenario, the last branch is the Else branch. An IfElse activity can have as many branches as you need. You can also nest IfElse activities. Each branch can contain as many activities as you need. To return a true or false value for the condition property, you can use a declarative rule condition, which is a simple expression, or a code condition, which is a method that is evaluated at runtime. Create a Workflow Project

To demonstrate the first two flow control activities in this tutorial, you will create a sequential workflow project to model checking the inventory for an item and if necessary restocking the item. To get started, in Visual Studio 2008 select File | New | Project to display the New Project dialog box. In the list of project types, select Workflow, displaying the list of templates. For this demonstration, select the Sequential Workflow Console Application template, name your application IfElseWhileDemo, and select an appropriate folder for the project. Click OK to create the project. Next, you will create an XML file containing inventory data for three products; you’ll be using this XML file instead of hard-coding the values in this sample project. To create the file, select Project | Add New Item, and select the XML File template. Name the file Inventory.xml and click Add. Add the following XML: XML

1

35

100 50
2 10 250 3 25 50


In the Solution Explorer, select Inventory.xml. Set the Copy to Output Directory property to Copy always. Now you will create the workflow. Return to the workflow designer by selecting the Workflow1 tab. You can also double click Workflow1 in the Solution Explorer. Select View | Code. The console application that you will create will pass the ID of the product for which you want to check inventory. To enable the workflow to accept this value, add the following property to the workflow: C#

public int ProductID { get; set; }

Now add the following declarations to store the amount on hand and the amount on order for the specific product: C#

public int onHand = 0; public int available = 0;

Add Activities to the Workflow

Select View | Designer to return to the workflow designer. Drag a Code activity From the Toolbox onto the workflow. Name this activity LookupProduct. Double-click LookupProduct and add the following code to the activity’s ExecuteCode event handler: C#

var xmlFile = System.Xml.Linq.XDocument.Load(   System.IO.Path.Combine(   AppDomain.CurrentDomain.BaseDirectory, "Inventory.xml")); var inventory =   from product in xmlFile.Descendants("Product")   where Convert.ToInt32(

36

product.Element("ProductID").Value) == ProductID select new { OnHand = Convert.ToInt32(product.Element("OnHand").Value), Available = Convert.ToInt32( product.Element("Available").Value) }; onHand = inventory.First().OnHand; available = inventory.First(). Available;

This code uses a LINQ To XML query to retrieve the OnHand and Available elements for the specified product. These are stored in the onHand and available fields. Select View | Designer to return to the workflow designer. You will instruct the workflow to take one action if there is sufficient quantity of the item in stock and another action if there is not. Drag an IfElse activity from the Toolbox onto the workflow below LookupProduct. Name this activity CheckOnHand. Rename ifElseBranchActivity1 to IfSufficientOnHand. Rename ifElseBranchActivity2 to IfNotSufficientOnHand. Conditions: Using a Declarative Rule Condition

At this point, the IfSufficientOnHand activity indicates an error. You haven’t told it yet what condition to evaluate. To do that, select IfSufficientOnHand. In the Properties window, find the Condition property, select the drop-down arrow to the right of the property’s value, and select Declarative Rule Condition (see Figure 1). For the Condition property, you have the option of either defining a rule in the Properties window (Declarative Rule Condition) or creating a condition in code (Code Condition). For this activity, you are selecting the Declarative Rule condition and defining the condition in the Properties window; you’ll visit the other option in our next IfElse activity.

37

Figure 1. Choose the type of condition you want to use. Click the “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to SufficientOnHand. The condition will get a default name if you don’t specify one, but naming the condition makes it easier for you to to identify the condition later on, as well as use it in a different place within your workflow. Click the Expression property and then click the ellipsis to the right of that property. Enter the condition shown in Figure 2. Click OK to close the editor.

Figure 2. Create the IfElseBranch activity’s rule condition. Using the WF Rule Condition Editor

The Rule Condition Editor is essentially a code editor. You can enter expressions into it just as you would if you were using expressions in your code. You get the benefit of IntelliSense, just as you do in the Visual Studio code editor (see Figure 3).

Figure 3. Use IntelliSense to help you create expressions. The Rule Condition Editor supports the following operators: •

Relational: ==, =, !=



Comparison: <, <=, >, >=



Arithmetic: +, - , *, /, MOD



Logical: AND, &&, OR, ||, NOT, !



Bitwise: &, |

38

Notice there is a mix of Visual Basic and C# syntax in this list. You can enter expressions using either language. However, be aware that Visual Studio uses the Visual C# compiler to validate expressions. The editor will convert simple Visual Basic syntax to C# prior to compiling. For example, you could enter me.onHand in the Rule Condition Editor. When you click OK, the editor with convert that to this.onHand. If you are a Visual Basic developer, you are used to case insensitivity (if you enter me.onhand in the code editor, Visual Studio will automatically convert that to Me.onHand). It is important to note that the Rule Condition Editor is case sensitive and, like the Visual C# code editor, will enforce case sensitivity. This means that if you enter me.onhand in the Rule Condition Editor and click OK, you will see the error message shown in Figure 4.

Figure 4. The Rule Condition Editor is case sensitive. Finish the Workflow

From the Toolbox, drag a Code activity into IfSufficientOnHand. Name the activity ReportOnHand. Doubleclick ReportOnHand and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine("There are {0} units of product {1} on hand\n" , onHand, ProductID);

Select View | Designer to return to the workflow designer. From the Toolbox, drag a Code activity into IfNotSufficientOnHand. Name the activity Reorder. Double-click Reorder and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine( "There are only {0} units of product {1} on hand" , onHand, ProductID); Console.WriteLine("It is time to reorder\n");

Select View | Designer to return to the workflow designer. The workflow should look like

39

Figure 5. The workflow should look like this. Calling the Workflow from the Console Application

Your final step will be to modify the console host application to pass the id of a product to the workflow. In the Solution Explorer, double click Module1.vb or Program.cs. Add the following code to the Main method, replacing the existing line of code that calls the CreateWorkflow method: C#

var parameters = new Dictionary<string, object>(); parameters.Add("ProductID", 1); WorkflowInstance instance = workflowRuntime.CreateWorkflow(   typeof(IfElseWhileDemo.Workflow1), parameters);

Save and press Ctrl + F5 to run your project. You should first see the following output: Output

There are 100 units of product 1 on hand Press any key to continue . . . Press any key to exit the application.

To see what happens if there is insufficient stock for a product, make the following modification to the Main method: C#

parameters.Add("ProductID", 2);

Save and press Ctrl + F5 to run your project. You should see the following output: Output

40

There are only 10 units of product 2 on hand It is time to reorder Press any key to continue . . . Press any key to exit the application. Modify the Workflow to Add a Nested IfElse Activity

Return to the workflow designer in Visual Studio. You will now add additional flow control logic to handle inventory reordering - instruct the workflow to perform an action if you can successfully reorder the product and a different action if you cannot. Drag an IfElse activity from the Toolbox into IfNotSufficientOnHand below Reorder. Name this activity CheckReorderStatus. Rename ifElseBranchActivity1 to IfReordered. Rename ifElseBranchActivity2 to IfNotReordered. The workflow should look like Figure 6.

Figure 6. The workflow should look like this. Conditions: Using a Code Condition

Select IfReordered. In the Properties window, find the Condition property, select the drop-down arrow to the right of the property’s value, and select Code Condition (see Figure 7).

41

Figure 7. Choose the type of condition you want to use. Click the “+” sign to the left of the Condition property, expanding the property. Set the Condition property to PlaceReorder. Press Enter. Visual Studio creates the PlaceReorder method and opens the code editor. The new method will look like the below: C#

private void PlaceReorder(object sender, ConditionalEventArgs e) { }

The PlaceReorder method is actually an event handler. When you specify Code Condition, Visual Studio creates an instance of the CodeCondition class and adds it to the workflow. The CodeCondition class has an Evaluate method. The workflow runtime calls this method when it needs to evaluate the condition. This method causes the Condition event to occur. The PlaceReorder method handles this event. The second argument in this method is of the type ConditionEventArgs. This class has a Result property, which is a Boolean value. Setting this property to true or false will cause the condition to evaluate to true or false. Add the following code to the PlaceReorder method: C#

e.Result = onHand + available >= 100;

For the purposes of this tutorial, a reorder is successful if there is enough of the product available to have at least 100 units in stock. Finishing the IfElse Workflow

Select View | Designer to return to the workflow designer. From the Toolbox, drag a Code activity into IfReordered. Name the activity ReportReorder. Double-click ReportReorder and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine("{0} units of product {1} will be ordered\n" ,

42

100 – onHand, ProductID);

Select View | Designer to return to the workflow designer. From the Toolbox, drag a Code activity into IfNotReordered. Name the activity ReportFailure. Double-click ReportFailure and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine("You need {0} units of product {1} " + "but only {2} are available", 100 - onHand, ProductID, available); Console.WriteLine("The product has not been reordered\n");

Select View | Designer to return to the workflow designer. The workflow should look like Figure 8.

Figure 8. The workflow should look like this. Save and press Ctrl + F5 to run your project. You should first see the following output: Output

There are only 10 units of product 2 on hand It is time to reorder 90 units of product 2 will be ordered Press any key to continue . . .

43

Press any key to exit the application.

To see what happens if there is insufficient availability for a reorder, make the following modification to the Main method: C#

parameters.Add("ProductID", 3);

Save and press Ctrl + F5 to run your project. You should see the following output: Output

There are only 25 units of product 3 on hand It is time to reorder You need 75 units of product 3 but only 50 are available The product has not been reordered Press any key to continue . . . Press any key to exit the application. Introducing the While Activity

The While activity allows you to control how many times an activity or a sequence of activities executes. The While activity is a container and can contain a single activity. You might think that severely limits its usefulness until you understand that the single activity can be a Sequence activity (or other composite activity), which can contain as many activities as you want. Similar to a while loop construct in code, as long as the While activity’s condition evaluates to true, the workflow will execute the activity or activities it contains. The workflow will continue to do this until the condition evaluates to false. For your next exercise in this tutorial, you will modify the workflow you created in the previous exercise. Rather than process one item at a time, you will have the workflow process all items. You will use a While activity and the condition will indicate whether there are items remaining. Modify the Workflow

Return to Visual Studio 2008, and open Workflow1 in the workflow designer. In the previous exercise, you used a Code activity to read an XML file. Because you are now iterating all items in the inventory, you need to modify the workflow to load in the entire file and iterate through it. To load the file into memory, you will use the Initialized event of the workflow. This event occurs after the workflow starts but before it executes any activities. In the Properties window, locate the Initialized property. Type WorkflowInititialized and press Enter; Visual Studio creates the Initialized event handler and open the code editor. Remove the code that defines the ProductID property since the workflow no longer works with a single ID. Then add the following declarations to the workflow class: C#

private IEnumerable<System.Xml.Linq.XElement> inventory = null; public int itemCount = 0; public int nextItemNumber = 0; private int productID = 0;

The inventory field will store the contents of the inventory list. You declare this as IEnumerable(Of XElement) so that you can query the Inventory.xml file using LINQ to XML. You will write that code next. The remaining 44

fields in the declaration will store the number of items in the inventory list, the next item number in the list and the ID of that item. Replace all instances of Me.ProductID or this.ProductID in your code with productID. Add the following code to the WorkflowInitialized method: C#

var xmlFile = System.Xml.Linq.XDocument.Load( System.IO.Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Inventory.xml")); inventory = from product in xmlFile.Descendants("Product")     select product; itemCount =   (from product in xmlFile.Descendants("Product")    select product.Element("ProductID").Value).Count();

In the previous exercise, you used a LINQ To XML query to retrieve inventory information for a single product. In this example, the query retrieves inventory information for all products. Select View | Designer to return to the workflow designer. Drag a While activity from the Toolbox onto the workflow above the LookupProduct activity. Name the While activity ProcessItems. In the Properties window, find the Condition property, select the drop-down arrow to the right of the property’s value, and select Declarative Rule Condition. Click the “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to MoreItems. Click the Expression property and then click the ellipsis to the right of that property. Enter this.nextItemNumber <= this.itemCount - 1 as the condition. Click OK to close the editor. ProcessItems still indicates an error because you haven’t added an activity to it yet. From the Toolbox, drag a Sequence activity into ProcessItems. Now select LookupProduct and CheckOnHand and drag them into sequenceActivity1. The workflow should look like Figure 9.

45

Figure 9. The workflow should look like this. In the previous exercise, the LookupProduct activity retrieved the XML file and found a single product. You will now change it to retrieve the next item in the inventory list. Double-click LookupProduct and replace the code in the activity’s ExecuteCode event handler with the following: C#

productID = Convert.ToInt32( inventory.ElementAt(nextItemNumber).Element("ProductID").Value); onHand = Convert.ToInt32( inventory.ElementAt(nextItemNumber).Element("OnHand").Value); available = Convert.ToInt32( inventory.ElementAt(nextItemNumber).Element("Available").Value); nextItemNumber += 1;

Calling the Workflow from the Console Application

Your final step will be to modify the console host application so that it does not pass a product id to the workflow. In the Solution Explorer, double click Module1.vb or Program.cs. Comment out the following code in the Main method: 46

C#

var parameters = new Dictionary<string, object>(); parameters.Add("ProductID", 3);

Make the following change to the code that starts the workflow: C#

WorkflowInstance instance = workflowRuntime.CreateWorkflow(   typeof(IfElseWhileDemo.Workflow1));

Save and press Ctrl + F5 to run your project. You should see the following output: Output

There are 100 units of product 1 on hand There are only 10 units of product 2 on hand It is time to reorder 90 units of product 2 will be ordered There are only 25 units of product 3 on hand It is time to reorder You need 75 units of product 3 but only 50 are available The product has not been reordered Press any key to continue . . . Press any key to exit the application. Introducing the Parallel Activity

You can use the Parallel activity to execute two or more branches of activities in parallel. They do not execute literally in parallel. The workflow executes an activity in the first branch and then switches to an activity in the next branch, and so on. You can have as many branches as you want and each branch can contain as many activities as you want. In addition, the branches do not need to contain the same activities. Often they will, but it is not a requirement. For your final exercise in this tutorial, you will create a new sequential workflow project to model an auction. This auction will consist of three bidders, each represented by a Parallel activity in the workflow. Each bidder has a budget. In each round of the auction, each bidder ups the current bid by half of his or her remaining budget. The auction continues until two of the bidders are out of money. The third bidder wins the auction. Create a Workflow Project

To get started, in Visual Studio 2008 select File | New | Project to display the New Project dialog box. In the list of project types, select Workflow, displaying the list of templates. For this demonstration, select the Sequential Workflow Console Application template, name your application ParallelDemo, and select an appropriate folder for the project. Click OK to create the project. Select View | Code. Now add the following declarations to store the price of the item the workflow will auction and the budgets of the three bidders: C#

private decimal budget1 = 5000M; private decimal budget2 = 10000M;

47

private decimal budget3 = 25000M; private decimal nextBid1 = 500M; private decimal nextBid2 = 1000M; private decimal nextBid3 = 2500M; public bool outOfMoney1 = false; public bool outOfMoney2 = false; public bool outOfMoney3 = false; private decimal currentBid = 100M; private int lastBidder = 0; public int biddersOut = 0;

The first three fields set the available budget for the three bidders. The next three fields keep track of the amount by which each bidder will increase the current bid when it is their turn. The next three fields keep track of whether each bidder is out of money, meaning they do not have enough to up the current bid. The currentBid field represents the current bid. The lastBidder field keeps track of which bidder bid last. The biddersOut field keeps track of how many bidders are finished bidding. Add Activities to the Workflow

Select View | Designer to return to the workflow designer. Drag a Code activity From the Toolbox onto the workflow. Name this activity StartBidding. Double-click StartBidding and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine("The starting bid is {0:C}\n" , currentBid);

Select View | Designer to return to the workflow designer. From the Toolbox, drag a While activity onto the workflow below StartBidding. Name the While activity Bidding. In the Properties window, find the Condition property, select the drop-down arrow to the right of the property’s value, and select Declarative Rule Condition. Click the “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to BiddersRemaining. Click the Expression property and then click the ellipsis to the right of that property. Enter this.biddersOut < 2 as the condition. Click OK to close the editor. From the Toolbox, drag a Parallel activity into StartBidding. From The Parallel activity contains two Sequence activities by default. To add a third Sequence activity, right click ParallelActivity1 and select Add Branch. Name the three Sequence activities Bidder1, Bidder2 and Bidder3. The workflow should look like Figure 10.

48

Figure 10. The workflow should look like this. Each branch in the Parallel activity represents one of the three bidders. Inside each branch you will model the process of bidding. If the bidder has money left he or she will increase the current bid by a certain amount. Drag an IfElse activity from the Toolbox into Bidder1. Right click on ifElseBranchActivity2 and select Delete. Rename ifElseBranchActivity1 to IfStillBidding1. In the Properties window, find the Condition property, select the drop-down arrow to the right of the property’s value, and select Declarative Rule Condition. Click the “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to NotOutOfMoney1. Click the Expression property and then click the ellipsis to the right of that property. Enter !this.outOfMoney1 as the condition. Click OK to close the editor. Copy ifElseActivity1 and paste it into Bidder2. Rename ifElseBranchActivity1 to IfStillBidding2. Click the “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to NotOutOfMoney2. Click the Expression property and then click the ellipsis to the right of that property. Enter ! this.outOfMoney2 as the condition. Click OK to close the editor. Copy ifElseActivity1 and paste it into Bidder3. Rename ifElseBranchActivity1 to IfStillBidding3. Click the “+” sign to the left of the Condition property, expanding the property. Set the ConditionName property to NotOutOfMoney3. Click the Expression property and then click the ellipsis to the right of that property. Enter ! this.outOfMoney3 as the condition. Click OK to close the editor. After you’ve accomplished the above, the Parallel activity should look like Figure 11.

49

Figure 11. The Parallel activity should look like this. Finishing the Parallel Workflow

To complete the workflow, you will now add the custom code activities into each of the branches to execute the business logic; in this case, you will have the bidders place their bids. From the Toolbox, drag a Code activity into IfStillBidding1. Name the activity Bid1. Double-click Bid1 and add the following code to the activity’s ExecuteCode event handler: C#

if (!(outOfMoney2 && outOfMoney3)) { if (currentBid + nextBid1 > budget1) { outOfMoney1 = true; biddersOut += 1; Console.WriteLine("Bidder 1 is out\n"); } else { currentBid += nextBid1; lastBidder = 1; Console.WriteLine("Bidder 1 bids {0:C}\n", currentBid); } }

Select View | Designer to return to the workflow designer. From the Toolbox, drag a Code activity into IfStillBidding2. Name the activity Bid2. Double-click Bid2 and add the following code to the activity’s ExecuteCode event handler: C#

if (!(outOfMoney1 && outOfMoney3))

50

{ if (currentBid + nextBid2 > budget2) { outOfMoney2 = true; biddersOut += 1; Console.WriteLine("Bidder 2 is out\n"); } else { currentBid += nextBid2; lastBidder = 2; Console.WriteLine("Bidder 2 bids {0:C}\n", currentBid); } }

Select View | Designer to return to the workflow designer. From the Toolbox, drag a Code activity into IfStillBidding3. Name the activity Bid3. Double-click Bid3 and add the following code to the activity’s ExecuteCode event handler: C#

if (!(outOfMoney1 && outOfMoney2)) { if (currentBid + nextBid3 > budget3) { outOfMoney3 = true; biddersOut += 1; Console.WriteLine("Bidder 3 is out\n"); } else { currentBid += nextBid3; lastBidder = 3; Console.WriteLine("Bidder 3 bids {0:C}\n", currentBid); } }

Select View | Designer to return to the workflow designer. From the Toolbox, drag a Code activity into the workflow below Bidding. Name the activity DeclareWinner. Double-click DeclareWinner and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine("Bidder {0} wins with a bid of {1:C}\n",

51

lastBidder, currentBid);

Save and press Ctrl + F5 to run your project. You should see the following output: Output

The starting bid is $100.00 Bidder 1 bids $600.00 Bidder 2 bids $1,600.00 Bidder 3 bids $4,100.00 Bidder 1 bids $4,600.00 Bidder 2 bids $5,600.00 Bidder 3 bids $8,100.00 Bidder 1 is out Bidder 2 bids $9,100.00 Bidder 3 bids $11,600.00 Bidder 2 is out Bidder 3 wins with a bid of $11,600.00 Press any key to continue . . . Press any key to exit the application. Extra Credit: Comparing the Parallel and the Replicator Activities

You have just seen the basics of how the Parallel activity works. The workflow processes each parallel branch in turn, starting with the first branch and continuing through to the last. In this example, the parallel activity evaluates a condition and then executes the Code activity if the condition evaluates to true. In this simple example, each bidder has the same strategy, so you used the same activities in each branch, using basically the same code for each bidder. In the next tutorial in this series, you will learn about the Replicator activity, which provides an alternative way to construct this workflow logic with less duplication of effort. The Replicator activity is the functional equivalent of a ForEach activity, and provides you with the ability to execute one or more activities for each member of a collection. If you used the Replicator here, you would have passed the three bidders as a collection to a Sequence activity containing an IfElse and Code activity within the Replicator. At runtime, the workflow would execute the activities for each bidder in turn. You are encouraged to work through the Replicator tutorial and then redo this example. Extra Credit: How WF Works with Multiple Activities in a Parallel Branch

In this example, each branch of the Parallel activity contained the same activities. You can therefore reliably expect the workflow to process each branch completely before processing the next branch. What if one branch had a different set of activities? For example, suppose the second bidder reevaluated his or her strategy each round prior to bidding. You might then add a Code activity named Reevaluate before the IfElse activity in the second branch. At runtime, the workflow would first execute the Bid1 activity. It would then execute the Reevaluate activity. It would then execute the Bid3 activity, followed by Bid1 and then it would execute the Bid2 activity. The workflow does not execute all of the activities in each branch before moving on to the next branch. This behavior is by design, and has to do with the way the workflow runtime schedules activities for execution. This can be very helpful when coordinating work across a series of asynchronous processes/services where you can 52

initiate the work in your parallel branches, and then gather them back up. But this topic goes beyond the scope of this tutorial, and the subject of its own article. If you wanted to have branch 2 execute completely before moving onto branch 3 - how would you construct this workflow? You could use a SynchronizationScope activity in branch 2 rather than a Sequence activity. The SynchronizationScope activity executes the activities in it in a sequential fashion. An easier solution would be to use the Replicator, because it completely executes its contained activities before moving on to the next item in the collection. Conclusion

In this tutorial, you learned how to use the IfElse, While and Parallel activities to control the flow of a workflow. You saw how to use these activities to specify when activities execute and for how long. For the sake of simplicity, you used custom code activities in command line applications; in your workflows, you will typically use custom activities in Windows or web applications, or running services. As you use these control flow activities to drive your WF workflows, one concern that is raised by WF developers is when to use the activity-based control flow and when to use code-based control flow. The IfElse activity performs a similar purpose to the If..Else (Visual Basic) and if..else (C#) blocks in code. For the IfElse activity, the decision of when to use an IfElse activity or perform branching in code tends to depend on what action you want to take. If the action involves one or more activities, then you will use the IfElse activity. If you can perform the action in code, then you will typically perform the branching within the code of your custom activity. The same applies to the While activity, which performs a similar purpose to several language constructs that support looping. If you simply want to execute five lines of code repeatedly, you should just use a Code activity and perform the looping in code. However, if you want to execute multiple activities repeatedly, then you should use a While activity.

Windows Workflow Tutorial: Using the Replicator for Windows Workflow Foundation To provide support for general programming control flow constructs, the Windows Workflow Foundation (WF) provides activities that you can easily recognize: the IfElse and While activities, for example, clearly allow you to make choices, and loop within a workflow. What if you need to execute an action for each element of some set of objects, however? You might look for a ForEach activity, but your search won’t find the activity you’re seeking. The activity you need is in the toolbox, however: It’s the Replicator activity. This activity, the functional equivalent of a ForEach activity, allows you to execute an activity for each member of a collection. Immediately, you start seeing problems: If the Replicator can only execute a single activity for each element of a collection, how can you execute complex actions for each element? The answer, of course, is that the Replicator can not only execute a simple activity, it can execute a composite activity (an activity that can contain other activities), such as a Sequence activity. To execute multiple activities, you can place a Sequence activity within the Replicator activity, and the workflow runtime executes all the activities within the Sequence activity for each element in the source collection. Although you can use the While activity to provide the same behavior as the Replicator activity, the Replication activity adds some important functionality that you would need to create yourself, if you were using the While activity. For example:

53



The Replicator activity automatically “visits” each item in its input collection, creating a separate instance of its contained activity for each item in the collection. You don’t need to keep track of the number of items in the collection, as you would if you were using the While activity.



The Replicator activity allows you to supply code that it calls before, during, and after processing of each item in the collection.



The Replicator activity allows you to specify whether you want to handle the instances of its contained activity in parallel, or serially. If you execute the activity instances in parallel, the Replicator initializes each instance of the child activity one after the other, then executes each child activity, and finally, completes the execution of each instance of the child activity. (This behavior is similar to using a Parallel activity, with one branch for each item in the data collection.) If you execute the activity instances serially, the Replicator activity initializes, executes, and completes each instance of the child activity before moving on to the next. (This behavior is similar to using a While activity.) Based on your needs, you’ll need to decide which technique works best for you.



The Replicator activity provides an Until property, which allows you to supply a condition (either a declarative condition, or a code condition). This condition allows your application to “short-circuit” the behavior of the activity. That is, if the Until condition becomes true, the activity stops executing the child activity instances, even if it hasn’t completed working through the entire input collection.

This tutorial walks you through the basics of working with the Replicator activity, demonstrating how you can use this activity to execute another activity (or activities) for each member of some collection of values or objects. Build the Sample Workflow

In this tutorial, you’ll create a simple workflow that processes all the files in a particular folder. You might, for example, want to monitor the status of the contents of a particular folder in the file system, and at regular intervals, sweep through the files and handle them in some particular way. In this sample workflow, you’ll simply sweep through all the files and report some information about each to the Console window, but you might want to back up each file, delete each file, or take some other action based on the existence of the file. NOTE Although Windows Workflow Foundation doesn’t include an activity to monitor the file system for changes, you can find a sample custom activity on Microsoft’s site that demonstrates this behavior. Browse to http://msdn.microsoft.com/en-us/library/ms741707.aspx for more information. If you want to send an email in response to finding a file (or for any other reason), you can make use of the sample email workflow activity. Browse to http://msdn.microsoft.com/en-us/library/ms742097.aspx to download a sample email activity. To get started, in Visual Studio 2008 select File | New | Project to display the New Project dialog box. In the list of project types, select Workflow, displaying the list of workflow templates. Select the Sequential Workflow Console Application template, name your application ReplicatorDemo, and select an appropriate folder for the project. Click OK to create the project. The point of this exercise is to create an instance of an activity for each element within a collection, and execute each instance in turn. Therefore, from the Toolbox window, drag an instance of the Replicator activity into the new workflow. Figure 1 shows the workflow, with the activity in place.

54

Figure 1. The Replicator activity indicates that it can contain multiple activities, but it cannot—it can contain only a single activity. Although the caption within the Replicator’s designer invites you to drop multiple activities within it, the invitation is misleading—you can only add a single activity. At this point, as you can see in Figure 1, the Replicator currently displays an error alert. Click on the red circle to view the error, and you’ll see a message indicating that the Replicator activity must contain one activity. To get started, from the Toolbox window, drag a Code activity to the interior of the Replicator activity. (Of course, at this point, the Code activity displays an error, because you haven’t yet supplied it code it needs. Disregard this error, for now.) Attempt to drag a second Code activity within the Replicator activity, to verify that you can only place a single activity within the Replicator. Your attempts to drop a second activity will simply never display any green dots within the activity—these green dots are your indication where you can drop an activity. TIP: In order to place multiple activities within the Replicator activity, use the Sequence activity; you can then place multiple activities within the Sequence activity. Once you’re done, the layout should resemble Figure 2.

55

Figure 2. Place a single activity within the Replicator activity. Add Support Code

In order to get started with the sample workflow, select replicatorActivity1. Because you’ll need to add code for each of the activity’s available handlers, the simplest way to set up all the procedures is to let the designer generate them for you. With the Replicator activity selected, you can either click the Generate Handlers item within the Properties window (see Figure 3), or select Workflow | Generate Handlers from Visual Studio’s menu. Either way, once you’ve generated the handlers, Visual Studio displays the workflow’s code window, with newly generated handler stubs for the Initialized, Completed, ChildInitialized, and ChildCompleted events.

Figure 3. Select Generate Handlers in the Properties window. TIP: If you don’t see the Generate Handlers link within the Properties window, right-click the Properties window and select the Commands option on the context menu. At the top of the code file, add the following statement: Imports System.IO using System.IO; Within the Workflow1 class, add the following two properties, which will track the search folder and the backup properties: Private searchFolderValue As String Public Property searchFolder() As String Get 56

Return searchFolderValue End Get Set(ByVal value As String) searchFolderValue = value End Set End Property Private backupFolderValue As String Public Property backupFolder() As String Get Return backupFolderValue End Get Set(ByVal value As String) backupFolderValue = value End Set End Property public string searchFolder { get; set; } public string backupFolder { get; set; } Add the following two private class-level variables, to keep track of the current file, and the array of files in the search folder: Private currentFile As FileInfo Public files As FileInfo() private FileInfo currentFile = null; public FileInfo[] files; In the replicatorActivity1_Initialized event handler, add the following code. This code retrieves the array of FileInfo objects corresponding to the files in the search folder, and also creates the backup folder if necessary: If Not String.IsNullOrEmpty(searchFolder) Then files = New DirectoryInfo(searchFolder).GetFiles("*.*") End If ' This example assumes that it can create the backup ' folder. If it already exists, this code silently ' does nothing. If the code can't create the folder, ' the workflow will fail. You should add fault handling, ' and terminate the workflow if you can't ' create the folder. See the tutorial on fault handling 57

' for more information on handling exceptions. If Not String.IsNullOrEmpty(backupFolder) Then Directory.CreateDirectory(backupFolder) End If Console.WriteLine("Replicator activity initialized.") if (!String.IsNullOrEmpty(searchFolder)) { files = new DirectoryInfo(searchFolder).GetFiles("*.*"); } // This example assumes that it can create the backup // folder. If it already exists, this code silently // does nothing. If the code can't create the folder, // the workflow will fail. You should add fault handling, // and terminate the workflow if you can't // create the folder. See the tutorial on fault handling // for more information on handling exceptions. if (!String.IsNullOrEmpty(backupFolder)) { Directory.CreateDirectory(backupFolder); } Console.WriteLine("Replicator activity initialized."); To use the Replicator activity, you must also set the InitialChildData property of the activity. This property indicates to the activity which set of data it should iterate through. You can either set this property in code, or you can set it in the designer. You can set the InitialChildData property to any collection that implements the IList interface, and that includes most collections and arrays. In this example, you must set the InitialChildData property to the array of FileInfo objects that you just created in code. To set the InitialChildData property, select View | Designer. In the Workflow designer, select the Replicator activity. In the Properties window, select the InitialChildData property. Click the ellipsis button to the right of the property value; this action displays a dialog box which allows you to select the value to which you will bind the property. In the dialog box, select the files property, as shown in Figure 4. Click OK when you’re done. Figure 4. Bind the InitialChildData property to the public files variable. TIP: If you want to set the InitialChildData property in the Workflow designer, you must ensure that, at runtime, the collection has been filled in before the ReplicatorActivity executes. In this case, you have accomplished this goal by filling in the array during the Initialized event handler for the Replicator activity. In the replicatorActivity1_Completed event handler, add the following code: 58

Console.WriteLine("Completed all the items in the data source.") Console.WriteLine("Completed all the items in the data source."); In the replicatorActivity1_ChildInitialized event handler, add the following code. This code uses the InstanceData property of the procedure’s ReplicatorChildEventArgs parameter to retrieve the value of the current FileInfo object, from the source array of files: currentFile = CType(e.InstanceData, FileInfo) Console.WriteLine("ChildInitialized event: " & currentFile.Name) currentFile = (FileInfo)e.InstanceData; Console.WriteLine("ChildInitialized event: " + currentFile.Name); In the replicatorActivity1_ChildCompleted event handler, add the following code. This code again uses the InstanceData property to display information about the currently processing item: Console.WriteLine("ChildCompleted event: " & _ CType(e.InstanceData, FileInfo).Name) Console.WriteLine("ChildCompleted event: " + ((FileInfo)e.InstanceData).Name); Before you can test your workflow, you must fix all its errors. To do this, select View | Designer to return to the design surface. Double-click codeActivity1. In the codeActivity1_ExecuteCode procedure, add the following code: Console.WriteLine("Handling a file: " & currentFile.Name) Console.WriteLine("Handling a file: " + currentFile.Name); Note that while executing the activities within the Replicator activity, you don’t have explicit access to the current data item. This example stores the current item into a variable named currentFile, but be warned: This technique doesn’t work if you set the Replicator activity’s ExecutionType property to Parallel (this property is set to Sequence, by default.) WARNING! Do not store the current data item into a variable within the Replicator activity’s ChildInitialized event handler, as you’ve done in this example. Although it grants you access to the data item when you set the Replicator activity’s ExecutionType property to Sequence, it will not work if you set the property to Parallel, as you’ll see later in this tutorial. Configure the Host Application The workflow’s host application must supply the workflow’s search path, and handle halting the application to wait for user input. In the Solution Explorer window, double-click Module1.vb or Program.cs. Within the class, locate the Main procedure. Within the Main procedure, replace the line of code that calls the CreateWorkflow method, inserting the following code block: Dim parameters = New Dictionary(Of String, Object) parameters.Add("searchFolder", "C:\test") parameters.Add("backupFolder", "C:\backup") workflowInstance = workflowRuntime.CreateWorkflow( _ GetType(Workflow1), parameters) 59

var parameters = new Dictionary<string, object>(); parameters.Add("searchFolder", @"C:\test"); parameters.Add("backupFolder", @"C:\backup"); WorkflowInstance instance = workflowRuntime.CreateWorkflow( typeof(Workflow1), parameters); This code sets up a dictionary containing the two parameters for your workflow, searchFolder and backupFolder. (Feel free to change the specific folder names if you want to monitor files and back them up to different locations.) When you pass the dictionary to the workflow in the call to CreateWorkflow, the Workflow Runtime sets the value of the matching properties within the workflow instance to the values you specify in the dictionary. Add the following procedure to the class: Shared Sub WaitForKeypress() Console.WriteLine() Console.WriteLine("Press any key to quit...") Console.ReadKey() End Sub static void WaitForKeypress() { Console.WriteLine(); Console.WriteLine("Press any key to quit..."); Console.ReadKey(); } Modify the Main procedure, adding a call to WaitForKeyPress immediately following the existing call to the WaitHandler.WaitOne method. Once you’re done, the end of the Main procedure should look like the following: workflowInstance.Start() WaitHandle.WaitOne() WaitForKeypress() End Using End Sub instance.Start(); waitHandle.WaitOne(); WaitForKeypress(); } } 60

Test the Workflow In order to test the workflow, ensure that the C:\Test folder (or whichever folder you specified in the searchFolder property) exists, and that it contains some old files, and at least one file that you created today. Execute the application, and examine the output. Imagine that the C:\Test folder contains three files, two of which (Document.tif and list.txt) have not been modified today, and one (Demo.txt) was created today. The output in the Console window might look like Figure 4. Figure 5. With three files, one of which is new, the output might look like this. Given the array of files that the code created by examining the C:\Test folder, the Replicator activity “visited” each file in turn, executing its ChildInitialized and ChildCompleted event handlers. In between the two, the Replicator activity executed its child activity, the Code activity, which displays text as it executes. As you can see in Figure 4, because the Replicator activity’s ExecutionType property has been set to its default value (Sequence), the replicator handles each instance of its child activity completely before moving on to the next. NOTE If the child activity executes more than once, the Replicator activity creates separate instances of the child activity. This allows the instances to execute independently (and also makes it possible for the workflow to execute the child activities in parallel—this wouldn’t be possible if the Replicator activity didn’t create a separate instance of the child activity for each item in the InitialChildData collection). If you want to retrieve a collection of all the child activities, use the Replicator activity’s DynamicActivities property from within the child activity’s code. So far, you’ve seen that the Replicator activity uses its InitialChildData property as a data source, and iterates through each item in the collection. For each child, the Replicator activity calls ChildInitialized and ChildCompleted events, passing each an event argument that includes a reference to the specific child. The Replicator activity’s child activity, however, has no specific information about the current data item, and you’ll need to take extra steps (as you’ll see later in this tutorial) to use that information from within the child activity. Compare Parallel and Serial Execution In the previous exercise, you ran the sample workflow with the Replicator activity’s ExecutionType property set to Sequence. Using that execution type, the Replicator activity initializes, executes, and completes each instance of the child activity before it moves on to the next. How does the execution change if you set the property to Parallel? It’s simple to demonstrate this behavior. Open Workflow1 in the workflow designer, and select the Replicator activity. In the Properties window, set the ExecutionType property to Parallel, and then save and execute the application. This time, you’ll see output similar to Figure 5. Examine the output carefully, and you’ll find major changes from the previous test: • The Replicator activity creates an instance of its child activity for each element of its InitialChildData property’s collection. • The Replicator activity raises the ChildInitialized event handler for each instance of the child activity, in turn. Then, it executes the child activity instances, one after the other. Finally, it raises the ChildCompleted event for each instance of the child activity. • The local currentFile variable is set during the ChildInitialized event handler. When you execute the Replicator activity using the Parallel execution mode, the current file name contains the last initialized child instance, before the Replicator activity starts executing the child activity instances. As you can see in Figure 5, this means that the currentFile variable contains the same value for each instance, and is therefore meaningless. As it is, there’s no way for the Code activity within the Replicator activity to retrieve information about the 61

current file. The recommended solution for this problem is to create a custom activity that contains information about the current item that the Replicator activity is handling, and to use it in place of the “dumb” activity that you’re currently using within the Replicator. You’ll learn more about this solution later in this tutorial. Figure 6. When you set the Replicator activity to run its child activities in parallel, the behavior changes significantly. Before continuing, make sure you set the ExecutionType property back to Sequence, its default value. Add an Until Condition In addition to executing its child activity for each item in the collection supplied in its InitialChildData property, you can supply an Until condition in the Replicator activity’s UntilCondition property. Just as when you’re working with an IfElse or While activity, this condition can be either a declarative rule condition, or it can be a code condition. Even if the Replicator activity hasn’t finished working through the items in the InitialChildData property, if the Until becomes true, the Replicator activity halts its march through the data items. WARNING! If you set the UntilCondition property, you must ensure that at some point, it becomes true. If the Replicator activity completes all its child activity instances and the UntilCondition property returns false, the Replicator activity simply hangs. Although you can solve this problem by programmatically adding new items to the data collection to somehow change the UntilCondition property or make it no longer be false, this is an extreme solution. You must plan for this problem, and ensure that the UntilCondition property is no longer false by the time all the child activity instances have completed executing. Imagine that you wanted to limit the number of files that your Replicator activity handled—you might want to, for example, limit it to only backing up two files (although this restriction seems arbitrary, it simple allows this tutorial to show off the use of the UntilCondition property). Based on the previous warning, if you set the UntilCondition so that it returns true only when you have handled a fixed number of files, but never reach that specified number of files, your workflow will hang. In this example, verify that you have more than two files in the test folder, and start by adding the following variable to the class-level variables: Private currentFile As FileInfo Private files As FileInfo() Private fileCount As Integer private FileInfo currentFile = null; private FileInfo[] files; private int fileCount; Within the replicatorActivity1_Initialized event handler, add code that initializes the fileCount variable: fileCount = 0 fileCount = 0; Within the replicator1_ChildInitialized event handler, add code that increments the fileCount variable: fileCount += 1 fileCount++; Finally, add the UntilCondition property setting. To do this, start by selecting View | Designer. Select the Replicator activity, and in the Properties window, find the UntilCondition property. Set the property’s value to Code Condition, and expand the + sign to the left of the property. Set the Condition sub-property to the name of 62

the new procedure, LimitFiles. Press Enter, and Visual Studio creates the procedure stub. (You could also set the UntilCondition property to Declarative Rule Condition, and specify the rule directly in the designer, in this case). Note that Visual Studio creates a stub for the procedure that includes a ConditionalEventArgs object as the second parameter. This object provides a Result property—set it to a Boolean value that, when True, will cause the Replicator to halt. Modify the procedure, adding code to limit the number of files so that once it has processed two files, the UntilCondition property returns true: Private Sub LimitFiles(ByVal sender As System.Object, _ ByVal e As System.Workflow.Activities.ConditionalEventArgs) e.Result = (fileCount > 1) End Sub private void LimitFiles(object sender, ConditionalEventArgs e) { e.Result = (fileCount > 1); } Make sure the ReplicatorActivity’s ExecutionType property has been set to Sequence, and run the application. You’ll find that the output shows only two files, instead of the original number. (For fun, try changing the value in the LimitFiles method so that it executes until you reach a number of files greater than the actual number of files that you have. You’ll see that the workflow simply halts, waiting for the fileCount variable to reach the number you specify. Because there aren’t enough files, it never does. You can simply close the Console window to end the running application, in that case.) Clearly, you need to be careful when using the UntilCondition property, to ensure that this situation never occurs. Create a Custom Activity Although the goal of the workflow was to back up each file to a specific folder, it currently doesn’t include this behavior. For this simple example, you could do the work in the ChildInitialized event, but what if you needed to execute workflow activities within the Replicator activity, and you needed to have information about the specific item from the Replicator’s data source from within those activities? At this point, the single Code activity within the Replicator receives no information about the current data item, and because the Replicator activity might be executing its contents in parallel fashion, you can’t simply store the value into a workflowlevel variable. The standard solution to this problem is to create a custom activity which has, as one of its properties, a public property that can receive, and work with, the current data item from the parent Replicator activity. For example, if you were to create a custom activity with an InputFile and BackupFolder property, you could pass information from the Replicator to the custom activity, and it could perform the work of backing up the specified file. In other words, to complete this simple workflow, you’re going to create a custom activity that has information about the file to be backed up, and the location to which to back it up, and you’ll replace the existing Code activity with this new activity. To get started, in Visual Studio, select Project | Add Activity. In the Add New Item dialog box, set the name to be FileBackupActivity, and click Add. Visual Studio creates a designer for the new activity; select View | Code to view the activity’s code. By default, the designer creates a new activity that can contain other activities (it inherits from SequenceActivity). Because you need to create a simple activity, start by changing the base class to Activity: Public class FileBackupActivity 63

Inherits Activity End Class public partial class FileBackupActivity : Activity { public FileBackupActivity() { InitializeComponent(); } } Within the activity’s class, you’ll need to create two public properties. Although you could use standard .NET properties for both, you should get in the habit of using Workflow’s dependency properties, which supply extra functionality, when your intention is to bind the properties using the designer. TIP: Dependency properties are useful when you intend to use the data binding capabilities in the Windows Workflow Foundation, but they do add extra overhead, when compared to simple .NET properties. As a rule of thumb, use dependency properties only when you intend to use workflow binding. In this example, the BackupFolder property needs to be bound to another property, but the InputFile property will not. For this example, there’s no need to make the InputFile property be a dependency property. Although a full discussion of dependency properties is beyond the scope of this tutorial, they serve a very important purpose within workflows: They allow the workflow runtime to provide a centralized repository for property instances, keeping track of a workflow’s state. When you create a dependency property, the workflow runtime registers the value of the property, for the particular activity instance, in a hash table within the workflow runtime. If your workflow never uses the property value, it never needs to be loaded from the hash table. This behavior allows the workflow to minimize memory usage. In addition, the workflow designer makes use of dependency properties in its support for property binding, so you’ll find working with the designer easier if you make use of dependency properties. (For more information on dependency properties, browse to this page: http://msdn.microsoft.com/en-us/library/ms734499(VS.85).aspx). To get started, add the the following statement to the top of the code file: Imports System.IO using System.IO; The easiest way to create a dependency property is to the use the code snippet built into Visual Studio 2008. To do that, within the FileBackupActivity class, create a blank line outside any other procedure, and type wdp followed by the Tab key (press the Tab key twice in C#). Fill in the various fields so that the property looks like the following code fragment (in C#, you do not need to supply the class name; in Visual Basic, you must): Public Shared BackupFolderProperty As DependencyProperty = _ DependencyProperty.Register("BackupFolder", GetType(String), _ GetType(FileBackupActivity)) _ _ _ 64

_ Public Property BackupFolder() As String Get Return (CType((MyBase.GetValue( _ FileBackupActivity.BackupFolderProperty)), String)) End Get Set(ByVal Value As String) MyBase.SetValue( _ FileBackupActivity.BackupFolderProperty, Value) End Set End Property public static DependencyProperty BackupFolderProperty = DependencyProperty.Register("BackupFolder", typeof(string), typeof(FileBackupActivity)); [DescriptionAttribute("BackupFolder")] [CategoryAttribute("BackupFolder Category")] [BrowsableAttribute(true)] [DesignerSerializationVisibilityAttribute( DesignerSerializationVisibility.Visible)] public string BackupFolder { get { return ((string)(base.GetValue( FileBackupActivity.BackupFolderProperty))); } set { base.SetValue( FileBackupActivity.BackupFolderProperty, value); } } 65

If you examine the code, you’ll see that the snippet creates a public static/shared field named InputFileProperty (and BackupFolderProperty)—therefore, there’s only one instance of this field in memory, no matter how many instances of the activity class get created by the workflow. The code also creates a public standard .NET property named InputFile (or BackupFolder), and this property simply gets and sets the value of the property from the hash table managed by the base class (DependencyObject). In addition, create a standard .NET FileInfo property named InputFile: Private inputFileValue As FileInfo Public Property InputFile() As FileInfo Get Return inputFileValue End Get Set(ByVal value As FileInfo) inputFileValue = value End Set End Property public FileInfo InputFile { get; set; } From the calling code’s perspective, the activity exposes two public properties: InputFile and BackupFolder. At this point, you must add code to cause the custom activity to take some action when it executes. Within the custom activity’s class, add the following code, which overrides the Execute method from the base class: Protected Overrides Function Execute( _ ByVal executionContext As ActivityExecutionContext) _ As ActivityExecutionStatus Try Dim outputFile As String = _ Path.Combine(BackupFolder, InputFile.Name) File.Copy(InputFile.FullName, outputFile) Console.WriteLine("Copied {0} to {1}.", _ InputFile, outputFile) Catch ex As Exception ' For this simple example, don't do anything if ' the file copy fails. End Try Return ActivityExecutionStatus.Closed End Function protected override ActivityExecutionStatus Execute( 66

ActivityExecutionContext executionContext) { try { string outputFile = Path.Combine(BackupFolder, InputFile.Name); File.Copy(InputFile.FullName, outputFile, true); Console.WriteLine("Copied {0} to {1}.", InputFile, outputFile); } catch (Exception) { // For this simple example, don't do anything if // the file copy fails. } return ActivityExecutionStatus.Closed; } The Execute method requires that you return the current state of the activity—in this example, the activity always returns Closed, indicating that it has finished its processing. Save and compile the project. This action allows Visual Studio to display your custom activity in the Workflow designer’s Toolbox window. Open the workflow in the designer, and verify that you see FileBackupActivity in the Toolbox window. In the workflow, delete the existing Code activity, and drag an instance of the FileBackupActivity from the toolbox into the Replicator activity. Now that you’ve placed the activity in the Replicator, you still must set its InputFile and BackupFolder properties. You can’t set the InputFile property until runtime, but you can set the BackupFolder property in the designer. To do that, select the activity, and then in the Properties window, locate the BackupFolder property. Click the ellipsis to the right of the property. In the dialog box, select the workflow’s backupFolder property, as shown in Figure 6. This allows you to declaratively select the binding for the activity’s BackupFolder property, setting its value to the backupFolder property of the workflow. Figure 7. Bind the activity’s property to a property of the workflow. The ChildInitialized event handler passes your code a ReplicatorChildEventArgs object as its second parameter. This object includes, in addition to the InstanceData property you already saw, an Activity property. This property provides a reference to the current child activity, so that you can work with the child activity (in this case, setting the InputFile property.) In the workflow’s code, add the following code at the end of the existing replicatorActivity1_ChildInitialized event handler so that it creates a reference to the current FileBackupActivity’s instance, and sets its InputFile property: 67

Dim fba As FileBackupActivity = _ CType(e.Activity, FileBackupActivity) fba.InputFile = currentFile FileBackupActivity fba = (FileBackupActivity)e.Activity; fba.InputFile = currentFile; Because the child activity exposes a public InputFile property, you can set the property value from the ChildInitialized event handler, and the child activity can use this information as it performs its actions. Without creating a custom activity, you couldn’t use that information from within the child activity’s actions. Save and execute the application. This time, you’ll see output as shown in Figure 7, including the output from the FileBackupActivity’s Execute override. Figure 8. The custom activity can use the information about the individual data item that it’s processing. You might also want to verify the behavior of the workflow using parallel execution. Because of the way the workflow sets up the file counter, the Until condition won’t operate correctly once you set the ExecutionType property to Parallel. To fix this, back in the Workflow designer, select the Replicator activity. In the Properties window, right-click the UntilCondition property, and select Reset from the context menu. Change the Replicator activity’s ExecutionType property to Parallel (rather than Sequence). You will see that all the files get copied correctly, because the ChildInitialized event handler sets up the properties of each separate instance of the child FileBackupActivity.

Windows Workflow Tutorial: Introduction to Fault Handling Introduction

In all applications you write, you need to trap for errors. This includes data entry errors, such as a user entering no or invalid information in a text box. You also need to trap for exceptions, which are unexpected errors that cause execution to stop. For example, if you have code to read a file and that file does not exist, the .NET Runtime will throw an exception. In code, your primary means of handling exceptions is the try-catch block. Windows Workflow Foundation provides an additional means of handling exceptions. In this tutorial, you will see how to use the FaultHandler activity to handle exceptions in workflows. Review the Sample Workflow

In this tutorial, you will use an existing sequential workflow that models checking the inventory for a list of items and if necessary restocking items. In the Basic Control Flow in Workflows tutorial in this series, you saw how to build this workflow. Rather that rebuild it, you will use this workflow as the starting point for this tutorial. To get started, in Visual Studio 2008 select File | Open | Project/Solution to display the Open Project dialog box. Navigate to the folder when you downloaded this tutorial’s sample project. Select FaultHandlingDemo.sln and click OK to open the project. In the Solution Explorer, double click Inventory.xml. This file contains inventory data for three products and contains the following XML. 68

XML

1 100 50 2 10 250 3 25 50

In the Solution Explorer, double click Workflow1 in the Solution Explorer to open the workflow designer. The workflow looks like Figure 1.

69

Figure 1. You will use this workflow in this tutorial. When the workflow starts, it executes the following code in the WorkflowInitialized event handler: C#

var xmlFile = System.Xml.Linq.XDocument.Load( System.IO.Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Inventory.xml")); inventory = from product in xmlFile.Descendants("Product")     select product; itemCount =   (from product in xmlFile.Descendants("Product")    select product.Element("ProductID").Value).Count();

This code uses a LINQ To Xml query to read the contents of the XML file and determine how many items exist. The workflow then starts executing the ProcessItems While activity. The activities in ProcessItems will execute as long as the KeepProcessing rule condition evaluates to true. This condition is the following: Rule

this.moreToProcess

The LookupProduct activity retrieves the next item in the inventory list. The activity’s ExecuteCode event handler contains the following code: 70

C#

productID = Convert.ToInt32( inventory.ElementAt(nextItemNumber).Element("ProductID").Value); onHand = Convert.ToInt32( inventory.ElementAt(nextItemNumber).Element("OnHand").Value); available = Convert.ToInt32( inventory.ElementAt(nextItemNumber).Element("Available").Value); nextItemNumber += 1; if (nextItemNumber >= itemCount) { moreToProcess = false; }

The workflow next checks if there is sufficient inventory on hand. The SufficientOnHand code condition checks if the amount on hand is greater than 100. If there is sufficient inventory, the ReportOnHand activity displays a message to that effect. If there is not sufficient inventory, the workflow executes the Reorder activity. The workflow next checks if it was able to reorder the product. The IfReordered branch of the CheckReorderStatus IfElse activity uses a code condition to execute the PlaceReorder method. For the purposes of this tutorial, a reorder is successful if there is enough of the product available to have at least 100 units in stock. If the reorder succeeds, the ReportReorder activity displays a message to that effect. If the reorder fails, the ReportFailure activity displays a message to that effect. The final activity reports that the workflow is finished. To review the workflow, press Ctrl + F5 to run the project. You should see the following output: Output

There are 100 units of product 1 on hand. There are only 10 units of product 2 on hand. It is time to reorder. 90 units of product 2 will be ordered. There are only 25 units of product 3 on hand. It is time to reorder. You need 75 units of product 3 but only 50 are available. The product has not been reordered. The workflow has finished executing. Press any key to continue . . .

Press any key to exit the application. Use the WorkflowTerminated Event Handler to Handle Faults

71

This workflow currently works and successfully evaluates each item in the list. You will now see what happens when faults occur in the workflow and you will see how you can handle faults. To introduce an error in the workflow, modify the WorkflowInitialized method so that it fails to load the XML file. The easiest way to do this is to misspell the name of the file. C#

var xmlFile = System.Xml.Linq.XDocument.Load( System.IO.Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "NotFound.xml"));

Save and press Ctrl + F5 to run your project. You should see the following output: Output

Could not find file 'D:\FaultHandlingDemo\bin\NotFound.xml'. Press any key to continue . . .

Press any key to exit the application. To see the code that handles the error, in the Solution Explorer window, double-click Module1.vb or Program.cs. There, you’ll find the following code: C#

workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e) { Console.WriteLine(e.Exception.Message); waitHandle.Set(); };

The workflow attempted to open a file that doesn’t exist. This caused an exception, which caused the workflow to terminate. The WorkflowTerminated event handler takes as an argument an instance of the WorkflowTerminatedEventArgs class. The Exception property of this class contains the exception and the Message property of the Exception class contains the actual message, which the code displays. When you create a workflow project and choose the Sequential Workflow Console Application or State Machine Workflow Console Application template, Visual Studio creates the code above. So by default, you have code to handle exceptions. If you choose any of the other templates, you will have to add this code yourself. Exception Handling in Workflows

The WorkflowTerminated event occurs if an unhandled exception occurs anywhere in the workflow. You should use this as a last resort because you have no opportunity to take an action specific to the error and you have no opportunity to recover from an error because the workflow terminated. A better practice is for you to handle exceptions as close to the source as possible. Activities take actions in a workflow and therefore activities throw exceptions. If the activity that threw the exception doesn’t handle it, the workflow runtime transfers the exception to the parent activity (if there is one). If that activity doesn’t handle the exception, it gets passed to the next activity in the workflow hierarchy. If no activities handle the workflow and the workflow itself doesn’t handle it, the workflow terminates. 72

When you ran the sample application, the workflow started and immediately attempted to load the XML file. The workflow then threw the exception, but did not handle it. So the workflow terminated. Use the FaultHandler Activity to Handle Exceptions

There are two ways you can handle exceptions in a workflow. The first is to use a try-catch block in your code. The try-catch block enables you to handle exceptions in the code that caused the exception. You can then write additional code to take an action. The second way to handle exceptions is to use the FaultHandler activity. This activity handles a specific fault type and enables you to execute workflow activities in response to an exception. You can use these activities to perform cleanup and recovery. You can associate a FaultHandler activity with the workflow itself or with any container activity in the workflow (with the exception of the TransactionScope and CompensatableTransactionScope activities). . You will now add a FaultHandler activity to the workflow to handle the file not found exception. There are multiple ways to do this, including: •

Right-click on the workflow and select View Fault Handlers.



Select View Fault Handlers from the workflow’s popup menu (see Figure 2).



Select Workflow | View Fault Handlers.



Select the View Fault Handlers link in the Properties window.

Figure 2. Use this menu item to add a fault handler to the workflow. After you select the View Fault Handlers option, the workflow designer changes to show you the FaultHandlersActivity designer (see Figure 3). The FaultHandlersActivity is a container activity. You can add one or more FaultHandler activities to it.

73

Figure 3. Use this menu item to add a fault handler to this activity. From the Toolbox, drag a FaultHandler activity into faultHandlersActivity1. Name this activity HandleFileNotFound. At this point, HandleFileNotFound indicates an error. You have not specified what fault type you are handling. To do that, click the FaultType property in the Properties window and then click the ellipsis to the right of that property. This displays the Browse and Select a .NET Type dialog box. In the Type list, expand the mscorlib node. Then select System.IO. In the right hand pane, select FileNotFoundException (see Figure 4) and click OK. Additionally, if you know the fault type, you can type it directly in the FaultType property’s text box.

Figure 4. This fault handler activity will handle FileNotFound exceptions. The FaultHandler activity is also a container. You can add to it one or more activities you want to execute when a FileNotFound exception occurs. From the Toolbox, drag a Code activity into HandleFileNotFound. Name this activity ReportFileNotFound. The workflow designer should now look like Figure 5.

74

Figure 5. This fault handler activity will handle FileNotFound exceptions. Double click ReportFileNotFound and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine( "The inventory information can not be found.\n" + "The workflow will stop executing.\n");

Save and press Ctrl + F5 to run your project. You should see the following output: Output

The inventory information can not be found. The workflow will stop executing. Press any key to continue . . .

Press any key to exit the application. The workflow handles the exception and displays the information you specified. Before you move on, fix the existing error. Modify the WorkflowInitialized method so that it correctly loads the XML file. C#

var xmlFile = System.Xml.Linq.XDocument.Load( System.IO.Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Inventory.xml"));

Next, you will introduce and handle an exception later on in the workflow. Modify the LookupProduct_ExecuteCode method so that it attempts to read an element that does not exist. C#

productID = Convert.ToInt32( inventory.ElementAt(nextItemNumber).Element("Product").Value);

Return to the workflow designer. Return to the view of the workflow itself by taking any of the following actions: •

Right-click on the workflow and select View SequentialWorkflow.



Select View SequentialWorkflow from the workflow’s popup menu.

75



Select Workflow | View SequentialWorkflow.

Save and press Ctrl + F5 to run your project. You should see the following output: Output

Object reference not set to an instance of an object. Press any key to continue . . .

Press any key to exit the application. Figure 6 shows the first few activities in the workflow. The exception occurred in the code in the LookupProduct activity’s Execute event handler. LookupProduct threw the exception, but did not handle it. The workflow runtime then checked if sequenceActivity1 handled the exception. It did not, so the workflow runtime checked if ProcessItems handled it. It did not, so the workflow runtime checked if the workflow handled it. The workflow would handle a FileNotFound exception, but LookupProduct threw a different exception. So the workflow terminated.

Figure 6. These are the first few activities in the workflow. As mentioned before, you can associate a FaultHandler activity with any container activity in the workflow (except the TransactionScope and CompensatableTransactionScope activities). In the workflow, the LookupProduct activity throws the exception. This is a Code activity, so you cannot associate a FaultHandler activity with it. LookupProduct’s parent activity is a Sequence activity (sequenceActivity1) and that activity’s parent activity is a While activity (ProcessItems). You can associate a FaultHandler activity with either sequenceActivity1 or ProcessItems or both. Before you add a FaultHandler activity you should know the exception type it will handle. The easiest way to know is to have Visual Studio tell you. Press F5 to run your project. When the exception occurs, Visual Studio will display the exception in the Exception Assistant (see Figure 7 for Visual Basic and Figure 8 for C#). Press Shift + F5 to stop the application.

76

Figure 7. The Exception Assistant displays the fault type.

Figure 8. The Exception Assistant displays the fault type. Return to the workflow designer. You will now add a FaultHandler activity to sequenceActivity1 to handle the exception. Select View Fault Handlers from the activity’s popup menu (see Figure 9).

Figure 9. Use this menu item to add a fault handler to this activity. From the Toolbox, drag a FaultHandler activity into faultHandlersActivity2. Name this activity HandleReadError. Click the FaultType property in the Properties window and then click the ellipsis to the right of that property to display the Browse and Select a .NET Type dialog box. In the Type list, select mscorlib. In the right hand pane, select NullReferenceException and click OK. 77

From the Toolbox, drag a Code activity into HandleFileNotFound. Name this activity ReportReadError. Double click ReportReadError and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine( "The inventory information can not be read.\n" + "The workflow will stop processing items.\n"); moreToProcess = false;

Setting the moreToProcess field to false ensures the workflow will stop executing the While loop. Save and press Ctrl + F5 to run your project. You should see the following output: Output

The inventory information can not be read. The workflow will stop processing items. The workflow has finished executing. Press any key to continue . . .

Press any key to exit the application. As you have just seen, if you properly handle exceptions that occur in activities, the workflow does not need to terminate. The HandleReadError FaultHandler activity handled the null reference exception and the workflow continued. The While activity ended and the Cleanup activity displayed the message that the workflow finished executing. Use the Throw Activity to Throw a Fault

A common programming pattern is to catch an exception in a try-catch block, perform some action and then rethrow the exception using the Throw statement in Visual Basic or the throw keyword in C#. This enables you to take some action where the exception occurred and then take additional action at a higher level in the program stack. You can apply a similar pattern in your workflows. Suppose you want to log errors that occur in the workflow and further suppose there are five places in the workflow where a particular error can occur. You could handle this in code and call a logging method from the five locations. Another option is to use the Throw activity. This activity enables you to throw an exception. More specifically, it enables you to handle an exception using a FaultHandler activity, perform an action and then rethrow the exception. This will cause the workflow runtime to pass the exception up the workflow hierarchy. You could then handle the exception in either a parent activity or at the workflow level. You can also handle the exception in the application that calls the workflow. To see how to do this, return to faultHandlersActivity2. From the Toolbox, drag a Throw activity into faultHandlersActivity2 below ReportReadError. The workflow designer should now look like Figure 10.

78

Figure 10. Add a Throw activity to the FaultHandler activity. You now need to specify what fault you are throwing. You have three options, including: •

Throw any .NET fault. You could handle a specific fault and then throw a more general fault.



Throw a custom exception. You could create a custom exception class in code and throw an instance of that class.



Rethrow the exception handled by the FaultHandler activity.

To rethrow the exception, click the Fault property in the Properties window and then click the ellipsis to the right of that property. This displays the Bind ‘Fault’ to an activity’s property dialog box. Expand the ProcessItems node, then expand the sequenceActivity1 node, then expand the faultHandlersActivity2 node, then expand the HandleReadError node. Finally, select Fault (see Figure 11). Click OK.

Figure 11. Specify the fault you want to rethrow.

79

The Fault property specifies the exception object. You also need to specify the type of exception. To do that, double click the small icon to the right of the FaultType property name (see Figure 12). This displays the Bind ‘FaultType’ to an activity’s property dialog box. Drill down to HandleReadError as you did before and select FaultType. Click OK.

Figure 12. Double click this icon to specify the type of exception to throw. At runtime, when the null reference exception occurs, the workflow will display the message that the inventory information cannot be read. It will then rethrow the null reference exception. The final step is to handle the exception the second time it occurs. To do that, return to the workflow level fault handlers, and either select View Fault Handlers from the workflow’s popup menu (see Figure 2) or right-click on the workflow and select View Fault Handlers. From the Toolbox, drag a FaultHandler activity into faultHandlersActivity1. Name this activity RehandleReadError. Set the Fault Type property to System.NullReferenceException. You can enter this directly or use the ‘Browse and Select a .NET Type’ dialog box as you did previously. From the Toolbox, drag a Code activity into RehandleReadError. Name this activity LogReadError. The workflow designer should now look like Figure 13.

Figure 13. The workflow will handle the null reference exception. Double click LogReadError and add the following code to the activity’s ExecuteCode event handler: C#

Console.WriteLine( "The workflow will log the read error.\n");

Save and press Ctrl + F5 to run your project. You should see the following output: 80

Output

The inventory information cannot be read. The workflow will stop processing items. The workflow will log the read error. Press any key to continue . . .

Press any key to exit the application. The FaultHandler activity associated with the Sequence activity handles the null reference exception first and the first message displays. The exception is rethrown and the workflow’s FaultHandler activity handles it. The second message displays and the workflow terminates. The Cleanup activity does not execute in this case. Handling Multiple Faults with a FaultHandlers Activity

As you just saw, you can include multiple fault handlers in a single FaultHandlers activity. This is similar to how you can handle multiple exceptions in a try-catch block. In both scenarios, you need to be mindful of the exception hierarchy. For example, the FileNotFoundException class in the System.IO namespace inherits from the IOException class in the same namespace. IOException inherits from the SystemException class in the System namespace and SystemException inherits from the Exception class. If you were handling these exceptions in a try-catch block, you would need to place the more specific handlers above the more general handlers, as shown in the following code: C#

try { // Code that reads a file } catch (FileNotFoundException fileNotFoundException) { // Code to handle this exception } catch (IOException ioException) { // Code to handle this exception } catch (SystemException systemException) { // Code to handle this exception } catch (Exception exception) { // Code to handle this exception }

If you have multiple fault handlers in a FaultHandlers activity, you need to follow the same rule regarding the exception hierarchy. The workflow evaluates FaultHandler activities from left to right so you must put more specific fault handles to the left of less specific ones. Suppose you add handlers for both the FileNotFoundException and IOException exception to a FaultHandlers activity. The FileNotFoundException fault handler must be to the left of the IOException fault handler.

Windows Workflow Tutorial: Rules-Driven WCF Services Windows Workflow Foundation (WF) ships with a robust business rule engine that can be incorporated into workflows to assist in managing business processes. This same business rule processing can be used in various capacities with Windows Communication Foundation (WCF) to enrich its message processing capabilities. This hands on article will walk through using the rules engine to provide routing logic for a WCF message router. Using rules in a web service router

81

When building web services there are times when it is important to be able to write an intermediary service that acts as a router making decisions about incoming messages and properly forwarding those messages onto an actual service or services. This type of router provides an excellent use case for applying business rules using the rules engine in Windows Workflow Foundation. By defining the routing decisions as rules, the criteria and endpoints can be expressed as a set of rules which can be managed separately from the router logic and configuration. An example solution ships in the samples that are part of the Windows Software Development Kit and can be found in the WCF directory (WCF\Extensibility\Rules\WCF_Router\). In this article we will build the rules used by the router and review the code used to invoke those rules to control the message flow in the router. This article also comes with a sample code implementation, available at URL; the article will walk through how and why that sample code project was created. Understanding the WCF router basics

A router acts as an intermediary service accepting messages from clients. After evaluating the message, the router acts as a proxy client, forwarding the message to the service where it can be processed. A router can receive many different messages and make dynamic decisions about the ultimate destination of those messages based on message content and context. Routers can be used for many different reasons in your solution including message filtering for security purposes and service versioning. Figure 1 shows the interaction of the client, services and router in this scenario.

Figure 1: WCF routing example In order for this scenario to work, when the client sends messages intended for the services, it must send them to an address that the router is listening on instead of directly to the service. When configuring an endpoint for a service, most WCF developers are familiar with configuring the address for the endpoint. However, in addition to an address, a ServiceEndpoint can also have a ListenUri property set. When no value is specified for the ListenUri the endpoint is initialized with the address and begins listening. When a value is present for the ListenUri, this becomes the physical address that the endpoint registers and uses to listen for messages arriving. Regardless of whether the endpoint has a ListenUri, it is the Address property that is used to create the service description and is made public to the client. In this scenario, the Address property describes an address that the router is listening on while the ListenUri describes the physical address on which the actual service is listening. Thus, when a client gets metadata from the service it contains the correct contracts and binding information for the service but the address of the router. Figure 2 shows how a given service exposes both the ListenURI and the Address. The Address is the address of an endpoint on the router which the client will use to call the service. The ListenURI is the address the service is listening on and which the router uses to communicate with the service to forward requests from the client.

82

Figure 2: Listen URI and address configuration Figure 3 shows the service configuration for the EchoService including the endpoint settings to handle the address. Notice that while both the address and listenUri settings use the same server and port, the virtual path is unique between them. Configuration

<service name="Microsoft.ServiceModel.Samples.EchoService" behaviorConfiguration="metadataBehavior"> <endpoint address="http://localhost:8000/services/soap12/text" listenUri="service" contract="Microsoft.ServiceModel.Samples.IEchoService" binding="wsHttpBinding" bindingConfiguration="ServiceBinding" />

Figure 3: Configuring an endpoint with a ListenUri In this example the routing decisions will be made by examining the message headers to determine which service should receive the message. For the calculator service, a custom header is used and is defined in the endpoint configuration. The client and service each have their endpoint configured with the details about the header to indicate that the header should be sent with all messages passing through the endpoint. Figure 4 shows the configuration in the app.config for the client. Notice the address points to the router and the header will help the router know where to send the message. Configuration

<endpoint address="net.tcp://localhost:31080/services/soap12/binary"

83

binding="netTcpBinding" bindingConfiguration="NetTcpBinding_ICalculatorService" contract="Microsoft.ServiceModel.Samples.ICalculatorService" name="NetTcpBinding_ICalculatorService">

Figure 4: Configuring a header on an endpoint Using rules in the router logic

The router is implemented as a WCF service and in the logic for the service, it uses a class named RoutingTable to make decisions about where to route messages. Open the Router.sln solution in the solution directory found in the download this article and open the RoutingTable.cs file in the router project. The fields in the RoutingTable class are shown in Figure 5 and include several properties that are only used by the rules, as well as a variable to hold a reference to the RuleEngine and the RuleSet. C#

public class RoutingTable     {         Random randomNumberGenerator;         Message currentMessage;         IList<EndpointAddress> possibleAddress;         EndpointAddress selectedAddress;         XmlNamespaceManager manager;         RuleSet ruleSet;         RuleEngine ruleEngine;       ….    }

Figure 5: The RoutingTable class definition When the RoutingTable class is instantiated by an extension to the ServiceHost, it initializes the RuleEngine and loads the RuleSet from an XML file. The RuleSet name and path to the file are stored as values in the app.config for the router application and are passed to a helper method, examined next, which loads the XML and deserializes it into a RuleSet object. In addition, two other helper variables are initialized which will be used by the rules: a derivative of the XmlNamespaceManager and the Random class. Figure 6 shows the constructor for the RoutingTable class. Notice that the rule engine is initialized with both a type and a ruleset as all rulesets are created based on a given type and must be executed against an instance of that type. C#

public RoutingTable() {             this.randomNumberGenerator = new Random();

84

this.manager = new XPathMessageContext();             this.ruleSet = GetRuleSetFromFile(               ConfigurationManager.AppSettings["SelectDestinationRuleSetName"],               ConfigurationManager.AppSettings["SelectDestinationRulesFile"]);         this.ruleEngine = new RuleEngine(ruleSet, typeof(RoutingTable)); }

Figure 6: Initializing the router table The GetRuleSetFromFile method is responsible for loading the serialized ruleset from the file location specified, then finding and returning the named RuleSet. The WorkflowMarkupSerializer class can be used to serialize and deserialize a RuleSet object to XML. Figure 7 shows the code necessary to load the rules from the file. In this example all exception handling code has been removed for clarity. C#

private RuleSet GetRuleSetFromFile(string ruleSetName, string ruleSetFileName) {        XmlTextReader routingTableDataFileReader = new XmlTextReader(ruleSetFileName);        RuleDefinitions ruleDefinitions = null;        WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer();        ruleDefinitions = serializer.Deserialize(routingTableDataFileReader) as RuleDefinitions;        RuleSet ruleSet = ruleDefinitions.RuleSets[ruleSetName];        return ruleSet; }

Figure 7: Loading a RuleSet from a file The final bit of code needed for the address logic is the method the router service can use to resolve the correct address. In the SelectDestination method of the RouterTable class a Message object is passed and the rules executed. This method is called by the router each time a message arrives in order to determine the destination address to use for forwarding. The business rules update the RoutingTable instance and set the selectedAddress field which can then be returned as the chosen address. The SelectDestination method is shown in Figure 8. C#

public EndpointAddress SelectDestination(Message message) {        this.currentMessage = message;        this.ruleEngine.Execute(this);        return this.selectedAddress; }

Figure 8: Executing rules to select the destination Defining the rules

With the router service in place and the RoutingTable completed, the routing rules need to be written and saved to a file so they can be consumed by the router. WF allows you to build your own UI for creating and editing 85

rules, and allows for rehosting the rules editor dialog that comes as part of WF 3.0. For this example, rather than creating our own UI for creating and editing rules, we will use the External RuleSet Toolkit sample. This sample demonstrates how to create a ruleset outside of Visual Studio and save the resulting XML to a file or a database; for our purposes, it provides an easy UI that we don’t have to code in this article. You can download the sample from MSDN using the link. Once you have downloaded the samples, run the installer to expand them and you will find the External Ruleset Toolkit in the following directory: \WCF\Extensibility\Rules\ExternalRuleSetToolkit. To setup the database used by the External RuleSet Toolkit, double-click the setup.cmd command file found with the sample. Note: The command file assumes that your instance of SQL Server Express is named .\sqlexpress. If you have named your instance something different or you are using another version of SQL Server, you will need to modify the file before executing it. In addition, you will need to update the configuration files in the toolkit projects to point to the correct instance of SQL Server. Once the command file has completed and the database has been configured, open the ExternalRuleSetToolkit.sln solution in Visual Studio. Make any necessary changes to the configuration file for your database to point at the database you just created, and set the start up project by right-clicking on the ExternalRuleSetTool project and selecting Set as startup project from the context menu. Press F5 to run the application and you should see a dialog as shown in Figure 9.

Figure 9: The RuleSet editor tool After starting the ExternalRuleSetTool project the main dialog appears. Click the New button to create a new RuleSet definition and set the Name field to “SelectDestination”. Next, the editor needs to know which CLR type to build the rules against, so click the Browse button to bring up the type selector dialog as shown in Figure 10. Click the Browse button in this new dialog and select the WCF_Router.Router.exe application in the \solution\router\bin directory. Then select the Microsoft.ServiceModel.Samples.RoutingTable type from the list. Once the type is selected the dialog shows all of the fields, properties and methods that will be available in the rules. 86

Figure 10: Type dialog browser in the ExternalRuleSetTool After selecting the type, click OK to return to the main window which shows the type and the RuleSet being edited as shown in Figure 11.

Figure 11: Main dialog ready for editing Click the Edit Rules button to open the Rule Set Editor dialog that ships as part of the .NET Framework runtime components. This editor can be re-hosted in Windows Forms and Windows Presentation Foundation (WPF) applications to enable the viewing, creation, or editing of rules and will be installed on a user’s machine as long as they have the runtime components; no SDK or developer tools need to be installed on the user’s computer. Creating rules involves defining a Condition, Then Actions, and Else Actions. The concept of an If/Then/Else statement is familiar to all .NET developers and makes creating rules a fairly simple procedure. In the case of the rules being defined for this scenario, there are three different steps in the rules in order to correctly identify and choose a destination address: Initialize, Match, and Select. When defining rules that need to execute in a particular order, each rule can be given a priority which instructs the rule engine to evaluate those rules from the 87

highest priority to the lowest. For more information on priority-based execution of rules, see the WF documentation on MSDN. The first rule used in this example initializes several variables before the actual rule processing occurs. Figure 12 shows the definition of the InitializeVariables rule in the RuleSet Editor. Notice that the priority for this rule has a value of “3”.

Figure 12: IntializeVariables rule in the RuleSet Editor To create this rule, click the Add Rule button and enter “InitializeVariables” for the Name and “3” for the Priority. For the Condition, enter “True” which will ensure that the rule always executes. In the Then Actions add the following code to initialize the fields in the RoutingTable class instance. Rule

this.possibleAddress = new System.Collections.Generic.List<System.ServiceModel.EndpointAddress>() this.selectedAddress = null this.manager = new System.ServiceModel.Dispatcher.XPathMessageContext() this.manager.AddNamespace("rt", "http://Microsoft.ServiceModel.Samples/Router")

The second set of rules involves examining the message received and determining if it is a match for a particular endpoint. This set of rules includes one rule for the calculator service and one for the echo service. The priority for both rules is “2” which means both will run after the variables have been initialized. The table below shows the definition for these rules, use the Add Rule button to add each rule and set the correct values for each field. Rule:Calcula torService Condition:

new System.ServiceModel.Dispatcher.XPathMessageFilter("/s12:Envelope/s12:Header/rt:Calculator", this.manager).Match(this.currentMessage) 88

Action:

this.possibleAddress.Add(new System.ServiceModel.EndpointAddress("net.tcp://localhost:31000/calculator/service"))

Rule: EchoService Condition:

new System.ServiceModel.Dispatcher.XPathMessageFilter("/s12:Envelope/s12:Header/wsa10:Action /text()='http://Microsoft.ServiceModel.Samples/IEchoService/Echo'", this.manager).Match(this.currentMessage)

Action:

this.possibleAddress.Add(new System.ServiceModel.EndpointAddress("http://localhost:8000/echo/service"))

Each of these rules adds the endpoint address for the service if a match is found. In the case of the CalculatorService, the rule looks for the custom header to be present on the message using the XPathMessageFilter class. For the EchoService rule the XPathMessageFilter looks at the SOAP:Action header to determine if the message should be routed to the service. Notice that the rules allow for creation of new object instances using defined constructors, making it possible to initialize fields and properties with new values for complex types. Once each of the match rules has executed, the collection of possible addresses has been populated with 0-2 addresses. The next set of rules is responsible for selecting the address to return. These three rules all have a priority of “1” indicating that they will run last. The priorities guarantee that these rules will run after all rules with a higher priority, but there is no guarantee about the order in which rules with the same priority will execute. Use the Add Rule button and the information below to create these three rules. Rule:OneMatch Condition:

this.possibleAddress.Count == 1

Action:

this.selectedAddress = this.possibleAddress[0]

Rule: Multiple Match Condition:

this.possibleAddress.Count > 1

Action:

this.selectedAddress = this.possibleAddress[this.randomNumberGenerator.Next(this.possibleAddress.Count - 1)]

Rule: No Match Condition:

this.possibleAddress.Count == 0

Action:

This.selectedAddress = null

If only a single address match was found, then that single address is used to set the selectedAddress field in the RoutingTable class. In the RoutingTable class, after the rules have executed, it is the selectedAddress field that is returned to the caller of the SelectDestination method. In the case where no matches are found, the selectedAddress is simply set to a null value. The slightly more complex scenario involves handling multiple matches. In this case the Random class is used to choose between the list of endpoints that were possible matches. 89

Once all of the rules have been created the ruleset is complete and ready to be saved to a file. Figure 13 shows the Rule Set Editor dialog when all rules have been created.

Figure 13: All rules in the editor Once all of the rules have been configured, click the OK button to complete the editing. Next choose the Data | Export command from the menu and save the file to “SelectDestionation.rules” in the \solution\router\bin directory. Once saved to the file as configured in the app.config for the router service the rules are available to be used by the router code. Return to the Visual Studio instance with the Router.sln solution loaded and run the solution by pressing F5. The client application will send two messages to the router, the rules will execute for each message and the messages will get forwarded to the correct service. You should be able to see information in the router console about each message being processed including requests and replies.

Windows Workflow Tutorial: Calling a Method in a Host from a Running Workflow Introduction

Although it may not otherwise be obvious, a running workflow and its host application run in separate threads. You may have a need, as you build your application, to provide communication between the host application and the running workflow; although the .NET Framework provides several cross-application communication mechanisms, none are suited specifically for use with Windows Workflow Foundation (WF). Instead, WF provides its own mechanism for allowing the host application to call a method in (and pass information to) a running workflow, and for a running workflow to call a method in the host application. The bi-directional communication between a workflow and its host is loosely coupled, and the mechanism makes it easy for a variety of different applications (perhaps a Windows application, a Web application, a Console application, and a Windows Service) to each host the same workflow. Because the workflow contains 90

no information about the specific implementer of the method it calls in its host, the workflow can simply execute a method, and know that its host reacts to the method call. Clearly, the the host and the running workflow need some mediation—some third party needs to control the communication. The Windows Workflow runtime takes on this task, and as you’ll see in this tutorial, you must indicate to the WF Runtime exactly where it should look for the class that provides the implementation of the interface that defines the communication layer between the host and the workflow. Although setting up the communication between the workflow and the host isn’t difficult, it requires several steps, and you must follow the instructions carefully. Therefore, the examples you’ll find in this tutorial aren’t even vaguely useful, but they do indicate the steps you’ll need to follow in order to communicate both from the host to the workflow, and from the workflow to the host. Calling a Method in the Host Application from the Running Workflow

Imagine that you’ve created a workflow, and this workflow processes files in a folder within the file system. As the workflow processes each file, you’d like the host application to display the file’s name, indicating the current progress of the workflow. Clearly, a Console application must handle this display differently than does a Windows application, but either way, the running workflow simply needs to execute a method in the host application which displays the file name using appropriate means. In order to call a method in the host application, the workflow must include an instance of the CallExternalMethod activity. This activity requires you to indicate a particular interface that defines the external procedure, and allows you to bind parameters (and the return value) for the procedure to properties of the workflow. You interact with the CallExternalMethod activity as you follow the steps in this tutorial. In order to create a method in the host application that you can call from the workflow, you’ll need to tackle a series of steps. You must: •

Create an interface that defines the procedure you want to call. You must attach the ExternalDataExchange attribute to this interface, which marks the interface as a local service interface. In this interface, define any methods that you want to be able to call from the workflow. (Note that overloading isn’t allowed in this context, even though it is a valid code construct.)



Create the workflow, including an instance of the CallExternalMethod activity. Supply the activity with information about the interface, including the specific method it should call.



Create the interface implementation, in the host application. Any class can implement the interface, and in this class, you must provide the actual method(s) that the workflow will call. This implementation of the interface provides the code that the WF Runtime executes when the workflow invokes the method.



Hook up the plumbing, in the code that starts up the Workflow Runtime. Here, you must add a new instance of the ExternalDataExchangeService class as a service for the Workflow Runtime, and you must indicate to the new service exactly which class instance it should look in for the method that the workflow called.

Create the Console Application

To get started, in Visual Studio 2008, create a new Sequential Workflow Library project named FindFilesWorkflow. Once you’ve created the new solution, add a new Sequential Workflow Console Application project to your solution, and name it ConsoleHost. (At this point, your solution contains two projects, each of which contains an empty workflow designer. You might wonder why you created both a workflow library project and a sequential workflow Console application—each project template makes it easier to interact with WF, because each project already includes the necessary assembly references, and the Sequential Workflow Console Application template includes the necessary Workflow startup code. By creating 91

this type of application, you don’t need to write that startup code yourself.) In order to create the separation between the host application and the workflow itself, for the purposes of this demonstration, you’ll delete the workflow designer from the host application, and modify the host’s startup code to refer to the workflow in the library application, instead. (See Figure 1.)

Figure 1. Your solution should contain two projects. In the Solution Explorer window, right-click the ConsoleHost project, and select Select as Startup Project from the context menu. Right-click the project again, and select Add Reference from the context menu. In the Add Reference dialog box, select the Projects tab, select the FindFilesWorkflow project, and click OK to add the reference. Now that you’ve added the reference to the workflow library, you can modify the console application’s start-up code so that it creates an instance of the library’s workflow. To do that, in the Solution Explorer window, double-click Module1.vb or Program.cs, loading the class into the code editor. In the code, find the call to the WorkflowRuntime.CreateWorkflow method. Replace the reference to ConsoleHost.Workflow1 so that the code creates an instance of FindFilesWorkflow.Workflow1: C#

WorkflowInstance instance = workflowRuntime.CreateWorkflow( typeof(FindFilesWorkflow.Workflow1));

In the Solution Explorer window, in the ConsoleHost project, delete the Workflow1.cs or Workflow1.vb project item. Create the Interface

The goal of your workflow is to search for files and take some action as it finds each file; you need some way to report the progress within the host application. Because the workflow has no information about the host application, it can’t display information itself. Instead, it must call a method, defined in an interface, that the host application implements. Although the shared interface could exist within any assembly, for the purposes of this tutorial, you’ll place it within the workflow library. Note: In a real application, this choice may not be a good one because of versioning issues. When using the same assembly, the whoe library’s version number will change every time you change a workflow in the library. You are better off placing the interface into a separate assembly. Once you’ve created the separate assembly, you will simply add references from both the host and the workflow assembly to the interface assembly. We will use the same assembly for the simplicity of this demonstration, but keep it in mind as you create your own applications.

92

In the Solution Explorer window, select the FindFilesWorkflow project. In the menus, select Project | Add New Item. In the Add New Item dialog box, select Interface. Name the new interface ICommunicate, and click Add to create the interface. In C#, add the public keyword, so that the interface is publicly available: C#

namespace FindFilesWorkflow { public interface ICommunicate { } }

Modify the ICommunicate interface, adding the ReportProgress method definition: C#

namespace FindFilesWorkflow { public interface ICommunicate { void ReportProgress(String fileName); } }

In order for the workflow to be able to interact with the interface you’ve created, it must include a marker attribute; that is, an attribute that indicates that the interface can be added as a service to the ExternalDataExchange service in Windows Workflow. To do this, add the ExternalDataExchange attribute to the ICommunicate interface, so that the code looks like the following: C#

[ExternalDataExchange] public interface ICommunicate void ReportProgress(String fileName);

In C#, you must also add a using statement to the top of the file (Visual Basic adds a project-wide Imports statement for the correct namespace). In C#, right-click the new attribute, and select Resolve from the context menu. In the fly-out menu, select the first option, which adds a using statement for the System.Workflow.Activities namespace to the file for you. (Take this as a warning: If you don’t add the ExternalDataExchange attribute to your interface, your workflow will not be able to call the corresponding method(s) in the host application.) Create the Workflow

Now that you’ve created the interface, you can set up the workflow so that it calls the method described in the interface. In the Solution Explorer window, in the FindFilesWorkflow project, double-click the Workflow1.vb or Workflow.cs item, opening the workflow in the Workflow designer. Add a While activity. Within the While activity, add a CallExternalMethod activity. When you’re done laying out activities, the workflow should look like Figure 2.

93

Figure 2. Lay out the sample workflow so that it looks like this. Select View | Code. Add the following statement at the top of the file: C#

using System.IO;

In the Workflow1 class, add the following variable declarations: C#

public string currentFileName; private string[] files; private int totalFiles; private int currentFile;

In the Workflow1 class, add the following procedure: C#

private void SetFiles(string Path) {   if (!String.IsNullOrEmpty(Path))   {     files = Directory.GetFiles(Path);     totalFiles = files.Length;     currentFile = 0;   } }

Within the Workflow1 class, add the following property (note that the property setter calls the SetFiles method, filling in the list of files in the selected path): C#

private string pathValue;

94

public string Path {   get { return pathValue;}   set   {     pathValue = value;     SetFiles(value);   } }

Select View | Designer. Select the While activity, and, in the Properties window, set the Condition property to Declarative Rule Condition. Expand the + to the left of the Condition property, and set the ConditionName property to MoreFiles. Select the Expression property, click the ellipsis to the right of the property value, and in the Rule Condition Editor window, add the following expression (see Figure 3), and click OK when you’re done: Rule

this.CurrentFile < this.totalFiles

Figure 3. Set the While activity’s condition. After configuring the While activity, the Properties window should look like Figure 4.

Figure 4. Configure the While activity. In order to set up the CallExternalMethod activity so that it can call a method outside the workflow, you must supply the name of the interface that defines the method the workflow will call, along with the name of the specific method (and how to bind any parameters, and the return value, if any) for the method. Select Build | Rebuild Solution. Visual Studio will return errors (because you haven’t completed setting up the required properties of some of the workflow activities). In the Workflow designer, select the 95

CallExternalMethod activity. In the Properties window, select the InterfaceType property. Click the ellipsis to the right of the property value, displaying the dialog box shown in Figure 5. This dialog box includes a list of all the available interfaces that have been marked with the ExternalDataExchange attribute. In this case, the list contains only the ICommunicate interface, so select that interface and click OK.

Figure 5. Select from the list of interfaces marked with an ExternalDataExchange attribute. In the Properties window, select the MethodName property. Click the drop-down button to the right of the property value, and select ReportProgress (the name of the method within the interface) from the list. Of course, the ReportProgress method requires a single parameter (the name of the file that the workflow has found), and the Workflow designer adds this property to the Properties window once you select the name of the method you want to call (see Figure 6). Because the parameter name appears within the Properties window, you can bind it to a property of the workflow, just like any other bindable Workflow property.

96

Figure 6. Once you specify an interface and a method within that interface, the designer offers to allow you to bind a value to each of the method’s parameters. In the Properties window, select the fileName property, and click the ellipsis to the right of the property value. In the dialog box, select the workflow’s currentFileName property, as shown in Figure 7. Click OK to create the data binding.

Figure 7. Bind the ReportProgress procedure’s fileName parameter to the workflow’s currentFileName property. Now that you have bound the ReportProgress procedure’s fileName parameter to the workflow’s currentFileName property, when the workflow calls the procedure, it will pass the value in the currentFileName property as the parameter to the ReportProgress procedure. (Note that you haven’t yet implemented the ICommunicate interface, which means you haven’t yet created the ReportProgress method that the workflow will call.) Of course, you must set the private currentFileName property each time the CallExternalMethod activity is about to call the external method. The activity provides its MethodInvoking property so that you can specify such a procedure. In the Properties window, select the MethodInvoking property, and supply the name for the procedure, SetFileName. Press Enter to create the procedure, and modify it so that it sets the current file name and increments the current file number: C#

private void SetFileName(object sender, EventArgs e) {   currentFileName = files[currentFile];   currentFile++; }

Select Build | Build Solution, and verify that the entire solution builds correctly. Create the Interface Implementation

97

You have created the workflow and the interface; now it’s time to create the interface implementation. In the Solution Explorer, right-click the ConsoleHost project, and select Add | Class from the context menu. In the Add New Item dialog box, set the name of the new class to UserInterface, and click Add. At the top of the new class file, add the following statement: C#

using FileFilesWorkflow;

Modify the new class so that it’s serializable, and so that it implements the ICommunicate interface. C#

[Serializable()] class UserInterface: ICommunicate

In Visual Basic, adding the Implements statement also adds the stub for the ReportProgress procedure. In C#, right-click the name of the interface, and select Implement Interface | Implement Interface from the context menu—this creates the ReportProgress procedure stub for you. Modify the new ReportProgress procedure, adding code to display the output in the Console window: C#

public void ReportProgress(string fileName) {   Console.WriteLine("Processing file: {0}", fileName); }

Build the entire solution again, and verify that it builds without errors. Hook Up the Plumbing

You’ve done most of the work, but the Workflow Runtime still doesn’t know how to find the code that you’ve just added, so it calls the code when the workflow executes its CallExternalMethod activity. To hook things up, you must configure the Workflow Runtime, adding an additional service to the runtime, and telling the service to look in a specific instance of the class you just created when it needs to call the ReportProgress method. In the Solution Explorer window, in the ConsoleHost project, double-click the Program.cs or Module1.vb item. In C#, at the top of the file, add the following statement: C#

using System.Workflow.Activities;

Within the Program class, add the following declaration: C#

private static UserInterface ui = new UserInterface();

In the Main procedure, immediately above the declaration of the workflowInstance variable, add the following code which creates and adds an instance of the ExternalDataExchangeService class as a service: C#

var dataService = new ExternalDataExchangeService(); workflowRuntime.AddService(dataService);

Continue the same block of code, adding the following line. This code adds an instance of the UserInterface class (the class that implements the ICommunicate interface), as a service for the ExternalDataExchange service 98

—in other words, the code indicates to the ExternalDataExchangeService instance where it can find the code it needs to call when the workflow instance calls its external method: C#

dataService.AddService(ui);

You still need to specify a path in which to search for files, as you create the workflow. Continue the same code block (immediately above the call to the CreateWorkflow method), adding the following code (this example looks for files in the C:\ folder—use any folder you like, in your code): C#

var parameters = new Dictionary<String, Object>(); parameters.Add("Path", "C:\\");

Finally, modify the call to the CreateWorkflow method, so that you pass the parameters dictionary to the workflow runtime as you create the workflow: C#

WorkflowInstance instance = workflowRuntime.CreateWorkflow(   typeof(FindFilesWorkflow.Workflow1), parameters);

At this point, you’ve done all the work necessary to try out your workflow. The workflow looks in the path you’ve specified for files. As it finds each one within its While activity, it calls the external method you created outside of the workflow. In order to do this, the Workflow Runtime uses the ExternalDataExchange service that you created. Because you specified to the ExternalDataExchange service instance where it could find the instance of the class that implements the interface and method that the workflow expects to call, it routes the method call appropriately. Press Ctrl+F5 to run the project. You should see output as shown in Figure 8. Although the output is somewhat underwhelming, it proves a point: You were able to call a method in the host application from the running workflow, using the ExternalDataExchange service.

Figure 8. The output should look like this. Conclusion

It seems like a lot of work to call a method in the host application from a workflow, but it’s not difficult as long as you follow the required steps. You must: 1.

Define the interface. Create a public interface that defines the communication between the workflow and the host. Make sure to add the System.Workflow.Activities.ExternalDataExchange attribute to the interface.

99

2.

Add the CallExternalMethod activity to the workflow. Set at least the InterfaceType and MethodName properties. Bind parameters and the return value, if you like, to properties of the workflow.

3.

Implement the method(s) in the host. Create a class that implements the communication interface, adding code for the implemented methods. Make this class serializable.

4.

Create and configure the data service. In the host application, create a new instance of the ExternalDataExchangeService class, and add it as a service to the Workflow Runtime. Then, create an instance of the communication class (the class that implements the communication interface) and add it as a service to the new ExternalDataExchangeService instance.

If you follow these steps carefully, you should be able to call methods defined in the host application from your running workflows. In a future tutorial, you’ll learn how to communicate from the host application to the workflow, sending information to the workflow, by raising an event in the host application. The workflow can use its HandleExternalEvent activity to handle the event, and retrieve the information passed in from the host. For now, try creating your own workflow that calls a method in the host application, and verify that you can follow the list of steps presented here.

Windows Workflow Tutorial: Handling an Event from the Host in a Workflow Introduction

In an earlier tutorial in this series, you learned how to call a method in the host application from a workflow (and how to pass information from the workflow to the host, in the parameters to the method). If you haven’t worked through that tutorial, you should do so now—without that information, this tutorial will not make much sense. (This tutorial starts with the finished solution from the earlier tutorial. If you don’t have that solution handy, you can download it here) This technique you’ve already learned doesn’t help, however, if you need to send information from the host application to the running workflow. Maybe you need to indicate to the workflow that a particular condition has been met, or that the application has gathered some information it needs to send to the workflow. Imagine that the host application started the workflow, but the workflow progresses to a point at which it needs input. It must get that input from the host application, so it must wait until the host has gathered the necessary information. In order to pass the information from the host application to the workflow, the host application can raise an event, so that the workflow can handle the event using the HandleExternalEvent activity. In order to pass information from the host to the workflow, you’ll need a class that defines the event arguments. If you weren’t building an application using Windows Workflow Foundation, you could simply inherit from the System.EventArgs class, adding the specific information required by your event. In this case, however, the stakes are a bit higher, and your event argument class must meet specific requirements. Your class must support these characteristics: •

It must inherit from System.Workflow.Activities.ExternalDataEventArgs.



It must be serializable.

• It must provide a non-default constructor that accepts a workflow instance ID (a Guid) as its parameter. Using this information, the event argument object can determine the specific instance of the workflow to which it was sent. 100

In order to pass information to the workflow, you’ll create a class that meets these criteria. Just as in the previous example, you’ll rely on the Windows Workflow Foundation’s ExternalDataExchange service. In this exercise, you’ll modify the workflow you created in the previous exercise, and have the workflow wait for you to supply a path in which to search, in the host application. Modify the Workflow In order to handle the event raised by the host application, the workflow needs to include a HandleExternalEvent activity, and this activity is useful when placed within a Listen activity. The Listen activity contains two or more branches, and the first activity in each branch must implement the IEventActivity interface—that is, the first activity must be configured so that it waits for some event to occur (either an event raised externally, or a timer event within the workflow, for example). The Windows Workflow Foundation includes two activities that implement this interface—the Delay activity and the HandleExternalEvent activity. Each branch, then, waits until its first child activity’s event occurs, and the Listen activity executes the remainder of the activities within the single branch. All other branches never execute. In other words, the first branch whose event occurs “wins”. For this simple example, however, you’ll work with the HandleExternalEvent activity on its own. If you closed the solution you created in the first part of this tutorial, re-open it in Visual Studio 2008 now. In the Solution Explorer window, double-click the Workflow1.vb or Workflow1.cs item, loading it into the Workflow designer. From the Toolbox, drag a HandleExternalEvent activity immediately above the existing While activity. When you’re done, the workflow should look like Figure 1. Figure 1. The completed layout should look like this. You should imagine that this small workflow is actually part of a larger workflow in which the workflow has reached a point in its processing at which it requires input from the user. For this simple workflow, you could simply supply the path as a parameter, as you did in the previous exercise. Use your imagination here. At this point, the workflow is broken—until you supply information about the event to the HandleExternalEvent activity, the application can’t run. The next few sections walk you through building all of the necessary infrastructure so that the workflow can handle the event. Add the Event Argument Class In order to pass information from the host to the workflow, you must create a class that meets specific requirements to act as the event argument. In the Solution Explorer window, right-click the FindFilesWorkflow project, and select Add | Class from the context menu. Name the new class InfoEventArgs, and click Add. Add the following statement to the top of the new code file: Imports System.Workflow.Activities using System.Workflow.Activities; Modify the InfoEventArgs class so that it inherits from the ExternalDataEventArgs class, and ensure that the class is public. Next, add the Serializable attribute to the class: <Serializable()> _ Public Class InfoEventArgs Inherits ExternalDataEventArgs End Class 101

[Serializable()] ublic class InfoEventArgs: ExternalDataEventArgs Inside the InfoEventArgs class, add a property to contain the path in which the workflow should search: Private pathValue As String Public Property Path() As String Get Return pathValue End Get Set(ByVal value As String) pathValue = value End Set End Property public string Path { get; set; } Inside the class, create a constructor that accepts a Guid value and the path value, and pass the Guid value to the base class’ constructor: Public Sub New(ByVal instanceId As Guid, Path As String) MyBase.New(instanceId) Me.Path = Path End Sub public InfoEventArgs(Guid instanceId, string Path): base(instanceId) { this.Path = Path; } Select File | Save All to save the entire solution. Modify the Interface In the previous exercise, you created an interface that defines the interaction between the host and the workflow. In this exercise, you’ll extend that interface, adding information about the event the host application will raise in order to send information to the workflow. In the Solution Explorer window, in the FindFilesWorkflow project, double-click the ICommunicate.vb or ICommunicate.cs item. Add the following declaration to the existing interface: Event PathReceived As EventHandler(Of InfoEventArgs) event EventHandler PathReceived; Note: In a real application, you are better off placing the shared interface in a separate assembly. As it is, in this example, if you modify the workflow, you modify the versioning associated with the interface. This can cause 102

trouble with persisted workflows. For this simple demonstration, it’s not worth the overhead of creating a separate project, but in more complex projects, place the shared interface in a separate assembly and reference it from both the host and the workflow assemblies. Finish the Workflow Now that you have completed the interface and the event arguments, you can finish the workflow by indicating the specific event that the workflow should wait for. Again open Workflow1 in the Workflow designer, and select the HandleExternalEvent activity. In the Properties window, select the InterfaceType property, click the ellipsis to the right of the property value, and select ICommunicate from the list of available interfaces. Select the EventName property, and from the drop-down list of event names, select PathReceived. (Note that this action adds e and sender properties to the Properties window, corresponding to the parameters passed into the event handler. You don’t need to interact with the sender property, but the e property provides information sent from the host, and you’ll need to gather than information.) Figure 2 shows the current state of the Properties window. Figure 2. The properties window, after you have set the InterfaceType and EventName properties. Because your workflow needs to capture the Path property of the event argument sent by the host to the workflow, you must modify the workflow so that it includes an InfoEventArgs variable into which to place the value. Select View | Code to load the workflow’s code, and add the following property to the class (note that this property sets the value of the workflow’s Path property, in the property setter): Private argsValue As InfoEventArgs Public Property args() As InfoEventArgs Get Return argsValue End Get Set(ByVal value As InfoEventArgs) argsValue = value Path = args.Path End Set End Property private InfoEventArgs argsValue; public InfoEventArgs args { get { return argsValue; } set { argsValue = value; this.Path = value.Path; } 103

} Select View | Designer. Select the HandleExternalEvent activity, and in the Properties window, select the e property. Click the ellipsis to the right of the property, and select the args property from the list of values, as shown in Figure 3. Click OK when you’re done. You have specified that the HandleExternalEvent activity should store the value in its e parameter into the workflow’s args property, which, in turn, sets the value of the workflow’s Path property. (Note that if you need addition processing when the event occurs, you can create a handler for the activity’s Invoked event. You can access the argument from this event handler, as well, and you can add any necessary processing in this handler.) Figure 3. Bind the second event parameter to the args property in the workflow. Modify the Host Interface Implementation Because you have modified the communications interface, you must modify the class within the host application that implements this interface. In the Solution Explorer window, in the ConsoleHost application, double-click the UserInterface.vb or UserInterface.cs item. In Visual Basic, click on the line of code including the Implements keyword, press End, and press Enter. This forces the editor to add the event declaration to the implementation. In C#, right-click the interface name, and select Implement Interface | Implement Interface from the context menu. This action adds the following statement to the implementation: Public Event PathReceived(ByVal sender As Object, _ ByVal e As FindFilesWorkflow.InfoEventArgs) _ Implements FindFilesWorkflow.ICommunicate.PathReceived public event EventHandler PathReceived; It’s up to you to add the code that raises this event, and to do that, add the following procedure to the UserInterface class. This code accepts the workflow’s instance ID (which you don’t yet have a way to capture) and the path to be searched. It sets up the new InfoEventArgs object, and raises the PathReceived event: Public Sub RaisePathReceived( _ ByVal instanceId As Guid, _ ByVal Path As String) Dim args As New InfoEventArgs(instanceId, Path) RaiseEvent PathReceived(Me, args) End Sub public void RaiseNameReceived( Guid instanceId, string Path) { if (PathReceived != null) { InfoEventArgs args = new InfoEventArgs(instanceId, Path); PathReceived(null, args); } 104

} Modify the Startup Code Everything is in place to send information from the host to the workflow, except for the actual code that raises the event. In this example, you must modify the application’s Main procedure, to prompt the user for a path. Given the path, the host raises the appropriate event to indicate to the workflow that the user has entered the information. (Note that you although you could place this user-interface code in one of the workflow events, such as the WorkflowIdled event, doing so would cause a problem. These events block the workflow thread and any longrunning work. If you want to gather user input in one of the workflow events, you should place the code in a separate thread. Investigate the System.Threading.ThreadPool.QueueUserWorkItem method to gather user input in a separate thread.) In the Solution Explorer window, double-click the Program.cs or Module1.vb item. Note that the Main procedure already includes code that adds the ExternalDataExchangeService, and an instance of the UserInterface class as the communications class—you don’t have to add this code, it already exists: Dim dataService As New ExternalDataExchangeService workflowRuntime.AddService(dataService) dataService.AddService(ui)) var dataService = new ExternalDataExchangeService(); workflowRuntime.AddService(dataService); dataService.AddService(ui); The code currently passes a dictionary containing information about the Path property to the workflow. To test the event mechanism, remove that code. To do that, start by commenting out the following two lines of code: ' Dim parameters As New Dictionary(Of String, Object) ' parameters.Add("Path", "C:\") // var parameters = new Dictionary<String, Object>(); // parameters.Add("Path", "C:\\"); Modify the call to the CreateWorkflow method, removing the parameters variable as the second parameter. When you’re done, the call to the CreateWorkflow method should look like this: workflowInstance = workflowRuntime.CreateWorkflow( _ GetType(FindFilesWorkflow.Workflow1)) WorkflowInstance instance = workflowRuntime. CreateWorkflow(typeof(FindFilesWorkflow.Workflow1)); In the Main procedure, immediately preceding the call to the waitHandle.WaitOne method, add the following code: Console.WriteLine("Enter the search path:") Dim path As String = Console.ReadLine() ui.RaisePathReceived(workflowInstance.InstanceId, path) 105

Console.WriteLine("Enter the search path:"); string path = Console.ReadLine(); ui.RaisePathReceived(workflowInstance.InstanceId, path); .Finally, press Ctrl+F5 to execute the workflow. When the workflow executes the HandleExternalEvent activity, it idles, waiting for the host to raise the PathReceived event. This code retrieves the path from you, and once you supply it, the code raises the event to the workflow, allowing it to continue processing, given the information you supplied. Conclusion Raising an event from the host application to the workflow sure seems like a lot of steps! It does require a lot of steps, and you need to pay attention to a lot of detail. To raise an event from the host to the workflow, you must at least accomplish these goals: 1. Handle event arguments. You must create a class that contains the event argument information you want your event to be able to send to the workflow. This class must inherit from System.Workflow.Activities.ExternalDataEventArgs, and it must be serializable. 2. Define the communications interface. You must create an interface marked with the System.Workflow.Activities.ExternalDataExchange attribute applied to it. In the interface, add the definition of any event you want to raise from the host to the workflow. 3. Define the host. Create the host application (a console application, a Windows application, or any other kind of application), including an implementation of the communications interface. The class that implements the interface must also be serializable. 4. Handle the event in the workflow. Add a HandleExternalEvent activity, and set at least its InterfaceType and EventName properties. If you want to receive information from the event, consider binding the event argument to a corresponding property in the workflow’s class. 5. Create and configure the data service. In the host application, create a new instance of the ExternalDataExchangeService class, and add it as a service to the Workflow Runtime. Then, create an instance of the communication class (the class that implements the communication interface) and add it as a service to the new ExternalDataExchangeService instance. 6. Raise the event. In the host application, at the appropriate time, raise the event. If you’ve followed all the steps carefully, the workflow receives the event, and handles it. Although it seems difficult to communicate between the host application and the workflow, it’s really not. Follow the steps carefully, and you will be able to send information from the host application to the workflow without any problems.

Windows Workflow Tutorial: Rules-Driven .NET Applications Windows Workflow Foundation ships with a robust business rule engine that can be incorporated into workflows to assist in managing business processes. What some developers do not realize is that the rule engine can be used outside of workflows in any .NET application to provide robust rule processing against any .NET object. This hands-on article will walk through how developers can take advantage of using the rule engine in .NET applications through examples using WPF and ASP.NET. Using rules in ASP.NET applications

In this example rules will be used to control the user interface for a user to ensure that all required information is selected. The user interface consists of simple wizard using the MultiView control to collect loan application 106

information (the sample is greatly simplified). Certain steps in the wizard are only required for applications in particular states and should not be shown to other applicants. Rules will be used to skip certain steps as the user progresses through the interface. The MultiView control was chosen over the Wizard control because it has better support for removing steps.

The image below show the main wizard steps in order as shown in Visual Studio.

Figure 1: LoanWizard in Visual Studio The starter user interface is included in the before directory of the downloadable code sample. Creating the rules

Once the user interface is created with all of the steps defined, the next step is to create the business rules. WF allows you to build your own UI for creating and editing rules, and allows for rehosting the rules editor dialog that comes as part of WF 3.0. For this example, rather than creating our own UI for creating and editing rules, we will use the External RuleSet Toolkit sample (http://msdn.microsoft.com/en-us/library/bb472424.aspx). This sample demonstrates how to create a ruleset outside of Visual Studio and save the resulting XML to a file or a database; for our purposes, it provides an easy UI that we don’t have to code in this article. You can download the sample from MSDN using the link. Once you have downloaded the samples, run the installer to expand them and you will find the External Ruleset Toolkit in the following directory: \WCF\Extensibility\Rules\ExternalRuleSetToolkit. To setup the database used by the External RuleSet Toolkit, double-click the setup.cmd command file found with the sample. Note: The command file assumes that your instance of SQL Server Express is named .\sqlexpress. If you have named your instance something different or you are using another version of SQL Server, you will need to modify the file before executing it. In addition, you will need to update the configuration files in the toolkit projects to point to the correct instance of SQL Server. Once the command file has completed and the database has been configured, open the ExternalRuleSetToolkit.sln solution in Visual Studio. Make any necessary changes to the configuration file for 107

your database to point at the database you just created, and set the start up project by right-clicking on theExternalRuleSetTool project and selecting Set as startup project from the context menu. Press F5 to run the application and you should see a dialog as shown in Figure 2.

Figure 2: External RuleSet Tool Click the “New” button to create a new ruleset with an initial version of 1.0, and change the name of the ruleset to “LoanWizardRules”. Typically Rules are articulated using the properties of a Fact – For e.g. in the following rule, Applicant and Loan are Facts and Age and IsApproved are properties of these facts. If Applicant.Age < 21 Then Loan.IsApproved = false

A Fact can be thought of as an object instantiation of a type. In the above example Applicant is an object of type LoanApplicant and Loan is an object of type LoanApplication. In Windows Workflow Foundation rather than authoring rules against many different fact types, a single type called the root type is used. Thus all rulesets are defined in relation to a single specific .NET type. For this example, the rules are defined against the System.Web.UI.WebControls.MultiView type. Defining rules against ASP.NET page types can be difficult as the type at runtime is generated from the ASPX being compiled so a specific control type was chosen for this sample. To select the type the rules will be authored against, click the “Browse” button. In the resulting dialog, shown in Figure 3, the specific type must be found. Click the “Browse” button and select the System.Web.dll assembly in the c:\windows\microsoft.net\framework\v2.0.50727\ directory. Browse through the list of types and select the System.Web.UI.WebControls.MultiView type as shown in Figure 3.

108

Figure 3: Choosing the target type for the ruleset Click OK to close the type selection dialog; you are now ready to edit the actual rules in the ruleset can Click the “Edit Rules” button which will open the Rule Set Editor dialog. The Rule Set Editor dialog is not part of the External RuleSet Toolkit, it is installed as part of the .NET Framework 3.0 runtime installation and resides in the System.Workflow.Activities assembly. The toolkit provides one example of how the dialog can be re-hosted in an application to allow rule display and/or editing. For this example, the rules should remove the NY and CA legal views if the user did not select one of those states on the first page. To add the rule for New York, click the Add Rule button, then edit the Name field to set the value as “NY”. Next, enter the following code for the Condition field; this is the Boolean expression as you would use in an IF statement in code. ((System.Web.UI.WebControls.DropDownList)this.FindControl("State")).SelectedValue != "NY"

This condition checks that the State dropdown list on the first page of the wizard is not set to “NY”. In the Then Action enter the following code to remove the page from the wizard that includes NY specific content. this.Views.RemoveAt(1)

Now add a new rule for California by clicking the Add Rule button and then entering “CA” for the Name field. For the Condition, enter a similar test of the State dropdown control, this time making sure the value is not “CA”. ((System.Web.UI.WebControls.DropDownList)this.FindControl("State")).SelectedValue != "CA"

In the Then Actions, enter the following code to remove the California specific content from the wizard. this.Views.RemoveAt(2)

Finally change the value in the Priority field to “2”, giving this rule a higher priority than the other. 109

The result of adding these two rules should look similar to the dialog in Figure 4.

Figure 4: Rules for removing wizard steps Each rule has a priority indicating that the California rule will run first as it has the higher priority. This ensures that views are removed based on the index from highest to lowest to so the correct views are removed. Click OK to exit the rule editor dialog and return to the main window of the toolkit application. Once the rules are defined, they must be saved in the External RuleSet Tool window. Press CTRL + S or choose the File | Save menu option). The tool will save the data in the SQL Server database created with the setup command earlier. Defining a wrapper class

Within an application, the code to execute the rules involves retrieving the ruleset from the database, file or other location and then executing them using the rule engine. Rather than code all of this into each page or window of an application, a static class can be used to simplify the code in each page and centralize the code used to retrieve and execute the rules. Right-click the RulesInASP website project and choose Add Reference from the context menu. Select the System.Workflow.Activities andSystem.Workflow.ComponentModel assemblies. These assemblies contain the rule related classes and the classes used to serialize rulesets. Next, right-click the App_Code directory in the RulesInASP website and choose Add New Item from the context menu. In the new item dialog, select the Class template and name the file “RulesMediator.cs”. Change the class definition to make the class public and static. Also add a dictionary for caching the rules and a static constructor which initializes the dictionary to an empty collection. The resulting class should look like Figure 5. C#

public static class RulesMediator

110

{ static Dictionary<string, RuleSet> ruleCache;     static RulesMediator()     {         ruleCache = new Dictionary<string, RuleSet>();     } }

Figure 5: RulesMediator class construction Next the class needs the ability to get the ruleset from the database which is a combination of getting the XML representation of the rules from the database and deserializing that into a RuleSet object. Simple ADO.NET data access can be used to get the XML as a string and the WorkflowMarkupSerializer class is used to deserialize the XML into a RuleSet. Add a static method to the RulesMediator class called GetRuleSet which selects the most recent version of a ruleset by name from the database created by the ExternalRuleSetToolkit. After retrieving the XML for the ruleset, use the WorkflowMarkupSerializer class’ deserialize method to get the XML converted to a RuleSet object as shown in Figure 6. C#

private static RuleSet GetRuleSet(string ruleSetName) { using (SqlConnection cnn = new SqlConnection( ConfigurationManager.ConnectionStrings[ "Rules"].ConnectionString)) { using (SqlCommand cmd = new SqlCommand( "SELECT TOP 1 [RuleSet] FROM RuleSet WHERE Name=@name ORDER BY MajorVersion DESC , MinorVersion DESC", cnn)) {                      cmd.Parameters.Add("@name",                            System.Data.SqlDbType.NVarChar, 128);                      cmd.Parameters["@name"].Value = ruleSetName;                      cnn.Open();                      string rules = cmd.ExecuteScalar().ToString();                      WorkflowMarkupSerializer serializer =                             new WorkflowMarkupSerializer();                      RuleSet ruleset =                            (RuleSet)serializer.Deserialize(                                   XmlReader.Create(                                          new StringReader(rules)));                      return ruleset;

111

} } }

Figure 6: Retrieving and deserializing a ruleset Rather than having to retrieve the rules for each execution they can be cached in the local dictionary. This is accomplished by creating a GetRules method which gets the rules from the cache or loads them from the database and then puts them in the cache. Add the method, shown in Figure 7, to the RulesMediator class. Notice that the GetRules method is public, while the previous GetRuleSet method is a private method. C#

public static RuleSet GetRules(string ruleSetName) {        if (ruleCache.ContainsKey(ruleSetName))               return ruleCache[ruleSetName];        else        {               RuleSet rules = GetRuleSet(ruleSetName);               ruleCache[ruleSetName] = rules;               return rules;        } }

Figure 7: Caching rules in memory for performance The other method to add to the RulesMediator is the RunRules method which provides the actual execution of a set of rules on a given instance of an object. Because rules are authored against a particular type, the execution of those rules must be against an instance of that type. The RunRules method is defined as a generic method where the type defines the type of the instance. In addition, the name of the RuleSet to run is passed as a parameter. Create the RunRules method in the RulesMediator class based on the sample in Figure 8. C#

public static void RunRules(T target, string rulesName) {        RuleSet rules = GetRules(rulesName);        RuleEngine engine = new RuleEngine(rules, typeof(T));        engine.Execute(target); }

Figure 8: Executing a ruleset In order to execute rules, the RuleSet must be created or retrieved as shown. Next, an instance of the RuleEngine must be created passing in the RuleSet and the type of the object on which the rules will execute. Finally, the execute method of the RuleEngine class is called which runs the rules. After this method has executed the object passed to the RuleEngine may have been updated by rules and can be inspected for changes. Essentially, the object passed to the RuleEngine as the input is also the output. 112

Executing the rules

To execute the rules in a given page of the .NET application is quite simple as it involves a single call to the static RunRules method of the RulesMediator class. Update the WizardNext_Click event handler with the code in Figure 9. This code will execute when the user clicks the next button and will ensure that the correct panels in the wizard are shown or hidden accordingly. C#

protected void WizardNext_Click(object sender, EventArgs e) {        RulesMediator.RunRules<MultiView>(               LoanWizard, "LoanWizardRules"); }

Figure 9: Executing rules in the web page Now the page has very simple code that runs the rules and updates the user interface based on those rules. To test the application, choose Debug | Start Debugging from the Visual Studio menus, or press F5. On the default page, select a state and then click the Next button. If you chose NY or CA, you should be presented with a legal page specific to that state. If not, you should skip directly to the view where you can input loan amount and term. What have we done so far

In this first example, the rules regarding data collection in a user interface were extracted from the code and encapsulated in an externally editable business rule definition. For this simple example it may not seem like an incredible achievement, but simply consider how much more complex the ASP.NET code would become with a real loan application and all the associated rules. The code in the web pages is extremely concise and simple, while the business rules can become much more complex.

Using rules in a WPF application

The same concepts used in the previous example can also be used in other .NET applications. In this example the Windows Workflow Foundation business rules engine is used in conjunction with the data validation features found in Windows Presentation Foundation. Instead of writing rules based on a user interface class, the rules in this application are written against a business object. The LoanApplication class

The business object used in this example is a simple LoanApplication class that implements the IDataErrorInfo interface. As of .NET Framework 3.5 WPF data binding supports the IDataErrorInfo interface for data validation. If a particular property on the class is in error, this interface exposes the error information returned using the Indexer property. To get started, open the HandsOn.WFRulesInNetApps.sln file from the before directory if you don’t already have it open from the previous example. Open the LoanApplication.cs file in the RulesInWPF project. The class provides a simple business object with properties for a name and loan terms. Add a declaration of a dictionary to hold any errors that are created by the business rules and two properties defined on the IErrorInfo interface as shown in Figure 10. C#

private Dictionary<string, string> errors = new Dictionary<string, string>(); string IDataErrorInfo.Error

113

{ get { return string.Empty; } } string IDataErrorInfo.this[string columnName] { get { RulesMediator.RunRules(                      this, "WPFLoanRules"); if (errors.ContainsKey(columnName)) return errors[columnName]; else return String.Empty; } }

Figure 10: Errors dictionary Make sure you have your class declared to implement the IErrorInfo interface as shown here: public class LoanApplication : IDataErrorInfo

Notice that as the first step in checking for errors in the indexer, the business rules policy is called by using the same RulesMediator class created in the ASP.NET example. This “validate” method runs all of the business rules for the loan application when requested by the user interface. In your own applications you may choose to put the validation triggers in different places based on your requirements and when it is most appropriate to validate your business objects. Writing the rules

Using the External RuleSet Toolkit the rules are built against the LoanApplication type. The first rule checks the LoanAmount property and if it exceeds 350,000 uses the SetError method to set an error message indicating this amount is too high. The second rule checks the combination of the LoanAmount and LoanTerm properties and if the LoanAmount is less than 30,000 and the LoanTerm is more than 5, sets an error on the LoanTerm property. To begin, open the ExternalRuleSetToolkit.sln solution from the SDK samples directory as you did in the earlier sample. Click the New button and enter “WPFLoanRules” for theRuleSet Name field. Click the Browse button on the main dialog, and then again on the Type Selection dialog. Browse to the RulesInWPF.exe assembly and select the LoanApplication type as shown in Figure 11.

114

Figure 11: Selecting the LoanApplication type Now that a type has been selected, click the Edit Rules button to invoke the Rule Set Editor. Click the Add Rule button and name the new rule “LoanAmount”. Set the Condition to the following code to test the LoanAmount: this.LoanAmount > 350000

In the Then Actions, use the following code to set the error on the LoanApplication: this.errors["LoanAmount"] = "Amount must be less than $350,000" Next, use the following code in the Else Actions pane to remove any existing errors for the LoanAmount property. this.errors.Remove("LoanAmount")

There are two interesting things to note about these statements. First, the Else Action is optional and in this case it makes sense to remove any existing errors for a particular field, but it may not in cases with more rules and multiple validation requirements for a given field. Also, the errors field is an internal field, but we are able to manipulate it directly in the rules rather than having to expose a public method allowing for setting error messages. Add another rule by clicking the Add Rule button and setting the Name to “InvalidTermAndAmount”. Also set the Priority of this rule to “2”, ensuring that it runs before the other rule. This is important as the mechanism for showing errors, discussed below, will only show a single error. For the Condition enter the following code to check both the LoanAmount and LoanTerm properties. this.LoanAmount < 30000 && this.LoanTerm > 5

Set the Then Actions to set errors on both the LoanAmount and LoanTerm properties if the conditions are met. this.errors["LoanAmount"] = "The loan term is too long for the loan amount"

115

this.errors["LoanTerm"] = "The loan term is too long for the loan amount"

Set the Else Actions to clear errors on both fields if the conditions are not met. this.errors.Remove("LoanAmount") this.errors.Remove("LoanTerm")

Figure 12 shows the completed rules in the editor.

Figure 12: WPFLoanRules defined Click the OK button to close the editor dialog and then choose File | Save from the window menu. Close the ExternalRuleSetTool application and return to the Visual Studio solution for this example. Provide user feedback

WPF supports the IDataErrorInfo interface and will highlight an input field when the data source reports an error. However, in order to provide the user the error message, the developer needs to apply a bit more code. One way this can be accomplished is using a style with a trigger. The example style below is included as a resource in the XAML file for the input window and causes the tooltip for the control to show the actual error message. XAML

<Style TargetType="TextBox"> <Style.Triggers>

116

<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>

Now when a user enters text in the dialog, the WPF databinding technology kicks in and when the IDataErrorInfo indexer is called the rules are executed. The images below show the validation in action on a simple entry form where the TextBox controls are bound to an instance of the LoanApplication class. Run the RulesInWPF application to test for yourself.

Figure 13: Rules in action Conclusion

In this example the validation of a business object in a WPF application is managed by an external ruleset. The rules about what makes the loan and it’s various properties valid are defined and managed in the rules and can be updated without having to re-deploy the application. In addition, the rules are integrated into the standard WPF data validation framework to provide immediate feedback to the user.

117

Related Documents