>
Blog
Book
Portfolio
Search

12/30/2009

12423 Views // 0 Comments // Not Rated

Extending SharePoint Approval Workflows Using Custom Initialization And Association Data

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:

Code Listing 1

  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=1.0.0.0, Culture=neutral, PublicKeyToken=97c72abbf6e1c840", "Request.EventHandlers");
  43. tasks.EventReceivers.Add(SPEventReceiverType.ItemUpdated, "Request, Version=1.0.0.0, 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:

Code Listing 2

  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);
  35. }
  36. });
  37. }

Everything boils down to Line #27 that invokes the StartWorkflow method on the WorkflowManager class, which hangs off of an SPSite object. This method takes in three parameters:

  • The SPListItem on which the workflow operates.
  • The SPWorkflowAssociation , which associates a workflow with a list.
  • A string representing the initialization data for this instance. In the Visual Studio Intellisense, the name of this parameter is "eventData" which is very misleading. Why isn't it named something more intuitive to imply that it represents the initialization data for this instance? Something like, say, "initializationData?"

It is this third parameter that inspired this post. Where the hell does the initialization data come from? If you Bing around, you'll see that the idea is to pass in XML (most desirably a serialized object) to define the metadata of the instance of the workflow. Now this probably isn't a big deal for a custom built workflow, but how do you anticipate what the initialization data will be for an OOTB workflow?

Turns out, it's the same XML as the association data! This can easily be inspected in debug mode by checking out the value of the "association" variable in the first listing above, after Line #33. Then, down to the second listing, all I do to set an approver is grab the SPUser object from the list item data in Line #22, and use its properties to fill in the XML of the initialization data string in Line #25.

Then simply pass your modified XML, along with the list item and the association object, to the StartWorkflow method, and you're off!

The last step here is to figure out how to get code to run when the workflow instance is approved. My approach was to use another event receiver, but this time on the workflow task list itself. Hooking the ItemUpdated event and checking the task item's "Outcome" column for a value that starts with "Approved" will get us there. Here's the code:

Code Listing 3

  1. public override void ItemUpdated(SPItemEventProperties properties)
  2. {
  3. //impersonation
  4. SPSecurity.RunWithElevatedPrivileges(() =>
  5. {
  6. try
  7. {
  8. //initialization
  9. base.ItemUpdated(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 the workflow item
  18. SPListItem item = properties.ListItem;
  19. SPList list = web.Lists[new Guid(item["WorkflowListId"].ToString())];
  20. SPListItem wfItem = list.GetItemById(Convert.ToInt32(item["WorkflowItemId"].ToString()));
  21. //make sure the item was approved
  22. if (item["Outcome"].ToString().StartsWith("Approved"))
  23. {
  24. //do stuff here
  25. }
  26. }
  27. }
  28. }
  29. catch (Exception ex)
  30. {
  31. //log error
  32. EventLog.WriteEntry("Request Workflow Task List Event Receiver - Item Updated", ex.ToString(), EventLogEntryType.Error);
  33. }
  34. });
  35. }

  • Lines #19 and #20 give us a reference to the list item that the workflow instance just approved (verses the task list item in the workflow task list).
  • Line #22 is, of course, a bit of a kludge, but safe for the Approval workflow. Don't use the "Status" column, as that is the status of the task itself, not the workflow. Hence the kludge.

That's it! As you can see, you can push OOTB workflows pretty far beyond the customizations available on the association page during configuration. But don't go too nuts extending these workflows; depending on your requirements, it might just take less time to build it from scratch!

Have fun!

2 Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.


Loading...