Export (0) Print
Expand All

Building a Scalable Business Process Automation Engine Using BizTalk Server 2002 and Visual Studio .NET

 

Doug Thews and Emmanuel Kothapally
divine Managed Services

February 26, 2002

Summary: Covers design and implementation of a scalable BizTalk automated business process application using BizTalk Server 2002, Visual Studio .NET, and SQL Server 2000. (61 printed pages)

Contents

Introduction
Business Process Automation System Infrastructure Design
Implementing Our Business Process Automation System Using BizTalk
Developing Code for the Core Automated Business Process
Developing Informants, Facilitators, and Automators
Summary
Appendix A: Ticket System Database Schema

Introduction

At the heart of any successful company is a business process. This can range from the assembly line process of preparing and delivering food at your favorite fast-food restaurant, to the complexities of sending and receiving funds within an e-banking system.

A common misconception is that business processes involve merely the routing and simple handling of information. For instance, routine approval/denial of expense reports is commonly cited as an example of a business process. While this is a legitimate, simple example, this idea needs to be expanded—to include the processing of dynamic information, which can move in multiple directions based upon any number of variables.

During the design phase of one of our major development efforts, it became clear that we needed to build a automated business process system that not only moved information, documents, and tasks from queue to queue, but it also needed to react differently to different circumstances. We needed to build a set of intelligence into the process infrastructure that would allow for an individual business process to be modified midstream based upon the available information.

By creating such a dynamic and scalable business process infrastructure, we could not only streamline company processes to reduce costs, we could also reduce the cost of developer hours to handle requests for business process modifications as the organization's processes changed.

A system for catching and responding to events is of little value without a context. It is the application of this system to a business process that provides the value. In this article we will design and build a business process automation infrastructure, and then build a help desk ticketing system on top of this infrastructure.

For the purposes of this article, we will reference a business process automation system as the infrastructure that moves tasks and steps from place to place. The places are usually business applications, such as an ERP system, organizations (such as a supplier), or singular roles, such as a person. This should not be confused with workflow collaboration, which typically involves role-based hierarchy and ad hoc processes.

Business Process Automation System Infrastructure Design

One of the first objectives in our design meetings was to evaluate and decide on a standard set of tools upon which to build our business process automation system infrastructure.

A common question during our design phase was, "Why not use Microsoft® Exchange as the foundation of the infrastructure?" While routing and basic processing of information can be implemented in Microsoft Exchange, we felt that it was not as open as we required. We were looking for a platform that could handle not only the basic core business processes, but could also:

  • Dynamically alter the business process based upon upstream information.
  • Quickly alter the production business process with a minimum amount of coding changes.
  • Progressively update the business process step from informant, to facilitator, to automator.

We decided to go with Microsoft® BizTalk® Server 2002, because we felt it could meet these requirements.

In our design, a business process is a set of steps that need to be executed. Each step contains a set of tasks to be completed. When the user is working on a step, they should be able to stop midway and save the information on the steps completed to that point.

The following characteristics apply to all steps in our business process automation system infrastructure:

  • Each step falls into one of three categories:
    1. Informant: In this type of step, the user is presented a set of information and performs the set of tasks within the step manually. Then the user notifies the system upon completion of the step.
    2. Facilitator: In this type of step, an automated data entry screen is presented to the user allowing the user to provide data for completion of the step. When the user completes the data entry screen(s), the step is considered complete.
    3. Automator: In this type of step, a separate process called by the business process automation system infrastructure automates the work. A notification that the step was completed can be sent, but all work in the step is handled without user interaction.
  • Steps can be defined to run in parallel.
  • Steps can contain dependencies on other steps in order to assist parallel business processes.

By classifying step types, you can easily implement a business process with a minimum set of requirements. By building a business process for an application with all informants, the system is easily implemented and can be put into production in a relatively short period of time. Each informant could be a HTML screen that gives instructions on what manual work should be performed.

Let's take a real-world example of a step in a help desk environment to explain the differences between the types of steps. Let's say that one of the steps in a ticket is executed when it is determined that a Microsoft® SQL Server log file is full. Were the step implemented as an informant, a HTML page giving the detailed instructions for the standard DBA policy for purging the log file would be displayed. A facilitator would provide a data entry screen giving the user information about the problem (SQL Server, error, date/time, and so on), with a button that would perform the desired action (per the standard DBA policy). An automator would execute in the background and provide end results (if required) as a notification.

As you can see, this approach is very beneficial, since you can start out with a business process that is primarily a routing engine. Then, as metrics become available, you can develop facilitators for business process steps that require large amounts of manual effort. Once comfortable with facilitators, the same business logic can be encapsulated in an automator to eliminate all manual work in the step. By taking this approach, you not only deliver functionality more quickly to the end user (nobody wants to wait 4 to 6 months for a business system), but you can also ensure that the effort applied to developing facilitators/automators will deliver the greatest cost reduction.

As an aside, you might wonder, "Why not go from an informant directly to an automator?"

First, not all steps can be completely automated. Some information may have to be manually entered into a screen before the rest of the step can be executed (through automation). Secondly, it is our experience that both users and managers are more comfortable with an intermediate step that proves the business process is working. Allowing a user to view the information before the step is executed builds confidence, and after a reasonable period of transition time that screen can be replaced by an automator with little to no disruption.

The following diagram shows the structure of our business process template that the business process automation system infrastructure acts upon. Notice that the step can have definitions for any or all of the three step types. This allows us to define a manual step, while working on the automation of that step. When we are ready for automation, we can "flip the switch" for the step and the system will automatically move from one type to the next.

Click here for larger image.

Figure 1. Business process automation system logical design (click thumbnail for larger image)

Now that we have a robust design for our business process automation system, we can get down to the details of the business process to be implemented. Since our design is open, the type and organization of process does not matter.

For our ticketing system, we need to define the business process that needs to be implemented. The basics needs of our ticketing business process are:

  • Receive ticketing information.
  • Dispatch requests for approvals before work begins.
  • Route work to be performed on a ticket.
  • Generate request for parts (if required).
  • Receive parts (if required).
  • Close ticket.
  • Notify requestor of ticket closure.

As you can see, this is a simplistic ticketing business process, but it represents all of the challenges of a good core business process.

Implementing Our Business Process Automation System Using BizTalk

Now that the business process has been identified, documented, and approved by the end user community we can use the Microsoft® BizTalk Server 2002 Orchestration tool to develop the steps and relationships. Using our Help Desk ticketing system example, we developed the following business process using the BizTalk Orchestration Manager:

Ee265625.biztalkvsautoeng_fig2(en-US,BTS.10).gif

Figure 2. Ticketing system business process

Creating the Core Business Process

To start developing your own business process, you will need to install BizTalk Server 2002 on your development workstation. Then, open the BizTalk Orchestration Manager and begin drawing the business process shown in Figure 2.

During this step we will create BizTalk actions, which describe the business side of the process. The first action is to provide the initialization logic that needs to be performed at the beginning of each and every process instance. The next action is to create a decision tree to select the desired department based upon the Department ID passed to the business process automation system infrastructure. Separate functional units are specified so that the dispatch of each different department's tasks can be developed differently, if required.

The next action in the process is to wait for approval from the dispatched task. Since only 1 department "owns" the task, we will be waiting on that department's approval/rejection of the ticket. Once the approval/rejection has been received, the process checks for the status of the ticket to see if it has been approved or denied. If it is denied, we simply notify the requestor and end the flow. If the ticket is approved, then the process forks itself into two parallel processes—one for the assignment task, and the other to see if there are any parts or materials required to fulfill this request. Observe the parts/materials required branch of the flow, and you will see that this is a conditional branch, as it dispatches a materials required task only if the ticket needs it.

Once the ticket is assigned to a worker and any required material is received (note the AND-ed join), the task to work on the ticket is dispatched. The assignee is notified before the task to work on a ticket is dispatched. Once the worker performs the task and closes the ticket, the requestor is notified of their request being fulfilled.

While this may be impractical for use as a production ticketing system, it utilizes the major components of a good business process automation system and it helps us demonstrate the key features in designing a scalable business system for your enterprise.

Now that the business logic has been identified, we need to create stub objects for each of the functional logic areas that are identified in the diagram. These functional logic areas (procedures) do not have to be written at this time, but they must be identified and available as COM objects, so that the BizTalk Server 2002 Orchestration Manager can be used to map the business logic points to actual components to be called by BizTalk.

To create the class library stub in our example, we will create a Microsoft® Visual Basic® project using Microsoft Visual Studio® .NET. Choose Class Library as the template for your project (Figure 3):

Click here for larger image.

Figure 3. Creating a stubbed class library (click thumbnail for larger image)

In this class library we will define stubs for the following functions:

  • InitWorkflow: The initial method used to pass the ticket information into the business process automation system.
  • DispatchTask: The method used for dispatching various tasks.
  • SendTicketingEmail: The method used for sending e-mails related to the ticket.
  • CompleteWorkflowInstance: The method used to record when a business process ends.
  • PartsRequiredForTicket: A method to determine whether parts are required for this ticket.
  • TicketStatus: A method used to query ticket information.
  • StartWorkflow: The method used to kickoff the business process automation that begins the ticketing process.

Since we are building our component in .NET, we need to add a reference to the library "System.Runtime.InteropServices", and we also need to make the Class available to COM. We can do this in two different ways: Either we create an interface and then implement that interface, or we can specify the Class Interface Type Attribute as "AutoDual". The recommended approach is to create an interface and implement that interface, but in this case, we used AutoDual because our component was being used by a single process.

This indicates that a dual class interface is automatically generated for the class and exposed to COM. Type information is produced for the class interface and published in the type library. Figure 4 shows all the stubbed methods we will tie to our actions.

Click here for larger image.

Figure 4. Creating stubbed class code (click thumbnail for larger image)

Before compiling the project, make sure that "Generate Strong Key Name" is checked in the project properties, so these components can be put on the Global Assembly Cache (GAC) for optimum performance. Now compile the project. After the project is compiled, register the DLLs using Regasm.exe, and put the DLLs on GAC using Gacutil.exe. Below is a script that we created to automate this process for our ticketing system. (Please note that you need to be in the directory that has the required DLLs.)

regasm /unregister TicketingSystemUtil.dll
regasm /unregister Interop.COMRUNTIME_1_0.dll
regasm /unregister Interop.MSXML2_3_0.dll
gacutil -u TicketingSystemUtil
gacutil -u Interop.COMRUNTIME_1_0
gacutil -u Interop.MSXML2_3_0
regasm Interop.COMRUNTIME_1_0.dll
regasm Interop.MSXML2_3_0.dll
regasm TicketingSystemUtil.dll /tlb:TicketingSystemUtil.tlb
gacutil -i Interop.COMRUNTIME_1_0.dll
gacutil -i Interop.MSXML2_3_0.dll
gacutil -i TicketingSystemUtil.dll

If there are any components that TicketingSystemUtil depends upon, you will also need to register these dynamic link libraries and put them on the GAC. COMRUNTIME_1_0 and MSXML2_3_0 are examples of two such DLLs. These 2 DLLs are in fact the .NET-generated wrappers for XLANG Scheduler Runtime type library, since .NET did not find a primary interop assembly for it. You register these when you add the reference to XLANG Scheduler runtime in your project.

