Workflow Communication


Workflow-to-host communication and host-to-workflow communication are vital components of Windows Workflow Foundation. Without the necessary hooks to send data back and forth, workflows would not be nearly as useful. Host applications are the most common locations where workflows receive information from the outside world.

For example, in a scenario where a user interacts with a Windows Forms application that is hosting a helpdesk ticket workflow, the Windows application needs to inform the workflow when the user starts a new ticket or updates an existing one. In addition, the workflow might need to tell the host application when an action of interest occurs, such as a request for further information from the user.

There are two main methods of workflow communication. The first, and the simpler of the two, uses parameters to pass data to a workflow when it is created. The second and richer form of communication is called local communication services. This technique uses method and events to facilitate communication. Both of these methods are covered in the following sections.

You might be thinking, “What about web services?” Although web services are becoming more vital for distributed application communication, they are outside the scope of this type of communication. That does not mean that web services and other distributed communication technologies are not important to Windows Workflow Foundation. Chapter 14 discusses Windows Workflow Foundation as it relates to web services, and Chapter 15 covers how Windows Communication Foundation relates to the workflow platform.

Parameters

Parameters provide a simple way to pass data to a workflow instance during its creation. A Dictionary <string, object> generics collection is used to pass parameters to the CreateWorkflow method of the WorkflowRuntime class. Because the collection is passed before the workflow is started, it is helpful only for initialization purposes and cannot be used to communicate with a workflow that is already running.

Each parameter key added to the dictionary collection must correspond to a public property in the workflow definition class that has a set accessor. These properties are used to hold the values added to the collection in the host.

Conversely, parameters can be passed from a workflow out to its host upon completion. Event handlers for the WorkflowCompleted event are passed an instance of the WorkflowCompletedEventArgs class. This class holds a property called OutputParameters, which is of type Dictionary<string, object>. Just as with the input parameters, the workflow class must expose its output parameters as public properties, but this time with the get accessor.

Using this method of communication is quite simple, as displayed in the following code. The input parameters are prepared in the host application and passed to the CreateWorkflow method. The runtime_WorkflowCompleted event handler method uses the OutputParameters collection to access the output parameters.

  public static void Main(string[] args) {     WorkflowRuntime runtime = new WorkflowRuntime();     runtime.WorkflowCompleted +=         new EventHandler<WorkflowCompletedEventArgs>             (runtime_WorkflowCompleted);     runtime.StartRuntime();     Dictionary<string, object> parameters = new Dictionary<string, object>();     parameters.Add("SomeMessage",         "This is a message which goes in to the workflow instance...");     WorkflowInstance wi =         runtime.CreateWorkflow(typeof(ParametersWorkflow), parameters);     wi.Start(); } private static void runtime_WorkflowCompleted(object sender,     WorkflowCompletedEventArgs e) {     Console.WriteLine("The workflow instance with the ID '" +         e.WorkflowInstance.InstanceId.ToString() + "' has completed.");     Console.WriteLine("It told us: " +         e.OutputParameters["SomeOtherMessage"]); } 

The following code shows the workflow definition class. As you can see, there is a property for SomeMessage, which is the input parameter, and SomeOtherMessage acts as the output parameter. These properties have a set and get accessor, respectively.

  public sealed partial class ParametersWorkflow : SequentialWorkflowActivity {     private string someMessage;     private string someOtherMessage;     public string SomeMessage     {         set { someMessage = value; }     }     public string SomeOtherMessage     {         get { return someOtherMessage; }     }     public ParametersWorkflow()     {          InitializeComponent();     }     private void caEchoInputMessage_ExecuteCode(object sender, EventArgs e)     {         Console.WriteLine("The host told me: " + this.someMessage);     }     private void caSetOutputMessage_ExecuteCode(object sender, EventArgs e)     {         this.someOtherMessage = "This message will be accessed by the host...";     } } 

Local Communication Services

You can use Windows Workflow Foundation to communicate back and forth between a host and an executing workflow instance. Essentially, you use standard .NET interfaces and classes to facilitate workflow communication through method calls and events.

When a workflow wants to tell something to the host, it calls a method predefined in a .NET interface and subsequently implemented in a concrete class. When the host is ready to notify the workflow of some event or data, it raises an event that is then handled by the workflow.

Relevant Classes

The following sections review several classes that enable local communication services.

Custom Communication Service Interfaces and Classes

