Professional VB 2005 with .NET 3.0 (Programmer to Programmer)

The actual workflow files in WF are XML files written in a version of XAML. This is the same XAML used to describe Windows Presentation Foundation (WPF) files. (See Chapter 17 for more details on WPF). They describe the actions to perform within the workflow, and the relationship between those actions. You can create a workflow using only a text editor, but Visual Studio makes creating these workflows much easier. It provides a graphical designer that enables developers to visually design the workflow, creating the XAML in the background.

<RuleDefinitions xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow"> <RuleDefinitions.Conditions> <RuleExpressionCondition Name="TranslationCallWorked"> <RuleExpressionCondition.Expression> <ns0:CodeBinaryOperatorExpression Operator="ValueEquality" xmlns:ns0="clr-namespace:System.CodeDom;Assembly=System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <ns0:CodeBinaryOperatorExpression.Left> <ns0:CodeBinaryOperatorExpression Operator="ValueEquality"> <ns0:CodeBinaryOperatorExpression.Left> <ns0:CodeMethodInvokeExpression> <ns0:CodeMethodInvokeExpression.Parameters> <ns0:CodeFieldReferenceExpression FieldName="OutputTextProperty"> <ns0:CodeFieldReferenceExpression.TargetObject> <ns0:CodeTypeReferenceExpression Type="TranslateActivity.TranslateActivity" /> </ns0:CodeFieldReferenceExpression.TargetObject> </ns0:CodeFieldReferenceExpression> </ns0:CodeMethodInvokeExpression.Parameters> <ns0:CodeMethodInvokeExpression.Method> <ns0:CodeMethodReferenceExpression MethodName="GetValue"> <ns0:CodeMethodReferenceExpression.TargetObject> <ns0:CodeThisReferenceExpression /> </ns0:CodeMethodReferenceExpression.TargetObject> </ns0:CodeMethodReferenceExpression> </ns0:CodeMethodInvokeExpression.Method> </ns0:CodeMethodInvokeExpression> </ns0:CodeBinaryOperatorExpression.Left> <ns0:CodeBinaryOperatorExpression.Right> <ns0:CodePrimitiveExpression /> </ns0:CodeBinaryOperatorExpression.Right> </ns0:CodeBinaryOperatorExpression> </ns0:CodeBinaryOperatorExpression.Left> <ns0:CodeBinaryOperatorExpression.Right> <ns0:CodePrimitiveExpression> <ns0:CodePrimitiveExpression.Value> <ns1:Boolean xmlns:ns1="clr-namespace:System;Assembly=mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">false</ns1:Boolean> </ns0:CodePrimitiveExpression.Value> </ns0:CodePrimitiveExpression> </ns0:CodeBinaryOperatorExpression.Right> </ns0:CodeBinaryOperatorExpression> </RuleExpressionCondition.Expression> </RuleExpressionCondition> </RuleDefinitions.Conditions> </RuleDefinitions>

The workflow is composed of a number of rule definitions. Each definition includes activities, conditions, and expressions. Activities are the steps involved in the workflow. They are executed based on the workflow’s design and the conditions included. Controlling the behavior of the workflow are conditions, which are evaluated and may result in code running. Finally, expressions describe the individual tests used as part of the conditions. For example, each side of an equality condition would be expressions. When building the workflow by hand, you are responsible for creating the markup. Fortunately, Visual Studio writes it as you design your workflow.

Adding Workflow with Windows Workflow Foundation

Windows Workflow Foundation is composed of a number of components that work together with your application to carry out the desired workflow. Six main components make up any WF application:

Figure 25-1 shows how the main components of WF fit together.

Figure 25-1

Windows Workflow Foundation supports two main styles of creating workflows: sequential and state machine. Sequential workflows (see Figure 25-2) are the classic flowchart style of process. They begin when some action initiates the workflow, such as the submission of an expense report or a user decision to check out a shopping cart. The workflow then continues stepwise through the activities until it comes to the end. There may be branching or looping, but generally the flow moves down the workflow. Sequential workflows are best when a set series of steps are needed for the workflow.

