C H A P T E R  26

Introducing Windows Workflow Foundation

The .NET platform supports a programming model termed Windows Workflow Foundation (WF). This API allows you to model, configure, monitor, and execute the workflows (which are used to model a business process) used internally by a given .NET program. Workflows are modeled (by default) using a declarative XML-based grammar named XAML where data used by the workflow is treated as a first class citizen.

If you are new to the topic of WF, this chapter begins by defining the role of business processes and describes how they relate to the WF API. As well, you will be exposed to the concept of a WF activity, common types of workflows and various project templates and programming tools. After we’ve covered the basics, we’ll build several example programs that illustrate how to leverage the WF programming model to establish business processes that execute under the watchful eye of the WF runtime engine.

images Note The entirety of the WF API cannot be covered in a single introductory chapter. If you require a deeper treatment of the topic than presented here, check out Pro WF 4.5 by Bayer White (Apress, 2012).

Defining a Business Process

Any real-world application must be able to model various business processes. Simply put, a business process is a conceptual grouping of tasks that logically work as a collective whole. For example, assume you are building an application that allows a user to purchase an automobile online. When the user submits the order, a large number of activities are set in motion. You might begin by performing a credit check. If the user passes the credit verification, you might start a database transaction in order to remove the entry from an Inventory table, add a new entry to an Orders table, and update the customer account information. After the database transaction has completed, you still might need to send a confirmation e-mail to the buyer, and then invoke a remote service to place the order at the car dealership. Collectively, all of these tasks could represent a single business process.

Historically speaking, modeling a business process was yet another detail that programmers had to account for, often by authoring custom code to ensure that a business process was not only modeled correctly but also executed correctly within the application itself. For example, you might need to author code to account for points of failure, tracing, and logging support (to see what a given business process is up to); persistence support (to save the state of long-running processes); and whatnot. As you might know firsthand, building this sort of infrastructure from scratch entails a great deal of time and manual labor.

Assuming that a development team did, in fact, build a custom business process framework for their applications, their work was not yet complete. Simply put, a raw C# code base cannot be easily explained to nonprogrammers on the team who also need to understand the business process. The truth of the matter is that subject matter experts (SMEs), managers, salespeople, and members of a graphical design team often do not speak the language of code. Given this, as programmers, we were required to make use of other modeling tools (such as Microsoft Visio, the office whiteboard, etc.) to graphically represent our processes using skill set–neutral terms. The obvious problem here is we now have two entities to keep in sync: if we change the code, we need to update the diagrams. If we change the diagrams, we need to update the code.

Furthermore, when building a sophisticated software application using the 100% code approach, the code base has very little trace of the internal “flow” of the application. For example, a typical .NET program might be composed of hundreds of custom types (not to mention the numerous types used within the base class libraries). While programmers might have a feel for which objects are making calls on other objects, the code itself is a far cry from a living document that explains the overall sequence of activity. While the development team might build external documentation and workflow charts, again there is the problem of multiple representations of the same process.

The Role of WF

In essence, the Windows Workflow Foundation API allows programmers to declaratively design business processes using a prefabricated set of activities. Thus, rather than using only a set custom of assemblies to represent a given business activity and the necessary infrastructure, we can make use of the WF designers of Visual Studio to create our business process at design time. In this respect, WF allows us to build the skeleton of a business process, which can be fleshed out through code where required.

When programming with the WF API, a single entity can then be used to represent the overall business process, as well as the code that defines it. In addition to being a friendly visual representation of the process, because a single WF document is used to represent the code driving the process, we no longer need to worry about multiple documents falling out of sync. Better yet, this WF document will clearly illustrate the process itself. With a little bit of guidance, even the most nontechnical of staff members should be able to get a grip on what your WF designer is modeling.

Building a Simple Workflow

As you build a workflow-enabled application, you will undoubtedly notice that it “feels different” from building a typical .NET application. For example, up until this point in the text, every code example began by creating a new project workspace (most often a Console Application project) and involved authoring code to represent the program at large. A WF application also consists of custom code; however, in addition, you are building directly into the assembly a model of the business process itself.

Another aspect of WF that is quite different from other sorts of .NET applications is that a vast majority of your workflows will be modeled in a declarative manner, using an XML-based grammar named XAML. Much of the time, you will not need to directly author this markup, as the Visual Studio IDE will do so automatically as you work with the WF designer tools. This is a big change in direction from the previous version of the WF API, which favored using C# code as the standard way to model a workflow.

images Note Be aware that the XAML dialect used within WF is not identical to the XAML dialect used for WPF. You will learn about the syntax and semantics of WPF XAML in Chapter 27, as unlike WF XAML, it is quite common to directly edit designer-generated WPF XAML.

To get you into the workflow mindset, open Visual Studio. From the New Project dialog box, pick a new Workflow Console Application project named FirstWorkflowExampleApp (see Figure 26-1).

images

Figure 26-1. Creating a new console-based workflow application

Now, consider Figure 26-2, which illustrates the initial workflow diagram generated by Visual Studio. As you can see, there is not too much happening at this point, just a message telling you to drop activities on the designer.

images

Figure 26-2. A workflow designer is a container for activities that model your business process

For this first simple test workflow, open the Visual Studio Toolbox, and locate the WriteLine activity under the Primitives section (see Figure 26-3).

images

Figure 26-3. The Toolbox will show you all the default activities of WF

Once you have located this activity, drag it onto of the designer’s drop target (be sure you drag it directly onto the area that says Drop activity here), and enter a friendly double-quoted string message into the Text edit box. Figure 26-4 shows one possible workflow.

images

Figure 26-4. The WriteLine activity will display text to a TextWriter, which is the console in this case