To allow communications to occur between a workflow host and workflow instances, you must define communication contracts that dictate which messages can be sent back and forth. These contracts are implemented through .NET interfaces, and they can contain any public methods that can be called from the workflow. The interface methods represent concrete methods that will exist on the workflow host. You can pass any type of data as parameters to these methods for communication purposes, and you can specify return values to set variables in the workflow instance. However, any type passed to a workflow and its host must be decorated with the Serializable attribute (more on this requirement later in this chapter).

After you define the communication interfaces, you must create concrete classes to implement the behavior specified in the interfaces. The following sections cover classes and entities important to local communication services. An example is then shown and discussed to further explain these concepts.

ExternalDataExchangeService

This class is a runtime service that manages all the communication service classes. To use local communication services, you must add an instance of this class to the workflow runtime (as you do with any other runtime service that uses the AddService method of WorkflowRuntime). Then you can add communication service classes to the ExternalDataExchangeService instance using its own AddService method, as follows:

  WorkflowRuntime workflowRuntime = new WorkflowRuntime(); // create an instance of the data exchange service ExternalDataExchangeService dataService = new ExternalDataExchangeService(); // add the external data exchange service to the runtime workflowRuntime.AddService(dataService); // create an instance of my custom communication service MyCommunicationService commService = new MyCommunicationService(); // add the communication service to the data exchange runtime service dataService.AddService(commService); 

The ExternalDataExchange service class exposes the following public methods for managing local communication services:

  • AddService - Adds communication service instances to the data exchange service.

  • GetService - Takes a Type reference as its sole parameter and returns any communication services of that type. Because GetService returns an object, you must first cast it to the appropriate type.

  • RemoveService - Takes a communication service instance as a parameter and removes it from the data exchange service. RemoveService throws an InvalidOperationException if the class reference passed is not already registered with the data exchange service.

The data exchange service is also responsible for managing the communications between the host and workflow instances. When the communication is handled through interfaces as described previously, the runtime uses .NET reflection to make method calls and raise events.

ExternalDataExchangeAttribute

This attribute is used to decorate custom communication-service interfaces. It acts as a marker so that the Windows Workflow Foundation infrastructure knows which interfaces are to be treated as a communication contract.

The following is a simple example of this attribute on a communication interface:

  [ExternalDataExchangeAttribute] public interface ICommService {    void CallTheHost();    event EventHandler<ExternalDataEventArgs> NotifyTheWorkflow; } 

When you decorate an interface with this attribute, Visual Studio perceives that interface as a communication contract. This is important when you want to designate an interface as the contract for communicating with the host in a workflow.

ExternalDataEventArgs

This class is passed to event handlers of local communication services and represents the context of the event. Like any other event in the .NET Framework, this class inherits from System.EventArgs. Any event that participates in the communication process between workflows and hosts must use this class or an inherited class to represent the event.

Just because you have to use this class for workflow communication events does not mean you are limited in what you information you can pass to the workflow. To create an event-arguments class that passes data specific to your problem domain, you can simply inherit from ExternalDataEventArgs. The following is an example of an inherited class that passes a person’s first and last name:

  [Serializable] public class NewPersonEventArgs : ExternalDataEventArgs {     private string firstName;     private string lastName;     public string FirstName     {         get { return this.firstName; }     }     public string LastName     {         get { return this.lastName; }     }     public NewPersonEventArgs(Guid instanceId, string firstName, string lastName)         : base(instanceId)     {         this.firstName = firstName;         this.lastName = lastName;     } } 

There are a couple of important things to notice in this example. First, the class is marked with the Serializable attribute. This is required because the EventArgs class is actually serialized when it is passed from the workflow’s host to the workflow instance. If you do not decorate your custom class with this attribute, an EventDeliveryFailedException is thrown when an event passing your custom class is raised.

Also notice that the constructor receives not only the firstName and lastName variables, but also an instanceId that is subsequently passed to the base class constructor. This is also a requirement because the runtime must know the workflow instance on which to raise an event.

Table 5-4 lists the properties in the ExternalDataEventArgs class.

Table 5-4: ExternalDataEventArgs Properties
Open table as spreadsheet

Property

Description

Identity

This property is the identity of the entity that is raising the event. This value is used for security purposes to ensure that the calling entity has access to pass data to the workflow.

InstanceId

This property is a Guid that maps to an existing workflow instance that is to handle the event.

WaitForIdle

This Boolean property indicates whether the event about to be raised should be raised immediately or when the workflow instance becomes idle.

WorkflowType

This property is a Type instance representing the type of the workflow instance.

WorkHandler

This property is of type IPendingWork and allows the workflow host to interact with the transactional work batch.

WorkItem

This property is a reference to the object that caused the current event to be raised.

