I recently took a look at using Windows Workflow Foundation to create a simple Human Workflow to manage a procurement process on SharePoint Portal Server. So I set about integrating SharePoint Portal Server (SPS), InfoPath and Windows Workflow Foundation to achieve this goal was it easy well yes.

Here are the blog entries I can remember which really helped me out.

Which Style of Workflow When

Windows Workflow + SharePoint 2003 + BizTalk Scenario Built Out (Unfortunately SharePoint Portal Server is not .net 2.0 compatible whereas SharePoint Services are)

Adventures with Windows Workflow – Hosting a Workflow with Tracing and Persistence

Windows Workflow Foundation ASP.Net State Machine

Basic user experience:

The InfoPath form was vaguely similar to the Purchase Request form which comes with InfoPath as a template.

  1. Go to a SharePoint form library and enter a procurement request using an InfoPath form and submit it
  2. An approver would then receive an email with a link to the InfoPath form asking them to click the link and then approve or reject the procurement they open the form, approve, reject and then submit it
  3. Reject send email with form link back to requester
  4. Approve send email to a higher approver or a PO issuer with form links they open the form, enter the PO number and then submit it

Note:

Only an approver can approve, only a PO issuer can issue PO numbers

I didn’t need to write a single line of code to create the InfoPath form it was super easy.

How it’s done

SharePoint Portal Server Event Sinks

As SPS 2003 is not .net 2.0 compatible I hosted my Win WF State Machine in an ASP.net 2.0 Web Service (I am going to go into greater detail of how to do this below).

I registered an ASP.net 1.0 event handler class (see following code) in the advanced settings of my Procurement Form Library on SPS. Each time an InfoPath from is saved or submitted this event handler is called. This just calls the ASP.net 2.0 Web Service passing the SPS site URL and the InfoPath file url so the State Machine can access, read and update properties on the InfoPath form.

Note you could just as easily not use the SPS event sink and call the web service from an InfoPath form, using the event sink just gave me an easy way of passing the SPS site URL and the InfoPath file url (from the SPListEvent arguement) to my Win WF hosting Web Service so I could gain control of the InfoPath form inside the state machine and modify it’s xml etc.

using System;

using System.Runtime.Serialization;

using System.Runtime.Serialization.Formatters.Binary;

using System.IO;

using Microsoft.SharePoint;

using System.Security.Principal;

namespace Synergy.Procurement.Net1

{

public class SPEventHandler : Microsoft.SharePoint.IListEventSink

{

void IListEventSink.OnEvent(Microsoft.SharePoint.SPListEvent spEvent)

{

WindowsImpersonationContext wic = WindowsIdentity.GetCurrent().Impersonate();

try

{

//Pass the enough information to instanciate the current SharePoint Task

//and the Microsoft.SharePoint.SPListEventType through a web service to Win Workflow

//this way all the workflow process logic is handeled by Win Workflow

//NOTE: need to call through a Web Service as only SPS runs on .Net 1.1 and

//Win Workflow on .Net 2.0

//Get the following information so we can reinstanciate the list item in the workflow

//site url

//list file url

SPWeb spActiveSite = spEvent.Site.OpenWeb();

string spSiteUrl = spActiveSite.Url;

string spFileUrl = spEvent.UrlAfter;

WSToWinWF.Service wsWWF = new Synergy.Procurement.Net1.WSToWinWF.Service();

wsWWF.Credentials = System.Net.CredentialCache.DefaultCredentials;

Boolean wsTestResult = wsWWF.Invoke(spSiteUrl, spFileUrl, System.Convert.ToByte(spEvent.Type));

}

catch (System.Exception e)

{

System.Diagnostics.EventLog.WriteEntry(this.ToString(),e.ToString());

}

finally

{

wic.Undo();

}

}

}

}

Hosting a State Machine in a Web Service

Once the .Net 2.0 Web Service is called its job is to:

  1. Open the infopath form on the SPS Forms Library and deserialize the InfoPath xml into a serializable class (generated by the xsd utility in .net 2.0)