Do understand that WF is far more than a pretty designer that allows you to model the activities of a business process. As you are building your WF diagram, your markup can always be extended using code to represent the runtime behavior of your process. In fact, if you wanted to do so, you could avoid the use of XAML all together and author the workflow using nothing but C#. If you were to this, however, you would be back to the same basic issue of having a body of code that is not readily understandable to nontechnical staff. In any case, if you were to run your application at this point, you would see your message display to the console window, like this:


First Workflow!
Press any key to continue . . .

Fair enough; however, what started this workflow? And how were you able to ensure that the console application stayed running long enough for the workflow to complete? The answers to these questions require an understanding of the workflow runtime engine.

The Workflow Runtime

The next thing to understand is that the WF API also consists of a runtime engine to load, execute, unload, and in other ways manipulate a workflow that you have defined. The WF runtime engine can be hosted within any .NET application domain; however, be aware that a single application domain can have only one running instance of the WF engine.

Recall from Chapter 17 that an AppDomain is a partition within a Windows process that plays host to a .NET application and any external code libraries. As such, the WF engine can be embedded within a simple console program, a GUI desktop application (Windows Forms or WPF), or exposed from a Windows Communication Foundation (WCF) service.

images Note The WCF Workflow Service Application project template is a great starting point if you want to build a WCF service (see Chapter 25) that makes use of workflows internally.

If you are modeling a business process that needs to be used by a wide variety of systems, you also have the option of authoring your WF within a C# Class Library project of a Workflow Activity Library project. In this way, new applications can simply reference your *.dll to reuse a predefined collection of business processes. This is obviously helpful in that you would not want to have to re-create the same workflows multiple times.

Hosting a Workflow Using WorkflowInvoker

The host process of the WF runtime can interact with said runtime using a few different techniques. The simplest way to do so is to use the WorkflowInvoker class of the System.Activities namespace. This class allows you to start a workflow using a single line of code. If you were to open up the Program.cs file of your current Workflow Console Application project, you will see the following Main() method:

static void Main(string[] args)
{
  // Create and cache the workflow definition.
  Activity workflow1 = new Workflow1();
  WorkflowInvoker.Invoke(workflow1);
}

Using the WorkflowInvoker is very useful when you simply want a workflow to kick off and don’t care to monitor it any further. The Invoke() method will execute the workflow in a synchronous blocking manner. The calling thread is blocked until the entire workflow has finished or has been terminated abruptly. Because the Invoke() method is a synchronous call, you are guaranteed that the entire workflow will indeed complete before Main() is terminated. In fact, if you were to add any code after the call to WorkflowInvoker.Invoke(), it would only execute when the workflow is completed (or in a worse-case situation, terminated abruptly).

static void Main(string[] args)
{
  // Create and cache the workflow definition.
  Activity workflow1 = new Workflow1();
  WorkflowInvoker.Invoke(workflow1);

  Console.WriteLine("Thanks for playing");
}
Passing Arguments to Your Workflow Using WorkflowInvoker

When a host process kicks off a workflow, it is very common for the host to send custom startup arguments. For example, assume that you want to let the user of your program specify which message to display in the WriteLine activity in place of the currently hard-coded text message. In normal C# code, you might create a custom constructor on a class to receive such arguments. However, a workflow is always created using the default constructor! Moreover, most workflows are defined only using XAML, not procedural code.

As it turns out, the Invoke() method has been overloaded multiple times, one version of which allows you to pass in arguments to the workflow when it starts. These arguments are represented using a Dictionary<string, object> variable that contains a set of name/value pairs that will be used to set identically named (and typed) argument variables in the workflow itself.

Defining Arguments Using the Workflow Designer

To define the arguments that will capture the incoming dictionary data, you will make use of the workflow designer. In Solution Explorer, right-click Workflow1.xaml and select View Designer. Notice on the bottom of the designer there is a button named Arguments. Click this button now, and from the resulting UI, add an input argument of type string named MessageToShow (no need to assign a default value for this new argument). As well, delete your initial message from the WriteLine activity by resetting the Text property of the WriteLine Activity via the Visual Studio Properties window. Figure 26-5 shows the end result.

images

Figure 26-5. Workflow arguments can be used to receive host-supplied arguments

Now, in the Text property of the WriteLine activity, you can simply enter MessageToShow as the evaluation expression. As you are typing in this token, you’ll notice IntelliSense will kick in (see Figure 26-6).

images

Figure 26-6. Using a custom argument as input to an activity

Now that you have the correct infrastructure in place, consider the following update to the Main() method of the Program class. Note that you will need to import the System.Collections.Generic namespace into your Program.cs file to declare the Dictionary<> variable.

static void Main(string[] args)
{
  Console.WriteLine("***** Welcome to this amazing WF application *****");
    
  // Get data from user, to pass to workflow.
  Console.Write("Please enter the data to pass the workflow: ");
  string wfData = Console.ReadLine();

  // Package up the data as a dictionary.
  Dictionary<string, object> wfArgs = new Dictionary<string,object>();
  wfArgs.Add("MessageToShow", wfData);

  // Pass to the workflow.
  Activity workflow1 = new Workflow1();
  WorkflowInvoker.Invoke(workflow1, wfArgs);

  Console.WriteLine("Thanks for playing");
}

Again, it is important to point out that the string values for each member of your Dictionary<> variable will need to be identically named to the related argument variable in your workflow. In any case, you will find output similar to the following when you run the modified program:


***** Welcome to this amazing WF application *****
Please enter the data to pass the workflow: Hello Mr. Workflow!
Hello Mr. Workflow!
Thanks for playing
Press any key to continue . . .

Beyond the Invoke() method, the only other really interesting members of WorkflowInvoker would be BeginInvoke() and EndInvoke(), which allow you to start up the workflow on a secondary thread using the .NET asynchronous delegate pattern (see Chapter 19). If you require more control over how the WF runtime manipulates your workflow, you can instead make use of the WorkflowApplication class.

Hosting a Workflow Using WorkflowApplication

