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)
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
Appendix A: Ticket System Database Schema
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.
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:
- 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.
- 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.
- 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.
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.
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:
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):
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.
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:
- 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.
- 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.
- 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.
- On the Interface Information page, click the _BizTalkUtilities interface. Click Next.
- On the Method Information page, select the following methods:
- Click Next.
- On the Advanced Port Properties page accept the defaults. Click Finish.
Your BizTalk Orchestration diagram should now look like Figure 5.
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:
- 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.
- 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:
- 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.
- 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.
- On the Queue Information page, click Create a new queue for every instance. Keep the default queue prefix that is provided. Click Next.
- 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:
- For the Task Assignment branch:
- Name the port WaitForTaskAssignment on the Welcome to the Message Queuing Binding Wizard page.
- 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.
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:
- 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
- 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
- On the Message Specification Information page, select InitWorkflow method. Click Finish
After performing these steps, your Orchestration diagram should look like Figure 7.
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 Name||Port Name||Method Name|
|Dispatch Default Approval Task||InitPort||DispatchTask|
|Dispatch SD Approval Task||InitPort||DispatchTask|
|Dispatch PRD Approval Task||InitPort||DispatchTask|
|Get Ticket Status||InitPort||TicketStatus|
|Notify Requestor Denial||InitPort||SendTicketingEmail|
|Dispatch Ticket Assignment||TaskAssignment||DispatchTask|
|Parts needed to close ticket||ReceiveMaterial||PartsRequiredforticket|
|Email material requirements||ReceiveMaterial||SendTicketingEmail|
|Dispatch ticket for Close||InitPort||DispatchTask|
|Notify requestor closed ticket||InitPort||SendTicketingEmail|
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:
- 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.
- 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.
- 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.
- 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.
- 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
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.
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 Tree||Rule Name||Script Expression|
|Department Determination||Ticket for Software Development||InitWorkFlow_in.DepartmentId = Constants.SoftwareDevelopmentDepartmentID|
|Ticket is for Product Programs||InitWorkFlow_in.DepartmentId = Constants.ProductProgramsDepartmentId|
|Approval/Rejection Determination||Ticket Approved||TicketStatus_out.pRetVal = Constants.TicketApprovedStatus|
|Part Requirement Determination||Parts Are Needed||PartsRequiredForTicket_out.pRetVal = 1|
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.
Figure 9. DispatchTask_In data connections (click thumbnail for larger image)
Table 4. XLANG schedule message links
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.
Figure 10. Completed XLANG schedule (click thumbnail for larger image)
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.
Figure 11. Business process automation system technical architecture (click thumbnail for larger image)
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"
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 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
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
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 '"email@example.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 = "firstname.lastname@example.org" 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
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
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
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
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:
- Task Wizard
- 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.
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:
- Type (informant, facilitator, automator)
- Link to the informant, facilitator, or automator
Refer to STEPDEFINITION.VB to reference the code for this 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.
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.
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.
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
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
- Create a new Visual Basic .NET ClassLibrary project.
- Add reference to TicketingSystemUtil, which has the Ticket Object
- 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.
Figure 12. Automating the Ticketing Assignment task (click thumbnail for larger image)
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.
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.
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.
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.
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.
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
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.
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.
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.