Professional Documents
Culture Documents
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.
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).
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.
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.
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
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).
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\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\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
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.
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.
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.
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.
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
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
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!
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
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.
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.
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.
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.
• 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.
28
Figure 11. The completed layout should look like this.
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.
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.
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 compile-
time 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.
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.
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?
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 “round-
robin” execution of multiple child activities.
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.
35
<OnHand>100</OnHand>
<Available>50</Available>
</Product>
<Product>
<ProductID>2</ProductID>
<OnHand>10</OnHand>
<Available>250</Available>
</Product>
<Product>
<ProductID>3</ProductID>
<OnHand>25</OnHand>
<Available>50</Available>
</Product>
</Products>
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;
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;
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.
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.
• Relational: ==, =, !=
• Arithmetic: +, - , *, /, MOD
• 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.
onHand, ProductID);
onHand, ProductID);
Select View | Designer to return to the workflow designer. The workflow should look like
39
Figure 5. The workflow should look like this.
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.
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.
42
100 – onHand, ProductID);
ProductID, available);
Select View | Designer to return to the workflow designer. The workflow should look like Figure 8.
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.
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.
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 =
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;
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.
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.
private decimal budget2 = 10000M;
47
private decimal budget3 = 25000M;
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.
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.
outOfMoney1 = true;
biddersOut += 1;
Console.WriteLine("Bidder 1 is out\n");
else
currentBid += nextBid1;
lastBidder = 1;
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
{
outOfMoney2 = true;
biddersOut += 1;
Console.WriteLine("Bidder 2 is out\n");
else
currentBid += nextBid2;
lastBidder = 2;
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))
outOfMoney3 = true;
biddersOut += 1;
Console.WriteLine("Bidder 3 is out\n");
else
currentBid += nextBid3;
lastBidder = 3;
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.
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.
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.
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.
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.
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 workflow-
level 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))
<Description("BackupFolder")> _
<Category("BackupFolder Category")> _
<Browsable(True)> _
64
<DesignerSerializationVisibility( _
DesignerSerializationVisibility.Visible)> _
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 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.
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.
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
<Products>
<Product>
<ProductID>1</ProductID>
<OnHand>100</OnHand>
<Available>50</Available>
</Product>
<Product>
<ProductID>2</ProductID>
<OnHand>10</OnHand>
<Available>250</Available>
</Product>
<Product>
<ProductID>3</ProductID>
<OnHand>25</OnHand>
<Available>50</Available>
</Product>
</Products>
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 =
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;
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 . . .
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 . . .
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.
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.
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:
• Select View Fault Handlers from the workflow’s popup menu (see Figure 2).
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.
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(
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 . . .
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:
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 . . .
76
Figure 7. The Exception Assistant displays the fault type.
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(
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 . . .
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.
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.
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(
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 . . .
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
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.
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.
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">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8000/echo" />
</baseAddresses>
</host>
<endpoint address="http://localhost:8000/services/soap12/text"
listenUri="service"
contract="Microsoft.ServiceModel.Samples.IEchoService"
binding="wsHttpBinding"
bindingConfiguration="ServiceBinding" />
<!-- Echo service metadata endpoint. -->
</service>
83
binding="netTcpBinding"
bindingConfiguration="NetTcpBinding_ICalculatorService"
contract="Microsoft.ServiceModel.Samples.ICalculatorService"
name="NetTcpBinding_ICalculatorService">
<headers>
<Calculator xmlns="http://Microsoft.ServiceModel.Samples/Router" />
</headers>
</endpoint>
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;
….
}
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));
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;
this.currentMessage = message;
this.ruleEngine.Execute(this);
return this.selectedAddress;
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.
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.
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.
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.
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.)
workflowRuntime.CreateWorkflow(
typeof(FindFilesWorkflow.Workflow1));
In the Solution Explorer window, in the ConsoleHost project, delete the Workflow1.cs or Workflow1.vb project
item.
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
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]
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.)
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;
private string[] files;
private int totalFiles;
private int currentFile;
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 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.
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()]
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)
Build the entire solution again, and verify that it builds without errors.
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;
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.
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.
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 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<InfoEventArgs> 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 long-
running 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.
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.
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.
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.
static Dictionary<string, RuleSet> ruleCache;
static RulesMediator()
{
ruleCache = new Dictionary<string, RuleSet>();
}
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
}
if (ruleCache.ContainsKey(ruleSetName))
return ruleCache[ruleSetName];
else
{
RuleSet rules = GetRuleSet(ruleSetName);
ruleCache[ruleSetName] = rules;
return rules;
}
RuleSet rules = GetRules(rulesName);
RuleEngine engine = new RuleEngine(rules, typeof(T));
engine.Execute(target);
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");
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.
Dictionary<string, string>();
string IDataErrorInfo.Error
113
{
get { return string.Empty; }
string IDataErrorInfo.this[string columnName]
get
RulesMediator.RunRules<LoanApplication>(
this, "WPFLoanRules");
if (errors.ContainsKey(columnName))
return errors[columnName];
else
return String.Empty;
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.
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")
<Style.Triggers>
116
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
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.
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