You’ll want to use WorkflowApplication (as opposed to WorkflowInvoker) if you need to save or load a long running workflow using WF persistence services, be notified of various events that fire during the lifetime of your workflow instance, work with WF “bookmarks,” and other advanced features. In this case, you will want to call the Run() method of WorkflowApplication.

When you do call Run(), a new background thread will be plucked from the CLR thread pool. Therefore, if you do not add additional support to ensure the main thread waits for the secondary thread to complete, the workflow instance might not have a chance to finish its work.

One way to make sure the calling thread waits long enough for the background thread to finish its work is to use an AutoResetEvent object of the System.Threading namespace. Here is an update to the current example, which now uses WorkflowApplication rather than WorkflowInvoker:

static void Main(string[] args)
{
  Console.WriteLine("***** Welcome to this amazing WF application *****");
    
  // Get data from user, to pass to workflow.
  Console.Write("Please enter the data to pass the workflow: ");
  string wfData = Console.ReadLine();

  // Package up the data as a dictionary.
  Dictionary<string, object> wfArgs = new Dictionary<string,object>();
  wfArgs.Add("MessageToShow", wfData);

  // Used to inform primary thread to wait!
  AutoResetEvent waitHandle = new AutoResetEvent(false);

  // Pass to the workflow.
  WorkflowApplication app = new WorkflowApplication(new Workflow1(), wfArgs);

  // Hook up an event with this app.
  // When I'm done, notifiy other thread I'm done,
  // and print a message.
  app.Completed = (completedArgs) => {
    waitHandle.Set();
    Console.WriteLine("The workflow is done!");
  };


  // Start the workflow!
  app.Run();

  // Wait until I am notified the workflow is done.
  waitHandle.WaitOne();

  Console.WriteLine("Thanks for playing");
}

The output will be similar to the previous iteration of the project:


***** Welcome to this amazing WF application *****
Please enter the data to pass the workflow: Hey again!
Hey again!
The workflow is done!
Thanks for playing
Press any key to continue . . .

The benefit of using WorkflowApplication is that you can hook into events (as you have done indirectly using the Completed property here) and can also tap into more sophisticated services (persistence, bookmarks, etc.).

images Note During our introductory look at WF, we will not dive into the details of these runtime services. Be sure to check out the .NET Framework 4.5 SDK documentation for details regarding the runtime behaviors and services of the Windows Workflow Foundation runtime environment.

Recap of Your First Workflow

While this example was very trivial, you did learn a few interesting (and useful) tasks. First, you learned that you can pass in a Dictionary object that contains name/value pairs that will be passed to identically named arguments in your workflow. This is really useful when you need to gather user input (such as a customer ID number, SSN, name of a doctor, etc.) that will be used by the workflow to process its activities.

You also learned that a .NET workflow is defined in a declarative manner (by default) using an XML-based grammar named XAML. Using XAML, you can specify which activities your workflow contains. At runtime, this data will be used to create the correct in-memory object model. Last but not least, you looked at two different approaches to kick off a workflow using the WorkflowInvoker and WorkflowApplicaion classes.

images Source Code The FirstWorkflowExampleApp project is included under the Chapter 26 subdirectory.

Examining the Workflow Activities

Recall that the purpose of WF is to allow you to model a business process in a declarative manner, which is then executed by the WF runtime engine. In the vernacular of WF, a business process is composed of any number of activities. Simply put, a WF activity is an atomic “step” in the overall process. When you create a new workflow application, you will find the Toolbox contains iconic representations of the built-in activities grouped by category.

These out-of-the-box activities are used to model your business process. Each activity in the Toolbox maps to a real class within the System.Activities.dll assembly (most often contained within the System.Activities.Statements namespace). You’ll make use of several of these baked-in activities over the course of this chapter; however, here is a walkthrough of many of these default activities. As always, consult the .NET Framework SDK documentation for full details.

Control Flow Activities

The first category of activities in the Toolbox allow you to represent looping and decision tasks in a larger workflow. Their usefulness should be easy to understand, given that we do similar tasks in C# code quite often. In Table 26-1, notice that some of these control flow activities allow for parallel processing of activities using the Task Parallel Library behind the scenes (see Chapter 19).

images

Flowchart Activities

Next are the flowchart activities, which are actually quite important given that the Flowchart activity will very often be the first item you place on your WF designer. This type of workflow allows you to build a workflow using the well-known flow chart model, where the execution of the workflow is based on numerous branching paths, each of which is based on the truth or falsity of some internal condition. Table 26-2 documents each member of this activity set.

images

Messaging Activities

A workflow can easily invoke members of an external XML web service or WCF service, as well as be notified by an external service using messaging activities. Because these activities are very closely related to WCF development, they have been packaged up in a dedicated .NET assembly, System.ServiceModel.Activities.dll. Within this library, you will find an identically named namespace defining the core activities seen in Table 26-3.

images

The most common messaging activities are Send and Receive, which allow you to communicate with external XML web services or WCF services.

The State Machine Activities

Under .NET 4.5, the WF API has been updated to include a new set of activities that allow you to model workflows that are based on state machines. In a nutshell, state machines allow you to define a workflow, which can be in any number of defined states at a given point of time, and the valid transitions between these states.

A well-known example of state machines is that of a soda-pop vending machine. At any given time, the “machine” can be in a single state such as (for example) “Waiting for Input,” “Dispensing soda,” “Refunding Payment,” “Returning Change,” “Displaying Selection Empty,” and what have you. Among these states are a set of valid transitions. For example, if the machine is in the “Displaying Selection Empty” state, valid transitions could include “Refunding Payment” or “Dispensing Soda” (provided the user picked a different option). To build such a workflow, .NET 4.5 includes the StateMachine, State, and FinalState activities.

The Runtime and Primitives Activities

