In the previous discussion we discussed about such type of communication between workflow and hosting environment in which data is communicated between workflow and hosting environment. We discussed about flow of data in both directions.
There might also be scenarios in which one needs to use some behaviors (methods) of other. Workflow might need to execute some methods for its operation or it might wait for some events from the hosting application.
As we know that workflows are hosted in the workflow runtime by the hosting application. The hosting application might provide various services which could be used by the workflow for its operations. The generally used services provided by WF are persistence, transaction, tracking and scheduling services. For communication with workflow the hosting application might also provide local services. These are called local services because they are used for in-process communication between workflow and the hosting application.
Hosting Application:
Hosting application must provide the following:
- Contract of services in the form of interface. The contracts specified should be decorated with ExternalDataExchangeAttribute. This is available in System.Workflow.Activities namespace.
- Implementation of service contract as a concrete class.
- Adding service definition to the workflow runtime.
We create hosting application project. We create it as sequential console workflow project.
Step # 01: (Contract Definition)
We create the following contract:
[ExternalDataExchange]
interface CommunicationServiceContract
{
public string concatenateStrings(string firstString, string secondString);
event EventHandler<ArgumentsToWF> myEvent;
void fireActivityEvent(Guid instanceID);
}
Step # 02: (Implementing the service contract)
Now we create a class CommunicationService. This class implements the above defined contract:
class CommunicationService : CommunicationServiceContract
{
#region CommunicationServiceContract Members
string concatenateStrings(string firstString, string secondString)
{
return firstString + secondString;
}
public event EventHandler<ArgumentsToWF> myEvent;
public void fireActivityEvent(Guid instanceID)
{
this.myEvent(null, new ArgumentsToWF(instanceID, "From Host"));
}
#endregion
}
We also define an event argument which would be used when we will be discussing HandleExternalEventActivity activity.
[Serializable]
class ArgumentsToWF : ExternalDataEventArgs
{
public string myData { get; set; }
public ArgumentsToWF(Guid instanceID,string myData) : base(instanceID)
{
this.myData = myData;
}
}
Step # 03 (Adding Service to the workflow runtime)
Since we have created sequential workflow console project, we have some default implementation. We use the same Program.cs. Insert the following code in the Main method just before creation of the workflow instance:
ExternalDataExchangeService myService = new ExternalDataExchangeService();
workflowRuntime.AddService(myService);
CommunicationService localService = new CommunicationService();
myService.AddService(localService);
In the above code we have added an external data exchange service to the workflow runtime. We have specified our custom CommunicationService as the expected service.
Use of Local Service by Workflow:
There are basically two activities provided by WF for these purpose. These services are as follows:
- CallExternalMethod: When workflow needs to call some method of the local service.
- HandleExternalEvent: When workflow waits for an event from the hosting application.
From above discussion, you might have established that the difference between the two services is mainly about the direction of communication. In the first, workflow is using some behavior as specified in the hosting application. In other, workflow behavior is being used by hosting application. For understanding these activities, we would have to discuss them individually.
For any of the above two activities, the workflow must know the contract of the service in advance so that it could map the parameters and return values of these methods to its own properties. These properties can be defined on workflow or activities level. The greatest thing with these activities is that all the settings can be done using property window. Even the parameters and return values can be assigned to properties in the workflow. This makes it not only more simple but also more manageable.
CallExternalMethod Activity:
As discussed, this activity can help workflow use some implementation provided by the host application in terms of local service. These methods might be notifications of any operation to the host process.
We drop a CallExternalMethod activity to our workflow (Workflow1.cs).
Now we need to specify the service contract of the service that this contract would be using. Since we have created contract in the same project as the service for simplicity, we can select current project and selected CommunicationServiceContract. If we have added a reference dll containing the service contract, we could select service from the referenced assembly.
As soon as we select the contract, all the available methods of the contract are available for selection. We select our intended method concatenateString.
As soon as we select this method, the properties window is updated dynamically giving options for mapping the parameters and returning value from the selected method to properties in the workflow. We can also map them to any field / property of the activities contained by the workflow.
We create the following string properties in our workflow code in Workflow1 class:
public string wfFirstString { get; set; }
public string wfSecondString { get; set; }
public string wfReturnString { get; set; }
Now we generate handlers for the assignment and display of our properties.
private void Workflow1_Initialized(object sender, EventArgs e)
{
this.wfFirstString = "First ";
this.wfSecondString = "Second ";
this.wfReturnString = "";
}
private void Workflow1_Completed(object sender, EventArgs e)
{
Console.WriteLine(this.wfReturnString);
Console.ReadLine();
}
Now we run this application. As expected the console window is displayed. It has the following printed on the console:
“First Second ”
The workflow has used the service added to the runtime and called its concateStrings method which has concatenated the strings provided as arguments and returns the concatenated string.
There is one more option provided by this activity. Sometimes we have to do some housekeeping work before calling any external service. This may include some initializations or something else. This activity provides the facility to fire an event just before calling external service. We create the following method:
private void initializeServiceArguments(object sender, EventArgs e)
{
this.wfFirstString = "First ";
this.wfSecondString = "Second ";
this.wfReturnString = "";
}
We can specify this method to the MethodInvoking property of the activity as follows:
Now you might be wondering if we add two services implementing the same contract then how it would determine which implementation to use. The simplest answer to this question is that you don’t need to worry about this. This is because two implementation of the same service contract can not be added to runtime. So the runtime would not take the trouble of resolving the intended implementation details.
HandleExternalEvent Activity:
This is synchronous wait for an event which makes it a blocking call. As soon as the control reaches to this activity, it waits until the event is triggered by the hosting application. You might figure out the consequences if never an event is received. Since this is blocking, it keeps on waiting forever. There are techniques to avoid such a scenario but we would discuss about them later.
To understand this activity, let us add another sequential workflow to our project and name it Workflow2. Drag and drop HandleExternalEventActivity to the designer and name it handleExternalEvent. We set the properties of this activity as follows:
Here we have specified that this activity should wait for myEvent event from the hosting environment. We are also adding handler myHandlerAcitivity_Handler which would handle the event. Let us implement this event handler as follows:
private void myHandlerActivity_Handler(object sender, ExternalDataEventArgs e)
{
Console.WriteLine(((ArgumentsToWF)e).myData);
}
We are just printing the data in the event argument to the console.
Instead of adding the instance of Workflow1, we need to add the instance of Workflow2 which is now under discussion. Also we need to fire the event concerned.
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(WFCommunication.Workflow2));
instance.Start();
localService.fireActivityEvent(instance.InstanceId);
Running the application this time should print the following on the console:
“From Host”
Performance Considerations:
We discussed two communication activities in this discussion i.e. CallExternalMethod and HandleExternalEvent activities. These activities use reflection to communicate with the provided services. As all of us know that using reflection has its performance implications which might not be acceptable for some scenarios. You would surely glad to know that there is another option.
You might be thinking if we are able to generate these activities based on the specific contract then workflow API would not have to resort to reflection to communicate with these services. If you think like that then you would be glad to know that your prayers are answered. Windows SDK provides a tool wca.exe. The name is basically the abbreviation of Windows Communication Activity generator. It generates specialized activities for us. The generated activities can be used in our workflows directly. These activities are created by specifying the contract information for the services which we intend to be used by the communication activities. Since these activities are created using the definition of contract that the services specified by handle or communicate activities would be implementing, there is no reflection is required to be used.
The following configurations can be set for WCA.exe. They are specified in WCA.exe.config file provided in the same folder as this tool. This file should look like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<generatePublisherEvidence enabled="false" />
</runtime>
</configuration>
As you can see, the only setting is for the publisher evidence. This is used to implement Code Access Security (CAS) policies. You can study further about this option from the following msdn link:
http://msdn.microsoft.com/en-us/library/bb629393.aspx
To generate the specialized activities, you have to have the contracts specified in an assembly (dll or exe). Out of the switches available, I want to discuss the following:
By using the different switches provided, we can specify the following options:
- Namespace: The namespace the generated activities should belong to. The default is the same namespace as the contract’s namespace (switch: /namespace).
- Output Directory: The output directory where we want code files to be created (switch: /out).
- Language: Currently we can generate activities in two possible languages. They are C# and VB. The default option is C# (switch: /language).
Ex: we have a dll containing the contract (interface) definition with the name WorkflowContracts.dll. We want to create the code files in c:\Activities folder. The language of the file should be vb. Then we need to execute the following command in command prompt after coming to the following folder:
wca.exe WorkflowContracts.dll /out:c:\Activities /language:cs