Note   This project was created before the BizTalk Server 2002 Toolkit for Microsoft .NET. As a result, we manually created and registered the Interop.COMRUNTIME and Interop.MSXML2 assemblies. With the BizTalk Server 2002 Toolkit for Microsoft .NET this step is no longer necessary, as the code-signed official versions of these assemblies are supplied in this toolkit.

Creating Ports to Link with Non-MSMQ Business Actions

Now that you have a component to work with, you can finish the XLANG schedule diagram. We will do this by telling BizTalk Server what COM objects it needs to call when reaching a piece of business logic. Using the BizTalk Orchestration Manager to complete this business process, we will:

  • Implement the ports that use COM components.
  • Implement the ports that use MSMQ messaging.
  • Tie messages between actions and ports bound to COM components through methods.
  • Tie messages between actions and ports bound to Message Queuing.
  • Create rules for decision trees.
  • Data handling.

The first step is to create BizTalk Server ports that will provide a map from the various business actions to and from the methods defined in our stubbed Workflow class. In order to allow BizTalk Server to call our business logic, and then resume when the logic is completed, a BizTalk Server input and output port is automatically created for each method within the COM object. These ports are tied to the business actions created earlier. To prevent actions from corrupting each other, we must create separate instantiations of a COM object (port).

In addition, separate ports for wait steps must be identified. You can do this by first creating private queues for each wait step, and by then tying the wait step with the port that is linked to the MSMQ instantiation linked to that port.

First, let's create the ports used to get to and from each action and its COM component counterpart. From the Implementation stencil in the BizTalk Orchestration Manager, drag the COM component shape onto the implementation area (this is on the right side of the separator). The COM Component Binding Wizard opens, and we perform the following steps:

  1. On the Welcome to the COM Component Binding Wizard page, type a name for the port that you want to create. BizTalk Orchestration Designer provides a default port name with a number appended to it for each new port implementation that is added. Change this name to InitPort. Click Next.
  2. On the Static or Dynamic Communication page, click Static, which means the component is automatically destroyed when the XLANG schedule instance ends. (For more information about static and dynamic communications, refer to the BizTalk Server 2002 documentation.) Click Next.
  3. On the Class Information page, click From a registered component. A tree control displays all components registered on your computer. Find and expand the folder for the TicketingSystemUtil, and click the BizTalkUtilities class. Click Next.
  4. On the Interface Information page, click the _BizTalkUtilities interface. Click Next.
  5. On the Method Information page, select the following methods:
    • InitWorkflow
    • CompleteWorkflowInstance
    • SendTicketingEmail
    • DispatchTask
    • TicketStatus
  6. Click Next.
  7. On the Advanced Port Properties page accept the defaults. Click Finish.

    Your BizTalk Orchestration diagram should now look like Figure 5.

    Click here for larger image.

    Figure 5. BizTalk Orchestration diagram after creating ports (click thumbnail for larger image)

Since we have parallel processing in our business process automation system infrastructure, we will need to repeat the above process of adding the same COM component twice for each parallel branch, with the following exceptions:

  1. For the Task Assignment branch:
    • Name the Port as TaskAssignment on the Welcome to the COM Component Binding page.
    • Select only the DispatchTask method on the Method Information page.
  2. For the Receive Parts branch:
    • Name the Port as ReceiveParts on the Welcome to the COM Component Binding page.
    • Select SendTicketingEmail, PartsRequiredForTicket and DispatchTask methods on the Method Information page.

Creating Ports to Link Wait Actions to MSMQ Private Queues

Now it's time to create links between wait actions and the message queues that will notify the wait action when to resume the business process. To do this, we will use the BizTalk Orchestration Manager, and from the Implementation stencil we will drag the MSMQ shape onto the implementation area on the right side of the separator. The Message Queuing Binding Wizard opens and we perform the following steps to create a queue linked to the Wait For Approval Response action:

  1. On the Welcome to the Message Queuing Binding Wizard page, click Create a new port. BizTalk Orchestration Designer provides a default port name with a number appended to it for each new port implementation that is added. Change this name to WaitForTasks. Click Next.
  2. On the Static or Dynamic Queue Information page, click Static queue. A static queue is a known, preexisting queue. This queue must be known at design time. (For more information about static and dynamic queues, see the BizTalk Server 2002 documentation.) Click Next.
  3. On the Queue Information page, click Create a new queue for every instance. Keep the default queue prefix that is provided. Click Next.
  4. On the Advanced Port Properties page, accept the defaults. In the Security area, click Not required, and in the Transaction support area, select the Transactions are required with this queue check box. Click Finish.

For each wait action, we need to create a separate private MSMQ queue and link it to the desired action. We can follow the same steps above to create queues for the WaitForParts and the WaitForTaskAssignment actions, with the following changes:

  1. For the Task Assignment branch:
    • Name the port WaitForTaskAssignment on the Welcome to the Message Queuing Binding Wizard page.
  2. For the Receive Parts branch:
    • Name the port WaitForReceiveParts on the Welcome to the Message Queuing Binding Wizard page.

    Your BizTalk Orchestration diagram should now look like Figure 6.

    Click here for larger image.

    Figure 6. BizTalk Orchestration diagram after adding MSMQ (click thumbnail for larger image)

Mapping Business Actions to COM Object Methods

Now it's time to tie each of the actions on the business side of the Orchestration diagram to methods or wait queues (which are associated with the ports) that we've just created.

Our first step is to tie all of our non-MSMQ actions to their respective COM component methods using the Method Communication Wizard. We will start by connecting the InitWorkflow action to the InitPort port. This will invoke the Method Communication Wizard. The wizard has three pages:

  1. On the Welcome to the Method Communication Wizard page, you can specify whether the XLANG Scheduler Engine will call a method or wait for a method call. Select Wait for a synchronous method call. Accept the default values for the other fields. If you specify that the XLANG Scheduler Engine will wait for a method call, you can set a latency value to indicate an amount of time in seconds that the XLANG Scheduler Engine is likely to have to wait before a message arrives. (See the BizTalk Server 2002 documentation for more information on latency.) Click Next
  2. On the Message Information page, you can specify whether a new message or a reference to an existing message should be created. For InitWorkflow action we will select Create a new message. Click Next
  3. On the Message Specification Information page, select InitWorkflow method. Click Finish

    After performing these steps, your Orchestration diagram should look like Figure 7.

    Click here for larger image.

    Figure 7. BizTalk Orchestration diagram after mapping InitWorkflow to InitPort (click thumbnail for larger image)

Next, we need to map actions to methods for each of the remaining non-MSMQ actions. We can do this using the same steps outlined above, except that we want to select Initiate a Synchronous Method Call, and change the name from InitWorkflow to the desired action in Step 3.

A synchronous method call is initiated instead of waiting for a synchronous call, because these actions need to be initiated from outside the business process (that is, starting a ticket in the business process automation system).

The table below summarizes the relationship between the actions, ports, and methods that we just created in the BizTalk Orchestration Manager:

Table 1. Action to port/COM object method relationships

Action NamePort NameMethod Name
Dispatch Default Approval TaskInitPortDispatchTask
Dispatch SD Approval TaskInitPortDispatchTask
Dispatch PRD Approval TaskInitPortDispatchTask
Get Ticket StatusInitPortTicketStatus
Notify Requestor DenialInitPortSendTicketingEmail
Dispatch Ticket AssignmentTaskAssignmentDispatchTask
Parts needed to close ticketReceiveMaterialPartsRequiredforticket
Email material requirementsReceiveMaterialSendTicketingEmail
Receive partsReceiveMaterialDispatchTask
Notify AssigneeInitPortSendTicketingEmail
Dispatch ticket for CloseInitPortDispatchTask
Notify requestor closed ticketInitPortSendTicketingEmail
Complete orchestrationInitPortCompleteWorkflowInstance

Now that we've tied the non-MSMQ actions to ports and methods, we need to describe the same thing for MSMQ actions. These differ because the actions (with the exception of InitWorkflow) will all generate a call to the desired method. MSMQ actions will be waiting until a message in the desired private message queue is received. To trigger the completion of the wait process, a method in one of our instantiated COM objects needs to insert a message into the desired queue.

So, let's make the linkage that connects the Wait for Approval Response action to the WaitForTasks port (which is tied to the private message queue [$WaitForTasks] created earlier). Connecting these two objects invokes the XML Communications Wizard, and you can follow these steps to complete the linkage:

  1. On the Welcome to the XML Communication Wizard page, you can specify whether the port will send a message to an action, or receive a message from an action. Click Receive. Click Next.
  2. On the Message Information page, you can specify whether a new message or a reference to an existing message should be created. Click Create a new message and type in the name for the message. Click Next.
  3. On the XML Translation Information page, you can specify whether you want messages sent to or received from the queue as XML-formatted data or as text strings. Click Send XML messages to the Queue. Click Next.
  4. On the Message Type Information page, you can specify a label that the XLANG Scheduler Engine should use to identify messages of the type you define. Enter TaskResponse as the message label. Click Next.
  5. On the Message Specification Information page, click Browse and select TaskResponse.xml as the specification. Click Finish.

Now that we've tied the Wait for Approval action to the desired message queue, we need to do the same for the following MSMQ actions:

  • Wait for Approval
  • Wait for Assignment
  • Wait for Parts
  • Wait for Close

You can do this by following the same steps for each MSMQ action as described above.

Creating Rules for Decision Trees

There are 3 decision trees in the diagram. The first tree is to determine which department the ticket needs to be routed to for approval. The second is to determine if the ticket has been approved or not. The third tree is to determine if the ticket requires parts to fulfill the request. Before doing these decision trees, you will need to click on the data page and open up the constants window to add the following constants that are required for this business process to function correctly.

Table 2. Constant values for the ticket system

Constant NameValue
SoftwareDepartmentId45
ProductProgramsDepartmentId 46
DefaultDepartmentId 47
TicketApprovedStatus2
EmailRejection1
EmailApproved2
EmailClose3
EmailAssigned4
EmailMaterialRequired5
TaskApproveSD121
TaskApprovePRD122
TaskApproveDefault120
TaskAssign123
TaskMaterialReceive124
TaskClose125

Next, we need to add rules that define what to do in a decision tree. To add a rule, double-click on a decision tree shape and click Add Rule. Figure 8 shows how we added a rule for the Ticket For Software Development decision tree.

Ee265625.biztalkvsautoeng_fig8(en-US,BTS.10).gif

Figure 8. Adding a rule

This rule tells the XLANG schedule to set the value of the Department ID for this instance of a business process equal to the constant for Software Development that we set up earlier. Table 3 shows the other rules that need to be set up.

Table 3. Ticketing system rules

Decision TreeRule NameScript Expression
Department DeterminationTicket for Software DevelopmentInitWorkFlow_in.DepartmentId = Constants.SoftwareDevelopmentDepartmentID
 Ticket is for Product ProgramsInitWorkFlow_in.DepartmentId = Constants.ProductProgramsDepartmentId
Approval/Rejection DeterminationTicket ApprovedTicketStatus_out.pRetVal = Constants.TicketApprovedStatus
Part Requirement DeterminationParts Are NeededPartsRequiredForTicket_out.pRetVal = 1