The next two categories in the Toolbox, Runtime and Primitives, allow you to build a workflow that makes calls to the workflow runtime (in the case of Persist and TerminateWorkflow) and performs common operations such as pushing text to an output stream or invoking a method on a .NET object. Consider Table 26-4, which shows some common activities of these categories.

images

InvokeMethod is maybe the most interesting and useful activity of this set because it allows you to call methods of .NET classes in a declarative manner. You can also configure InvokeMethod to hold onto any return value send from the method you call. TerminateWorkflow can also be helpful when you need to account for a point of no return. If the workflow instance hits this activity, it will raise the Competed event, which can be caught in the host, just like you did in the first example.

The Transaction Activities

When you are building a workflow, you might need to ensure that a group of activities work in an atomic manner, meaning they must all succeed or all fail as a collective group. Even if the activities in question are not directly working with a relational database, the core activities seen in Table 26-5 allow you to add a transactional scope into a workflow.

images

The Collection and Error Handling Activities

The final two categories to consider in this introductory chapter allow you to declaratively manipulate generic collections and respond to runtime exceptions. The collection activities are great when you need to manipulate objects that represent business data (such as purchase orders, medical information objects, or order tracking) on the fly in XAML. Error activities, on the other hand, allow you to essentially author try/catch/throw logic within a workflow. Table 26-6 documents this final set of WF activities.

images

Now that we have seen many of the default activities at a high level, we can start to build some more interesting workflows that make use of them. Along the way, you will learn about the two key activities that typically function as the root of your workflow, Flowchart and Sequence.

Building a Flowchart Workflow

In the first example, I had you drag a simple WriteLine activity directly on the workflow designer. While it is true that any activity seen in the Visual Studio Toolbox can be the first item placed on the designer, only a few of them are able to contain subactivities (which represent a collection of related activities grouped together). When you are building a new workflow, the chances are very good that the first item you will place on your designer will be a Flowchart or Sequence activity.

Both of these built-in activities have the ability to contain any number of internal child activities (including additional Flowchart or Sequence activities) to represent the entirety of your business process. To begin, let’s create a brand new Workflow Console Application named EnumerateMachineDataWF. Once you have done so, rename your initial *.xaml file to MachineInfoWF.xaml.

Now, under the Flowchart section of your Toolbox, drag a Flowchart activity onto the designer. Next, using the Properties window, change the DisplayName property to something a tad more catchy, such as Show Machine Data Flowchart (as I am sure you can guess, the DisplayName property controls how the item is named on the designer). At this point, your workflow designer should look something like Figure 26-7.

images

Figure 26-7. The initial Flowchart activity

Be aware that there is a grab-handle on the lower right of the Flowchart activity, which can be used to increase or decrease the size of the flowchart designer space. You’ll need to increase the size as you add more and more activities.

Connecting Activities in a Flowchart

The large Start icon represents the entry point to the Flowchart activity, which in this example is the first activity in our entire workflow and will be triggered when you execute the workflow using the WorkflowInvoker or WorkflowApplication classes. This icon can be positioned anywhere on your designer, and I’d suggest you move it to the upper left, just to make some more space.

Your goal is to assemble your flowchart by connecting any number of additional activities together, making use of the FlowDecision activity during the process. To start, drag a WriteLine activity on the designer, changing the DisplayName to Greet User. Now, if you hover your mouse over the Start icon, you will see one docking tab on each side. Click and hold the docking tab closest to the WriteLine activity, and drag it to the docking tab of the WriteLine activity. After you have done so, you should see a connection between these first two items, signifying that the first activity that will be executed in your workflow will be Greet User.

Now, similar to the first example in this chapter, add a workflow argument (via the Arguments button) named UserName of type string with no default value. This will be passed in dynamically via the custom Dictionary<> object in just a bit. Finally, set the Text property of the WriteLine activity to the following code statement:

"Hello" + UserName  

Add a second WriteLine activity to your designer, which is connected to the previous. This time, define a hardcoded string value of "Do you want me to list all machine drives?" for the Text property, and change the DisplayName property to Ask User. Figure 26-8 shows the connections between current workflow activities.

images

Figure 26-8. Flowchart workflows connect activities together

Working with the InvokeMethod Activity

Because the majority of a workflow is defined in a declarative manner using XAML, you are sure to make good use of the InvokeMethod activity, which allows you to invoke methods of real objects at various points in your workflow. Drag one of these items to your designer, change the DisplayName property to Get Y or N, and make a connection between it and the Ask User WriteLine activity.

The first property to configure for an InvokeMethod activity is the TargetType property, which represents the name of the class that defines a static member you want to invoke. Using the drop-down list box for the TargetType of the InvokeMethod activity, pick the Browse for Types... option (see Figure 26-9).

images

Figure 26-9. Specifying a target type for InvokeMethod

From the resulting dialog box, pick the System.Console class of mscorlib.dll (if you enter the name of the type within the Type Name edit area, the dialog will automatically find the type). When you have found the System.Console class, click the OK button.

Now, using the InvokeMethod activity on the designer, enter ReadLine as the value for the MethodName property. This will configure your InvokeMethod activity to invoke the Console.ReadLine() method when this step of the workflow is reached.

As you know, Console.ReadLine() will return a string value that contains the keystrokes entered on the keyboard before the Enter key is pressed; however, you need to have a way to capture the return value! You will do this next.

Defining Workflow-Wide Variables

Defining a workflow variable in XAML is almost identical to defining an argument, in that you can do so directly on the designer (this time with the Variables button). The difference is that arguments are used to capture data passed in by the host, whereas variables are simply points of data in the workflow that will be used to influence its runtime behavior.

Using the Variables aspect of the designer, add a new string variable named YesOrNo. Notice that if you have multiple parent containers in your workflow (for example, a Flowchart containing another Sequence), you can pick the scope of the variable. Here, your only choice is the root Flowchart (see Figure 26-10).

images

