Friday, 8 January 2010

Add and manage workflows using the API

Programmatically applying and configuring an out-of-the-box workflow to a SharePoint list isn’t the most obvious thing to do but fortunately it’s not exactly hard either. I had to dig around a bit to find the pieces I need to do this and thought I’d share…

List Settings - Versioning

In my case I’m applying an approval workflow to a Pages library created by the Publishing feature; before I wrangle the workflow I need to configure the versioning settings on my Pages list. Through the UI, I can do this by configuring these settings as follows:Configure_List_Versioning_for_WorkflowEssentially, content approval is required, users need to be able to create major and minor (draft) versions, all editors should be able to see all drafts, and checkout is required to edit documents.

To do this setup programmatically, I set the following properties on my Pages list:

SPList pages = web.Lists ["Pages"];

// Require content approval
pages.EnableModeration = true;
// Create major and minor versions
pages.EnableVersioning = true;
pages.EnableMinorVersions = true;
// Who should see draft items
pages.DraftVersionVisibility = DraftVisibilityType.Author;
// Require documents to be checked out before they can be edited
pages.ForceCheckout = true;

pages.Update ();

Remove Existing Workflows

In some cases, you may also want to cleanup a list’s existing workflows (i.e. remove them) and this is easily accomplished using the SPList.RemoveWorkflowAssociation() method. Note several examples on the interwebs suggest doing this within a foreach loop but doing so will invalidate the enumerator after the first item is removed; use a for loop or a while loop instead:

// Remove existing workflows
for (int associationIndex = 0; associationIndex < pages.WorkflowAssociations.Count; associationIndex++)
{
    SPWorkflowAssociation workflowAssociation = pages.WorkflowAssociations [associationIndex];
    pages.RemoveWorkflowAssociation (workflowAssociation);
}

If you need to remove individual workflows, inspect the SPWorkflowAssociation.Name property.

Workflow Setup…

With that out of the way, let’s move on to the workflow configuration proper. In short, it’s all about the collection of SPWorkflowAssociation objects exposed through the WorkflowAssociations property of the SPList class. The AssociationData property exposed by the SPWorkflowAssociation class is also crucial.

Task and History List Management

When you setup a workflow through the UI, you’ll be prompted to specify names for the task and history lists (the latter of which will be hidden); when you’re doing this programmatically, you have to manage this part yourself. It would have been nice if the API did this automatically but it doesn’t. The CreateListAssociation method we’ll examine in a moment also requires a reference to each list so you need to a) determine if each list exists or create it and b) retrieve a reference to each list. To wrap all of this logic up together, here’s my helper method (based on this by Marco):

private void EnsureWorkflowLists (SPWeb web, out SPList taskList, out SPList historyList)
{
    const string WORKFLOW_TASK_LIST_NAME = "Workflow Tasks";
    const string WORKFLOW_HISTORY_LIST_NAME = "Workflow History";

    // Task list

    try
    {
        taskList = web.Lists [WORKFLOW_TASK_LIST_NAME];

        Console.WriteLine ("Tasks list found.");
    }
    catch (System.ArgumentException)
    {
        // List does not exist so create it
        Guid taskListId = web.Lists.Add (WORKFLOW_TASK_LIST_NAME, string.Empty, SPListTemplateType.Tasks);
        taskList = web.Lists [taskListId];

        Console.WriteLine ("Created task list for web.");
    }

    // History List

    try
    {
        historyList = web.Lists [WORKFLOW_HISTORY_LIST_NAME];
        Console.WriteLine ("History list found.");
    }
    catch (ArgumentException)
    {
        Guid historyListId = web.Lists.Add (WORKFLOW_HISTORY_LIST_NAME, string.Empty, SPListTemplateType.WorkflowHistory);
        historyList = web.Lists [historyListId];
        historyList.Hidden = true;
        historyList.Update ();

        Console.WriteLine ("Created history list for web.");
    }                        
}

Call EnsureWorkflowLists method like so:

SPList taskList;
SPList historyList;
EnsureWorkflowLists (web, out taskList, out historyList);

Workflow Template

Before you can finally create the new workflow association, you’ll also need an SPWorkflowTemplate. You can do this by calling the GetTemplateByName method hanging indirectly off the SPWeb object:

SPWorkflowTemplate approvalTemplate = web.WorkflowTemplates.GetTemplateByName ("Approval", System.Globalization.CultureInfo.CurrentCulture);