Communication Activities

The classes discussed so far are related mostly to the workflow runtime host. However, several important entities facilitate communication on the workflow side, as described in the following sections.

The CallExternalMethod Activity

The CallExternalMethod activity is used to call methods in the workflow host that have been defined with the workflow communication contracts. To do this, you place this activity in a workflow and set a couple of key properties.

First, you set the InterfaceType property to specify that this interface should be used to define the workflow communication.. In Visual Studio, you set the InterfaceType property in the .NET type browser. The Browse and Select a .NET Type dialog box, shown in Figure 5-2, enables you to select interfaces that have been decorated with the ExternalDataExchangeAttribute. To access this dialog box, click the ellipsis button in the InterfaceType property box in the properties window.

image from book
Figure 5-2

From here, you can select interfaces defined in the current project or in referenced assemblies, which is useful for separating key code entities. For example, you can place all interfaces in a single Visual Studio project for ease of versioning and reusability.

After setting the interface, you need to define which method on that interface is called when the CallExternalMethod activity is executed. You do this with the MethodName property. A drop-down list is provided so that you can choose among all methods defined in the communication interface.

When you select the method, the properties list changes based on parameters that are to be passed to that method and that method’s return value. These new properties allow you to specify which properties or fields in your workflow are bound to the method parameters and return value. You have several options for binding values.

The first binding option is to select a field or property declared in the workflow’s code-beside class or a property of the same type on another activity. For example, if the method chosen has a parameter of type System.String, you can choose a string field defined in the workflow. This string field is then passed to the method in the workflow host. The same holds true for the method’s return value - the field selected is set after the method is finished and returns.

The other option is to have Visual Studio create a new property or field to which the method’s return value or parameters can be bound. You can accomplish both binding options by using the dialog box that appears when you click the ellipsis in one of the parameter’s or return value’s properties dialog box. Figure 5-3 shows the first tab of this dialog box. Figure 5-4 shows the second tab, which allows you to create a new member to which the value will be bound.

image from book
Figure 5-3

image from book
Figure 5-4

When you create a new property, a DependencyProperty instance is created in the workflow definition. Dependency properties represent the data storage and retrieval processes for workflow activities. The generated dependency property points at a newly generated class property of the same type as the method parameter that was promoted. This new property uses the dependency property instance to access and retrieve the data associated with the parameter. The following is an example of the code generated during property promotion:

  public static DependencyProperty MyParameterProperty =     DependencyProperty.Register("MyParameter",         typeof(System.Boolean),         typeof(LocalCommunication.EnrollmentWorkflow)); [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)] [BrowsableAttribute(true)] [CategoryAttribute("Parameters")] public Boolean MyParameter {     get     {         return ((bool)(base.GetValue(             LocalCommunication.EnrollmentWorkflow.MyParameterProperty)));     }     set     {         base.SetValue(LocalCommunication.EnrollmentWorkflow.MyParameterProperty,             value);     } } 

Notice the attributes that are decorating the MyParameter property. These are used by the Visual Studio designer to provide important information during design time. DesignerSerializationVisibility Attribute defines how the property is serialized, BrowsableAttribute dictates whether the property will be visible in the properties window of Visual Studio, and CategoryAttribute is used by the properties window when properties are categorized (as opposed to being sorted alphabetically).

The HandleExternalEvent Activity

The HandleExternalEvent activity is used to handle events raised from the workflow host - specifically, events defined in communication interfaces. Just like the CallExternalMethod activity, this activity has an InterfaceType property that defines the interface with the event. After setting the InterfaceType property, you must set the EventName property from the drop-down menu.

This activity is very important because it acts as the listener in the workflow, waiting for a message from the outside host. You can use many different patterns to initiate and coordinate conversations between workflow instances and their hosts. For example, the HandleExternalEvent activity is commonly used directly after a CallExternalMethod activity. In this scenario, a workflow uses a method on a workflow data service to request information and then waits for a response through an event. More patterns are discussed in the next section.

The Listen and EventDriven Activities

These activities work together to allow a workflow to wait and listen for an event from the workflow host. The Listen activity can have two or more branches, but only one is actually executed. Each branch on this activity is an EventDriven activity.

EventDriven activity instances are parents to other activities that are executed by an event. To support this behavior, the first child activity of an EventDriven activity must be an event-handling activity. More specifically, the first child activity must implement the IEventActivity interface - HandleExternal EventActivity, for example. After execution in a particular branch is started through the event-handling activity, no other branch can be executed. This type of behavior is useful when the workflow does not know what type of action to anticipate next in its series of events.