Figure 26-10. Defining a workflow variable

Next, select the InvokeMethod activity on the workflow designer, and using the Properties window of Visual Studio, set the Result property to your new variable (see Figure 26-11).

images

Figure 26-11. The fully configured InvokeMethod

Now that you can grab a piece of data from an external method call, you can use it to make a runtime decision in your flowchart using the FlowDecision activity.

Working with the FlowDecision Activity

A FlowDecision activity is used to take two possible courses of action, based on the truth or falsity of a Boolean variable, or a statement that resolves to a Boolean value. Drag one of these activities onto your designer, and connect it to the InvokeMethod activity (see Figure 26-12).

images

Figure 26-12. A FlowDecision can branch in two directions

images Note If you need to respond to multiple branching conditions within a flowchart, make use of the FlowSwitch<T> activity. This allows you to define any number of paths, which will be entered based on the value of a defined workflow variable.

Set the Condition property (using the Properties window) of your FlowDecision activity to the following code statement, which you can type directly into the editor (here, you are testing an uppercase version of your YesOrNo variable against the value “Y”):

YesOrNo.ToUpper() == "Y"

Working with the TerminateWorkflow Activity

You now need to build the activities that will occur on each side of the FlowDecision activity. On the “false” side, connect a final WriteLine activity that prints out a hardcoded message of your choosing, followed by a TerminateWorkflow activity (see Figure 26-13).

images

Figure 26-13. The “false” branch

Strictly speaking, you don’t need to use the TerminateWorkflow activity, as this workflow would simply end once you reach the end of the false branch. However, by using this activity type, you can throw back an exception to the workflow host, informing them exactly why you are stopping. This exception can be configured in the Properties window.

Assuming you have selected the TerminateWorkflow activity on the designer, use the Properties window, and click the ellipse button for the Exception property. This will open up an editor that allows you to throw back an exception, just like if you were doing so in code (see Figure 26-14).

images

Figure 26-14. Configuring an exception to throw when the TerminateWorkflow activity is encountered

Complete the configuration of this activity by setting the Reason property to “YesOrNo was false”.

Building the “True” Condition

To begin building the “true” condition of the FlowDecision, connect a WriteLine activity, which simply displays a hardcoded string confirming the user has agreed to proceed. From here, connect to a new InvokeMethod activity, which will call the GetLogicalDrives() method of the System.Environment class. To do so, set the TargetType property to System.Environment and the MethodName property to GetLogicalDrives (see Figure 26-15).

images

Figure 26-15. The configured InvokeMethod activity

Next, add a new workflow-level variable (using the Variables button of the workflow designer) named DriveNames of type string[]. To specify you want an array of strings, pick Array of [T] from the Variable Type drop-down list, and pick String from the resulting dialog box.  Finally, set the Result property of this new InvokeMethod activity to your DriveNames variable by selecting the InvokeMethod activity on the designer and investigating the Properties window.

Working with the ForEach<T> Activity

The next part of your “true path” will be to print out the names of each drive to the console window, which is to say you need to loop over the data exposed by the DriveNames variable, which has been configured as an array of string objects. The ForEach<T> activity is the WF equivalent of the C# foreach keyword, and it is configured in a very similar manner (at least conceptually).

Drag a ForEach<T> activity on your designer and connect it to the previous InvokeMethod activity. You will configure the ForEach<T> activity in just a minute, but to complete the true condition branch, place one final WriteLine activity on your designer to close things off. Figure 26-16 shows the final top-level look at your workflow.

images

Figure 26-16. The completed top-level workflow

To get rid of the current designer error, you need to finish the configuration of the ForEach<T> activity. First, use the Properties window to specify the type argument to the generic, which in this example will be a String type. The Values property is where the data is coming from, which will be your DriveNames variable (see Figure 26-17).

images

Figure 26-17. Setting the type of the ForEach enumeration

This particular activity needs to be further edited by double-clicking on the designer in order to open a mini designer just for this activity. Not all WF activities can be double-clicked on to yield a new designer, but you can easily tell if this is an option on the activity itself (it will literally say “Double-click to view”). Double click on your ForEach<String> activity, and add a single WriteLine activity, which will print out each string value in the DriveNames return value (see Figure 26-18).

images

Figure 26-18. The final configuration step of the ForEach<String> activity

images Note You can add as many activities to the ForEach<T> mini designer as you require. The collective whole of these activities will execute with each iteration of the loop.

After you are done configuring the “subactivities” of ForEach<T>, you can use the links at the upper left of the workflow designer to return to the top-level workflow (you’ll use these breadcrumbs quite a bit when drilling into a set of activities; see the mouse icon in Figure 26-19).

images

Figure 26-19. The workflow designer “breadcrumbs” allow you to return to the top-level activity

Completing the Application

You are just about done with this example! All you need to do is update the Main() method of the Program class to catch the exception that will be raised if the user says “NO” and thereby triggers the Exception object. Update your code as so (and ensure the System.Collections.Generic namespace is imported in your code file):

static void Main(string[] args)
{
  try
  {
    Dictionary<string, object> wfArgs = new Dictionary<string, object>();
    wfArgs.Add("UserName", "Mel");
    Activity workflow1 = new Workflow1();
    WorkflowInvoker.Invoke(workflow1, wfArgs);
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.Data["Reason"]);
  }            
}

Notice that the “Reason” for the exception can be obtained using the Data property of System.Exception. So, if you run the program and enter “Y” when asked to enumerate your drives, you’ll see the following type of output:


Hello Andrew
Do you want me to list all machine drives?
y
Wonderful!
C:
D:
E:
F:
G:
H:
I:
Thanks for using this workflow

However, if you enter “N” (or any other value other than “Y” or “y”), you will see the following:


Hello Andrew
Do you want me to list all machine drives?
n
Too bad. All done
YesOrNo was false

Reflecting on What We Have Done