If you have any issues here, ensure the Routing Workflows feature (or the relevant feature for whatever OOB workflow you’re dealing with) is enabled at the site collection level (/_layouts/ManageFeatures.aspx?Scope=Site).

Create the Workflow Association

With the leg work behind us, actually applying a new workflow association to the list and configuring it is quite easy, with one exception I’ll cover in a moment. To begin, create a new workflow association using the SPWorkflowAssociation.CreateListAssociation method and supply the workflow template and list references created previously:

SPWorkflowAssociation approvalWorkFlowAssociation = SPWorkflowAssociation.CreateListAssociation (approvalTemplate, "Page Approval", taskList, historyList);

Note the name parameter “Page Approval” is arbitrary but will appear in the UI and notification emails; I therefore aim to make it user friendly and somewhat specific.

The created object can then be configured, in particular using the AllowManual, AutoStartChange, and AutoStartCreate properties.

As mentioned, there’s one last hurdle and that’s providing the XML string to the SPWorkflowAssociation.AssociationData property. As far as I’m aware, this is the only way to specify the nominated approvers group and a few other properties such as the ability to delegate tasks and whether a change to the list item should interrupt the current workflow. I believe the easiest way to create this string is to copy an example (below) or configure the workflow as desired in the UI and use SharePoint Manager to extract the XML from the workflow association (highlighted in the image below).

WorkflowAssociations_AssociationData_SPM

<my:myFields xml:lang="en-us" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:my="http://schemas.microsoft.com/office/infopath/2003/myXSD">
<my:Reviewers>
<my:Person>
<my:DisplayName>Approvers</my:DisplayName>
<my:AccountId>Approvers</my:AccountId>
<my:AccountType>SharePointGroup</my:AccountType>
</my:Person>
</my:Reviewers>
<my:CC></my:CC>
<my:DueDate xsi:nil="true"></my:DueDate>
<my:Description></my:Description>
<my:Title></my:Title>
<my:DefaultTaskType>1</my:DefaultTaskType>
<my:CreateTasksInSerial>false</my:CreateTasksInSerial>
<my:AllowDelegation>true</my:AllowDelegation>
<my:AllowChangeRequests>true</my:AllowChangeRequests>
<my:StopOnAnyReject>true</my:StopOnAnyReject>
<my:WantedTasks xsi:nil="true"></my:WantedTasks>
<my:SetMetadataOnSuccess>false</my:SetMetadataOnSuccess>
<my:MetadataSuccessField></my:MetadataSuccessField>
<my:MetadataSuccessValue></my:MetadataSuccessValue>
<my:ApproveWhenComplete>true</my:ApproveWhenComplete>
<my:TimePerTaskVal xsi:nil="true"></my:TimePerTaskVal>
<my:TimePerTaskType xsi:nil="true"></my:TimePerTaskType>
<my:Voting>false</my:Voting>
<my:MetadataTriggerField></my:MetadataTriggerField>
<my:MetadataTriggerValue></my:MetadataTriggerValue>
<my:InitLock>true</my:InitLock>
<my:MetadataStop>false</my:MetadataStop>
<my:ItemChangeStop>true</my:ItemChangeStop>
<my:GroupTasks>true</my:GroupTasks>
</my:myFields>

Drop the XML into the SPWorkflowAssociation object you just created using the AssociationData property and you’re all set to add the association to your list using the SPList.AddWorkflowAssociation method:

pages.AddWorkflowAssociation (approvalWorkFlowAssociation);

Before updating the list list, you may also want to configure your new workflow as the default content approval workflow:

pages.DefaultContentApprovalWorkflowId = approvalWorkFlowAssociation.Id;

Follow that with pages.Update () and you’re done!

4 comments:

  1. Hi Michael

    You are a genius! I never found a so complete article about that topic.

    Many thanks,
    Roli

    ReplyDelete
  2. The only post I've read that covers this in this level of detail, frankly any detail- thank you very much

    ReplyDelete
  3. Great post! Thank you. This is good, too. Seems to be nearly the same ...

    http://social.msdn.microsoft.com/forums/en-US/sharepointworkflow/thread/6765e73a-a3bb-4f0a-9d9e-532bd40fd147

    ReplyDelete
  4. Great Post Micheal! I couldn't find better explanation on how to use the API for managing workflows. You saved my evening!! :)

    ReplyDelete

Spam comments will be deleted

Note: only a member of this blog may post a comment.