Data Handling

Click the data tab of the XLANG schedule to link all the messages. You will see:

  • One message shape for every message in the XLANG schedule.
  • One Constants message.
  • One Port References message containing a port field for each port within the XLANG schedule drawing.
  • Diagrammatic connections showing the flow of data between the message fields.

Messages consist of a set of uniquely named fields, each containing one data item of a specific data type. Every message in the XLANG schedule is displayed on the Data page as a table. Each table displays the name of the message and a listing of field names and their corresponding data types. Connections on the Data page point from the right side of a source message field, to the left side of a destination message field. This connection indicates that the source message field will provide the data for the destination message field. At run time, the XLANG Scheduler Engine will copy the data from the source message field into the destination message field when the destination message has to be created. If the source message has not arrived yet, a run-time error will occur. See Figure 9 below for completing data connections for the very first message that is DispatchTask_In.

Click here for larger image.

Figure 9. DispatchTask_In data connections (click thumbnail for larger image)

Table 4. XLANG schedule message links

MessageFieldFrom MessageField
DispatchTask_inInstanceIDInitWorkflow_InInstanceId
DispatchTask_inTaskDefinitionIDConstantsTaskApproveDeptSD
DispatchTask_inCallBackQPort ReferencesWaitForTasks
DispatchTask_in_1InstanceIDInitWorkflow_InInstanceId
DispatchTask_in_1TaskDefinitionIDConstantsTaskApproveDeptPRD
DispatchTask_in_1CallBackQPort ReferencesWaitForTasks
DispatchTask_in_2InstanceIDInitWorkflow_InInstanceId
DispatchTask_in_2TaskDefinitionIDConstantsTaskApproveDefaultDept
DispatchTask_in_2CallBackQPort ReferencesWaitForTasks
DispatchTask_in_3InstanceIDInitWorkflow_InInstanceId
DispatchTask_in_3TaskDefinitionIDConstantsTask
DispatchTask_in_3CallBackQPort ReferencesWaitForTasks
DispatchTask_in_4InstanceIDInitWorkflow_InInstanceId
DispatchTask_in_4TaskDefinitionIDConstantsTaskreceiveParts
DispatchTask_in_4CallBackQPort ReferencesWaitForReceiveParts
DispatchTask_in_5InstanceIDInitWorkflow_InInstanceId
DispatchTask_in_5TaskDefinitionIDConstantsTaskAssign
DispatchTask_in_5CallBackQPort ReferencesWaitForTaskAssignment
DispatchTask_in_6InstanceIDInitWorkflow_InInstanceId
DispatchTask_in_6TaskDefinitionIDConstantsTaskClose
DispatchTask_in_6CallBackQPort ReferencesWaitForTasks
CompleteWorkflowInstance_InWorkflowDOMInitWorkflow_InWorkflowInfo
SendTicketingEmail_InWorkflowDOMInitWorkflow_InWorkflowInfo
SendTicketingEmail_InEmailTypeConstantsEmailRejection
SendTicketingEmail_In_2WorkflowDOMInitWorkflow_InWorkflowInfo
SendTicketingEmail_In_2EmailTypeConstantsEmailAssigned
SendTicketingEmail_In_3WorkflowDOMInitWorkflow_InWorkflowInfo
SendTicketingEmail_In_3EmailTypeConstantsEmailClose
SendTicketingEmail_In_4WorkflowDOMInitWorkflow_InWorkflowInfo
SendTicketingEmail_In_4EmailTypeConstantsEmailMaterialsRequired
TicketStatus_InTicketIdInitWorkflow_InTicketId
PartsRequiredForTicket_InTicketIdInitWorkflow_InTicketId

When mapping business actions to COM object methods, you can either wait for the synchronous call to be made by an external agent or let the XLANG Scheduler application initiate the synchronous call. For all non-MSMQ business actions in our XLANG schedule, the first action requires the wait for synchronous method call, as we need a way to pump the data into the business process automation system when the Workflow Instance Starter kicks off the XLANG schedule instance. Once that data is inside the XLANG schedule instance we can let the XLANG Scheduler application initiate the other calls.

Figure 10 shows the completed XLANG schedule. To complete the functionality of our ticketing system, we need to create the code that executes behind the COM objects that we stubbed out earlier.

Click here for larger image.

Figure 10. Completed XLANG schedule (click thumbnail for larger image)

Developing Code for the Core Automated Business Process

Overview

Up until this point, we've been working on the functional design of the ticketing system. We've created a business process automation system using the BizTalk Orchestration Manager, and we linked business actions to stubbed object methods that will ultimately end up running the business rules behind each step in the business process.

Now it's time to start writing the code that captures our business rules for each object method specified earlier in our COM object.

First, let's take a look at the technical design of how all of our code modules fit together inside the environment. Figure 11 shows the technical design diagram.

Click here for larger image.

Figure 11. Business process automation system technical architecture (click thumbnail for larger image)

BizTalk Utilities

Our first task is to set up the code behind the BizTalkUtilities module. The primary Purpose of BizTalkUtilities is to encapsulate all the core business process automation functionality into methods that can be tied to actions within BizTalk XLANG schedules. Earlier, we created the BiztalkUtilities COM object in a stubbed format to complete our XLANG Scheduler diagram. In this section, we'll review code for some of the more important sections of this object.

This first section of code shows all of the core constants and enumerators used throughout our core business process automation system:

Private Enum EmailTypeEnum
  Created = 0
  Rejection = 1
  Approval = 2
  Close = 3
  Assigned = 4
  MaterialAlert = 5
End Enum
Private Const c_TicketStartupDOM As String = _
                            "<Ticket>" & _
                            "<Header>" & _
                            "<Properties>" & _
                            "<TicketID><#TICKETID#></TicketID>" & _
                            "DepartmentID><#DEPTID#></DepartmentID>" & _
                            "</Properties>" & _
                            "</Header>" & _
                            "</Ticket>"
Private Const c_ActionStartupDOM As String = _
                            "<Task>" & _
                            "<Header>" & _
                            "<Properties>" & _
                            "</Properties>" & _
                            "</Header>" & _
                            "</Task>"

Private Const c_TicketToken As String = "<#TICKETID#>"

Private Const c_DepartmentToken As String = "<#DEPTID#>"

Private Const c_AssignedToToken As String = "<#ASSIGNEDTO#>"

Private Const c_TicketRejection As String = _
                  "Your Ticket <#TICKETID#> has been " & _ 
                  "rejected. Please contact the Department " & _
                  "<#DEPTID#> for more details"

Private Const c_TicketApproval As String = _
                  "Your Ticket <#TICKETID#> has been " & _ 
                  "Approved. Please contact the Department " & _
                  "<#DEPTID#> for more details"

Private Const c_TicketClosed As String = _
                 "Your Ticket <#TICKETID#> has been Closed. " & _
                 "If you are not satisfied please contact the " & _
                 "Department <#DEPTID#> for reopening the ticket."

Private Const c_TicketAssigned As String = _
                 "Ticket <#TICKETID#> has been Assigned " & _
                 "to you. If you have questions please contact the " & _
                 "Department <#DEPTID#>."

Private Const c_TickeMaterialAlert As String = _
                 "Please send the material required for fulfilling" & _
                 "Ticket <#TICKETID#>.  If you have questions please " & _
                 "contact the Department <#DEPTID#>."

Private Const c_MaterialAlertEmail As String = _
                 "MaterialHandling@xyz.com"

Private Const WorkflowUserGUID As String = _
                 "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"

StartWorkFlow

The StartWorkflow function is used to kickoff an XLANG schedule instance. It requires a TicketID, DepartmentID and WorkflowID to be passed as parameters. The TicketID represents the ticket that was created for this business process. The DepartmentID represents the department that will handle this ticket. The WorkflowID identifies the record corresponding to the XLANG schedule representing the ticketing business process.

The first action this function performs is to validate the input parameters. Then it instantiates the Instance object, sets the values for user, DOM, WorkflowID and then simply calls the Start method. The remainder of the start process is handled by the Start method. The code section below shows the StartWorkFlow function:

Public Function StartWorkflow(ByVal TicketId As Integer, _
                              ByVal DepartmentId As Integer, _
                              ByVal WorkflowId As Integer) As Boolean
  Dim sDOM As String
  Dim oInstance As Instance

  StartWorkflow = False
  If TicketId < 1 Or DepartmentId < 1 Or WorkflowId < 1 Then
    Throw New Exception _
      ("Cannot start a process without proper parameters. " & _ 
       TicketId = " & TicketId & " DepartmentID = " & _
       DepartmentId & " WorkflowID = " & WorkflowId)
  End If
  Try
    oInstance = New Instance()
    oInstance.WorkflowId = WorkflowId
    oInstance.User = m_User
    sDOM = c_TicketStartupDOM
    sDOM = Replace(sDOM, c_TicketToken, CType(TicketId, String))
    sDOM = Replace(sDOM, c_DepartmentToken, CType(DepartmentId, String))
    oInstance.DOM = sDOM
    oInstance.Start()
    StartWorkflow = True
    Catch Except As Exception
      Return False
    End Try
End Function

InitWorkflow

InitWorkflow function does nothing more than give us a way to pass data into an XLANG schedule instance. The Start method of the Instance object automatically invokes this as soon as the XLANG schedule instance is started successfully. Here is the code for the InitWorkFlow function:

Public Function InitWorkFlow(ByVal WorkflowInfo As String, _
                             ByVal InstanceId As Integer, _
                             ByVal TicketId As Integer, _
                             ByVal DepartmentId As Integer) _
                             As Boolean
  InitWorkFlow = True
End Function

DispatchTask

The DispatchTask function will create a WorkPool item for a given instance, a Task Definition ID, and a Callback Queue.

The function validates the passed parameters Task, Definition, and Instance IDs. Next, it creates an action record with Instance and Task Definition IDs representing the XLANG scheduler action that is dispatching the task. Then, it instantiates the WorkPool item and adds it to global work pool. Here is the code for the DispatchTask function.

Public Function DispatchTask(ByVal InstanceId As Integer, 
                             ByVal TaskDefinitionId As Integer, _
                             ByVal CallbackQ As String) As Boolean
  Dim oWPItem As WorkPoolItem
  Dim oWP As WorkPool
  Dim oTask As TaskDefinition
  Dim oTasks As Tasks
  Dim sActionName As String
  Dim sActionDescription As String
  Dim oAction As Action
  Dim sTaskDOM As String
  Dim oInstance As Instance
  Dim oInstances As Instances
  Dim sFilter As String

  oTasks = New Tasks(" TaskDefinitionId = " & TaskDefinitionId.ToString)
  If oTasks.Count() = 1 Then
    oTask = oTasks.Item(0)
    sActionName = "Dispatch Task - " & oTask.TaskName
    sActionDescription = "Action to Dispatch " & oTask.TaskName & _
                         " Task for Instance " & InstanceId.ToString
    sTaskDOM = c_ActionStartupDOM
    oAction = CreateTaskAction(InstanceId, sActionName, _
                               sActionDescription, sTaskDOM)
    If oAction Is Nothing Then
      Throw New Exception("Action Create failed.")
    End If
    sFilter = "InstanceId = " + InstanceId.ToString
    oInstances = New Instances(sFilter)
    If oInstances.Count = 1 Then
      oWPItem = New WorkPoolItem()
      With oWPItem
        .ActionId = oAction.ActionId
        .InstanceId = InstanceId
        .PostBackQueue = CallbackQ
        .TaskDefinitionId = TaskDefinitionId
        .StepDefinitionId = 0
        .WorkType = WorkTypeEnum.WorkTypeTask
        .User = m_User
      End With
      oWP = New WorkPool()
      oWP.Add(oWPItem)
      DispatchTask = True
    Else
      Throw New Exception("InstanceId passed to DispatchTask is " & _
                          "not valid. Instance Id = " &
                          InstanceId.ToString)
    End If
  Else
    Throw New Exception("TaskDefinitionId passed to DispatchTask " & _
                        "is not valid. Task Id = " & _
                        TaskDefinitionId.ToString)
  End If