Now, if you are new to working with a workflow environment, you might be wondering what you have gained by authoring this very simple business process using WF XAML rather than pure C# code. After all, you could have avoided Windows Workflow Foundation all together and authored a C# class similar to the following:

class Program
{
  static void Main(string[] args)
  {
    try
    {
      ExecuteBusinessProcess();
    }
    catch (Exception ex)
    {
      Console.WriteLine(ex.Message);
      Console.WriteLine(ex.Data["Reason"]);
    }
  }


  private static void ExecuteBusinessProcess()
  {
    string UserName = "Andrew";
    Console.WriteLine("Hello {0}", UserName);
    Console.WriteLine("Do you want me to list all machine drives?");

    string YesOrNo = Console.ReadLine();
    if (YesOrNo.ToUpper() == "Y")
    {
      Console.WriteLine("Wonderful!");
      string[] DriveNames = Environment.GetLogicalDrives();
      foreach (string item in DriveNames)
      {
        Console.WriteLine(item);
      }
      Console.WriteLine("Thanks for using this workflow");
    }
    else
    {
      Console.WriteLine("K, Bye...");
      Exception ex = new Exception("User Said No!");
      ex.Data["Reason"] = "YesOrNo was false";
    }
  }
}

The output of the program would be absolutely identical to the previous XAML-based workflow. So, why bother tinkering with all these activities in the first place? First of all, remember that not everyone is comfortable reading C# code. Be honest: If you had to explain this business process to a room full of salespeople and nontechnical managers, would you rather try to explain this C# code or show them the flowchart? More importantly, remember that the WF API has a whole slew of additional runtime services, including persistence of long running workflows to a database, automatic tracking of workflow events, and so on (alas, I don’t have time to cover them here). When you think of the amount of work you would need to do in order to replicate this functionality in a new project, the utility of WF is even clearer.

All of this being said, the WF API is not necessarily the correct tool of choice for all .NET programs. However, for most traditional business applications, the ability to define, host, execute and monitor workflows in this manner is a very good thing indeed. Like any new technology, you will need to determine if this is useful for your current project. Let’s see another example of working with the WF API, this time by packaging up a workflow in a dedicated *.dll.

images Source Code The EnumerateMachineDataWF project is included under the Chapter 26 subdirectory.

Building a Sequence Workflow (in a Dedicated DLL)

While making a Workflow Console Application is great for experimenting with the WF API, a production-ready workflow will certainly need to be packaged up into a custom .NET *.dll assembly. By doing so, you can reuse your workflows at a binary level across multiple projects.

While you could begin using a C# Class Library project as a starting point, the easiest way to build a workflow library is to start with the Activity Library project, under the Workflow node of the New Project dialog. The benefits of this project type are that it will set the required WF assembly references automatically and give you a *.xaml file to create your initial workflow.

This workflow will model the process of querying the AutoLot database to see whether a given car of the correct make and color is in the Inventory table. If the requested car is in stock, you will build a nicely formatted response to the host via an output parameter. If the item in not in stock, you will generate a memo to the head of the sales division requesting that they find a car of the correct color.

Defining the Initial Project

Create a new Activity Library project named CheckInventoryWorkflowLib (see Figure 26-20). After the project is created, rename the initial Activity1.xaml file to CheckInventory.xaml.

images

Figure 26-20. Building an Activity Library

Now, unfortunately, when you rename a workflow XAML file, the underlying class is not renamed as expected. To fix the problem, right-click on your CheckInventory.xaml file in the Solution Explorer, and elect to view the code. Modify the opening <Activity> root element to reflect the correct name, as seen here:

<Activity mc:Ignorable="sap sap2010 sads"
  x:Class="CheckInventoryWorkflowLib.CheckInventory"
  ...
>

This workflow will make use of a Sequence activity as the primary activity, rather than Flowchart. Drag a new Sequence activity onto your designer (you’ll find it under the Control Flow area of your Toolbox) and change the DisplayName property to Look Up Product. Figure 26-21 shows the designer thus far.

images

Figure 26-21. A topmost Sequence activity

As the name suggests, a Sequence activity allows you to easily create sequential tasks, which occur one after the other. This does not necessarily mean the children activities must follow a strict linear path, however. Your sequence could contain flowcharts, other sequences, parallel processing of data, if/else branches and whatever else might make good sense for the business process you are designing.

Importing Assemblies and Namespaces

Because your workflow will be communicating with the AutoLot database, the next step is to reference your AutoLot.dll assembly using the Add Reference dialog box of Visual Studio. This example will make use of the disconnected layer, so I’d suggest you reference the final version of this assembly created in Chapter 22 (AutoLotDAL (Version 3)).

This workflow will also be making use of the LINQ to DataSet API to query the returned DataTable in order to discover whether you have the requested item in stock. Therefore, you should also set a reference to System.Data.DataSetExtensions.dll, as this is not automatically included for new Activity Library projects.

