Pluggable workflow services is one of the most highly anticipated new workflow features for SharePoint 2010. This is because SharePoint 2007 workflows lacked the ability to communicate with the outside world. The most basic scenario is a workflow that needs to go idle and wait for a message from separate system, like a line of business application such as CRM or something. Another desired technique was for inter workflow communication, where one workflow needs to send a message to another workflow. A third scenario would be a long running process. If you had a calculation or web service call that took thirty minutes to execute, there's no sense in keeping the workflow instance in memory. It would be better to hydrate the instance, and dehydrate it when the process is finish.
All three of those examples were not easily accomplished in SharePoint 2007. Now, Windows Workflow Foundation on the .NET 3.5 framework has always has the ability to meet these needs via Workflow Communication Services. Since SharePoint 2007 is on the 3.5 framework, you'd think it wouldn't have been a problem. Since SharePoint was the hosting provider there was no class that was provide to easily get at workflow instances and raise events into those instances that the workflow was listening form. This changes in SharePoint 2010 with the introduction of a new class, SPWorkflowExternalDataExchangeService.
Just as in Workflow Communication Services, in SharePoint 2010 workflows you can create a Local Service that your workflows can use to communication with each other. By using the CallExternalMethod activity and the HandleExternalEvent activity, SharePoint workflows and .NET applications can send and receive messages between one another (Figure 1).
Figure 1 Windows communication services is now friendly with SharePoint workflows. By using the SharePoint workflow external data exchange service, your workflows can send and receive messages from other workflows or .NET applications.
Before we get into how to setup a Local Service that uses the SharePoint external data exchange services, let's talk briefly about the example. The example we're going to build is going to be a glorified Hello World example. A workflow is going to say "Hello Event Handler!" to an event receiver on an announcements list by creating a new announcement. Then, an event receiver is going to say "Hello Workflow!" back to the workflow. To accomplish this, the workflow will call into a Local Service. The Local Service then creates an announcement in an announcements list. Thereafter, an event receiver responds to the new announcement by raising an event via the Local Service that the workflow is listening for. This example will serve to demonstrate how a SharePoint workflow can communicate with a .NET application. Follow these steps to build the Hello World pluggable workflow service:
1) Create a new Visual Studio 2010 project:
A) Open Visual Studio 2010 and create a new sequential workflow project titled
PluggableWorkflowServices and click OK.
B) Type the URL of the site you'll use to debug, click Next, select a Site Workflow, and click Finish.
A Local Service is basically composed of two components, a service interface and a service class. The service interface lets the sending and receiving parties know what type of data to send to each other. This is done by declaring a method the sender calls and an event the receiver listens for. To tap into the external data exchange services, the interface must be declared with an ExternalDataExchance attribute.
2) Create a new class to use for our Local Service and add our interface:
A) Create a new class titled HelloWorldService.cs.
B) Add the following using statements to the class file:
using System.Workflow.Activities;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using System.Workflow.Runtime;
C) Above the HelloWorldService class, add the following interface:
[ExternalDataExchange]
public interface IHelloWorldService
{
event EventHandler<HelloWorldEventArgs>
HelloWorkflow;
void HelloHost(string message);
}
How this works: the HelloHost method is called by the workflow via the CallExternalMethod activity, in our example, which then creates the announcement. The event receiver then executes and invokes the HelloWorkflow event which the workflow is listening for via the HandleExternalEvent activity.
3) Extend the HelloWorldService class, and add the HelloHost method and the event defined in the interface:
A) Make the HelloWorldService class extend Microsoft.SharePoint.Workflow.SPWorkflowExternalDataExchangeService.
B) Make the HelloWorldService class implement the IHelloWorldService interface.
C) Add the following code into the HelloWorldService class.
public event EventHandler<HelloWorldEventArgs> HelloWorkflow;
public void HelloHost(string message)
{
SPWeb web = this.CurrentWorkflow.ParentWeb;
SPList list = web.Lists["Announcements"];
SPListItem item = list.Items.Add();
item["Title"] = message;
item["Instance"] = WorkflowEnvironment.WorkflowInstanceId.ToString();
item.Update();
}
In the HelloHost method we want to do two things, create the event handler and the method that is defined in the interface. Within the method we're creating the new announcement and passing the workflow's instance ID into the "Instance" column within the announcement. This is how the event receiver will know which workflow to send a message to.
After you added the code as well as the code found in 2C, you may have noticed that the compiler cannot find the class for HelloWorldEventArgs. This class we have yet to define, but it will allow our event receiver to send a custom message to our workflow:
4) Add the following code below the HelloWorldService class:
[Serializable]
public class HelloWorldEventArgs : ExternalDataEventArgs
{
public HelloWorldEventArgs(Guid id) : base(id) { }
public string Answer;
}
Notice how our custom arguments take essentially two values, a GUID that will store the workflow instance ID, and the Answer, which is the message the event receiver will pass to the workflow. This message will eventually get logged into the workflow's History List.
There's one more thing we must do before our Local Service is complete and we can move on to build the workflow and the event receiver. There are three more methods we need to add to satisfy an interface within SPExternalDataExchangeService.
5) Add the following code into the HelloWorldEventArgs class:
public override void CallEventHandler(
Type type, string eventName,
object[] parameters, SPWorkflow workflow,
string identity,
System.Workflow.Runtime.IPendingWork handler,
object item)
{
switch (eventName)
{
case "HelloWorkflow":
var args = new HelloWorldEventArgs(workflow.InstanceId);
args.Answer = parameters[0].ToString();
this.HelloWorkflow(null, args);
break;
}
}
public override void CreateSubscription(
MessageEventSubscription subscription)
{ throw new NotImplementedException(); }
public override void DeleteSubscription(
Guid subscriptionId)
{ throw new NotImplementedException(); }
The CallEventHandler method gets called each time an event is being requested in the Local Service. The first thing we do is check to see which event is being requested. If it's our HelloWorkflow event, we create a new HelloWorldEventArgs instance and pass in the workflow's instance ID to so the event knows which workflow it's invoking the event with. We next pass in the message string from the event receiver and lastly actually invoke the event.
With the Local Service now complete, we can start building the workflow and the event receiver that interface with this service. Continue the steps to build the workflow and the event receiver:
6) Configure the CallExternalMethod activity:
A) Within Workflow1, add the CallExternalMethod activity.
B) Within the properties of the activity, click the ellipsis next to the
InterfaceType property and specify the IHelloWorldService interface and click OK:
C) Change the MethodName property to be HelloHost.
D) Change the message property to be "Hello Event Handler!"
7) Configure the HandleExternalEvent activity:
A) Add the HandleExternalMethod activity below the CallExternalEvent activity.
B) Within the properties of the activity, click the ellipsis next
to the InterfaceType property and specify the IHelloWorldService interface and click OK.
C) Change the EventName property to be HelloWorkflow.
Note: this is the only event the workflow will listen for. The event receiver must invoke this event to communicate with the workflow.
D) Bind the "e" property to a new field handleArgs by clicking
the ellipses and by choosing Field in the Bind to New Member tab and thereafter clicking OK.
E) Go to the code view of the workflow and take the " = new …"
off the end of the handleArgs property so it looks like this:
public HelloWorldEventArgs handleArgs;
8) Configure a Log to History List activity:
A) below the HandleExternalEvent activity, add a LogToHistoryList activity.
B) Right click the LogToHistoryList activity and choose Generate Handlers.
C) Within the activity's MethodInvoking method, add the following line of code
to write the event receiver's message to the history:
logToHistoryListActivity1.HistoryDescription =
handleArgs.Answer;
9) Add an "Instance" column onto an Announcements list:
A) find or create the announcements list on the site you're unit testing on,
and under List Settings, click Create Column.
B) Type a name of "Instance" and choose a Single Line of Text column type and click OK.
10) Add a new Event Receiver to the project:
A) Right click the project, and choose Add -> New Item select the Event Receiver item.
B) Give the receiver a name of AnnouncementsReciever and click Add.
C) Choose List Events, Announcements, and An item was added, and click Finish:
D) Add the code below in the ItemAdded method of the receiver:
if (properties.ListTitle == "Announcements")
{
Guid instance = new Guid(properties.ListItem["Instance"].ToString());
string answer = "Hello Workflow!";
SPWorkflowExternalDataExchangeService.RaiseEvent(
properties.Web, instance, typeof(IHelloWorldService),
"HelloWorkflow", new object[] { answer });
}
The first thing this listing does is grabs the workflow's instance ID out of the Instance column and sets it to a GUID. This GUID is passed as a parameter into the RaiseEvent method which is how the RaiseEvent method knows which workflow to send the message to. Other parameters of interest are the SharePoint site where the workflow is running, the event to invoke ("HelloWorld" event), as well as our message to the workflow, "Hello Workflow!". That last parameter is actually an object array, so you can load that up with any serializeable object you think the workflow needs to do its business.
The very last think we need to do before we can test is register our Local Service with the SharePoint workflow runtime. This is done by adding an entry into the web.config. Follow this last step to register our service:
11) Register the HelloWorldService with the SharePoint workflow runtime:
A) open your web application's web.config under c:\inetpub\wwwroot\wss\virtual directories\
B) Find the <WorkflowServices> element.
C) Add the following WorkflowService in the WorkflowServices element (all on one line):
<WorkflowService Assembly="PluggableWorkflowServices, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c1c16502a94a0846" Class="PluggableWorkflowServices.HelloWorldService">
</WorkflowService>
Note: if you find yourself getting a "The workflow failed to start due to an internal error" error when you try to start a workflow, you probably didn't perform step 11C correctly or the DLL isn't found in the GAC.
We're FINALLY ready to test this thing. Build and Deploy your project. Navigate to your SharePoint site, and under View Site Content, click Site Workflows. Start your pluggable workflow (should be named PluggableWorkflowServices - Workflow1). Navigate to the Announcements list and you should see a new announcement titled "Hello Event Handler!", as is seen in Figure 2:
Figure 2 The workflow wrote to an announcements lists, and an event receiver on that is will respond by calling back into the hydrated workflow instance.
Go back to Site Workflow and click the "Completed" status of the PluggableWorkflowServices - Workflow1 workflow. You should see the event receiver's response of "Hello Workflow!" (Figure 3).
Figure 3 The event receiver has called back into the hydrated workflow instance and the workflow logged the string message received from the event receiver.
Cheers!
Phil