End Function

SendTicketingEmail

Various business actions use the SendTicketingEmail function to send notifications. It requires the WorkFlowDOM and EmailType as its input parameters. Based on the EmailType enumerated type values, it builds the correct From, To, Subject, and Body strings, and then fires the notifications. Please refer to the Constants and Enumerations code section above for these values. Notice that this function calls GetTicketId and GetEmailForUser private functions. GetTicketId simply parses the XML DOM and returns the node value for TicketID. The getEmailForUser function uses the Core Workflow security object ADS to get user information and then simply returns an e-mail address by concatenating the SAMAccount and the company extension. Here is the code for the SendTicketingEmail function.

Public Function SendTicketingEmail(ByVal WorkflowDOM As String, _
                                   ByVal EmailType As EmailTypeEnum) _
                                   As Boolean
  Try
    Dim Tostr As String
    Dim FromStr As String = "TicketingSystem@xyz.com"
    Dim EmailBody As String
    Dim SubStr As String
    Dim TicketId As Integer
    Dim DepartmentId As Integer
    Dim Ticket As Ticket

    TicketId = GetTicketId(WorkflowDOM)
    Ticket = New Ticket(TicketId)
    DepartmentId = Ticket.DepartmentId
    Select Case EmailType
      Case EmailTypeEnum.Approval
        EmailBody = c_TicketApproval
        EmailBody = Replace(EmailBody, c_TicketToken, _
                            CType(TicketId, String))
        EmailBody = Replace(EmailBody, c_DepartmentToken, _
                            CType(DepartmentId, String))
        Tostr = GetEmailforUser(Ticket.RequestedUser)
        SubStr = "Ticket " & Ticket.ID & " has been approved."
      Case EmailTypeEnum.Assigned
        EmailBody = c_TicketAssigned
        EmailBody = Replace(EmailBody, c_TicketToken, _
                            CType(TicketId, String))
        EmailBody = Replace(EmailBody, c_DepartmentToken, _
                            CType(DepartmentId, String))
        Tostr = GetEmailforUser(Ticket.AssignedUser)
        SubStr = "Ticket " & Ticket.ID & " has been assigned to you."
      Case EmailTypeEnum.Close
        EmailBody = c_TicketClosed
        EmailBody = Replace(EmailBody, c_TicketToken, _
                            CType(TicketId, String))
        EmailBody = Replace(EmailBody, c_DepartmentToken, _
                            CType(DepartmentId, String))
        Tostr = GetEmailforUser(Ticket.RequestedUser)
        SubStr = "Ticket " & Ticket.ID & " has been closed."
      Case EmailTypeEnum.MaterialAlert
        EmailBody = c_TickeMaterialAlert
        EmailBody = Replace(EmailBody, c_TicketToken, _
                            CType(TicketId, String))
        EmailBody = Replace(EmailBody, c_DepartmentToken,_
                            CType(DepartmentId, String))
        Tostr = c_MaterialAlertEmail '"myaddress@company.com"
        SubStr = "Ticket " & Ticket.ID & " needs Material."
      Case EmailTypeEnum.Rejection
        EmailBody = c_TicketRejection
        EmailBody = Replace(EmailBody, c_TicketToken, _
                            CType(TicketId, String))
        EmailBody = Replace(EmailBody, c_DepartmentToken, _
                            CType(DepartmentId, String))
        Tostr = GetEmailforUser(Ticket.RequestedUser)
        SubStr = "Ticket " & Ticket.ID & " has been rejected."
    End Select
    SendTicketingEmail = SendEmail(Tostr, FromStr, SubStr, EmailBody)
  Catch ex As Exception
            SendTicketingEmail = False
  End Try
End Function

Private Function SendEmail(ByVal ToStr As String, _
                           ByVal FromStr As String, _
                           ByVal SubStr As String, _ 
                           ByVal EmailBody As String) As Boolean
  Try
    Dim mailobj As New SmtpMail()
    Dim msgobj As MailMessage

    msgobj = New MailMessage()
    msgobj.To = ToStr
    msgobj.From = FromStr
    msgobj.Subject = SubStr
    msgobj.Body = EmailBody
    mailobj.Send(msgobj)
    SendEmail = True
  Catch ex As Exception
    SendEmail = False
  End Try
End Function

Private Function GetEmailforUser(ByVal UserGUID As String) As String

  Dim ads As New ADS()
  Dim UserGuidInputArray As New ArrayList()
  Dim ADSUserInfoArray As ArrayList
  Dim ADSUserInfo As ADSUserInfo
  
  UserGuidInputArray.Add(UserGUID)
  ADSUserInfoArray = ads.GetUserInformation(UserGuidInputArray)
  If ADSUserInfoArray.Count > 0 Then
    ADSUserInfo = CType(ADSUserInfoArray(0), ADSUserInfo)
    GetEmailforUser = ADSUserInfo.SamAccount & "@xyz.com"
  Else
    GetEmailforUser = "deadletter@xyz.com"
  End If
End Function

Private Function GetTicketId(ByVal WorkflowDOM As String) As Integer

  Dim xDOM As New Xml.XmlDocument()
  Dim xNode As Xml.XmlNode
  Dim InstanceId As Integer
  
  GetTicketId = 0
  xDOM.LoadXml(WorkflowDOM)
  xNode = xDOM.GetElementsByTagName("TicketID").Item(0)
  If Not xNode Is Nothing Then
    InstanceId = CType(xNode.InnerText, Integer)
  End If
  GetTicketId = InstanceId
End Function

TicketStatus

This function instantiates a ticket object corresponding to the Ticket ID, and returns its status.

    Public Function TicketStatus(ByVal TicketID As Integer) As Integer
        Dim Ticket As New Ticket(TicketID)
        Return Ticket.TicketStatus
    End Function

PartsRequiredForTicket

This function instantiates a ticket object based on the Ticket ID passed, and then simply returns 1 or 0 based on the value of the TicketType property.

Public Function PartsRequiredForTicket(ByVal TicketID As Integer) _
                                       As Integer
  Dim Ticket As New Ticket(TicketID)
  If Ticket.TicketType = _
     Ticket.TicketTypeEnum.RequiresMaterialsOrTools Then
    PartsRequiredForTicket = 1
  Else
    PartsRequiredForTicket = 0
End If
End Function

CompleteWorkflowInstance

The CompleteWorkflowInstance method is called from the last business action of the XLANG schedule for the ticketing system. The sole purpose of this function is to provide bookkeeping for the core business process automation system. It checks to see if there are any child processes associated with this instance, and if so, makes sure all of the children have run to completion. Once these prerequisites are met, it sets the Instance Status to Completed. Most of this functionality is accomplished by the Instance object's Complete method.

Public Function CompleteWorkflowInstance(ByVal WorkflowDOM As String) _
                                         As Boolean
  Dim oInstances As Instances
  Dim oInstance As Instance
  Dim InstanceId As Integer = 0
  Dim xDOM As New XmlDocument()

  CompleteWorkflowInstance = False
  xDOM.LoadXml(WorkflowDOM)
  InstanceId = GetInstanceId(WorkflowDOM)
  If InstanceId < 1 Then
    Throw New Exception("Cannot complete a process without InstanceId" &_
                        " value within WorkflowDOM. DOM = " & WorkflowDOM)
  End If
  oInstances = New Instances("InstanceId = " + InstanceId.ToString)
  If (oInstances.Count = 1) Then
    oInstance = CType(oInstances(0), Instance)
    If (oInstance.IsOkayToEnd() = True) Then
      oInstance.User = m_User
      oInstance.Complete()
      Return True
    Else
      Throw New Exception("Cannot Complete the Instance. Child " & _
                          "Instances running is the likely cause")
    End If
  Else
    CompleteWorkflowInstance = False
    Throw New Exception("Cannot create Instance object. " & _
                        "Invalid Instance Id is the likely cause")
  End If
End Function

Task Object

The Task object describes a unit of work that can be assigned to an individual. It comprises one or more steps. Our ticketing system dispatches work at the task level. A task can have the following properties:

  • Name
  • Type
  • Task Wizard
  • Description
  • Status
  • Team Assignment
  • Organization Assignment

The Task type can be either Self Assignable (if the task is available, it can be assigned when the worker opens it). Grant Ownership requires a manager to assign it to a worker. The last type is Auto Start, which provides the mechanism for the task to be completed by an automator without any physical assignments.

Please refer to TASKDEFINITION.VB to reference the code for this object.

Step Object

The Step object is a unit of work that can be accomplished by an informant, facilitator, or an automator. The difference between a step and a task is that a task can have many steps, but only tasks are an assignable unit of work.

The business process for our ticketing system only uses single steps for each task. However, it is important to note that the core business process automation system infrastructure can easily support multiple steps per task using this object model.

The step has the following properties:

  • Name
  • Description
  • Type (informant, facilitator, automator)
  • Link to the informant, facilitator, or automator

Refer to STEPDEFINITION.VB to reference the code for this object.

Workflow Object

The Workflow object provides access to the individually defined BizTalk business process we created using the BizTalk Orchestration Manager. You must have access to this file to instantiating an instance of a particular process.

In our ticketing system, we defined only one business process, but this same object model can be easily used to support multiple processes. This is especially useful for complex ticketing systems where a task in one process can kickoff an entirely different process associated with the type of work to be performed.

Please refer to WORKFLOWTEMPLATE.VB to reference the code for this object.

Ticket Object

The Ticket object represents a user request to get something done that is fulfilled by running an XLANG schedule. It stores the information about who the requestor is, what department usually approves or deals with the request, whether or not there are any materials required to fulfill the ticket, the current status of the ticket, and who is assigned to work on the ticket.

Refer to TICKET.VB to reference the code for this object.

Instance Object

The Instance object represents a running XLANG schedule instance that is used to fulfill something, in our case a ticket. It stores the information about who started the instance of the business process, what is the GUID that uniquely represents it, the data passed into it (this is stored in instance header entity), and the current status of the process.

Refer to INSTANCE.VB to reference the code for this object.

Action Object

The Action object represents a significant business action in an XLANG diagram. An instance is comprised of one or more actions. In our case, the examples of actions are all business actions in the diagram that dispatch a task.