A Communication Example

This section provides an example of local communication services using the classes and concepts introduced in this chapter. The code is for an application-for-enrollment process at an educational institution.

When the sample workflow instance starts, a college application is created and passed to the workflow. The first step in the sample workflow is to request an application status from the host. Because this is just a sample workflow application showcasing local communication services, the software simply asks the host for an approved or nonapproved status. In a more complex scenario, the external method call could present the user with a rich user interface requesting further data about the application’s status.

Next, the workflow listens for an answer from the host. It does this using the Listen, EventDriven, and HandleExternalEvent activities. The Listen activity also has a branch containing a Delay activity. Because this activity implements IEventActivity interface, it can cause the Listen activity to execute a given branch if nothing happens within a configured amount of time. Remember that only one branch of a Listen activity executes, so as soon as one branch executes, the activity ends its execution. Figure 5-5 shows the completed workflow in Visual Studio.

image from book
Figure 5-5

The following listing shows the code in the workflow host. In this example, the host is simply a console application that starts a new workflow instance and passes the applicant’s name as a parameter. Also notice that an instance of the ExternalDataExchangeService is added to the workflow runtime. Subsequently, an instance of a custom local communication service, EnrollmentService, is added to the ExternalDataExchangeService instance.

  public class Program {     static AutoResetEvent waitHandle = new AutoResetEvent(false);     public static void Main(string[] args)     {         // create a workflow runtime instance         WorkflowRuntime runtime = new WorkflowRuntime();         runtime.WorkflowCompleted += new             EventHandler<WorkflowCompletedEventArgs>(                 runtime_WorkflowCompleted);         // create the data exchange runtime service         // and add it to the runtime         ExternalDataExchangeService dataService =             new ExternalDataExchangeService();         runtime.AddService(dataService);         // add a new instance of the enrollment communication         // service and add it to the data exchange service         dataService.AddService(new EnrollmentService());         // create a parameters object to pass the "application"         Dictionary<string, object> parms = new Dictionary<string, object>();         parms.Add("ApplicantName", "Todd Kitta");         // create an instance of the enrollment workflow and pass         WorkflowInstance wi =             runtime.CreateWorkflow(typeof(EnrollmentWorkflow), parms);         // start the workflow instance         wi.Start();         waitHandle.WaitOne();     }     static void runtime_WorkflowCompleted(object sender,         WorkflowCompletedEventArgs e)     {         waitHandle.Set();     } } 

The following code shows the communication contract that is implemented as a .NET interface decorated with the ExternalDataExchange attribute. The communication service allows the back-and-forth passing of data; the workflow can request a status from the workflow host, and the host can raise an event to the workflow signifying that an application has been approved or rejected.

  [ExternalDataExchange] public interface IEnrollmentService {     void RequestEnrollmentStatus(string name);     event EventHandler<ExternalDataEventArgs> EnrollmentApproved;     event EventHandler<ExternalDataEventArgs> EnrollmentRejected; } 

The data event arguments class that is passed to the workflow event handlers is shown in the following code. Notice that the EnrollmentEventArgs class inherits from ExternalDataEventArgs. Remember, this is a requirement for all event-arguments classes used in workflow communication. In this example, the inherited class simply adds a property for the applicant’s name. Although the workflow should already have a copy of the applicant’s name, this example illustrates the flexibility of the communication services.

  [Serializable] public class EnrollmentEventArgs : ExternalDataEventArgs {     private string applicantName;     public string ApplicantName     {         get { return this.applicantName; }     }     public EnrollmentEventArgs(Guid instanceId, string applicantName)         : base(instanceId)     {         this.applicantName = applicantName;     } } 

The next block of code shows the communication service itself, which is called EnrollmentService. The class implements the IEnrollmentService interface (defined earlier). The RequestEnrollment Status method calls another private method, which then requests a response through the console interface. If the user enters y, the EnrollmentApproved event is raised with a new instance of the EnrollmentEventArgs class. Otherwise, the EnrollmentRejected event is raised. After one of the events is raised, the workflow continues from its idle state and executes the appropriate branch. (That is, of course, unless the workflow times out based on the Delay activity’s configuration.)

  public class EnrollmentService : IEnrollmentService {     public void RequestEnrollmentStatus(string name)     {         ThreadPool.QueueUserWorkItem(GetResponse,             new EnrollmentEventArgs(                 WorkflowEnvironment.WorkflowInstanceId,                 name));     }     private void GetResponse(object o)     {         EnrollmentEventArgs args = o as EnrollmentEventArgs;         Console.WriteLine("Will you approve the application for " +             args.ApplicantName + "?");         // read the user's response from the command line         char response = Console.ReadKey().KeyChar;         Console.WriteLine();         // check the user's response         // and raise the appropriate event         if (response == 'y')         {             EnrollmentApproved(null, new EnrollmentEventArgs(                 args.InstanceId,                 args.ApplicantName));         }         else         {             EnrollmentRejected(null, new EnrollmentEventArgs(                 args.InstanceId,                 args.ApplicantName));         }     }     public event EventHandler<ExternalDataEventArgs> EnrollmentApproved;     public event EventHandler<ExternalDataEventArgs> EnrollmentRejected; } 

The workflow’s code follows. There are a few important things to observe in this code. First, notice the ApplicantName property. This is set through the parameters collection passed from the host upon the workflow’s creation. A couple of private class members act as flags. The first, responseReceived, is set to true when one of the two branches with HandleExternalEvent activities executes. The next, isApplicationApproved, is set to true if the EnrollmentApproved event is raised from the host. These flags are then used in the ExecuteCode event handler of the Code activity called printOutcome. This method simply prints a status message to the console window.

  public sealed partial class EnrollmentWorkflow : SequentialWorkflowActivity {     private string applicantName;     public string ApplicantName     {         set { this.applicantName = value; }     }     private EnrollmentEventArgs enrollmentEventArgs;     private bool responseReceived = false;     private bool isApplicationApproved = false;     public EnrollmentWorkflow()     {         InitializeComponent();     }     private void printOutcome_ExecuteCode(object sender, EventArgs e)     {         string message;         if (this.responseReceived)         {             message = "The workflow produced an outcome of ";             if (this.isApplicationApproved)             {                 message += "approved.";             }             else             {                 message += "rejected.";             }         }         else         {             message =                 "The workflow timed out before it received a response.";         }         Console.WriteLine(message);     }     private void applicationApproved_Invoked(object sender,         ExternalDataEventArgs e)     {         this.responseReceived = true;         this.isApplicationApproved = true;     }     private void applicationRejected_Invoked(object sender,         ExternalDataEventArgs e)     {         this.responseReceived = true;         this.isApplicationApproved = false;     } } 

Developing Custom Communication Activities

The previous section showed you how to develop custom communication services and then how to use these services in a workflow with the HandleExternalEvent and CallExternalMethod activities. In this section, you learn how to take those concepts a step further to build a library of custom activities that represent the communication interfaces built for a specific problem domain.

Doing this is a relatively simple process - it involves inheriting from the activities introduced in the preceding section. The following code listings expand on the college application communication service. Remember, the IEnrollmentService interface defined one method, RequestEnrollmentStatus, and two events, EnrollmentApproved and EnrollmentRejected. Custom communication activities need to be created for the method and each of the events.

In the following code, notice the ToolboxItemAttribute that decorates the class. This lets Visual Studio know that it should add the custom activity to the Toolbox so the developer can drag and drop it on to the workflow designer. Also, the name parameter of the RequestEnrollmentStatus method is represented as a DependencyProperty wrapped in a public property.

  [ToolboxItemAttribute(typeof(ActivityToolboxItem))] public class RequestEnrollmentStatus : CallExternalMethodActivity {     public static DependencyProperty nameProperty = DependencyProperty.Register(         "name", typeof(string), typeof(RequestEnrollmentStatus));     public RequestEnrollmentStatus()     {         this.InterfaceType = typeof(IEnrollmentService);         this.MethodName = "RequestEnrollmentStatus";     }     [ValidationOptionAttribute(ValidationOption.Required)]     public string name     {         get         {             return ((string)                 (this.GetValue(RequestEnrollmentStatus.nameProperty)));         }         set         {             this.SetValue(RequestEnrollmentStatus.nameProperty, value);         }     }     protected override void OnMethodInvoking(EventArgs e)     {         this.ParameterBindings["name"].Value = this.name;     } } 

The following code is similar to the previous RequestEnrollmentStatus class. However, it represents one of the two events in the IEnrollmentService interface, EnrollmentApproved. Because the class represents one of the events on the interface, it inherits from HandleExternalEventActivity. It also represents the EnrollmentEventArgs and sender parameters of the event handler.

  [ToolboxItemAttribute(typeof(ActivityToolboxItem))] public class EnrollmentApproved : HandleExternalEventActivity {     public static DependencyProperty senderProperty =         DependencyProperty.Register(             "sender", typeof(object), typeof(EnrollmentApproved));     public static DependencyProperty eventArgsProperty =         DependencyProperty.Register(             "eventArgs", typeof(EnrollmentEventArgs), typeof(EnrollmentApproved));     public object sender     {         get         {             return (object)base.GetValue(EnrollmentApproved.senderProperty);         }         set         {             base.SetValue(EnrollmentApproved.senderProperty, value);         }     }     public EnrollmentEventArgs eventArgs     {         get         {             return (EnrollmentEventArgs)base.GetValue(                 EnrollmentApproved.eventArgsProperty);         }         set         {             base.SetValue(EnrollmentApproved.eventArgsProperty, value);         }     }     public EnrollmentApproved()     {         this.InterfaceType = typeof(IEnrollmentService);         this.EventName = "EnrollmentApproved";     }     protected override void OnInvoked(EventArgs e)     {         this.eventArgs = (EnrollmentEventArgs)e;     } } 

Generating Communication Activities

Although you can create strongly typed communication activities, as shown in the previous section, you can also generate these classes using wca.exe, a command-line utility included with the Windows Workflow Foundation SDK.

wca.exe is a very useful utility that basically does what you did manually in the previous section. Table 5-5 lists the command-line switches that you can use to modify the behavior of this utility.

Table 5-5: wca.exe Command-Line Switches
Open table as spreadsheet

Command Option

Description

/collapseArgs, /c

Collapses all public properties in the event-arguments class into a single public property. By default, the generated classes contain a property for each property of the event-arguments class.

/includeSender, /I

Includes the sender parameter as a public property of the generated classes. This does not appear by default.

/language:, l:<language>

Allows you to specify in which language the generated classes are created. Possible values are CS for C# and VB for Visual Basic .NET. CS is the default if a value is not provided.

/out:, /o:<directoryName>

Allows you to indicate which directory should be used as the output for the generated files. If this option is not specified, the current folder is used.

/namespace:, /n:<namespace>

Allows you to specify the namespace to which the generated classes belongs. If the namespace option is not specified, the namespace in which the communication interface is defined is used.

The following is an example of how you can use the wca.exe utility. The utility checks LocalCommunication.exe for any interfaces decorated with the ExternalDataExchange attribute and then generates strongly typed communication classes in the GeneratedActivities folder:

  wca.exe LocalCommunication.exe /o:GeneratedActivities 

The following code is an example of what the wca.exe utility generates given the EnrollmentRejected event of the IEnrollmentService interface:

  [ToolboxItemAttribute(typeof(ActivityToolboxItem))] public partial class EnrollmentRejected : HandleExternalEventActivity {     public static DependencyProperty ApplicantNameProperty =         DependencyProperty.Register("ApplicantName", typeof(string),             typeof(EnrollmentRejected));     public EnrollmentRejected() {         base.InterfaceType = typeof(LocalCommunication.IEnrollmentService);         base.EventName = "EnrollmentRejected";     }     [BrowsableAttribute(false)]     [DesignerSerializationVisibilityAttribute(         DesignerSerializationVisibility.Hidden)]     public override System.Type InterfaceType {         get {             return base.InterfaceType;         }         set {             throw new InvalidOperationException(                 "Cannot set InterfaceType on a derived                     HandleExternalEventActivity.");         }     }     [BrowsableAttribute(false)]     [DesignerSerializationVisibilityAttribute(         DesignerSerializationVisibility.Hidden)]     public override string EventName {         get {             return base.EventName;         }         set {             throw new InvalidOperationException(                 "Cannot set EventName on a derived HandleExternalEventActivity.");         }     }     [ValidationOptionAttribute(ValidationOption.Required)]     public string ApplicantName {         get {             return ((string)                (this.GetValue(EnrollmentRejected.ApplicantNameProperty)));         }         set {             this.SetValue(EnrollmentRejected.ApplicantNameProperty, value);         }     }     protected override void OnInvoked(System.EventArgs e) {         LocalCommunication.EnrollmentEventArgs castedE =             ((LocalCommunication.EnrollmentEventArgs)(e));         this.ApplicantName = ((string)(castedE.ApplicantName));     } } 

Notice that the generated class inherits from HandleExternalEventActivity, as you’d probably expect. The properties that you would otherwise have to set manually are set for you automatically. Take a look at the InterfaceType property for an example. Its value is set in the constructor, but its set accessor throws an exception if called. This is by design and prevents other code from inadvertently setting it to something that doesn’t make sense. Also interesting is the overridden OnInvoked method. Here, the ApplicantName property is set to the relevant value whenever the event is raised.

Correlation

Correlation describes the concept of correctly delivering messages to a particular event handler in a workflow instance. As you have seen so far, you can easily get your message to its intended workflow by using the InstanceId GUID. However, there may be times when you want to deliver a message to a specific HandleExternalEvent activity that is waiting for its associated event to be raised.

Think about a workflow scenario that requires several inputs from outside sources in no particular order. To illustrate this point, take the college enrollment process a step further. After a student has been accepted to a school, there are several items the institution might need before the enrollment can be considered complete. For example, the student may be required to have his or her medical history, financial records, and high school transcript sent to the university before classes and living arrangements can be assigned.

To do this in Windows Workflow Foundation, you can use the Parallel activity. The activity should have at least three branches, one each for the items required by the school. You could also add a fourth branch with a Delay activity that sends a reminder to the appropriate parties if items are not received within an acceptable timeframe.

Figure 5-6 is a workflow that meets these requirements. The workflow contains a Parallel activity that contains three branches. Each branch represents the request and subsequent retrieval of required data for the college enrollment process. The first branch is for medical data, the second for the high school transcript, and the third for financial data.

image from book
Figure 5-6

The nice thing about how this workflow is designed is that any of the three activities can occur in any order. All three requests are sent out simultaneously, but the responses can occur at very staggered intervals, even if one response occurs weeks after another. The important thing to note is that after the requests are sent out and responses are subsequently received, the response data has to know which ExternalEventHandlerActivity activity to execute.

The following sections explain the Windows Workflow Foundation infrastructure that handles message correlation. After that, the code for this example scenario is provided.

CorrelationParameterAttribute

The CorrelationParameterAttribute attribute is used to decorate the communication service definition interface. Its sole parameter is a string that represents the name of the variable acting as the correlation identifier. This variable name can exist on any method in the interface as well as any event. When present, it is used to tie the incoming or outgoing message to a specific activity instance in the workflow.

For example, think about a scenario where multiple messages are sent out to external sources, all of which eventually receive some kind of response. This could be applied in a document approval or voting workflow. After messages are sent out requesting a response, HandleExternalEvent activities are waiting for the messages. The workflow must somehow know which activity to notify upon the arrival of a message. The incoming message must have a copy of the unique correlation identifier that was mapped when the message was originally sent out of the workflow. The CallExternalMethod and HandleExternalEvent activities have a property called CorrelationToken, which is of type CorrelationToken. Because both activities point to the same CorrelationToken instance, you can map them to each other during runtime. Don’t forget - the CallExternalMethod activity you are using to send out a message must pass a variable that acts as the correlation identifier defined by the CorrelationParameterAttribute on the communication service interface.

CorrelationInitializerAttribute

The CorrelationInitializerAttribute is used to decorate the method on the communication service interface that first introduces the correlation identifier to the message correlation process. The decorated method must contain a parameter that matches the name supplied to the CorrelationParameter Attribute. This attribute marks the method so that the workflow runtime knows when to start a new message conversation.

CorrelationAliasAttribute

The CorrelationAliasAttribute enables you to provide the correlation identifier in a method or event but by using a different parameter name from what was defined using the CorrelationParameter Attribute. This comes in handy with events that provide the identifier in an event-arguments class. For example, the original correlation parameter could have been defined as employeeId, but the event-arguments class, e, might have a property called EmployeeID. In this case, you give the event an attribute definition similar to the following:

  [CorrelationAlias("employeeId", "e.EmployeeID")] event EventHandler<MyCustomEventArgs> MyEvent; 

A Correlation Example

Now back to the example introduced earlier. The scenario involved a request for data required for a student to enroll at a college. The workflow sends out three simultaneous requests for a student’s medical data, high school transcript, and financial information that are used for financial aid purposes.

The first logical step in developing such a solution is to define the communication interface, as follows:

  [ExternalDataExchange] [CorrelationParameter("itemType")] public interface IObtainRequiredItemsService {     [CorrelationInitializer]     void RequestItem(string ssn, string itemType);     [CorrelationAlias("itemType", "e.ItemType")]     event EventHandler<RequiredItemEventArgs> ItemReceived; } 

The interface looks a lot like a standard communication service interface, without a few extra attributes. The CorrelationParameterAttribute tells the workflow runtime to pay attention to any method or event that passes the itemType variable. The ItemReceived event that is decorated with the CorrelationAliasAttribute, which tells the workflow runtime to map the itemType identifier to the e.ItemType property on the RequiredItemEventArgs instance passed to the event handler.

The following code is the service implementation itself. Because the RequestItem method is called for any type of item request, it has a switch statement that checks the itemType variable. In a real-life solution, this method might send an e-mail or call an external web service requesting the desired information. The service also adds three public methods not included in the interface. These methods are called from the host when the requested data becomes available. Each method then raises the ItemReceived event, passing the appropriate itemType flag so the correct HandleExternalEvent activity is executed in the workflow. The item type passed is crucial for the correlation to work correctly.

  public class ObtainRequiredItemsService : IObtainRequiredItemsService {     public void RequestItem(string ssn, string itemType)     {         switch (itemType)         {             case "medical":                 Console.WriteLine(                     "Medical records were requested! Get on it!");                 break;             case "highschool":                 Console.WriteLine(                     "High school transcript was requested! Get on it!");                 break;             case "financial":                 Console.WriteLine(                     "Financial records were requested! Get on it!");                 break;         }     }     public event EventHandler<RequiredItemEventArgs> ItemReceived;     public void SubmitMedicalRecords(Guid instanceId, object data)     {         if (ItemReceived != null)         {             ItemReceived(null, new RequiredItemEventArgs(                 instanceId, "medical", data));         }     }     public void SubmitHighSchoolTranscript(Guid instanceId, object data)     {         if (ItemReceived != null)         {             ItemReceived(null, new RequiredItemEventArgs(                 instanceId, "highschool", data));         }     }     public void SubmitFinancialRecords(Guid instanceId, object data)     {         if (ItemReceived != null)         {             ItemReceived(null, new RequiredItemEventArgs(                 instanceId, "financial", data));         }     } } 

The following code shows a portion of the workflow code-behind. Notice the three instances of RequiredItemEventArgs. These fields are set when a message is received into the workflow through the HandleExternalEvent activities.

  public sealed partial class ObtainRequiredItemsWorkflow     : SequentialWorkflowActivity {     private RequiredItemEventArgs medialArgs;     private RequiredItemEventArgs highSchoolArgs;     private RequiredItemEventArgs financialArgs;     ... } 

The workflow host code follows. Of interest here is the WorkflowIdled event hander. This method is called after the request messages are sent from the workflow and the workflow enters a waiting state. At this point, the code simulates delays and then passes the requested data to the workflow through the communication service class.

  public class Program {     private static ObtainRequiredItemsService itemService;     private static WorkflowInstance instance;     public static void Main(string[] args)     {         WorkflowRuntime workflowRuntime = new WorkflowRuntime();         ExternalDataExchangeService dataService =             new ExternalDataExchangeService();         workflowRuntime.AddService(dataService);         itemService = new ObtainRequiredItemsService();         dataService.AddService(itemService);         AutoResetEvent waitHandle = new AutoResetEvent(false);         workflowRuntime.WorkflowCompleted +=             delegate(object sender, WorkflowCompletedEventArgs e)                 { waitHandle.Set(); };         workflowRuntime.WorkflowIdled +=             new EventHandler<WorkflowEventArgs>(workflowRuntime_WorkflowIdled);         Dictionary<string, object> parms = new Dictionary<string, object>();         parms.Add("Ssn", "111223333");         instance = workflowRuntime.CreateWorkflow(             typeof(CorrelationWorkflow.ObtainRequiredItemsWorkflow), parms);         instance.Start();         waitHandle.WaitOne();     }     public static void workflowRuntime_WorkflowIdled(object sender,         WorkflowEventArgs e)     {         // sleep for 2 seconds to simulate a delay in submission         Thread.Sleep(2000);         itemService.SubmitMedicalRecords(instance.InstanceId,             "All shots up-to-date.");         Thread.Sleep(2000);         itemService.SubmitHighSchoolTranscript(instance.InstanceId,             "Graduated top of the class.");         Thread.Sleep(2000);         itemService.SubmitFinancialRecords(instance.InstanceId,             "Qualifies for aid.");     } } 

To bring this example together, think about why correlation was needed in the first place. There were several activities waiting for the same event to be raised from the outside world. However, even though each activity was waiting for the same event, a distinction existed for each that could be used to allow the correct activity to receive the event. This distinction, known as the correlation parameter, is specified in the communication contract interface. Therefore, any communication between the workflow and host related to these activities needs to include this correlation parameter. If the facilities for correlation did not exist in Windows Workflow Foundation, the runtime would get confused when delivering messages to the workflow, which could be received by more than one activity.



Professional Windows Workflow Foundation
Professional Windows Workflow Foundation
ISBN: 0470053860
EAN: 2147483647
Year: 2004
Pages: 118
Authors: Todd Kitta

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net