Figure 25-2

State machine workflows (see Figure 25-3) are less linear than sequential workflows. They are typically used when the data moves through a series of steps toward completion. At each step, the state of the application has a particular value. Transitions move the state between steps. One example of a state machine workflow that most people are familiar with (unfortunately) is voice mail. Most voice-mail systems are collections of states, represented by a menu. You move between the states by pressing the keys of your phone. State machine workflows can be useful when the process you are modeling is not necessarily linear. There may still be some required steps, but generally the flow may iterate between the steps for some time before completion.

Figure 25-3

A good way to identify a candidate for a state machine workflow is determining whether the process is defined in terms of modes. For example, a shopping site is a classic example of a state machine. The user is either in browse mode or cart view mode. Selecting checkout would likely initiate a sequential workflow, as the steps in that process are more easily described in a linear fashion.

A Simple Workflow

As with any other programming endeavor, the best way to understand WF is to create a simple workflow and extend it with additional steps. Start Visual Studio and create a new Sequential Workflow Console Application (see Figure 25-4) called HelloWorkflow.

Figure 25-4

This project creates two files: a module that includes the Main for the application and the workflow. The sequential workflow begins life with only two steps: start and finish, as shown in Figure 25-5. You build the workflow by adding steps between these two.

Figure 25-5

To begin, drag a Code activity between the start and finish markers. Note the red exclamation mark on the new activity in the diagram (shown without color in Figure 25-6). WF makes heavy use of these tips to help you identify the next step in the process.

Figure 25-6

Click the code tip and select the menu item Property 'ExecuteCode’ is not set. This will bring up the Properties window for the Code activity. Enter SayGreetings and press Enter. This brings up the code window for the activity. Add the following code:

Private Sub SayGreetings(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Console.WriteLine("Hello world, from workflow") Console.WriteLine("Press enter to continue") Console.ReadLine() End Sub

Notice that coding the action for the activity is the same as any other event. Run the project to see the console window (see Figure 25-7), along with the message you should be expecting.

Figure 25-7

While trivial, the project makes a useful test bed for experimenting with the various activities. Add an IfElse activity before the Code activity. IfElse activities are one of the main ways to add logic and control of flow to your workflows. They have a condition property that determines when each half of the flow will be executed. The condition may be code that executes or a declarative rule. For this example, declarative rules are enough. You create these rules in the Rule Condition Editor (see Figure 25-8). To display the Rule Condition Editor, select Declarative Rule Condition for the Condition property of the ifElseBranchActivity component. Once you have selected Declarative Rule Condition, you can click the ellipsis to display the dialog.

Figure 25-8

Clicking New brings up the Rule Condition Editor (see Figure 25-9). This enables you to create simple expressions that will be used by the IfElse activity to determine flow.

Figure 25-9

Set the rule on the If half of the IfElse activity to determine whether the current time is before noon:

System.DateTime.Now.TimeOfDay.Hours < 12

Right-click on the activity and select Add Branch to create a third branch to the IfElse activity. Set the condition for this one as you did for the first activity, but use 18 for the value to check for hours.

Add Code activities to each of the three sections of the diagram (see Figure 25-10). You will use these activities to affect the message that is displayed. Assign the properties as follows:

Open table as spreadsheet

Activity

Property

Value

codeActivity2

ExecuteCode

SetMessageMorning

codeActivity3

ExecuteCode

SetMessageAfternoon

codeActivity4

ExecuteCode

SetMessageEvening

Figure 25-10

Finally, change the code in the workflow to the following (note that this replaces the SayGreetings method created earlier):

Public class Workflow1 Inherits SequentialWorkflowActivity Private Message As String Private Sub SayGreetings(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Console.WriteLine(Message & ", from workflow") Console.WriteLine("Press enter to continue") Console.ReadLine() End Sub Private Sub SetMessageMorning(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Message = "Good morning" End Sub Private Sub SetMessageAfternoon(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Message = "Good afternoon" End Sub Private Sub SetMessageNight(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Message = "Good night" End Sub End Class

Each of the three SetMessage methods changes the greeting as appropriate. The final greeting is displayed in the SayGreetings method. Run the project again. You should be greeted appropriately for the time of day.

While this workflow is probably overkill to generate a simple message, the example does show many of the common steps in defining a workflow. Workflows are composed of multiple activities. Many activities can in turn be composed of other activities. Activities may use declarative properties, or code may be executed as needed.

Standard Activities

The standard WF activities can be divided into five major categories:

In order for a workflow to begin, there must be some way for external code to initiate it. In addition, a workflow would be limited if there were no way for the workflow to execute external code and/or Web Services. The standard activities that are used to communicate with external code include the following:

Open table as spreadsheet

Activity

Description

CallExternalMethod

As the name implies, this activity calls an external method. The activity requires two properties. The first identifies an interface shared by the workflow and the external code. The second identifies the method on that interface that will be called. If the method requires additional parameters, then they appear on the property grid after setting the other two properties. This method is frequently used in combination with the HandleExternalEvent activity. This activity executes the external method synchronously, so be cautious when calling external methods that will take a long time to execute.

HandleExternalEvent

Receives a trigger from an external block of code. This is a commonly used activity to initiate a workflow when the workflow is running in the context of a Windows Forms or ASP.NET application. As with the CallExternalMethod activity, it requires at least two properties. The first identifies a shared interface and the second identifies the event on that interface that will be received.

InvokeWebService

Calls an external Web Service. You assign a WSDL file to the activity and it generates a proxy class for the Web Service. You must also identify the method on that class that will be called. The SessionId property is used to identify the session that will be used for the requests. All requests with the same SessionId value share the session. If the SessionId is blank, then this activity creates a new session per request.

InvokeWorkflow

Calls another workflow. This is a useful activity for chaining multiple workflows together, reducing the complexity of each workflow. The external workflow must complete before the current workflow continues.

WebServiceInput

Receives an incoming Web Service request. You must publish the workflow containing this activity for it to work. You publish the workflow by selecting Publish as Web Service from the Project menu. This generates a new Web Service project that includes the output from the workflow project as well as an ASMX file that serves as the address for the workflow.

WebServiceOutput

Produces the output for a Web Service request. This activity is used in partnership with the WebServiceInput activity.

WebServiceFault

Triggers a Web Service error. This is used in partnership with the WebServiceInput activity to signal an error with the Web Service call.

All programming languages need some form of control of flow to regulate the applications. Visual Basic includes language elements such as If..Else, Do..While, For..Next, and Select Case to perform these actions. WF includes a number of activities to perform similar actions, although the options are more limited:

Open table as spreadsheet

Activity

Description

IfElse

Provides for executing two or more different workflow paths based on the status of a condition. The condition may be code or an expression. This is a commonly used activity to branch a workflow.

Listen

Provides for executing two or more different workflow paths based on an event. The path chosen is selected by the first event that occurs. This is a useful activity to use to monitor a class that could generate multiple events (such as a class that could either approve or reject a request).

Policy

Provides for executing multiple rules. Each rule is a condition with some resulting action. This activity provides a way to group multiple related rules into a single activity.

Replicator

Enables the workflow to create multiple instances of an activity for processing. The resulting child activities may run serially or in parallel. This is an excellent way to divide a large task: For example, you could have the Replicator activity create multiple child activities that are responsible for mailing a newsletter to a large list. The child activities could run in parallel, dividing the list into smaller groups for faster processing.

While

Loops the workflow until a condition has been met. The condition may be the result of code or an expression. This is typically used to receive multiple input values or to process multiple requests, such as a batch job.

Several composite activities may cooperate to complete a single logical action by grouping other activities:

Open table as spreadsheet

Activity

Description

CompensatableSequence

Similar to the Sequence activity (see below), this activity differs in that it supports “undoing” the child activities. You can think of this in terms of a transaction: If one child activity fails, then the completed activities must be undone. The CompensatableSequence activity includes handles that enable the developer to perform this correction.

ConditionedActivityGroup

Includes a number of child activities that are run based on a condition. All child activities will execute until some defined condition occurs. This provides a means of grouping a number of related activities into a single activity.

EventDriven

Responds to an external event to initiate a set of activities. This is similar to the HandleExternalEvent activity, but the events are internal to the workflow. This activity is commonly used in a state-machine workflow to move between the states.

FaultHandler

Enables handling an error within a workflow. You use the FaultHandler activity to either correct or report the error gracefully. For example, a timeout may occur, triggering a fault condition in the workflow. This handler would contain other activities that are responsible for an alternate method of processing the item.

Parallel

Contains a series of child activities that run concurrently. You should only use this if the child activities either do not affect the data or the order of change is not important.

Sequence

Contains a series of child activities that run in order. This is the default model for an workflow. Each child activity must complete before the next one begins.

State activities represent the current state of the data and process for the workflow. They are only used within state-machine workflows:

Open table as spreadsheet

Activity

Description

State

Represents the current state of the workflow. For example, in a workflow driving a voice-mail system, the state would represent the current menu that the client is on.

StateFinalization

Provides an activity to handle the actions needed as a given state is completed. This would provide a place to record the user’s selection or to free up resources used by the state.

StateInitialization

Provides an activity to handle the actions needed before the given state is entered. This would enable the creation of any data or code needed to prepare for the state functioning.

The final group of activities are those that perform some action. You already saw this activity type in the form of the Code activity. These activities are the cornerstone of any workflow. The standard activities in this group include the following:

Open table as spreadsheet

Activity

Description

Code

Enables custom Visual Basic code to be performed at a stage in the workflow. You can use these wherever you need to perform some action not done by another activity. Whenever you use one of these - especially if you use the same type of code frequently - you should consider moving the code into a custom activity.

Compensate

Enables custom code to undo a previous action. This is typically done if an error occurs within the workflow.

Delay

Pauses the flow of the workflow. This is typically used to schedule some event. For example, you might have a workflow that is responsible for printing a daily report. The Delay activity could be used to schedule this printout so that it is ready as the workers come in to read it. You can either set the delay explicitly by setting the TimeoutDuration property or set it via code using the event identified in the InitializeTimeoutDuration property.

Suspend

Temporarily stops the workflow. This is usually due to some extraordinary event that you would want an administrator or developer to correct. The workflow will continue to receive requests, but not complete them past the Suspend activity. The administrator may then resume the workflow to complete processing.

Terminate

Ends the workflow immediately. This should only be done in extreme situations such as when the workflow is not capable of any further processing (e.g., it has lost the connection to a database or other needed resource).

Throw

Creates an exception that can be caught by the code hosting the workflow. This provides a means of propagating an error from the workflow to the containing code.

Building Custom Activities

In addition to the standard activity library, WF supports extensibility through the creation of custom activities. Creating custom activities is a matter of creating a new class that inherits from Activity (or one of the existing child classes). Several available attributes enable the customization of the activity and how it appears when you use it in your workflows.

Creating custom activities is the primary means of extending WF. You might use custom activities to simplify a complex workflow, grouping a number of common activities into a single new activity. Alternatively, custom activities can create a workflow that is easier to understand, using terms that are more familiar to the developers and business experts. Finally, custom activities can be used to support software used within the business, such as activities to communicate with a CRM or ERP system.

To see the steps required for creating a custom activity, the next exercise creates a simple activity that wraps the Google translation service. Create a new project using the Workflow Activity Library template. This project will create a DLL that contains the activities you create. Name the project TranslationActivity. It will include a single custom activity initially. This activity inherits from SequenceActivity, so it might include multiple child activities. You can change this as needed, but it’s a good enough default for most activities. Drag a Code activity onto the designer. This activity does the actual translation work. Alternately, you could change the top-level class to inherit from Activity and override the Execute method to perform the translation. Name the new activity TranslateActivity.

Because the new activity will be used to convert between a number of set language pairs, create an enumeration containing the valid options. This enumeration can be expanded as new options become available. You can either add this enumeration to a new class file or add it to the bottom of the current module (after the End Class statement):

Public Enum TranslationOptions As Integer EnglishToFrench EnglishToSpanish EnglishToGerman EnglishToItalian EnglishToRussian EnglishToChinese FrenchToEnglish SpanishToEnglish GermanToEnglish ItalianToEnglish RussianToEnglish ChineseToEnglish End Enum

The new activity has three properties: the input text, a language pair that defines the source and target languages, and the output text (the latter being a read-only property). You can create properties normally in an activity, but it is beneficial to create them so that they participate in the workflow and are available to other activities. In order to do this, use the following pattern to describe your properties:

Public Shared SomeProperty As DependencyProperty = _ DependencyProperty.Register("PropertyName", _ GetType(ReturnType), _ GetType(ClassName)) Public Property PropertyName () As ReturnType Get Return CType(MyBase.GetValue(SomeProperty), _ ReturnType) End Get Set(ByVal value As ReturnType) MyBase.SetValue(SomeProperty, value) End Set End Property

The initial shared field of type DependencyProperty identifies the field that will be used to communicate with other activities. DependencyProperty is a common type used in WPF programming, enabling easier communication between nested types. The Public property enables the more common use of the property. Notice that it stores the data in the shared property between all instances of the type.

As described, there are three properties in the translation activity:

Public Shared InputTextProperty As DependencyProperty = _ DependencyProperty.Register("InputText", _ GetType(System.String), _ GetType(TranslateActivity)) Public Shared TranslationTypeProperty As DependencyProperty = _ DependencyProperty.Register("TranslationType", _ GetType(TranslationOptions), _ GetType(TranslateActivity)) Public Shared OutputTextProperty As DependencyProperty = _ DependencyProperty.Register("OutputText", _ GetType(System.String), _ GetType(TranslateActivity)) <DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)> _ <BrowsableAttribute(True)> _ <DescriptionAttribute("Text to be translated")> _ Public Property InputText() As String Get Return CStr(MyBase.GetValue(InputTextProperty)) End Get Set(ByVal value As String) MyBase.SetValue(InputTextProperty, value) End Set End Property <DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)> _ <BrowsableAttribute(False)> _ <DescriptionAttribute("Translated text")> _ Public ReadOnly Property OutputText() As String Get Return CStr(MyBase.GetValue(OutputTextProperty)) End Get End Property <DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)> _ <BrowsableAttribute(True)> _ <DescriptionAttribute("Language pair to use for the translation")> _ Public Property TranslationType() As TranslationOptions Get Return CType(MyBase.GetValue(TranslationTypeProperty), TranslationOptions) End Get Set(ByVal value As TranslationOptions) MyBase.SetValue(TranslationTypeProperty, value) End Set End Property

Attributes are added to the properties to enable communication with the designer. The core translation method is assigned to the ExecuteCode property of the Code activity. It calls the Google translation service and extracts the result from the returned HTML:

Private Const SERVICE_URL As String = "http://translate.google.com/translate_t" Private Sub Translate(ByVal sender As System.Object, _ ByVal e As System.EventArgs) 'uses Google's translation service to translate text ' Dim req As String = _ String.Format("hl=en&ie=UTF8&text={0}&langpair={1}", _ Encode(Me.InputText), _ BuildLanguageClause(Me.TranslationType)) Dim client As New Net.WebClient Dim resp As String Try resp = client.UploadString(SERVICE_URL, req) If Not String.IsNullOrEmpty(resp) Then MyBase.SetValue(OutputTextProperty, _ Decode(ExtractText(resp))) End If Catch ex As Exception Console.WriteLine("Error translating text: " & ex.Message) End Try End Sub

A typical request to the Google translation service is performed using the service’s Web page, available at www.google.com/translate_t. However, you can make the same type of request the Web page would, and parse the resulting HTML to extract the returned text. The request is made using a POST, to enable sending large blocks of text. For safety, the text is URL encoded using the HttpUtility class.

The routines used by the Translate method are as follows:

Private _langOptions As New List(Of String)() Public Sub New() ' This call is required by the Windows Form Designer. InitializeComponent() ' Add any initialization after the InitializeComponent() call. _langOptions.Add("en|fr") _langOptions.Add("en|es") _langOptions.Add("en|de") _langOptions.Add("en|it") _langOptions.Add("en|zn-CH") _langOptions.Add("en|ru") _langOptions.Add("fr|en") _langOptions.Add("es|en") _langOptions.Add("de|en") _langOptions.Add("it|en") _langOptions.Add("ru|en") _langOptions.Add("zn-CH|en") End Sub Private Sub Translate(ByVal sender As System.Object, _ ByVal e As System.EventArgs) 'uses Google's translation service to translate text ' Dim req As String = _ String.Format("hl=en&ie=UTF8&text={0}&langpair={1}", _ Encode(Me.InputText), _ BuildLanguageClause(Me.TranslationType)) Dim client As New Net.WebClient Dim resp As String Try resp = client.UploadString(SERVICE_URL, req) If Not String.IsNullOrEmpty(resp) Then MyBase.SetValue(OutputTextProperty, _ Decode(ExtractText(resp))) End If Catch ex As Exception Console.WriteLine("Error translating text: " & ex.Message) End Try End Sub Private Function Encode(ByVal value As String) As String Return Web.HttpUtility.UrlEncode(value) End Function Private Function Decode(ByVal value As String) As String Return Web.HttpUtility.HtmlDecode(value) End Function Private Function BuildLanguageClause( _ ByVal languages As TranslationOptions) As String Dim result As String = String.Empty result = _langOptions.Item(languages) Return result End Function Private Function ExtractText(ByVal value As String) As String Dim result As String = String.Empty Dim r As RegularExpressions.Regex Dim m As RegularExpressions.Match r = New RegularExpressions.Regex("<div?[^>]*>(?<result>[^<]*)</div", _ RegularExpressions.RegexOptions.IgnoreCase Or _ RegularExpressions.RegexOptions.Multiline Or _ RegularExpressions.RegexOptions.IgnorePatternWhitespace) m = r.Match(value) If m IsNot Nothing Then result = m.Groups.Item("result").Value End If Return result End Function

The _langOptions list is used to track the strings needed by the various language pairs. This is used by the BuildLanguageClause method to write the appropriate pair to the posted data. The order of the items in the TranslationOptions enumeration matches the order in which items are added to the list, so the BuildLanguageOptions method simply does a lookup into the list.

The ExtractText function uses a regular expression to extract the translated text. The translated text appears in a <div> tag within the resulting HTML. Fortunately, it is the only <div>, although you could modify the regular expression to look for a div with the id of result_box.

<div id=result_box dir=ltr>Bonne chance, Mandrin. Je pense que ceci pourrait juste fonctionner </div>

The resulting activity can now be compiled and included in other workflows. Just as with custom controls, you can add this DLL to the Toolbox using the Choose Toolbox Items dialog after it has been compiled. If the Workflow Activity project is in the same solution as the workflow, it will be automatically added to the Toolbox after it has been compiled. Figure 25-11 shows the Translate activity added to the earlier example.

Figure 25-11

Recall that the Message field was used to store the message you wanted the workflow to generate. This is the text you want to translate. Click the ellipsis button on the InputText property in the property grid to bring up the Bind property dialog (see Figure 25-12). This enables you to visually connect the Message field to the input of the TranslateActivity.

Figure 25-12

The last change to the workflow is to update the text you output. Change the code for the SayGreetings method to display the OutputText of the TranslateActivity, as shown here:

Private Sub SayGreetings(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Console.WriteLine(Me.translateActivity1.OutputText & ", from workflow") Console.WriteLine("Press enter to continue") Console.ReadLine() End Sub

Select the TranslationType and run the test project. Depending on the time of day and the language selected, you should see something similar to Figure 25-13.

Figure 25-13

Категории