Refer to ACTION.VB to reference the code for this object.

Global Workpool Object

You can manipulate an instance of a step for a ticket by setting up the global Workpool object. A good example is the handling of a ticket in the Waiting For Approval step in our ticketing system.

The lifecycle of a Workpool object for each step passes through the following states:

  • Item is queued.
  • Item is assigned.
  • Item is in progress.
  • Item is completed.

Here is the code for completing a business process task:

    Public Function CompleteWork(ByVal hrs As Decimal, _
                                 Optional ByVal RespDOM As String = "") _
                                 As Boolean
        Dim parameters(4) As SqlParameter
        Dim sResp As String = WorkDoneResponse
        Dim oAct As Action
        Dim oActs As Actions
        Dim oTasks As Tasks
        Dim oTask As TaskDefinition
        Dim oWP As WorkPool
        Dim strFilter As String
        Dim bCanComplete As Boolean = True
        CompleteWork = False

        If (RespDOM <> "") Then
            'set RespDOM as response message to CallBackQ
            sResp = RespDOM
            'set the actions DOM out and call Done method on it
            oActs = New Actions("ActionId = " & m_ActionId)
            oAct = CType(oActs(0), Action)
            oAct.User = m_User
            oAct.Done(RespDOM)
        End If
        'check if this is a step or a task
        If (m_WorkType = WorkTypeEnum.WorkTypeStep Or _
            m_WorkType = WorkTypeEnum.WorkTypeReworkStep) Then
            If (DoCompleteWork(hrs) = True) Then
                oTasks = New Tasks("TaskDefinitionId = " & _
                                   m_TaskDefinitionId)
                If (oTasks.Count() = 1) Then
                    oTask = CType(oTasks(0), TaskDefinition)
                    If (oTask.TaskWizardType = _
                        WizardType.SerialDispatch) Then
                        DispatchTaskStepsInSerial()
                    ElseIf (oTask.TaskWizardType = 
                              WizardType.CustomWorkFlow) Then
                        'this is a custom business process
                        'so try to get into it
                        If (m_PostBackQueue <> "" _
                            And m_PostBackQueue <> Nothing) Then
                            ExecuteMSMQ(m_PostBackQueue, sResp)
                        Else
                            'there is no queue to get back into the
                            'business process. Put error handling code
                            'here if required
                        End If
                    End If
                End If
            Else
                Throw New Exception("DoCompleteWork() for Step Failed")
            End If
            CompleteWork = True
        ElseIf (m_WorkType = WorkTypeEnum.WorkTypeTask Or _
                m_WorkType = WorkTypeEnum.WorkTypeReworkTask) Then
            'check if all the steps are done else throw an exception
            'saying some steps are still running
            strFilter = "InstanceId = " & m_InstanceId & " and _
                         ActionId = " & m_ActionId & " and _
                         TaskDefinitionId = " & m_TaskDefinitionId & " _
                         and isnull(StepDefinitionId,0) <> 0 and _
                         WorkStateType <> " & _
                         CType(WorkStateType.WorkCompleted, Integer)
            oWP = New WorkPool(strFilter)
            If (oWP.Count > 0) Then
                bCanComplete = False
            End If
            If bCanComplete = True Then
                'update workpoolitem record with hours
                'use business process callbackq
                'to get back into business process.
                If (DoCompleteWork(hrs) = True) Then
                    If (m_PostBackQueue <> "" And _
                        m_WorkType <> WorkTypeEnum.WorkTypeReworkTask) _
                    Then
                        ExecuteMSMQ(m_PostBackQueue, sResp)
                    Else
                    End If
                Else
                    Throw New Exception("DoCompleteWork()" & _
                                        "for Task Failed")
                End If
            Else
                Throw New Exception("Task Cannot be completed " & _
                                    "because some steps are " & _
                                    "still running and/or has " & _
                                    "dependent rework tasks " & _
                                    "still running")
            End If
        End If
        CompleteWork = True
    End Function

A facilitator calls the CompleteWork method. The facilitator passes in the hours it took to complete the task.

The WorkPool object is a collection of WorkPoolItem objects.

Workflow Instance Starter

The Workflow Instance Starter is a facilitator in the ticketing system that is used to create tickets and kickoff a business process for the newly created ticket.

For the ticket system, our Workflow Instance Starter (CreateTickets) is a Web form presented to the user to fill in a description, department, and expected date. When the user clicks Apply, the ticket fields are validated and the ticket is saved using the Ticket object described in CoreWorkflow system. Then an XLANG schedule instance is kicked off using the StartWorkflow method of the BiztalkUtilities object.

Sample code for creating a Workflow Instance Starter ticket:

    Private Sub btnButtonApplySmall_1_Click(ByVal sender As Object,_
              ByVal e As System.Web.UI.ImageClickEventArgs) Handles _
                                          btnButtonApplySmall_1.Click
        Dim sValidationError As String = ""
        Dim Ticket As New Ticket()
        If ddlDepartment.SelectedItem.Value = "-1" Then
            sValidationError = _
                         "A valid Department needs to be selected. "
        End If
        If ddlStatus.SelectedItem.Value = "-1" Then
            sValidationError = sValidationError & _
                            " A valid Status needs to be selected. "
        End If
        If Not IsDate(txtExpectedDate.Text) Then
            sValidationError = sValidationError & _
                                   " Date entered is not valid."
        End If
        If sValidationError <> "" Then
            sValidationError = "Found these validation error(s). " & _
                   sValidationError & " Pleas fix them and try again."
            ShowError(sValidationError)
        Else
            Try
                Ticket.DepartmentID = _
                     CType(ddlDepartment.SelectedItem.Value, Integer)
                If chkRequiresMaterial.Checked Then
                    Ticket.TicketType = _
                       Ticket.TicketTypeEnum.RequiresMaterialsOrTools
                Else
                    Ticket.TicketType = _
                       Ticket.TicketTypeEnum.RequiresJustLabor
                End If
                Ticket.ExpectedDate = _
                       CType(txtExpectedDate.Text, DateTime)
                Ticket.Description = txtDescription.Text
                Ticket.TicketStatus = _
                       CType(ddlStatus.SelectedItem.Value, Integer)
                Ticket.RequestedUser = WKFSecurity.UserId
                Ticket.Add()
                Dim oBizUtil As New BizTalkUtilities()
                oBizUtil.StartWorkflow(Ticket.ID, _
                              Ticket.DepartmentID, c_TicketingWorkflow)
                pnlSubmitTicket.Visible = False
                ShowInfo("Ticket has been submitted. TicketId = " + _
                        Ticket.ID.ToString)
            Catch ex As Exception
                ShowError(ex.ToString)
            End Try
        End If
    End Sub

Developing Informants, Facilitators, and Automators

Overview

So, now we've developed the underlying business process automation system. We've also encapsulated the business logic for moving a task/step through a specific business process, identified by a BizTalk XLANG schedule. The last step is to create informants, facilitators, or automators, for each of the business process steps. These modules give the user the opportunity to interact with an order that is passing through the process (with the exception of an automator, which merely performs the business logic and then signals the business process automation system that it has completed the work).

Developing a Base Page

Before we go off and create a bunch of ASP.NET screens, we probably want to create a common appearance for all screens. One of the best ways to do this in .NET is to create a base page that every developer can inherit from. In this base page, we can specify what a page, data grid, or any other visual item looks like when instantiated by the developer. This page will ensure the developer need not worry about the details of paging or filtering for objects, like a table or data grid, because they have already been developed and tested. This will speed up the overall time required to develop data screens. The base page also further separates presentation logic from business logic, and allows parallel UI and functional development to occur with minimal side effects.

The new feature of Visual Studio .NET gives a development team structured control over the user interface (UI), by forcing standardization across the UI of the entire system. This inheritance also allows (if enabled) the developer to tailor the base page to their needs. However, this feature should be carefully analyzed as too much customization defeats the purpose of creating a standard UI.

Our base page provides the following functionality:

  • Security information for the current user.
  • Standard style sheets and images.
  • WorkPool ID and ticket information.
  • WorkDone function to get back into the instance of the business process.

Security information is a set of permissions stored as XML DOM that describes which applications the user can access. The DOM also describes the modules and functions within a module that a user can access. Since this article is not about a security model, we will limit our discussion to this model.

BasePage, with the help of the BasePageBuilder object loads the standard style sheets, images, and other properties based on the skin. As mentioned above, this allows us to change our overall UI without greatly impacting the underlying application.

If a WorkPool ID is passed in the QueryString to the LoadPage method, it is stored in a private variable and a protected property is exposed for the derived pages to use. In addition to storing the WorkPool ID, it will fetch the Ticket ID and instantiate the Ticket object, while exposing it as a protected property for the derived pages.