After you have referenced these assemblies, click the Imports button located at the bottom of the workflow designer. At the top of this editor, is a text box where you can enter names of the .NET namespaces you want to make use of in your workflow scope (think of this area as a declarative version of the C# using keyword).

You can add namespaces from any referenced assembly by typing in the text box mounted on the top of the Imports editor. Import AutoLotDisconnectedLayer using this text area. By doing so, you can reference the contained types without needing to use fully qualified names. Figure 26-22 shows the Imports area once you are finished.

images

Figure 26-22. The Imports area allows you to include .NET namespaces into your workflow

Defining the Workflow Arguments

Next, we need to define two new workflow-wide input arguments, named RequestedMake and RequestedColor, both of which will be of type String. Like the previous examples, the host of the workflow will create a Dictionary object that contains data that maps to these arguments, so there is no need to assign a default value to these items using the Arguments editor. As you might have guessed, this workflow will use these incoming values to perform the database query.

As well, you can use this same Arguments editor to define an output argument named FormattedResponse of type String. When you need to return data from the workflow back to the host, you can create any number of output arguments that can be enumerated by the host when the workflow has completed. Figure 26-23 shows the current workflow designer.

images

Figure 26-23. Input and output arguments

Defining Workflow Variables

At this point, you need to declare a member variable in your workflow that corresponds to the InventoryDALDisLayer class of AutoLotDAL.dll. Recall from Chapter 22 that this class allows you to get all data from the Inventory returned as a DataTable. Select your Sequence activity on the designer, and using the Variables button, create a variable named AutoLotInventory. In the Variable Type drop-down list box, pick the Browse For Types... menu option, and type in InventoryDALDisLayer (see Figure 26-24).

images

Figure 26-24. Recall that workflow variables provide a way to declaratively define variables within a scope

Now, making sure your new variable is selected, go to the Visual Studio Properties window and click on the ellipse button of the Default property. This will open up a code editor that you may resize as you see fit (very helpful when entering lengthy code). This editor is much easier to use when entering complex code for a variable assignment. Enter the following code (on a single line), which allocates your InventoryDALDisLayer variable:

new InventoryDALDisLayer(@"Data Source=(local)SQLEXPRESS;Initial Catalog=AutoLot;Integrated
Security=True")

Using the WF designer, declare a second workflow variable of type System.Data.DataTable named Inventory, again using the Browse For Types... menu option (set the default value of this variable to null; see Figure 26-25).

images

Figure 26-25. Declaring a DataTable variable in the workflow

You will be assigning the Inventory variable to the result of calling GetAllInventory() on the InventoryDALDisLayer variable in a later step.

Working with the Assign Activity

The Assign activity allows you to set a variable to a value, which can be the result of any sort of valid code statements. Drag an Assign activity (located in the Primitives area of your Toolbox) into your Sequence activity (see Figure 26-26).

images

Figure 26-26. The Assign activity

In the left-side edit box, specify your Inventory variable. In the right-hand edit box, enter the following code:

AutoLotInventory.GetAllInventory()

Once the Assign activity has been encountered in your workflow, you will have a DataTable that contains all records of the Inventory table. However, you need to discover whether the correct item is in stock using the values of the RequestedMake and RequestedColor arguments sent by the host. To determine if this is the case, you will make use of the LINQ to DataSet API and a workflow If activity.

Working with the If and Switch Activities

Drag an If activity onto your Sequence node directly under the Assign activity (see Figure 26-27).

images

Figure 26-27. The If activity

As the If activity allows you to make runtime decisions, you first need to configure the If activity to test against a Boolean expression. In the Condition editor, enter the following check LINQ to DataSet query:

(from car in Inventory.AsEnumerable()
  where (string)car["Color"] == RequestedColor &&
    (string)car["Make"]  == RequestedMake select car).Any()

This LINQ query uses the RequestedColor and RequestedMake arguments supplied by the workflow host to fetch all records in the DataTable of correct make and color. The call to the Any() extension method will return a true or false value based on if the result of the query contains any results.

Your next task is to configure the set of activities that will execute when the specified condition is true or false. Recall that your ultimate goal here is to send a formatted message to the user if you do indeed have the car in question. However, to spice things up a tad, you will return a unique message based on which make of automobile the caller has requested (BMW, Yugo, or anything else).

Drag a Switch<T> activity (located in the Flow Control area of the Toolbox) into the Then area of the If activity. As soon as you drop this activity, Visual Studio displays a dialog box asking for the type of the generic type parameter; specify String here. After this point, use the workflow designer to set the Expression field of your Switch activity, type RequestedMake (see Figure 26-28).

images

Figure 26-28. The Switch activity

You will see that a default option for the Switch activity is already in place, but you have to expand it, in order to add subactivities (by clicking “Add an Activity”). Add a single Assign activity. After you have added the Assign activity to the Default edit area, assign the FormattedResponse argument to the following code statement:

String.Format("Yes, we have a {0} {1} you can purchase",
              RequestedColor, RequestedMake)

At this point, your Switch editor will look like what is shown in Figure 26-29.

images

Figure 26-29. Defining the default task for a Switch activity

Now, click on the “Add New Case” link and enter BMW (without any double quotes) for the first case, and once again for a final case of Yugo (again, no double quotes). Within each of these case areas, drop an Assign activity, both of which assign a value to the FormattedResponse variable. For the case of BMW, assign a value such as:

String.Format("Yes sir! We can send you {0} {1} as soon as {2}!",
              RequestedColor, RequestedMake, DateTime.Now)

For the case of the Yugo, use the following expression:

String.Format("Please, we will pay you to get this {0} off our lot!",
              RequestedMake)

The Switch activity will now look something like what is shown in Figure 26-30.

images

Figure 26-30. The final Switch activity

Building a Custom Code Activity

As expressive as the workflow designer experience is with its ability to embed complex code statements (and LINQ queries) in your XAML file, there will certainly be times when you just need to write code in a dedicated class. There are a number of ways to do so with the WF API, but the most straightforward way is to create a class extending CodeActivity, or if your activity needs to return a value, CodeActivity<T> (where T is the type of return value).

Here, you will make a simple custom activity that will dump out data to a text file, informing the sales staff that a request has come in for a car that is currently not in the inventory system. First, activate the Project images Add New Item menu option and insert a new Code Activity named CreateSalesMemoActivity.cs (see Figure 26-31).

images

Figure 26-31. Inserting a new Code Activity

If your custom activity requires inputs to process, they will each be represented by a property encapsulating an InArgument<T> object. The InArgument<T> class type is a WF API–specific entity, which provides a way to pass through data supplied by a workflow to the custom activity class itself. Your activity will need two such properties representing the make and color of the item not in stock.

As well, a custom code activity will need to override the virtual Execute() method, which will be called by the WF runtime when this activity is encountered. Typically, this method will use the InArgument<> properties to get the workload accomplished. To get the real underlying value, you will need to do so indirectly using the GetValue() method of the incoming CodeActivityContext.

Here then is the code for your custom activity, which generates a new *.txt file describing the situation to the sales team:

public sealed class CreateSalesMemoActivity : CodeActivity
{
  // Two properties for the custom activity.
  public InArgument<string> Make { get; set; }
  public InArgument<string> Color { get; set; }

  // If the activity returns a value, derive from CodeActivity<TResult>
  // and return the value from the Execute method.
  protected override void Execute(CodeActivityContext context)
  {
    // Dump a message to a local text file.
    StringBuilder salesMessage = new StringBuilder();
    salesMessage.AppendLine("***** Attention sales team! *****");
    salesMessage.AppendLine("Please order the following ASAP!");
    salesMessage.AppendFormat("1 {0} {1} ",

      context.GetValue(Color), context.GetValue(Make));
    salesMessage.AppendLine("*********************************");
    
    System.IO.File.WriteAllText("SalesMemo.txt", salesMessage.ToString());
  }
}

Compile your workflow assembly. Ensuring your workflow designer is the active window within the Visual Studio IDE, examine the top area of your Toolbox. You should see your custom activity is present and accounted for (see Figure 26-32).

images

Figure 26-32. Custom code activities appear in the Visual Studio Toolbox

First, drag a new Sequence activity into the Else branch of your If activity. Next, drag your custom activity into the Sequence. At this point, you can assign values to each of the exposed properties using the Properties window. Using your RequestedMake and RequestedMake variables, set the Make and Color properties of your activity as shown in Figure 26-33.

images

Figure 26-33. Setting the properties of your custom code activity

To complete this workflow, drag a final Assign activity into the Sequence activity of the Else branch, and set the FormattedResponse to the string value “Sorry, out of stock”. Figure 26-34 shows the final If activity.

images

Figure 26-34. The completed If activity

Compile your project and move on to the final part of this chapter, where you will build a client host to make use of the workflow.

images Source Code The CheckInventoryWorkflowLib project is included under the Chapter 26 subdirectory.

Consuming the Workflow Library

Any sort of application can make use of a workflow library; however, here you will opt for simplicity and build a simple Console Application named WorkflowLibraryClient. After you make the project, you will need to set a reference not only to your CheckInventoryWorkflowLib.dll and AutoLotDAL.dll assemblies, but also the key WF library, System.Activities.dll. Add these library references now.

Once each assembly reference has been set, update your Program.cs file with the following logic:

using System;
...
using CheckInventoryWorkflowLib;

namespace WorkflowLibraryClient
{

  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("**** Inventory Look up ****");
            
      // Get user preferences.
      Console.Write("Enter Color: ");
      string color = Console.ReadLine();
      Console.Write("Enter Make: ");
      string make = Console.ReadLine();

      // Package up data for workflow.
      Dictionary<string, object> wfArgs = new Dictionary<string, object>()
      {
        {"RequestedColor", color},
        {"RequestedMake", make}
      };

      try
      {
        // Send data to workflow!
        WorkflowInvoker.Invoke(new CheckInventory(), wfArgs);
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
      }
    }
  }
}

