Windows Workflow + SharePoint 2003 + BizTalk Scenario Built Out

So I had a use case from a customer to build a sample using BizTalk Server, Windows Workflow Foundation, and Windows SharePoint Services 2003. It was
an interesting journey, and wanted to show how I got all these pieces to play nicely.

Before diving into each technical component, here's a summary of the use case.

  1. Purchase Order document received from ERP system.
  2. BizTalk looks at the document, assigns a reviewer, and publishes it to SharePoint.
  3. SharePoint kicks off a workflow which creates a Task item for that user, and potentially sends them an email.
  4. User loads up PO, and approves it.
  5. SharePoint again triggers workflow which then marks users Task as "complete."

Now as you may know, the next version of SharePoint (I think called SharePoint 2007) will have Windows Workflow integrated. But for now, it doesn't. So
there are multiple components that I put together to make this work.

Component: SharePoint Doc Library Configuration

First things first, I had to create the SharePoint document library. I did this by creating a BizTalk XSD schema, then creating an InfoPath form
that used it, and finally publishing that InfoPath form to my SharePoint site, thus generating the library with an associated template. Once I had
the library, I had to view the Change Advanced Settings page under Modify Columns and Settings. From here, I can define an event
handler which will fire whenever a change is made to the SharePoint library. So, once I have it built, I'll reference my Event Handler class here.

Component: SharePoint Event Handler

This one was fun. I created a class that referenced the Microsoft.SharePoint assembly and implemented the IListEventSink interface. It also
references the System.Workflow.Runtime assembly. The OnEvent method accepts a parameter of type SPListEvent. This is great since
that object contains all sorts of info about the document and library that spawned the event. My first decision in this method is to determine
which event just fired:

if (listEvent.Type == SPListEventType.Insert || listEvent.Type == SPListEventType.Update)

Then I created an instance of the WorkflowRuntime object that will execute the workflow. Since a workflow can accept any sort of parameter in,
I fill the input dictionary with the SharePoint event (SPListEvent) and a string indicating whether an Insert or Update has occurred.
Finally, I stop the runtime when the Workflow has completed. Now, after throwing the assembly in the GAC, I can go back to SharePoint and add my
Event Handler class.

Component: Windows Workflow

My workflow looks like this:

This class references the core SharePoint library again so that I can interact with the site. My first workflow shape simply initiates all my member
variables that are used by later steps. For instance, I have a member variable that is a pointer to my SharePoint site. I just took the SPListEvent
that came into my workflow and used its Site member to gain my reference:

activeSite = currentSPEvent.Site.OpenWeb();

Then I went about setting all sorts of variables for sending email, and so forth. One tricky thing to be aware of. So when I created my SharePoint document
library via InfoPath, I promoted a number of columns. Now, those values are available to me in the SPListEvent object. However, the names of the
columns don't come through, but rather, a GUID. That mapping between the name of the promoted column and the GUID identifier is stored in the SharePoint
library's Forms folder in the properties.xfp file. So, I just grabbed the GUID for the column I wanted and set my variable as such:

taskStatus = Convert.ToString(currentSPEvent.PropertiesAfter["xd_{5264C7A5-9142-488F-923B-0721E8770775}"]);

Got that?

The next shape that fires (assuming an Insert operation) is the CreateTask action. The code is fairly simple and looks like this:

SPListItemCollection listItems = activeSite.Lists[taskListName].Items;

SPListItem wfTask = listItems.Add();

//set task values

wfTask["ReqID"] = reqID;

wfTask["Title"] = taskTitle;

wfTask["Assigned To"] = activeSite.Users[@taskAssignment];

wfTask["Status"] = taskStatus;

wfTask["Start Date"] = DateTime.Now;

//insert task into collection

wfTask.Update();

For the SendMail shape, I simply utilize the System.Net.Mail assembly to build out a simple mail message. If an update is occuring, I need
to see if the status is Complete, and if so, mark the appropriate SharePoint task as Complete. So if the task is Complete I use the
SPQuery object to build a query that only returns the task with the corresponding ReqID value, then update that task.

//build query to pull back task with specific ReqID

SPQuery itemQuery = new SPQuery();

itemQuery.Query = "<Where><Eq><FieldRef Name='ReqID'></FieldRef><Value Type='Text'>" + reqID + "</Value></Eq></Where>";

SPList taskList = activeSite.Lists[taskListName];

SPListItemCollection taskListItems = taskList.GetItems(itemQuery);

//if task still exists ...

if (taskListItems.Count > 0)

{

//set status to completed

SPListItem wfTask = taskListItems[0];

wfTask["Status"] = "Completed";

try

{

wfTask.Update();

System.Diagnostics.EventLog.WriteEntry("WF", "Task Updated");

}

catch (Exception ex)

{

System.Diagnostics.EventLog.WriteEntry("WF", ex.ToString());

}

And that's pretty much the workflow piece.

Component: BizTalk Server

My BizTalk piece right now is fairly brain-dead. Just picks up a file, sends it to the SharePoint library, thus triggering the workflow.

Final solution

So what does this look like right now? A file gets put in my SharePoint library ...

Then, a task is created ...

Next an email is sent ...

User changes the doc, and saves it, and the Task gets updated to Complete ...

Gotchas

I glossed over a few things that were indeed comically tricky. So, here are a few notes, and links that helped me out ...

  • SharePoint site that hosts the document library must be running under ASP.NET 2.0
  • Only a single workflow runtime can be loaded in the App Domain at one time. So, I'll have modify my process for starting workflow instances
    in order to handle parallel load.
  • Had to incorporate some impersonation in order to get the Workflow the necessary permissions to interact with SharePoint. Used the
    WindowsImpersonationContext object and during variable initialization in the Workflow I started the impersonation, and at the end of the
    process I executed the Undo() method to reset permissions on the thread.
  • Absolutely invaluable SharePoint SDK for the full SharePoint API.
  • MSDN article demonstrating some SharePoint eventing
  • Part 1 and Part 2 of useful blog series
  • Nice, but a bit outdated Windows Workflow article on MSDN.
  • Helpful post on SharePoint events and .NET 2.0