//Get the infopath file

SPWeb activeSite = new SPSite(spSiteUrl).OpenWeb();

SPFile spFile = activeSite.GetFile(spFileUrl);

//Get the infopath xml from the byte array of the file and read it into a memory //stream

Byte[] byteBuffer = spFile.OpenBinary();

MemoryStream xmlStream = new MemoryStream(byteBuffer);

//Now deserialise the infopath xml into a class

XmlSerializer serializer = new XmlSerializer(typeof(procurementRequest));

procurementRequest _pRequest;

_pRequest = (procurementRequest)serializer.Deserialize(xmlStream);

  1. Start and stop the workflow runtime and add the ExternalDataExchangeService the in the Global.asax

void Application_Start(object sender, EventArgs e)

{

// NOTE: This requires the configuration section to be named “WorkflowRuntime”.

System.Workflow.Runtime.WorkflowRuntime workflowRuntime = new System.Workflow.Runtime.WorkflowRuntime(“WorkflowRuntime”);

Application[“WorkflowRuntime”] = workflowRuntime;

//Add the procurement service to the runtime

System.Workflow.Activities.ExternalDataExchangeService dataService = workflowRuntime.GetService();

Synergy.Procurement.Net2.LocalServices.ProcurementService procurementService = new Synergy.Procurement.Net2.LocalServices.ProcurementService();

dataService.AddService(procurementService);

//Start the runtime

workflowRuntime.StartRuntime();

}

void Application_End(object sender, EventArgs e)

{

System.Workflow.Runtime.WorkflowRuntime workflowRuntime = Application[“WorkflowRuntime”] as System.Workflow.Runtime.WorkflowRuntime;

workflowRuntime.StopRuntime();

}

  1. Create a new instance of the state machine or rehydrate an existing instance from SQL Server (I save the WorkFlow instance ID in the InfoPath Document) from the SqlWorkflowPersistenceService

private Guid StartWorkflowInstance()

{

WorkflowRuntime workflowRuntime = (WorkflowRuntime)Application[“WorkflowRuntime”];

//Now get a reference to the ManualWorkflowSchedulerService

_schedulerService = workflowRuntime.GetService();

//Get the current procurement service if haven’t already

if (_procurementService == null)

{

ExternalDataExchangeService dataService = workflowRuntime.GetService();

_procurementService = (ProcurementService)dataService.GetService(typeof(ProcurementService));

}

//Get the instanceid from the infopath document

System.Guid WorkflowInstanceId;

//Start the instance if not started

if (_spListEventType == SPListEventType.Insert)

{

WorkflowInstanceId = Guid.NewGuid();

_workflowInstance = workflowRuntime.CreateWorkflow(typeof(MainStateMachine), null, WorkflowInstanceId);

_workflowInstance.Start();

}

//Load an existing instance

else

{

//Get the instanceid from the infopath document

WorkflowInstanceId = new Guid(_pRequest.referenceNumber);

_workflowInstance = workflowRuntime.GetWorkflow(WorkflowInstanceId);

_workflowInstance.Load();

}

// Now run the workflow. This is necessary when

// …using the ManualWorkflowSchedulerService

_schedulerService.RunWorkflow(WorkflowInstanceId);

return WorkflowInstanceId;

}

  1. Decide upon which implemented ExternalDataExchange event to raise. This is based on the SPListEventType and the properties in the serializable class representing the InfoPath document (Windows Workflow Foundation ASP.Net State Machine). Create an ExternalDataExchange interface and class with which to call through to event handlers in the StateActivities on your state machine. I pass through the following event arguments:

o WorkFlow instance ID (GUID)

o SPS site URL

o InfoPath file url

o The serializable class representing the InfoPath document

Note:

o You will need to impersonate the current windows identity before accessing the SharePoint site (call the OpenWeb method etc) and the users must have access to SharePoint

WindowsImpersonationContext wic = WindowsIdentity.GetCurrent().Impersonate();

o It is important in your Service class to set the following ExternalDataEventArgs property before raising the event

e.WaitForIdle = true;