As you have done in other examples, you are using the WorkflowInvoker to spin off the workflow in a synchronous manner. While this is all well and good, how are you to get back the return value of the workflow? Remember, once the workflow terminates, you should get back a formatted response!

Retrieving the Workflow Output Argument

The WorkflowInvoker.Invoke() method will return an object implementing the IDictionary<string, object> interface. Because a workflow can return back any number of output arguments, you will need to specify the name of a given output argument as a string value to the type indexer. Update your try/catch logic as so:

try
{
  // Send data to workflow!
  IDictionary<string, object> outputArgs =
    WorkflowInvoker.Invoke(new CheckInventory(), wfArgs);

  // Print out the output message.
  Console.WriteLine(outputArgs["FormattedResponse"]);
}

catch (Exception ex)
{
  Console.WriteLine(ex.Message);
}

Now, run your program and enter a make and color that is currently in your copy of the Inventory table of the AutoLot database. You’ll see output such as the following:


**** Inventory Look up ****
Enter Color: Black
Enter Make: BMW
Yes sir! We can send you Black BMW as soon as 2/17/2012 9:23:01 PM!
Press any key to continue . . .

However, if you enter information for an item not currently in stock, you will not only see output such as:


**** Inventory Look up ****
Enter Color: Pea Soup Green
Enter Make: Viper
Sorry, out of stock
Press any key to continue . . .

You will also find a new *.txt file in the inDebug folder of the client application. If you open this in any text editor, you’ll find the following “sales memo”:


***** Attention sales team! *****
Please order the following ASAP!
1 Pea Soup Green Viper
*********************************

That wraps up your introductory look at the WF API. While this chapter has only touched on some of the key aspects of this particular aspect of the .NET platform, I hope you feel confident that you can dive into the topic further if you are interested.

images Source Code The WorkflowLibraryClientproject is included under the Chapter 26 subdirectory.

Summary

In essence, WF allows you to model an application’s internal business processes directly within the application itself. Beyond simply modeling the overall workflow, however, WF provides a complete runtime engine and several services that round out this API’s overall functionality (persistence and tracking services, etc.). While this introductory chapter did not examine these services directly, do remember that a production-level WF application will most certainly make use of these facilities.

In this introduction to the topic, you learned about two key top-level activities, namely Flowchart and Sequence. While each of these types control the flow of logic in unique manners, they both can contain the same sort of child activities and are executed by the host in the same manner (via WorkflowInvoker or WorkflowApplication). You also learned how to pass host arguments to a workflow using a generic Dictionary object and how to retrieve output arguments from a workflow using a generic IDictionary-compatible object.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.140.197.10