Now let's talk about the WorkDone function. BasePage implements this function so the facilitators and informants can fully utilize this functionality and concentrate on the facilitator functionality, rather than on BizTalk Orchestration. The WorkDone function first checks to see if the call was made in the context of a WorkPool ID. If so, it will set the response and hours if the optional parameters are not passed in. Then it calls the CompleteWork method on the WorkPoolItem object. After that, it checks to make sure there are no additional steps associated with this task that are not completed. If this is the only step or the last step, then it will call CompleteWork on the WorkpoolItem object that is associated with the task this step belongs to. Please note that the CompleteWork method will set the appropriate status and then returns to the business process automation system by posting the appropriate message to MSMQ. Here's the Base page source code:

    Protected Overridable Function LoadPage _
                    (Optional ByVal pgLoad As Page = Nothing) As Page
        If Session("UserID") = "" Then
            If IsNothing(Page.Request.Cookies("UserID")) Then
                Response.Redirect(m_LoginPageURL)
            Else
                m_SecurityPermission.UserId = _
                        Request.Cookies("UserID").Value
                Session("UserID") = Request.Cookies("UserID").Value
            End If
        Else
            m_SecurityPermission.UserId = CType(Session("UserID"), String)
        End If

        If m_ApplicationName <> "" Then
            m_Permissions = _
                 m_SecurityPermission.GetSecurity(m_ApplicationName & _
                 ".*.*", OptionType.LeastRestrictive)
            Session(m_ApplicationName & "Permissions") = m_Permissions
        End If

        If Not pgLoad Is Nothing Then
            WKFPageBuilder.LoadPageImages(pgLoad)
        End If

        If Request.QueryString("WorkPoolId") <> "" Then
            Dim WP As WorkPool
            Dim WpItem As WorkPoolItem
            m_WorkPoolId = CType(Request.QueryString("WorkPoolId"), _
                                 Integer)
            WP = New WorkPool("workpoolid = " & m_WorkPoolId)
            WpItem = WP(0)
            m_Ticket = New Ticket  _
                      (CType(WpItem.HeaderValues("TicketId").Value, _
                       Integer))
        End If
        m_bLoadPage = True
    End Function

    Protected Function WorkDone(Optional ByVal hrs As System.Decimal=0, _
                                Optional ByVal resp As String = "") _
                                As Boolean
        Dim Hours As System.Decimal
        Dim Response As String
        Dim oWorkPool As WorkPool
        Dim oWorkPoolItem As WorkPoolItem
        Dim oTaskWorkPoolItem As WorkPoolItem
        If WorkPoolId > 0 Then
            oWorkPool = New WorkPool("workpoolid = " & WorkPoolId)
            oWorkPoolItem = oWorkPool(0)
            WorkDone = False
            With oWorkPoolItem
                oWorkPool = New WorkPool("TaskDefinitionId = " & _
                            .TaskDefinitionId & " AND WorkType = " & _
                            WorkTypeEnum.WorkTypeTask & _
                            "AND (StepDefinitionId IS NULL OR _
                            StepDefinitionId = 0) AND InstanceId = " & _
                            .InstanceId & " AND ActionId = " & .ActionId)
                If oWorkPool.Count = 1 Then
                    oTaskWorkPoolItem = oWorkPool(0)
                End If
            End With
            If hrs = 0 Then
                Hours = DefaultStepHours
            Else
                Hours = hrs
            End If
            If resp = "" Then
                Response = DefaultStepResponse
            Else
                Response = resp
            End If
            oWorkPoolItem.User = WKFSecurity.UserId
            If oWorkPoolItem.CompleteWork(Hours, Response) Then
                'Check to see if Task also needs to be completed.
                If oWorkPoolItem.TaskDefinition.Steps.Count = 1 Then
                    ' this was the only step so complete the task
                    Response = DefaultTaskResponse
                    oTaskWorkPoolItem.User = WKFSecurity.UserId
                    oTaskWorkPoolItem.CompleteWork(Hours, Response)
                ElseIf oWorkPoolItem.TaskDefinition.Steps.Count > 1 Then
                    ' Check if all the steps are done if so 
                    ' complete the task
                    With oTaskWorkPoolItem
                        oWorkPool = New WorkPool("TaskDefinitionId = " & _
                                   .TaskDefinitionId & _
                                    " AND WorkType = " & _
                                    WorkTypeEnum.WorkTypeStep & _
                                    "AND WorkStateType <> " & _
                                    WorkStateTypeEnum.WorkCompleted & _
                                  " AND InstanceId = " & .InstanceId & " _
                                    AND ActionId = " & .ActionId)
                        If oWorkPool.Count = 0 Then 
                            ' no steps left that are not done.
                            Response = DefaultTaskResponse
                            oTaskWorkPoolItem.User = WKFSecurity.UserId
                            oTaskWorkPoolItem.CompleteWork(Hours,Response)
                        End If
                    End With
                End If
                WorkDone = True
            End If
        Else
            Throw New Exception("This function cannot be invoked " & _
                                "as the WorkPool ID was not available")
        End If
    End Function

Now that we've created the Base page functionality, it makes sense to show you a code snippet of how this can be invoked. Here's a sample Base page inheritance .aspx file:

<%@ Page Language="vb" AutoEventWireup="false" 
Codebehind="WebForm1.aspx.vb" Inherits="TicketingSystem.WebForm1"%>
<html>
  <head>
    <title></title>
    <meta name="GENERATOR" content="Microsoft Visual Studio.NET 7.0">
    <meta name="CODE_LANGUAGE" content="Visual Basic 7.0">
   <LINK href="<%=WKFPageBuilder.WKFStyleSheet%>" 
         type="text/css" rel="stylesheet">
  </head>
  <body>
    <form id="Form1" method="post" runat="server">
    </form>
  </body>
</html>

And here's the Base page inheritance code-behind file:

Public Class WebForm1
    Inherits BasePage

#Region " Web Form Designer Generated Code "
    'This call is required by the Web Form Designer.
    <System.Diagnostics.DebuggerStepThrough()> _
    Private Sub InitializeComponent()
    End Sub
    Private Sub Page_Init(ByVal sender As System.Object, _
                     ByVal e As System.EventArgs) Handles MyBase.Init
        'CODEGEN: This method call is required by the Web Form Designer
        'Do not modify it using the code editor.
        InitializeComponent()
    End Sub
#End Region

    Private Sub Page_Load(ByVal sender As System.Object, _
                   ByVal e As System.EventArgs) Handles MyBase.Load
        
        LoadPage(Me)
        'put other initialization here..
    End Sub

End Class

Creating an Informant

In our business process design, an informant is a read-only screen that presents some static information to the user, which is associated with a specific step for the item (ticket) inside the process.

A good example of an informant is when a sales person calls a customer one month after buying a car, thanking them for the purchase. In this instance, the system would bring up a static page instructing the sales person to call the customer and thank them. An informant does not update anything in the system, other than letting the business process automation system know that it is done with the task. Generally, an informant is used in the beginning stages of business process development. Informants are then gradually replaced by facilitators, and then by automators to provide a totally automated step.

In our ticketing system, we implemented an informant to ask the worker to receive material before he can work on a ticket and close it. For this, we need a set of instructions telling where to get the material from and perhaps the ticket number and requestor name. (Note that a facilitator or automator can easily be upgraded at a later point to automate the retrieval of materials.) There is nothing that gets saved to the ticket item here, but the work done needs to be provided so that the user can inform the business process that the task has been completed.

Below is the ASP.NET MaterialReceiving.aspx file that implements this informant. This Web page has just a few controls on it, providing some static instructions with a few dynamic fields, like Ticket ID and expected date.

<%@ Page Language="vb" AutoEventWireup="false" 
Codebehind="MaterialReceiving.aspx.vb" 
Inherits="TicketingSystem.MaterialReceiving" %>
<%@ Register TagPrefix="WKF" TagName="ErrorMessagePanel" 
src="..\controls\ucErrorPanel.ascx" %>
<%@ Register TagPrefix="WKF" Tagname="ucTableheader" 
Src="..\controls\ucTableHeader.ASCX" %>
<HTML>
  <HEAD>
   <meta name="GENERATOR" content="Microsoft Visual Studio.NET 7.0">
   <meta name="CODE_LANGUAGE" content="Visual Basic 7.0">
   <LINK href="<%=WKFPageBuilder.WKFStyleSheet%>" type="text/css"
    rel="stylesheet">
  </HEAD>
  <body class="bodyPage">
   <form id="example" method="post" runat="server">
       <table cellpadding="0" cellspacing="0" border="0" width="600">
       <tr>
         <td class="tableBorder">
           <table cellpadding="0" cellspacing="0" border="0"
            width="100%">
            <tr>
              <td width="100%">
                <WKF:ucTableHeader id="drhHeader" Runat="Server"
                 width="100%" />
              </td>
            </tr>
           </table>
         </td>
       </tr>
       <TR>
         <TD class="tableBorder">
           <TABLE cellSpacing="1" cellPadding="0" width="100%"
            border="0">
             <tr>
               <td>
                <WKF:ErrorMessagePanel id="ErrorMessagePanel"
                 Runat="Server" Width="600" PanelType="Error"
                 visible="false" />
                <WKF:ErrorMessagePanel id="InformationMessagePanel"
                 Runat="Server" Width="600" PanelType="Information"
                 visible="false" />
                <asp:Panel ID="pnlSubmitTicket" Runat="server">
                <table cellSpacing="5" cellPadding="0" width="100%"
                 border="0" class="tableBackground">
                <tr>
                  <td align="left">
                    <asp:label id="lblInstructions" 
                     text="Instructions: "
                     runat="Server" cssclass="fontFieldLabelNorm" />
                  </td>
                </tr>
                <tr>
                  <td align="left">
                    <asp:textbox id="txtInstructions" width="350" 
                     textmode="MultiLine" Rows="15" Wrap="True"
                     runat="Server" maxlength="1000" Enabled="False" />
                  </td>
                </tr>
                <tr>
                  <td align="right">
                    <asp:imagebutton id="btnButtonDoneSmall_1"
                     runat="server" />
                  </td>
                </tr>
                </table>
                </asp:Panel>
              </td>
             </tr>
           </TABLE>
         </TD>
       </TR>
     </table>
   </form>
 </body>
</HTML>

The code behind is much simpler and smaller for an informant, since the only function that is supported is the work done. Below are the code sections for PageLoad and the Done button, and even handlers for the Receive Materials informant. The code should be very straightforward to understand. HideError and HideInfo functions just hide the error and information panels from previous post backs.

Private Const c_Instructions As String = _
              "Please get the Material for the Ticket <#TICKETID#>." & _
              " The Due Date for the Ticket is  <#DUEDATE#>." & _
              " Click done when you are finished"

    Private Sub Page_Load(ByVal sender As System.Object, _
                        ByVal e As System.EventArgs) Handles MyBase.Load
        Try
            HideError()
            HideInfo()
            LoadPage(Me)
            drhHeader.Title = "Receive Materials for Ticket"
            If Not IsPostBack Then
                txtInstructions.Text = _
                            c_Instructions.Replace("<#TICKETID#>", _
                            Ticket.ID.ToString).Replace("<#DUEDATE#>", _
                            Ticket.ExpectedDate.ToShortDateString)
            End If
        Catch except As Exception
            ShowError(except.ToString)
        End Try
    End Sub

    Private Sub btnButtonDoneSmall_1_Click(ByVal sender As Object, _
                    ByVal e As System.Web.UI.ImageClickEventArgs)  _
                    Handles btnButtonDoneSmall_1.Click
        Try
            WorkDone()
            ShowInfo("Task done successful.")
            pnlSubmitTicket.Visible = False
        Catch except As Exception
            ShowError(except.ToString)
        End Try
    End Sub

Creating a Facilitator

A facilitator is the next level of automation for a business process step. The facilitator provides a data entry screen to interact with the system before completing the step and signaling the business process automation system to move on.

An example of a facilitator would be one of Microsoft's many wizards that are common across Microsoft® Office and Microsoft Windows® XP. The thought here is that the user would be better served being walked through the process of, for example, setting up a printer, if they are presented with easy-to-use screens that query them for simple information. The wizard actually does any complex work behind the scenes based on the information provided by the user.

One of the facilitators in the ticketing system is the Approve Ticket facilitator. It is the job of this facilitator to give the user a chance to approve or reject an incoming ticket. The facilitator handles behind-the-scenes work regarding what to do for approvals or rejections (thus, it's not an informant), but user interaction is required (hence, it's not an automator). Let's take a look at the Approve Ticket facilitator in more detail.

Approve Ticket is a simple Web form that accesses 2 user controls, one for displaying error or information messages, and the other for table headers. At the very beginning, notice the use of WKFPageBuilder to set the standard style sheet. WKFPageBuilder was one of the protected properties exposed off of the Base page.

The very first control on the form is a user control, ucTableHeader, which is used to display the heading for the facilitator form. This form also uses 2 instances of ucErrorPanel control—one for displaying errors and the other for displaying information messages. Following user control are the regular controls for entering description, selecting a department, a check box to indicate whether materials will be required, and an Expected Date to be filled in by the user. Lastly, the form has two buttons: One button to apply the Changes, and the other to complete this task and signal the business process to continue.

Here is the ASP.NET source code for TicketApproval.aspx file:

<%@ Page Language="vb" AutoEventWireup="false" 
Codebehind="TicketApproval.aspx.vb" 
Inherits="TicketingSystem.TicketApproval" %>
<%@ Register TagPrefix="TCKT" TagName="ErrorMessagePanel" 
src="..\controls\ucErrorPanel.ascx" %>
<%@ Register TagPrefix="TCKT" Tagname="ucTableheader" 
Src="..\controls\ucTableHeader.ASCX" %>
<HTML>
   <HEAD>
      <meta name="GENERATOR" content="Microsoft Visual Studio.NET 7.0">
      <meta name="CODE_LANGUAGE" content="Visual Basic 7.0">
      <LINK href="<%=WKFPageBuilder.WKFStyleSheet%>" 
            type="text/css" rel="stylesheet">
   </HEAD>
   <body class="bodyPage">
      <form id="example" method="post" runat="server">
         <!--Open the main table-->
         <table cellpadding="0" cellspacing="0" border="0" width="600">
            <tr>
               <td class="tableBorder">
               <table cellpadding="0" cellspacing="0" 
                      border="0" width="100%">
                 <tr>
                   <td width="100%">
                     <TCKT:ucTableHeader id="drhHeader" 
                      Runat="Server" width="100%" />
                   </td>
                 </tr>
               </table>
               </td>
            </tr>
            <TR>
               <TD class="tableBorder">
                 <TABLE cellSpacing="1" cellPadding="0" 
                  width="100%" border="0">
                   <tr>
                     <td>
                       <TCKT:ErrorMessagePanel id="ErrorMessagePanel"
                        Runat="Server" Width="600" PanelType="Error"
                        visible="false" />
                        <asp:validationsummary id="ValidSummary"
                        runat="Server" 
                       headertext="The following errors were found:"
                       cssclass="panelError" forecolor="white"
                       showSummary="True" displayMode="List" />
                      <TCKT:ErrorMessagePanel id="InformationMessagePanel"
                       Runat="Server" Width="600" PanelType="Information"
                       visible="false" />
                      <asp:Panel ID="pnlSubmitTicket" Runat="server">
                      <table cellSpacing="5" cellPadding="0" width="100%"
                       border="0" class="tableBackground">
                      <tr>
                        <td align="right" width="35%">
                          <asp:label id="lblTicketId" text="* Ticket ID: "
                           runat="Server" cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                          <asp:textbox id="txtTicketID" width="150"
                           runat="Server" maxlength="4" enabled="False" />
                          <asp:requiredfieldvalidator id="valtxtTicketID"
                           runat="Server" controltovalidate="txtTicketID"
                           errorMessage="You must enter a ticket id." 
                           display="static">*</asp:requiredfieldvalidator>
                        </td>
                      </tr>
                      <tr>
                        <td align="right" width="35%">
                          <asp:label id="lblRequestor" 
text="* Requestor: "
                           runat="Server" cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                          <asp:textbox id="txtRequestor" width="200"
                           runat="Server" maxlength="25" Enabled="False"/>
                        </td>
                      </tr>
                      <tr>
                        <td align="right" width="35%">
                          <asp:label id="lblDescription" 
                           text="* Description: " runat="Server"
                           cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                          <asp:textbox id="txtDescription" width="350"
                           textmode="MultiLine" Rows="5" Wrap="True"
                           runat="Server" maxlength="1000" />
                          <asp:requiredfieldvalidator id="valDescription"
runat="Server" controltovalidate="txtDescription"
                           errorMessage="You must enter a description." 
                           display="static">*
                          </asp:requiredfieldvalidator>
                         </td>
                      </tr>
                      <tr>
                        <td align="right" width="35%">
                          <asp:label id="lblDepartment" 
                           text="* Department: "
                           runat="Server" cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                          <asp:dropdownlist id="ddlDepartment"
                           runat="Server" AutoPostBack="False"
                           Enabled="False" />
                        </td>
                     </tr>
                     <tr>
                        <td align="right" width="35%">
                            <asp:label id="lblRequiresMaterial" 
                           text="* Materials Required : " runat="Server"
                           cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                              <asp:checkbox id="chkRequiresMaterial"
                           runat="Server" />
                        </td>
                     </tr>
                     <tr>
                        <td align="right" width="35%">
                           <asp:label id="lblStatus" text="* Status: "
                           runat="Server" cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                          <asp:dropdownlist id="ddlStatus" runat="Server"
                           AutoPostBack="False" />
                        </td>
                     </tr>
                     <tr>
                        <td align="right" width="35%">
                          <asp:label id="lblExpectedDate" 
                           text="* Expected Date (MM/DD/YY): "
                           runat="Server" cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                          <asp:textbox id="txtExpectedDate" width="150"
                           runat="Server" maxlength="10" />
                          <asp:requiredfieldvalidator id="valExpectedDate"
                           runat="Server"
                           controltovalidate="txtExpectedDate"
                           errorMessage=
"You must enter the expected date."
                           display="static">*
                          </asp:requiredfieldvalidator>
                        </td>
                     </tr>
                     <tr>
                        <td align="right" width="35%">
                          <asp:label id="lblAssignedTo" 
                           text="* Assigned To: " runat="Server"
                           cssclass="fontFieldLabelNorm" />
                        </td>
                        <td>
                          <asp:dropdownlist id="ddlAssignedTo"
                           runat="Server" AutoPostBack="False"
                           enabled="False" />
                        </td>
                     </tr>
                     <tr>
                        <td align="left" valign="bottom">
                          <asp:label id="Label2" text="* Required Field"
                           cssclass="fontStdSmall" runat="Server" />
                        </td>
                        <td align="right">
                          <asp:imagebutton id="btnButtonDoneSmall_1"
                           runat="server" />
                            <asp:imagebutton id="btnButtonApplySmall_1"
                           runat="server" />
                        </td>
                     </tr>
                  </table>
                </asp:Panel>
                 </td>
              </tr>
            </TABLE>
          </TD>
        </TR>
      </table>
    </form>
  </body>
</HTML>

Now let's take a look at the code-behind form for the TicketApproval.aspx file.

This form inherits from the Base page. On the Page_Load event, the LoadPage method is invoked, which will do security validations and load the necessary ticket information. If this is not a form post back, the form will populate the drop-down lists for Department and Status; then it calls initMembers method to set the drop-down lists to their current values. HideError and HideInfo hide previous errors or information from post back.

PopulateDepartmentList and PopulateStatusList add items to the drop-down lists that are valid. InitMembers will use the Ticket object that is exposed as a protected property of the Base page and set the values of all controls. Each of these values should be straightforward, other than GetRequestorName, which uses the ADS component to retrieve the username.

At this point, the form is submitted to the user and the user can either save the changes to the form and/or mark the work as "Done" with the Approve Ticket task.

Let's examine the code behind the Apply Changes. The Apply code merely sets properties of the Ticket object with the values from the form and calls the Save method on the Ticket object.

The Done button code validates that the status was either set to approved or rejected, and then calls the protected method WorkDone off of the Base page. So here's the TicketApproval.aspx Visual Basic code-behind file:

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As _
                          System.EventArgs) Handles MyBase.Load
        Try
            HideError()
            HideInfo()
            LoadPage(Me)
            drhHeader.Title = "Approve Ticket for " & GetDepartmentName()
            If Not IsPostBack Then
                PopulateDepartmentList()
                PopulateStatusList()
                InitMembers()
            End If
        Catch except As Exception
            ShowError(except.ToString)
        End Try
    End Sub

    Private Sub InitMembers()
        Dim listitem As ListItem
        txtRequestor.Text = GetRequestorName()
        txtTicketID.Text = Ticket.ID.ToString
        txtDescription.Text = Ticket.Description
        txtExpectedDate.Text = Ticket.ExpectedDate
        chkRequiresMaterial.Checked = (Ticket.TicketType = _
                           Ticket.TicketTypeEnum.RequiresMaterialsOrTools)
        listitem = ddlDepartment.Items.FindByValue(Ticket.DepartmentID)
        If Not IsNothing(listitem) Then
          ddlDepartment.Items.FindByValue(Ticket.DepartmentID).Selected _
                                                                = True
        End If
        listitem = ddlStatus.Items.FindByValue(Ticket.TicketStatus)
        If Not IsNothing(listitem) Then
          ddlStatus.Items.FindByValue(Ticket.TicketStatus).Selected = True
        End If
    End Sub

    Private Function GetRequestorName() As String
        Dim oADS As New ADS()
        Dim oADSUserInfo As ADSUserInfo
        Dim UserListArray As New ArrayList()
        UserListArray.Add(Ticket.RequestedUser)
        oADSUserInfo = oADS.GetUserInformation(UserListArray)(0)
        Return oADSUserInfo.UserName
    End Function

    Private Sub btnButtonApplySmall_1_Click(ByVal sender As Object, _
                       ByVal e As System.Web.UI.ImageClickEventArgs)_
                                  Handles btnButtonApplySmall_1.Click
        Try
            With Ticket
                If chkRequiresMaterial.Checked Then
                    .TicketType = _
                            Ticket.TicketTypeEnum.RequiresMaterialsOrTools
                Else
                    .TicketType = _
                            Ticket.TicketTypeEnum.RequiresJustLabor
                End If
                .ExpectedDate = CType(txtExpectedDate.Text, DateTime)
                .Description = txtDescription.Text
                .TicketStatus = _
                   CType(ddlStatus.SelectedItem.Value, Integer)
                .Save()
                ShowInfo("Ticket ID " & .ID & " was Saved Successfully")
            End With
        Catch ex As Exception
            ShowError(ex.ToString)
        End Try
    End Sub

