To help the workflow understand the service, the workflow’s code file defines the IBugReportService interface shown in the following code. This interface defines all of the service methods that the workflow can invoke, and defines all of the service events that the workflow can catch.
Imports System.Workflow.Runtime Imports System.Workflow.Activities Imports System.Workflow.Activities.Rules Imports System.Workflow.ComponentModel <ExternalDataExchange()> _ Public Interface IBugReportService Event EngineerAssigned As EventHandler(Of StringArgs) Event BugWorked As EventHandler(Of ExternalDataEventArgs) Event BugRejected As EventHandler(Of ExternalDataEventArgs) Event TestPassed As EventHandler(Of ExternalDataEventArgs) Event TestFailed As EventHandler(Of ExternalDataEventArgs) Sub AssignEngineer(ByVal bug_description As String) Sub WorkBug(ByVal bug_description As String) Sub TestBug(ByVal bug_description As String) End Interface
Tip | To use the WF components, you must add references to the appropriate libraries. Open Solution Explorer, double-click My Project, select the References tab, and click the Add button. Select the libraries System .Workflow.Runtime, System.Workflow.Activities, and System.Workflow.ComponentModel, and click OK. |
This interface defines five events that let the workflow know when the service has assigned an engineer to the bug, when the bug is worked, when the bug is rejected as not fixed, when the bug fix passes its tests, and when the bug fix fails its tests.
The interface also defines methods to let the workflow ask the service to assign an engineer to the bug, work the bug, and test the bug fix.
The following code shows the beginning of the BugReportWF workflow’s definition:
Public Class BugReportWF Inherits SequentialWorkflowActivity #Region "Properties" Private m_BugDescription As String = "" Public Property BugDescription() As String Get Return m_BugDescription End Get Set(ByVal value As String) m_BugDescription = value End Set End Property Private m_AssignedEngineer As String = "" Public Property AssignedEngineer() As String Get Return m_AssignedEngineer End Get Set(ByVal value As String) m_AssignedEngineer = value End Set End Property Private m_Status As String = "" Public Property Status() As String Get Return m_Status End Get Set(ByVal value As String) m_Status = value End Set End Property #End Region ' Properties Private WithEvents m_AssignEngineer As CallExternalMethodActivity Private WithEvents m_EngineerAssignedEvent As HandleExternalEventActivity Private WithEvents m_WorkBug As CallExternalMethodActivity Private WithEvents m_BugWorkedEvent As HandleExternalEventActivity Private WithEvents m_BugRejectedEvent As HandleExternalEventActivity Private WithEvents m_TestBug As CallExternalMethodActivity Private WithEvents m_TestPassedEvent As HandleExternalEventActivity Private WithEvents m_TestFailedEvent As HandleExternalEventActivity
The class inherits from SequentialWorkflowActivity. The code defines three properties: BugDescrip?tion to hold a description of the bug, AssignedEngineer to hold the assigned engineer’s name, and Status to record the bug report’s final resolution. These are straightforward properties implemented with property procedures.
Next, the code declares several WF activity variables. These are the activities that will make up the workflow. They are declared with the WithEvents keyword to make catching their events easy.
The following code shows the BugReportWF class’s constructor. This routine builds the activities that make up the workflow, and is one of the more complicated parts of the workflow code.
Public Sub New() Me.CanModifyActivities = True ' Assign an engineer. m_AssignEngineer = _ New CallExternalMethodActivity("invoke_AssignEngineer") m_AssignEngineer.InterfaceType = GetType(IBugReportService) m_AssignEngineer.MethodName = "AssignEngineer" Dim assign_param As New WorkflowParameterBinding assign_param.ParameterName = "bug_description" assign_param.Value = "" ' We will change this later. m_AssignEngineer.ParameterBindings.Add(assign_param) ' Catch the EngineerAssigned event. m_EngineerAssignedEvent = _ New HandleExternalEventActivity("catch_EngineerAssigned") m_EngineerAssignedEvent.InterfaceType = GetType(IBugReportService) m_EngineerAssignedEvent.EventName = "EngineerAssigned" ' Work the bug. m_WorkBug = New CallExternalMethodActivity("invoke_WorkBug") m_WorkBug.InterfaceType = GetType(IBugReportService) m_WorkBug.MethodName = "WorkBug" Dim work_param As New WorkflowParameterBinding work_param.ParameterName = "bug_description" work_param.Value = "" ' We will change this later. m_WorkBug.ParameterBindings.Add(work_param) ' Listen for either the BugWorked or BugRejected event. ' Catch the BugWorked event. m_BugWorkedEvent = New HandleExternalEventActivity("catch_BugWorked") m_BugWorkedEvent.InterfaceType = GetType(IBugReportService) m_BugWorkedEvent.EventName = "BugWorked" ' Catch the BugRejected event. m_BugRejectedEvent = New HandleExternalEventActivity("catch_BugRejected") m_BugRejectedEvent.InterfaceType = GetType(IBugReportService) m_BugRejectedEvent.EventName = "BugRejected" ' Make activities driven by the BugWorked and BugRejected events. Dim eventdriven_worked As New EventDrivenActivity("eventdriven_Worked") eventdriven_worked.Activities.Add(m_BugWorkedEvent) Dim eventdriven_rejected As New EventDrivenActivity("eventdriven_Rejected") eventdriven_rejected.Activities.Add(m_BugRejectedEvent) ' Listen to see if it was worked. Dim listen_bug_worked As New ListenActivity("listen_Worked") listen_bug_worked.Activities.Add(eventdriven_worked) listen_bug_worked.Activities.Add(eventdriven_rejected) ' Test the bug fix. m_TestBug = New CallExternalMethodActivity("invoke_TestBug") m_TestBug.InterfaceType = GetType(IBugReportService) m_TestBug.MethodName = "TestBug" ' If the bug was fixed, test it. eventdriven_worked.Activities.Add(m_TestBug) ' Listen for either the TestPassed or TestFailed event. ' Catch the TestPassed event. m_TestPassedEvent = New HandleExternalEventActivity("catch_TestPassed") m_TestPassedEvent.InterfaceType = GetType(IBugReportService) m_TestPassedEvent.EventName = "TestPassed" ' Catch the TestFailed event. m_TestFailedEvent = New HandleExternalEventActivity("catch_TestFailed") m_TestFailedEvent.InterfaceType = GetType(IBugReportService) m_TestFailedEvent.EventName = "TestFailed" ' Make activities driven by the BugWorked and BugRejected events. Dim eventdriven_passed As New EventDrivenActivity("eventdriven_Passed") eventdriven_passed.Activities.Add(m_TestPassedEvent) Dim eventdriven_failed As New EventDrivenActivity("eventdriven_Failed") eventdriven_failed.Activities.Add(m_TestFailedEvent) ' Listen to see if it was worked. Dim listen_test_passed As New ListenActivity("listen_Passed") listen_test_passed.Activities.Add(eventdriven_passed) listen_test_passed.Activities.Add(eventdriven_failed) ' After we test, listen for these events. eventdriven_worked.Activities.Add(listen_test_passed) ' Add the activities. Me.Activities.Add(m_AssignEngineer) Me.Activities.Add(m_EngineerAssignedEvent) Me.Activities.Add(m_WorkBug) Me.Activities.Add(listen_bug_worked) Me.CanModifyActivities = False End Sub
The first activity the constructor makes is m_AssignEngineer. This represents the task of someone assigning an engineer to the bug report. This is a CallExternalMethodActivity object that calls the service’s AssignEngineer subroutine. The InterfaceType and MethodName properties tell the object that it will be invoking the AssignEngineer subroutine provided by a service that implements the IBugReportService interface.
The AssignEngineer subroutine takes as a single parameter a string named bug_description. At this point, the workflow doesn’t know the bug’s description, but it binds a parameter to the activity anyway, giving it a blank temporary value. The code will set this value to the actual bug description before it invokes the activity.
When the m_AssignEngineer activity executes, the service program’s AssignEngineer method prompts the user for the name of the engineer to assign to the bug. When it has finished, the service raises the EngineerAssigned event and the workflow must catch this event.
The constructor next makes a HandleExternalEventActivity object. Its InterfaceType and Event?Name properties indicate that it will respond to an EngineerAssigned event raised by a service that implements IBugReportService.
After it handles this event, the workflow must call the service’s WorkBug method. The constructor makes a CallExternalMethodActivity object similar to the m_AssignEngineer object it created earlier, except this one calls the service’s WorkBug method.
The service’s WorkBug method lets the engineer fix the bug, and then indicate whether the bug has been fixed. This corresponds to the Bug Fixed? condition in Figure 30-2. The service then raises either the BugWorked or BugRejected event, and the workflow must wait until it sees one of those events.
To look for those events, the constructor makes two HandleExternalEventActivity objects, one for each event. It then makes two corresponding EventDrivenActivity objects and adds the Handle?ExternalEventActivity objects as their children. Finally, it creates a ListenActivity object and adds the two EventDrivenActivity objects as its children.
If the workflow receives a BugWorked event, it needs to test the bug. The constructor makes a Call?External?MethodActivity object to call the service’s TestBug method. This is similar to the m_AssignEngineer and m_WorkBug objects created earlier.
The constructor adds the m_TestBug object to the children of the EventDrivenActivity object that responds to the BugWorked event. If the service raises this event, then the workflow executes the new m_TestBug activity.
Depending on whether the bug fixed worked, the service raises either the TestPassed or TestFailed event. Just as it waited for either the BugWorked or BugRejected event, it must now wait for either the TestPassed or TestFailed event. The constructor creates a similar set of HandleExternalEvent?Activity objects with corresponding EventDrivenActivity objects, and puts them inside a new ListenActivity.
The constructor adds the new ListenActivity object to the EventDrivenActivity object that responds to the BugWorked event. If the service originally raised the BugWorked event, then the workflow executes the m_TestBug activity and then listens for either the TestPassed or TestFailed event.
Finally, after it has created all of the activity objects, the constructor adds the main objects to its Activities collection. First, the workflow executes the m_AssignEngineer activity to call the service’s Assign?Engineer method. Next, it executes m_EngineerAssignedEvent to wait for the EngineerAssigned event. It then executes m_WorkBug to call the service’s WorkBug method. It finishes by performing the listen_bug_worked activity, which waits for the BugWorked or BugRejected events, calls the TestBug method, and waits for the TestPassed and TestFailed events.
The BugReportWF class’s constructor sets up the sequence of activities. Much of the hard work in the workflow is performed by the service application, which assigns an engineer, fixes the bug, and tests the results, but there are still some chores that the workflow’s code must handle.
When the service raises the EngineerAssigned event, the m_EngineerAssignedEvent activity raises its Invoked event. The following code catches that event and processes it:
' Save the assigned employee's name. Private Sub m_EngineerAssignedEvent_Invoked(ByVal sender As Object, _ ByVal e As System.Workflow.Activities.ExternalDataEventArgs) _ Handles m_EngineerAssignedEvent.Invoked ' Convert to AssignedEngineerArgs. Dim assigned_args As StringArgs = DirectCast(e, StringArgs) ' Save the value. m_AssignedEngineer = assigned_args.Value Debug.WriteLine("Assigned: " & m_AssignedEngineer) End Sub
The event handler’s parameter e is actually a StringArgs object. StringArgs is a subclass of External?DataEventArgs, and the event handler refers to it as an ExternalDataEventArgs object, so the code converts it into a StringArgs object. When it raises the EngineerAssigned event, the service sets this object’s Value property to the name of the engineer it assigned to the bug. This code saves that name in the workflow’s m_AssignedEngineer variable and displays the name in the Immediate window.
The following code shows the workflow’s other event handlers that catch events raised by the service application:
' The bug was rejected. Private Sub m_BugRejectedEvent_Invoked(ByVal sender As Object, _ ByVal e As System.Workflow.Activities.ExternalDataEventArgs) _ Handles m_BugRejectedEvent.Invoked Debug.WriteLine("Bug rejected") Debug.WriteLine("") Status = "Rejected" End Sub ' The bug was worked. Private Sub m_BugWorkedEvent_Invoked(ByVal sender As Object, _ ByVal e As System.Workflow.Activities.ExternalDataEventArgs) _ Handles m_BugWorkedEvent.Invoked Debug.WriteLine("Bug worked") End Sub ' The test failed. Private Sub m_TestFailedEvent_Invoked(ByVal sender As Object, _ ByVal e As System.Workflow.Activities.ExternalDataEventArgs) _ Handles m_TestFailedEvent.Invoked Debug.WriteLine("Test failed") Debug.WriteLine("") Status = "Test Failed" End Sub ' The test passed. Private Sub m_TestPassedEvent_Invoked(ByVal sender As Object, _ ByVal e As System.Workflow.Activities.ExternalDataEventArgs) _ Handles m_TestPassedEvent.Invoked Debug.WriteLine("Test passed") Debug.WriteLine("") Status = "Test Passed" End Sub
The m_BugRejectedEvent activity’s Invoked event handler prints a message in the Immediate window and sets the workflow’s Status property to Rejected. After this activity finishes, the workflow has no more activities to run, so it ends.
The m_BugWorkedEvent activity simply displays a message in the Immediate window. The workflow will next execute the m_TestBug activity.
The m_TestFailedEvent and m_TestPassedEvent activities work similarly to m_BugRejectedEvent, displaying messages in the Immediate window and setting the workflow’s Status property appropriately.
The following code shows the workflow’s final two event handlers:
' We are about to invoke this method. ' Copy the bug's description into the Private Sub m_AssignEngineer_MethodInvoking(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles m_AssignEngineer.MethodInvoking m_AssignEngineer.ParameterBindings.Item("bug_description").Value = _ m_BugDescription End Sub Private Sub m_WorkBug_MethodInvoking(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles m_WorkBug.MethodInvoking m_WorkBug.ParameterBindings.Item("bug_description").Value = _ m_BugDescription End Sub Private Sub m_TestBug_MethodInvoking(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles m_TestBug.MethodInvoking m_TestBug.ParameterBindings.Item("bug_description").Value = _ m_BugDescription End Sub
These MethodInvoking events fire just before their corresponding activities execute. They update the parameters bound to their activities to give them the bug’s correct description. When the activities invoke the service’s methods, they pass those methods the bug’s description.
The final piece to the BugReportWF class’s code module is the definition of the StringArgs class shown in the following code:
<Serializable()> _ Public Class StringArgs Inherits ExternalDataEventArgs Public Value As String Public Sub New(ByVal instance_id As Guid, ByVal new_value As String) MyBase.New(instance_id) Value = new_value End Sub End Class
The program uses this class to pass string values into workflow event handlers. It inherits from External?DataEventArgs because that’s the type of argument that the event handlers expect to receive. Look in the following section to see how the code that raises these events uses this class.
Tip | Note that the StringArgs class must have the Serializable attribute or the workflow will throw a rather cryptic “The workflow failed validation” exception. |
The main service application creates the workflow runtime, builds the workflow object, and starts it running. It provides methods to perform the workflow’s major tasks: assigning the bug, working the bug, and testing the bug fix.
The following code shows how the main program’s form code begins:
Imports System.Workflow.Runtime Imports System.Workflow.Runtime.Hosting Imports System.Workflow.Activities Public Class Form1 Implements IBugReportService Private WithEvents m_WorkflowRuntime As WorkflowRuntime Private m_ExchangeService As ExternalDataExchangeService Private m_WorkflowInstance As WorkflowInstance ' Prepare the workflow runtime engine. Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles Me.Load m_WorkflowRuntime = New WorkflowRuntime() m_ExchangeService = New ExternalDataExchangeService() m_WorkflowRuntime.AddService(m_ExchangeService) m_ExchangeService.AddService(Me) m_WorkflowRuntime.StartRuntime() End Sub
Note that the form’s class implements the IBugReportService interface defined in the workflow’s code module. After the Implements statement, the code declares a few variables work dealing with WF. It declares a WorkflowRuntime variable to represent the WF runtime engine, it makes an External?DataExchangeService object to make exchanging data with the workflow easier, and it declares a WorkflowInstance object to process a bug report.
The form’s Load event handler creates new WorkflowRuntime and ExternalDataExchangeService objects. It adds the ExternalDataExchangeService to the runtime engine’s list of services. It also adds itself to the list or services (remember that the form implements the IBugReportService interface). The Load event handler then starts the runtime engine.
When you enter a bug description in the form’s text box and click Submit, the following code executes:
' Start the workflow. Private Sub btnSubmit_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnSubmit.Click ' Get the workflow's type. Dim wf_type As Type = GetType(BugReportWF) ' Construct workflow arguments. Dim args As New Dictionary(Of String, Object)() args.Add("BugDescription", txtBugDescription.Text) ' Create and start the workflow. m_WorkflowInstance = _ m_WorkflowRuntime.CreateWorkflow(wf_type, args) m_WorkflowInstance.Start() End Sub
This code starts by making a Type object to represent the BugReportWF type. Next, it creates a Dictionary object to hold parameters that will be used to create the workflow instance. It sets the value of the Bug?Description entry to the value you entered in the form’s text box.
The program then calls the runtime engine’s CreateWorkflow method, passing it the type of the workflow to create and the Dictionary. The runtime engine creates the workflow object, and then uses the Dictionary’s entries to set the workflow’s properties. In this example, it sets the workflow object’s BugDescription property to the text you entered on the form.
Tip | If there’s anything wrong with the workflow, the call to CreateWorkflow will probably throw a rather uninformative “The workflow failed validation” exception. For example, if the workflow’s constructor tries to build an illegal workflow (such as an activity assigned to two parents or a CallExternalMethodActivity object referring to a non-existent service method), the call throws this error. Other mistakes that can cause this error include an Event?DrivenActivity that has no child activities, a ListenActivity that has fewer than two child activities, a HandleExternalEventActivity without a specified InterfaceType or EventName, and many others. As is always the case when error information is scarce, you’ll probably save time if you build the workflow gradually, testing the pieces frequently so that you catch any validation errors right away. |
Note that the engine creates the workflow first and then uses the Dictionary to set property values. That means the workflow’s constructor cannot use the property values because they are not yet set.
The code then calls the workflow object’s Start method to get things rolling.
In this example, the workflow’s first activity is a CallExternalMethodActivity object named m_AssignEngineer. It calls the form’s AssignEngineer method shown in the following code:
Public Delegate Sub AssignEngineerDelegate(ByVal bug_description As String) Public Sub AssignEngineer(ByVal bug_description As String) _ Implements IBugReportService.AssignEngineer If Me.InvokeRequired Then Me.Invoke(New AssignEngineerDelegate _ (AddressOf Me.AssignEngineer), New Object() {bug_description}) Else Dim engineer_name As String = _ InputBox("Who do you want to assign to work bug report:" & _ vbCrLf & vbCrLf & bug_description, _ "Assign Engineer", "Debugger Dan") ' Tell the workflow who we assigned. Dim args As New StringArgs( _ m_WorkflowInstance.InstanceId, engineer_name) RaiseEvent EngineerAssigned(Nothing, args) End If End Sub
The workflow runs on a thread other than the user interface (UI) thread so that its call to AssignEngineer cannot directly interact with the UI. Because the program needs to interact with the user, it calls Invoke?Required to see if it is running in the UI thread. If InvokeRequired returns true, then it is not running in the UI thread, so it uses Invoke to call AssignEngineer on the UI thread. The code passes an Assign?Engineer?Delegate initialized with the AssignEngineer method’s address and the bug_description parameter to the call to Invoke.
Invoke then calls AssignEngineer on the UI thread. This time, InvokeRequired returns false, so the routine executes the code after the Else statement. The subroutine displays an input box where you can enter the name of the engineer who should be assigned to the bug report. The code then creates a new StringArgs object to store the engineer’s name and raises the EngineerAssigned event. The workflow catches the event and saves the engineer’s name in its AssignedEngineer property.
At that point, control returns to the workflow. Having finished running its first activity, m_Assign?Engineer, the workflow runs activity m_WorkBug. That activity invokes the service’s WorkBug method shown in the following code:
Public Delegate Sub WorkBugDelegate(ByVal bug_description As String) Public Sub WorkBug(ByVal bug_description As String) _ Implements IBugReportService.WorkBug If Me.InvokeRequired Then Me.Invoke(New WorkBugDelegate _ (AddressOf Me.WorkBug), New Object() {bug_description}) Else ' Tell the workflow whether we fixed the bug. If MessageBox.Show("Did you fix the bug:" & _ vbCrLf & vbCrLf & bug_description, _ "Bug Fixed?", MessageBoxButtons.YesNo, _ MessageBoxIcon.Question) _ = Windows.Forms.DialogResult.Yes _ Then RaiseEvent BugWorked(Nothing, _ New ExternalDataEventArgs(m_WorkflowInstance.InstanceId)) Else RaiseEvent BugRejected(Nothing, _ New ExternalDataEventArgs(m_WorkflowInstance.InstanceId)) End If End If End Sub
This code is similar to the AssignEngineer subroutine shown previously. It uses InvokeRequired to see if it is running in the UI thread, and invokes to that thread if necessary. It then displays a message box asking whether the engineer has fixed the bug. If you click Yes, the code raises the BugWorked event. If you click No, the code raises the BugRejected event. The workflow catches whichever event the service code raises, and responds appropriately.
If the workflow catches the BugWorked event, it then executes its m_TestBug activity. That activity calls the service’s TestBug method shown in the following code:
Public Delegate Sub TestBugDelegate(ByVal bug_description As String) Public Sub TestBug(ByVal bug_description As String) _ Implements IBugReportService.TestBug If Me.InvokeRequired Then Me.Invoke(New TestBugDelegate _ (AddressOf Me.TestBug), New Object() {bug_description}) Else ' Tell the Bugflow whether we fixed the bug. If MessageBox.Show("Did the test pass for:" & _ vbCrLf & vbCrLf & bug_description, _ "Test Passed?", MessageBoxButtons.YesNo, _ MessageBoxIcon.Question) _ = Windows.Forms.DialogResult.Yes _ Then RaiseEvent TestPassed(Nothing, _ New ExternalDataEventArgs(m_WorkflowInstance.InstanceId)) Else RaiseEvent TestFailed(Nothing, _ New ExternalDataEventArgs(m_WorkflowInstance.InstanceId)) End If End If End Sub
Subroutine TestBug is very similar to subroutine WorkBug. It invokes itself if necessary, and asks if the bug passed its tests. It then raises either the TestPassed or TestFailed event.
After the workflow finishes executing its last activity, the WorkflowInstance object in the service application receives a WorkflowCompleted event and executes the event handler shown in the following code:
Private Sub m_WorkflowRuntime_WorkflowCompleted(ByVal sender As Object, _ ByVal e As System.Workflow.Runtime.WorkflowCompletedEventArgs) _ Handles m_WorkflowRuntime.WorkflowCompleted Dim txt As String = "Workflow completed" & vbCrLf & vbCrLf txt &= "Description: " & e.OutputParameters("BugDescription").ToString() & _ vbCrLf txt &= "Assigned to: " & e.OutputParameters("AssignedEngineer").ToString() & _ vbCrLf txt &= "Result: " & e.OutputParameters("Status").ToString() & vbCrLf MessageBox.Show(txt) End Sub
This code builds a summary message and displays it in a message box. The event handler’s e parameter has an OutputParameters property that contains the values of the workflow’s properties when it finishes. This routine adds the workflow’s BugDescription, AssignedEngineer, and Status properties to its message.