o The Win WF Beta2 ManualWorkflowSchedulerService has a bug which may be fixed by calling it’s RunWorkflow method after each time you raise an ExternalDataExchange event in the web service.

The State Machine

It looks like this (simple aye):

The purpose of the state machine is to send emails to various managers requesting approval for procurement, notify procurement requesters of approval or rejection and email office admin staff to assign a PO number. It also updates values on the InfoPath form hosted on the SharePoint site for example the WorkFlow instance ID I talked about earlier. It decides what emails/updates to make based on what events are raised in which particular state. A procurement may need to be approved by several managers up the management chain thus many approval events may be raised (by different managers) once in the ProcurementApproved State Activity.

Below is the code to save back the InfoPath document to the SharePoint site once various updates have been made.

private void SaveInfoPathDoc()

{

//Create an XmlTextWriter to add processing instructions and write the contents out to a memory stream

MemoryStream memoryStream = new MemoryStream();

XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, System.Text.Encoding.UTF8);

//FIX: Add the processing instructions to the top of the xml document this is

//because they are lost in the serialisation\deserialisation process

xmlTextWriter.WriteProcessingInstruction(“mso-infoPathSolution”,

ConfigurationManager.AppSettings.Get(“InfoPathPI_mso-infoPathSolution”));

xmlTextWriter.WriteProcessingInstruction(“mso-application”,

ConfigurationManager.AppSettings.Get(“InfoPathPI_mso-application”));

xmlTextWriter.WriteProcessingInstruction(“mso-infoPath-file-attachment-present”,

ConfigurationManager.AppSettings.Get(“InfoPathPI_mso-infoPath-file-attachment-present”));

//Serialise the xml flush the writer and get the byte array _supportEmail the stream

XmlSerializer serializer = new XmlSerializer(typeof(procurementRequest));

serializer.Serialize(xmlTextWriter, _pRequest);

xmlTextWriter.Flush();

Byte[] byteBuffer = memoryStream.ToArray();

//FIX: Sometimes sharepoint locks the infopath document even though it has been submitted

//in the config file are two appSettings values: RetrySaveInterval & TimesToRetrySave

//which are the retry parameters in case this occurs

int timesToRetrySave = Convert.ToInt32(ConfigurationManager.AppSettings.Get(“TimesToRetrySave”));

//Convert seconds to milli seconds

int retrySaveInterval = Convert.ToInt32(ConfigurationManager.AppSettings.Get(“RetrySaveInterval”)) * 1000;

for (int i = 0; i < timesToRetrySave; i++)

{

try

{

//Get hold of the active site and infopath file

SPWeb activeSite = new SPSite(_pse.SharePointSiteUrl).OpenWeb();

SPFile spFile = activeSite.GetFile(_pse.SharePointFileUrl);

//Overwrite the infopath xml back

spFile.SaveBinary(byteBuffer);

break;

}

catch (Microsoft.SharePoint.SPException ex)

{

//If we’ve run out of retries then write an error to the event log

if (i >= timesToRetrySave – 1)

{

System.Diagnostics.EventLog.WriteEntry(this.ToString(), ex.ToString(), System.Diagnostics.EventLogEntryType.Error);

}

else

{

//Wait for the retry interval

System.Threading.Thread.Sleep(retrySaveInterval);

}

}

catch (Exception ex)

{

System.Diagnostics.EventLog.WriteEntry(this.ToString(), ex.ToString(), System.Diagnostics.EventLogEntryType.Error);

break;

}

}

}

Extras

  1. Used the External RuleSet Demo to make decisions based on attributes in InfoPath doc instance like BusinessUnit, TotalCost on which manager should receive an email to approve\reject a procurement (I was surprised how easy it was to fit this in).
  2. InfoPath form access security: I made active directory lookups on email addresses got user logons and updated them in the InfoPath doc which allowed me in InfoPath to ensure only authorised managers/systems administrator’s had access to update approval information in controls on the InfoPath form.
  3. For debugging I found this invaluable Windows WorkFlow Tracing