Creating an Automator

An automator is the ultimate level of automation for a business process step. In this example, all of the work is automated and shielded from the user. Once the work is performed, the automator signals the business process automation system to move on to the next step, all without any user interaction.

An example of an automator might be the last step in a purchasing system. After gaining all of the approvals and checking for the best prices, the last step would be to actually order the item(s). In this day and age of online ordering, this could be accomplished by using an automator to contact the vendor (possibly using Web Services and SOAP) to generate an order for the desired item.

The task for an automator needs to be setup as an AutoStart task. Then the step that uses the automator needs to have its facilitator type set as AutoStep. The facilitator format string should hold the information about the assembly, class, and function names that make up the automator.

Once the automator setup is completed, the DLL that has the automator business logic needs to be developed and copied to the BizTalk Server.

Let's take a look at an example of how we created an automator for our ticketing system. In this example, we used an existing task that was previously a facilitator, and is now being upgraded to a full automator. To do this you will need:

  • A .NET assembly that has the functionality to automatically assign a ticket to a worker.
  • To change Task Definition and Step Definition setups to be automators.

Creating an Automator Object

  1. Create a new Visual Basic .NET ClassLibrary project.
  2. Add reference to TicketingSystemUtil, which has the Ticket Object
  3. Add 2 methods—one to retrieve the least loaded employee, and the other to assign the least loaded employee to a given ticket.

    (Since this task is so specific to ticket assignment, the second parameter TaskDefinitionId is redundant, but it is very useful in cases where the automators are used for multiple task definitions.)

Now let's take a look at the source code for our test automator that we just created.

