Wednesday, December 30, 2009

Extending SharePoint Approval Workflows Using Custom Initialization And Association Data

Chris Domino, Director, Enterprise Architect

It is always with a bit of hesitation that I recommend using Out-Of-The-Box SharePoint Workflows to my clients. It's not because they are exactly terrible or anything; it's just another application of the 80-20 rule. And since I take such good care of my clients, they seem to become fairly well acclimated to never hearing phrases like "No" or "That's out of scope" or "That's not in the budget" or "No thanks, I've already eaten."

So when the need arises for a quickie approval process on a document library or list, the OOTB Approval Workflow should bubble into your mind immediately. But as soon as a client squeaks about some minor customization here, or drops a "would it be possible" there, the bubble will quickly pop, sending you off to Visual Studio 2008 and the WF Workflow Designer, ultimately reinventing about ninety percent of the wheel.

Despite the fact that there are a lot of configuration options for the OOTB SharePoint workflows, modeling a business process is so specific to an organization's way of doing things that a generic "Approval" workflow can rarely be expected to get the job done. Except for the most remedial cases, the Approval workflow is not a panacea for content approval.

However, that doesn't mean that it's time to immediately build a workflow from scratch! By using the SharePoint workflow API and some nifty manipulations of the Association and Initialization data, we can leverage the OOTB Approval workflow and customize, or, more accurately, force it to do what we need.

One customization that always seems to come up is assigning a dynamic approver to an instance of an Approval workflow. On the association screen, you can specify a list of static approvers, but that's really it. If your workflow is set to kick off automatically (via an ItemAdded or ItemUpdated event), there's no opportunity to specify initialization data for that instance.

So what I'm going to show is how to programmatically create and start an instance of an Approval workflow with custom initialization data that is built from metadata on the list that the workflow is associated with. Let's start by an overview of the architecture of this scenario:

  1. A custom list is associated with the Approval workflow. One of the columns is of type "Person" which accepts an SPUser object. This user will be the approver of the instance of this workflow.
  2. A custom web part captures data from a user, and creates an item in a list.
  3. A Feature provisions the above list, and welds on the Approval workflow with custom Association data.
  4. Event receivers are installed for the custom list as well as the task list the workflow uses. These will kick off the workflow, and execute code when it is approved.

Taking the last item first, here's some sample code for a feature receiver's FeatureActivated event, scoped at the site level:

  1. public override void FeatureActivated(SPFeatureReceiverProperties properties)
  2. {
  3. try
  4. {
  5. //open site
  6. using (SPSite site = properties.Feature.Parent as SPSite)
  7. {
  8. //open web
  9. site.AllowUnsafeUpdates = true;
  10. using (SPWeb web = site.RootWeb)
  11. {
  12. //create list for workflow
  13. web.AllowUnsafeUpdates = true;
  14. SPList list = web.Lists[web.Lists.Add("Request List", "This list holds the requests to be approved.", SPListTemplateType.GenericList)];
  15. //add fields to list
  16. List<SPField> fields = new List<SPField>();
  17. fields.Add(list.Fields.GetFieldByInternalName(list.Fields.Add("Client Name", SPFieldType.Text, true)));
  18. //hide title
  19. SPField title = list.Fields["Title"];
  20. title.Required = false;
  21. title.Hidden = true;
  22. title.Update();
  23. //add approver field
  24. SPField field = list.Fields.GetFieldByInternalName(list.Fields.Add("Approver", SPFieldType.User, false));
  25. field.Hidden = true;
  26. field.Update();
  27. //create lists for workflow tasks and history
  28. SPList tasks = web.Lists[web.Lists.Add("Workflow Tasks", "This list holds the tasks used for the Request workflow approvals.", SPListTemplateType.Tasks)];
  29. web.Lists.Add("Workflow History", "This list holds the historical data for Request workflows.", SPListTemplateType.WorkflowHistory);
  30. //get workflow association
  31. SPWorkflowTemplate wk = web.WorkflowTemplates.GetTemplateByName("Approval", new CultureInfo(1033));
  32. SPWorkflowAssociation association = SPWorkflowAssociation.CreateListAssociation(wk, "Request Approval Workflow", web.Lists["Workflow Tasks"], web.Lists["Workflow History"]);
  33. //configure workflow
  34. association.AllowManual = false;
  35. association.AutoStartCreate = false;
  36. association.AutoStartChange = false;
  37. association.AllowAsyncManualStart = false;
  38. association.AssociationData = association.AssociationData.Replace("<my:AllowDelegation>true</my:AllowDelegation>", "<my:AllowDelegation>false</my:AllowDelegation>");
  39. association.AssociationData = association.AssociationData.Replace("<my:AllowChangeRequests>true</my:AllowChangeRequests>", "<my:AllowChangeRequests>false</my:AllowChangeRequests>");
  40. list.AddWorkflowAssociation(association);
  41. //wire up event handlers
  42. list.EventReceivers.Add(SPEventReceiverType.ItemAdded, "Request, Version=, Culture=neutral, PublicKeyToken=97c72abbf6e1c840", "Request.EventHandlers");
  43. tasks.EventReceivers.Add(SPEventReceiverType.ItemUpdated, "Request, Version=, Culture=neutral, PublicKeyToken=97c72abbf6e1c840", "Request.EventHandlers");
  44. tasks.Update();
  45. //update main view
  46. SPView view = list.Views["All Items"];
  47. view.ViewFields.DeleteAll();
  48. foreach (SPField f in fields)
  49. view.ViewFields.Add(f);
  50. //save
  51. view.Update();
  52. list.Update();
  53. web.Update();
  54. }
  55. }
  56. }
  57. catch (Exception ex)
  58. {
  59. //log and throw
  60. EventLog.WriteEntry("Request Workflow Feature Receiver - Activated", ex.ToString(), EventLogEntryType.Error);
  61. throw;
  62. }
  63. }

I know that's a long method and there's a lot going on, so let's look at some of the important lines in the above listing. The rest are examples of how I like to customize my sites via code executed in feature receivers, instead of mammoth XML site definition files.

  • Line #28 creates the task list for the workflow, and stores a reference to it.
  • Lines #31 and #32 are the meat and potatoes for programmatically creating a workflow.
  • Lines #34 - #39 configure the workflow. Notice that some of the properties of the association can be set "normally" while others are set via (shitty) string manipulation of the association metadata. This is the first taste of how we will be setting a dynamic approver. The association metadata should be manipulated when you want to customize default settings per instance of a workflow; the Boolean properties are for the workflow's behavior as a whole.
  • Lines #42 and #43 implement the wiring of the event receivers for the actual request list, as well as the workflow task list. Notice in Line #35 we are not auto-starting any workflow instances, because this does not give us the opportunity to customize our initialization data. Instead, we use an event receiver on the list to read in the list data and programmatically kick off a workflow. In Line #43 , I hook the same ItemAdded event on the workflow task list, so, pending the outcome, I can react to approvals or rejections.

Next, let's look at the code that kicks off the workflow:

  1. public override void ItemAdded(SPItemEventProperties properties)
  2. {
  3. //impersonation
  4. SPSecurity.RunWithElevatedPrivileges(() =>
  5. {
  6. try
  7. {
  8. //initialization
  9. base.ItemAdded(properties);
  10. //open site
  11. using (SPSite site = new SPSite(properties.SiteId))
  12. {
  13. //open web
  14. site.AllowUnsafeUpdates = true;
  15. using (SPWeb web = site.RootWeb)
  16. {
  17. //get list
  18. SPListItem item = properties.ListItem;
  19. SPList list = web.Lists[properties.ListId];
  20. SPWorkflowAssociation wfAssociation = list.WorkflowAssociations[0];
  21. //set approver
  22. SPUser user = properties.ListItem["Approver"] as SPUser;
  23. string data = wfAssociation.AssociationData.Replace("<my:Reviewers>", string.Format("<my:Reviewers><my:Person><my:DisplayName>{0}</my:DisplayName><my:AccountId>{1}</my:AccountId><my:AccountType>User</my:AccountType></my:Person>", user.Name, user.LoginName));
  24. //set description
  25. data = data.Replace("<my:Description></my:Description>", string.Format("<my:Description>{0}</my:Description>", item.Title));
  26. //start workflow
  27. site.WorkflowManager.StartWorkflow(item, wfAssociation, data);
  28. }
  29. }
  30. }
  31. catch (Exception ex)
  32. {
  33. //log error
  34. EventLog.WriteEntry("Request Workflow List Event Receiver - Item Added", ex.ToString(), EventLogEntryType.Error);