Imports TicketingSystemUtil
Public Class Assigner
    Private Function GetLeastLoadedEmployee() As String
        Return "AAAAAAAA-1111-BBBB-2222-CCCCCCCCCCCC" ' Test GUID
    End Function
    Public Function DoAssignment(ByVal TicketId As Integer, 
Optional ByVal TaskDefinitionID _
                                 As Integer = 0) As Boolean
        Dim Ticket As Ticket
        Try
            Ticket = New Ticket(TicketId)
            Ticket.AssignedUser = GetLeastLoadedEmployee()
            Ticket.Save()
            Return True
        Catch ex As Exception
            'Log the Error to central Logging System 
            'here or notify key people who can _
             recover/rerun this task
            Return False
        End Try
    End Function
End Class

Setting up Tasks and Steps as Automators

A task is an automator if it runs as soon as it is dispatched to the global Workpool. This means that no manual effort is needed to get the task started. When a task starts, the steps that are associated with it are dispatched either in serial or parallel.

For a normal task, it is dispatched, assigned, and then the user can start it by selecting it in the Workpool Management screen. That means the steps within the task are not dispatched until the user actually selects the task to be started.

For an auto-start task, steps are dispatched either in serial or parallel, based on how the task is associated with its steps during setup.

A step is an automator if it has its facilitator type set as AutoStep and its facililitator format string has name value pairs for the DLL, class, and function that does the automation work. When an AutoStep gets dispatched, the automator runs using the .NET reflection. After it returns from automator, the step is marked as complete. In addition, if the task that has the current automator step is an auto task, and if all other steps are completed, the task also is marked as complete.

The StartStep and ExecuteAutoStep functions listed below should help to understand the concept of implementing an automator.

    Public Enum FacilitatorType
         Automatic = 46
         URL = 47
    End Enum

    Public Enum TaskType
         SelfAssignable = 43
         GrantOwnership = 44
         AutoStart = 220
    End Enum    

    Private Function StartStep() As String
        Dim ds As DataSet
        Dim row As DataRow
        Dim parameters(4) As SqlParameter

        StartStep = ""
        If StepDefinition.FacilitatorType = FacilitatorType.Automatic Then
            If m_WorkStateType <> WorkStateTypeEnum.WorkCompleted Then
                ExecuteAutoStep()
                CompleteWork(0, DefaultStepResponse)
                Dim WorkPool As WorkPool
                Dim strFilter As String
                Dim TaskWorkPoolItem As WorkPoolItem
                ' if there are no steps left in this task and the task is
                ' an auto start then complete the task as well 
                strFilter = "InstanceId = " & m_InstanceId & _
                            " and ActionId = " & m_ActionId & _
                            " and TaskDefinitionId = " & _
                            TaskDefinitionId  & _
                            " and isnull(StepDefinitionId,0) <> 0 " &_
                            " and WorkStateType <> " _
                             & CType(WorkStateType.WorkCompleted, Integer)
                WorkPool = New WorkPool(strFilter)
                If WorkPool.Count = 0 Then
                    strFilter = "InstanceId = " & m_InstanceId & _
                                " and ActionId = " & m_ActionId & _
                                " and TaskDefinitionId = " & _
                                 m_TaskDefinitionId & _
                                 " and isnull(StepDefinitionId,0) = 0 "
                    WorkPool = New WorkPool(strFilter)
                    TaskWorkPoolItem = WorkPool(0)
                    If TaskWorkPoolItem.TaskDefinition.TaskType = _
                            TaskType.AutoStart Then
                        TaskWorkPoolItem.CompleteWork _
                            (0, DefaultTaskResponse)
                    End If
                End If
            Else
                Throw (New Exception("Step is already done"))
            End If
        ElseIf StepDefinition.FacilitatorType = FacilitatorType.URL Then
            :
            :
        Else
            Throw New Exception("Invalid Step Facilitator Type. _" & 
                                "Data corrupted in the WKF tables")
        End If
    End Function

   Private Function ExecuteAutoStep() As Boolean
        Dim tokfld As String
        Dim tokval As String
        Dim startdelim As String = "<#"
        Dim enddelim As String = "#>"
        Dim sindx As Integer
        Dim eindx As Integer
        Dim len As Integer
        Dim sFilter As String
        Dim strAssemblyName As String
        Dim strClassName As String
        Dim strFuncName As String
        Dim toklen As Integer
        sindx = 1
        eindx = 1
        len = m_StepDefinition.FacilitatorFormat.Length
        ' Get AppName, ModName, FuncName from the 
  ' Facilitator Format String and then invoke the auto step
        While sindx + 1 < len
            sindx = InStr(eindx, m_StepDefinition.FacilitatorFormat, _
                     startdelim, Microsoft.VisualBasic.CompareMethod.Text)
            If (sindx > 0) Then
                eindx = InStr(sindx, _
                           m_StepDefinition.FacilitatorFormat, enddelim, _
                           Microsoft.VisualBasic.CompareMethod.Text)
                If eindx > 0 Then
                    tokfld = Mid(m_StepDefinition.FacilitatorFormat, _
sindx + 2, eindx - sindx - 2)
                    Select Case tokfld.Substring(0, 3)
                        Case "DLL"
                           strAssemblyName=tokfld.Substring(4, toklen - 4)
                        Case "CLS"
                            strClassName = tokfld.Substring(4, toklen - 4)
                        Case "FUN"
                            strFuncName = tokfld.Substring(4, toklen - 4)
                        Case Else
                            Throw New Exception _
                              ("Unrecognized token in Facilitator." &_ 
                               " Format string in the Step Definition")
                    End Select
                End If
            End If
            sindx = eindx + 2
            eindx = sindx
        End While
        If strAssemblyName <> "" And strClassName <> "" _
            And strFuncName <> "" Then
            Dim oAssembly As [Assembly]
            Dim oType As Type
            Dim oMod As [Module]
            Dim propertyInfo As PropertyInfo
            Dim methodInfo As MethodInfo
            Dim objInstance As Object
            Dim MethodParameters(1) As Object
            oAssembly = [Assembly].LoadFrom(strAssemblyName)
            If oAssembly Is Nothing Then
                Throw New Exception _
                 ("Invalid AssemblyName. AssemblyName = " & _
                  strAssemblyName)
            End If
            For Each oMod In oAssembly.GetModules
                For Each oType In oMod.GetTypes
                    If oType.Name.ToUpper = strClassName.ToUpper Then
                        methodInfo = oType.GetMethod(strFuncName)
                        objInstance = Activator.CreateInstance(oType)
                        If Not methodInfo Is Nothing Then
                            MethodParameters(0) = _
CType(HeaderValues("TicketId").Value,_
                                    Integer)
                            MethodParameters(1) = m_TaskDefinitionId
                            Return methodInfo.Invoke(objInstance, _
                                                     MethodParameters)
                        Else
                            Throw New Exception
("Invalid MethodName passed. " &_
                             MethodName = " & strFuncName)
                        End If
                        Exit For
                    End If
                Next
                Throw New Exception
                  ("Invalid ClassName passed. ClassName = " & _
                    strClassName)
            Next
        Else
            Throw New Exception
               ("Auto Step Cannot be executed because either the" &_
                " Applcaition Name, Module Name or Function Name" &_
                " is not set")
        End If
    End Function

Note that automators have to follow the pre-defined standard format. For our ticketing system, the automator takes two parameters. The first one is the Ticket ID and the second one is the Task Definition ID. he function must return a Boolean. The facilitator format string should include the information about the DLL, class, and function that comprises the automator.

Now that we've developed the automator, we can perform the actual setup process. Using Enterprise Manager, open the WKF database and then open the Task Definition table. Set the task type to 220, which implies an AutoStart task (see the enumerations above for a listing of task types). Now open the Step Definition Table and select the step associated with the Assign Ticket task. Change the facilitator type to 46, which implies an automatic step (see facilitator type enumeration in the code listing above). Set the Facilitator Format String to name value pairs that represent the DLL, class, and the function for the automator itself. The screen capture below summarizes the setup changes for task and step definitions for automation of the Ticketing Assignment task.

Click here for larger image.

Figure 12. Automating the Ticketing Assignment task (click thumbnail for larger image)

Summary

In this article we've demonstrated how to develop a scalable business process automation system built upon the BizTalk Server 2002 and Visual Studio .NET platforms. As you can see, this system is capable of supporting our simplistic ticketing application, but can also easily support a more complex enterprise business system.

In future releases of BizTalk Server, the direction will be to allow XLANG schedules, which are the heart of any business process system, to be built and released by the business community. In doing so, you can easily see how our philosophy of informants can be used to provide quick support for all tasks and steps. Then, as the business learns where cost savings can be realized, an upgrade to facilitators and automators can be performed.

In our experience, this type of development activity is most successful, since the details are fixed and the users can see results fairly quickly. Compare this with other complex development projects that you read about, where the user doesn't get to use these new features for months.

Appendix A: Ticket System Database Schema

TABLE: m_WKF_Workflow

This table stores the Workflow template information that corresponds to an XLANG schedule. The file name represents the compiled version of the XLANG schedule. The queue name represents the name of the queue under which the tasks are dispatched from the instance.

TABLE: m_WKF_Instance

This table stores the running instances of XLANG schedules instantiated by the core business process system. It stores the user who started the XLANG schedule instance, a pointer to the XLANG schedule (represented by m_WKF_Workflow entity above), the state the instance is in, any data that is passed into the process during the start, the GUID associated with the XLANG schedule instance, and the Module GUID associated with the GUID.

TABLE: m_WKF_Action

This table stores an action within an XLANG schedule. It stores any input data associated with the action, and the instance this action is associated with.

TABLE: m_WKF_Task

This table stores assignable units of work that an employee/worker needs to complete. It stores the information like name of the task, the team that usually does this type of work, the organization that usually oversees this work, whether an employee can grab the task or needs to be assigned to an employee, and any dispatch rules that need to be adhered to.

TABLE: m_WKF_Step_Definition

This entity represents a unit of work that can be handled by a facilitator, informant, or an automator. It stores name, description, facilitator format string, and facilitator type information that are typical of a step definition

TABLE: m_WKF_Task_Step

This table stores the mapping of tasks to steps. A task can have one or more steps associated with it, and a step can be associated with one or more tasks.

TABLE: m_WKF_Work_Pool

This is the primary table that represents instantiation of tasks and steps that are associated with an action and an instance. It has a WorkType attribute, that can be either a task or a step. It has a WorkStateType attribute that tells if the WorkPool item is queued, assigned to someone, being worked on by someone, or completed. It has pointers to the task definition and step definition that this WorkPool item represents. It has pointers to instance and action that created this WorkPool item. It also stores the information about who is working on it, which team is responsible for handling this work, and which organization oversees this work.

TABLE: m_WKF_Ticket

This table stores the ticketing information. It has the attributes to capture requestor, ticket ID, department this ticket will be handled by, the description of the request, status, and whether or not parts/material are required to fulfill this request.

About the Authors

Doug Thews is the Director of Software Development for divine Managed Services. He has over 17 years of software development experience in C/C++ and Visual Basic, and has been the program manager for divine's Visual Studio .NET JDP partnership since December 2000. Doug can be reached at Doug.Thews@divine.com.

Emmanuel Kothapally is a Senior Developer for divine Managed Services. He has over 10 years of software development experience in Visual Basic and Visual C++, and is a member of the divine Visual Studio .NET JDP team. Emmanuel can be reached at Emmanuel.Kothapally@divine.com.

Was this page helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft