Getting Started with the Workflow Object Model in SharePoint Foundation 2010

Summary:  Get started with coding against the workflow object model in Microsoft SharePoint Foundation 2010 by developing console applications that manage farm workflows.

Applies to: Business Connectivity Services | Open XML | SharePoint Designer 2010 | SharePoint Foundation 2010 | SharePoint Online | SharePoint Server 2010 | Visual Studio

Provided by:  Tyler Sanderson, Microsoft Corporation

Contents

  • Getting Acquainted with the Workflow Object Model

  • Common Workflow Classes in SharePoint Foundation

  • Conclusion

  • Additional Resources

Getting Acquainted with the Workflow Object Model

You are familiar with workflow and other common concepts in Microsoft SharePoint 2010, but you want to write code that harnesses the power of the workflow object model in Microsoft SharePoint Foundation 2010. This article focuses on patterns that you can use to develop stand-alone console applications for managing workflows of a farm, rather than on code that is part of a custom workflow or workflow activity, although the two areas do overlap. A brief overview of the workflow object model in SharePoint Foundation 2010 leads into specific examples that illustrate common administrative tasks in the SharePoint workflow paradigm.

Common Workflow Classes in SharePoint Foundation

Table 1 summarizes the commonly used workflow classes. For a comprehensive and authoritative reference, see the Microsoft.SharePoint.Workflow namespace.

Table 1. Common workflow classes

Class

Description

SPWorkflow

Represents an instance of a workflow association that is either currently running or has been run on a site.

This class contains information about the state of the running workflow, and pointers to related objects.

SPWorkflowAssociation

Represents the association of a workflow template (that is, a definition) with a specific list, content type, or site.

After a template is associated with a list, content type, or site, your code can start instances of the template.

SPWorkflowAssociationCollection

Represents a collection of workflow associations that have been added to a list (either directly or via content type) or a site.

SPWorkflowCollection

Represents a collection of workflow instances.

SPWorkflowFilter

Represents filter criteria that can be applied to a workflow or workflow task collection, to limit the number of items available in the collection.

SPWorkflowManager

A static class that provides much of the functionality needed to administer workflows on sites and site collections.

SPWorkflowTask

Represents a single workflow task for a given instance of a workflow.

SPWorkflowTaskCollection

Represents a collection of the workflow tasks for a workflow instance.

SPWorkflowTaskProperties

Represents the properties of a workflow task.

SPWorkflowTemplate

Represents a single workflow template (that is, a definition) that has been deployed on the SharePoint site.

A template must be associated with a list, content type, or site before your code can start instances of the workflow.

SPWorkflowTemplateCollection

Represents the collection of workflow templates that are currently deployed on a site.

Tip

On the Microsoft SharePoint team blog, Eilene Hao posted Introduction to SharePoint Workflow, a helpful overview of SharePoint workflow as it existed in Windows SharePoint Services 3.0 and Microsoft Office SharePoint Server 2007.

Managing Non-Workflow Support Objects in SharePoint Foundation

In most cases, creating solutions to administer workflows requires working with related SharePoint objects. This section provides examples and tips on how to avoid common pitfalls when you use these objects in common design patterns.

Working with SharePoint List Items

As with any development in SharePoint, you use certain programming patterns throughout a project. Because workflows work with the basic components of SharePoint, it is worth a brief overview of how to work with the list items to which you intend to attach workflows.

Creating List Items

The following is a simple example of creating a new Announcements list item. The key point of interest is the use of the Factory design pattern in SharePoint. Most objects don't have public constructors (SPContentType is an exception); they are created by calling the Add method of the parent object.

using (SPSite site = new SPSite("http://locahost"))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPList list = web.Lists["Announcements"];
        SPListItem item = list.AddItem();
        item.Title = "New Announcement";
        item.Update();
    }
}

Accessing List Item Fields

In this example, the priority field is updated for the task list item whose ID = 1. Notice that the generic SPListItem class doesn't have properties for each list item field. You must use the correct case in the field name—and use the internal name of the field, not the display name. If the field name contains a discrepancy, attempting to access the field throws the exception "One or more field types are not installed properly."

using (SPSite site = new SPSite("http://locahost"))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPList list = web.Lists["Tasks"];
        SPListItem item = list.GetItemById(1);
        item["Priority"] = 1;
        item.Update();
    }
}

Querying Specific List Items by Using Non-Unique Fields

This example manually constructs a CAML query and uses it with SPQuery to retrieve the first list item whose title contains the search string. A collection of list items is returned from the call to GetItems; the code captures the first item in the collection. A common pattern in workflow console applications is to use SPWorkflowManager to start a workflow on items in the list item collection that were returned by the query.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPList list = web.Lists["Announcements"];
        SPListItem item = null;
        SPQuery query = new SPQuery();
        query.Query = "<Where><Contains><FieldRef Name='Title'/>
          <Value  Type='Text'>Getting started with Microsoft</Value>
          </Contains></Where>";
        query.RowLimit = 1;

        SPListItemCollection items = list.GetItems(query);
        if (items.Count > 0)
        {
            item = items[0];
        }
    }
}

Warning

Problems with the query format can cause all items in the list to be returned. When copying this example, be sure to replace the quote characters with ASCII(39).

Keeping SharePoint Objects in Sync

The SharePoint object model has the benefit that it abstracts database communication from layers that build on it. As a result, though, determining how to keep SharePoint objects synchronized with the content database in a performant manner can be tricky.

The following are some general "rules of thumb":

  • The Update method of an object pushes local changes to the content database, but does not pull any changes made by other applications.

  • New items are not created in the content database until Update is called.

  • To pull changes from the content database, an object must be reacquired from its parent.

  • Trying to maintain multiple SPWeb instances in different user contexts is not a good idea.

Pushing a New Workflow Association to the Content Database

In this example, the last line—announcements.Update()—is the call that actually creates the new workflow association in the content database.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPList announcements = web.Lists["Announcements"];
        SPList tasks = web.Lists["Tasks"];
        SPList history = web.Lists["Workflow History"];
        SPWorkflowTemplate template =   
          web.WorkflowTemplates.GetTemplateByName("Approval", web.Locale);
        SPWorkflowAssociation association = 
          SPWorkflowAssociation.CreateListAssociation(template, 
          "Announcements Approval", tasks, history);
        announcements.WorkflowAssociations.Add(association);
        announcements.Update();
    }
}

Refreshing a Workflow Association

In this example, the while loop reacquires the association by using the GetAssociationByName method. This call pulls the latest version from the content database, so that a meaningful comparison can be made on association.RunningInstances.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPList announcements = web.Lists["Announcements"];

        // Wait until the association has one or more running instances.
        bool isRunning = false;
        while (!isRunning)
        {
            SPWorkflowAssociation association = 
                announcements.WorkflowAssociations.GetAssociationByName(
                "Announcements Approval", web.Locale);
            isRunning = (association.RunningInstances > 0);
            System.Threading.Thread.Sleep(100);
        }
    }
}

Starting an On-Edit Triggered Workflow as Two Different Users

This example first creates an admin web to get the user tokens, and then creates a site and web in the context of each user. List-item updates occur in the context of that user, and thus also start any on-edit trigger workflows as the same user. Notice that the site and web of the first user is disposed before creating the site and web of the second user. Following this pattern avoids the complications of keeping the two different webs synchronized.

using (SPSite adminSite = new SPSite("https://server"))
{
    using (SPWeb adminWeb = adminSite.OpenWeb())
    {
        SPUser user1 = adminWeb.Users["domain\\user1"];
        SPUser user1 = adminWeb.Users["domain\\user2"];

        // Update list item to start on-edit workflow (as user1).
        using (SPSite user1Site = new SPSite("https://server", user1.UserToken))
        {
            using (SPWeb user1Web = user1Site.OpenWeb())
            {
                SPList list = user1Web.Lists["Announcements"];
                SPListItem item = list.GetItemById(1);
                item.Title += " [updated by user1]";
                item.Update();
            }
        }

        // Update list item to start on-edit workflow (as user2).
        using (SPSite user2Site = new SPSite("https://server", user2.UserToken))
        {
            using (SPWeb user2Web = user2Site.OpenWeb())
            {
                SPList list = user2Web.Lists["Announcements"];
                SPListItem item = list.GetItemById(2);
                item.Title += " [updated by user2]";
                item.Update();
            }
        }
    }
}

Managing Workflows in SharePoint Foundation

The following sections examine management of the common workflow objects: workflow tasks, associations, collections, and instances.

Working with SharePoint Workflow Tasks

Tasks are a key component of SharePoint workflows. This section covers a few examples of how to interact with workflow tasks from an administrative application.

Starting an Approval Workflow and Verifying Task Creation

This example starts a new instance of an already associated SharePoint 2010 Approval workflow by using the StartWorkflow method on SPWorkflowManager. The SPWorkflowTask object is then retrieved, and the title is verified.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        // Start a new workflow instance.
        SPList list = web.Lists["Announcements"];
        SPListItem item = list.GetItemById(1);
        SPWorkflowAssociation assoc = 
          list.WorkflowAssociations.GetAssociationByName("Approval");
        site.WorkflowManager.StartWorkflow(item, assoc, String.Empty);

        // Basic delay to accommodate workflow spin-up and task creation.
        System.Threading.Thread.Sleep(1000);

        // Verify the title of the task.
        item.EnsureWorkflowInformation();
        SPWorkflowTask task = item.Tasks[0];
        String title = task["Title"];
        if (title == null || !title.StartsWith("Please approve"))
        {
            throw new System.FormatException(
              "Unexpected task title: This task was not assigned " +
              "by the SharePoint 2010 Approval workflow.");
        }
    }
}

Adding Extended Task Properties

This example starts an already associated approval workflow, waits for the workflow to assign a task, adds a property to the extended properties collection of the task, and then verifies the existence of the property. Normally extended properties are added to a full-trust workflow template via the workflow.xml file, but in this case the property is added programmatically, for illustration purposes. The key point of interest is that the property must be defined with the "ows_" prefix, although the call to GetExtendedPropertiesAsHashtable strips out the prefix.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    { 
        // Start a new workflow instance.
        SPList list = web.Lists["Announcements"];
        SPListItem item = list.GetItemById(1);
        SPWorkflowAssociation assoc = 
          list.WorkflowAssociations.GetAssociationByName("Approval");
        site.WorkflowManager.StartWorkflow(item, assoc, String.Empty);

        // Basic delay to accommodate workflow spin-up and task creation.
        System.Threading.Thread.Sleep(5000);

        item.EnsureWorkflowInformation();
        SPWorkflowTask task = item.Tasks[0];

        // Add an extended task property.
        task[SPBuiltInFieldId.ExtendedProperties] += "ows_Color='Blue'";

        // Verify extended property creation.
        Hashtable properties = 
          SPWorkflowTask.GetExtendedPropertiesAsHashtable(task);
        if (properties["Color"] != "Blue")
        {
            throw new Exception("The extended property 'Color' " +
              "was not set to the expected value 'Blue'.");
        }
    }
}

Working with SharePoint Workflow Collections

This section provides some examples of how to manage workflow collections, including some pitfalls that workflow developers might encounter.

Retrieving Workflow Templates from a Template Collection

In this example, the template ID uniquely identifies the template in the workflow template collection of the site collection. This is the most robust way to retrieve a template, but you must first know the template ID of the workflow that you need. One way to find this ID is to browse for workflow.xml files (full-trust workflows) in your SharePoint features folder (%ProgramFiles%\Common Files\Microsoft Shared\web server extensions\14\TEMPLATE\FEATURES\workflow_feature_name), or the contents of the corresponding workflow.config.xml file (SharePoint Designer workflows).

When searching your features folder for the IDs of the workflows that are included with SharePoint, be aware that the workflow template names don't always match the feature folder names. (For example, on a server that is set up for the en-us locale ID, the 2010 Approval workflow resides in a feature folder named "ReviewWorkflowsSPD1033".)

You can also retrieve workflow templates by template name, using the SPWorkflowTemplateCollection.GetTemplateByName method.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPWorkflowTemplate template = 
          web.WorkflowTemplates.GetTemplateByBaseID(new 
          Guid("3BC0C1E1-B7D5-4e82-AFD7-9F7E59B60409"));
        if (template == null || !template.Name.Contains("Approval"))
        {
            throw new ArgumentException("Expected to find the Approval workflow " +
              "template. Please verify the template ID and server type.");
        }
    }
}

Handling Workflow Collection Errors

This example presents two ways to retrieve a workflow template: by template ID and by index. The key point to notice is that failed attempts to match a template by template ID return null; failed attempts to access a template by index throw an IndexOutOfRangeException exception. If you want error handling for the first case (template ID), you must add it manually.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        // Retrieve a workflow template by GUID (custom error handling).
        SPWorkflowTemplate template = 
          web.WorkflowTemplates[new Guid("00000000-0000-0000-0000-000000000000")];
        if (template == null)
        {
            throw new ArgumentException("The specified template could not be found.");
        }

        // Retrieve a workflow template by GUID (built-in error handling).
        template = web.WorkflowTemplates[995];
    }
}

Bad Coding Practice: How Local Collection Changes Affect Data Synchronization

This example attempts to track a collection of workflow associations that need to be updated. The collection begins with the associations on the Links list, but it might branch at some point in the future. The interesting point is that the data for the second collection is fetched on first request—in this case, when the Count property is accessed in the first call to Console.WriteLine. At this point the second collection is marked as no longer dirty, and the data for that collection won't be fetched again, for performance reasons. When the second workflow association is added, the workflowsToUpdate collection doesn't pick up the update, so the second Console.WriteLine call also outputs "1".

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPWorkflowAssociationCollection workflowsToUpdate = null;

        // Set up lists and template.
        SPList links = web.Lists["Links"];
        SPList tasks = web.Lists["Tasks"];
        SPList history = web.Lists["Workflow History"];
        SPWorkflowTemplate template = web.WorkflowTemplates[0];

        // Initialize collection.
        workflowsToUpdate = links.WorkflowAssociations;

        // Add to association collection.
        SPWorkflowAssociation workflow1 = 
          SPWorkflowAssociation.CreateListAssociation(template, 
          "Workflow 1", tasks, history);
        links.WorkflowAssociations.Add(workflow1);
        Console.WriteLine(String.Format(
          "Workflows associations in workflowsToUpdate: {0}"), 
          workflowsToUpdate.Count);

        // Add another to association collection.
        SPWorkflowAssociation workflow2 = 
        SPWorkflowAssociation.CreateListAssociation(template, 
          "Workflow 2", tasks, history);
        links.WorkflowAssociations.Add(workflow2);
        Console.WriteLine(String.Format(
          "Workflows associations in workflowsToUpdate: {0}"), 
          workflowsToUpdate.Count);
    }
}

The way to make this scenario work is to not maintain local copies of collections, or to store the IDs of interest in a separate construction, such as an ArrayList.

Starting New Workflow Instances

This section provides examples of various ways to start new instances of workflows by using SPWorkflowManager.

Starting New Workflow Instances by Using the Various Start Type Options

This example creates an association of the already deployed Basic Workflow template on the Links list. The Synchronous option (from the SPWorkflowRunOptions enumeration) starts the workflow immediately on list item 1, bypassing the workflow throttling settings. The SynchronousAllowPostpone option attempts to start the workflow synchronously; if it fails, it queues the workflow to be started later, exactly the same way that on-edit and on-create workflow associations are triggered today. The Asynchronous option queues the workflow to be started by the workflow timer job at the next timer interval. Refer to StartWorkflow for a comprehensive description of the various workflow start options.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        // Create association.
        SPList links = web.Lists["Links"];
        SPList tasks = web.Lists["Tasks"];
        SPList history = web.Lists["Workflow History"];
        SPWorkflowTemplate template = 
          web.WorkflowTemplates.GetTemplateByName("Basic Workflow");
        SPWorkflowAssociation workflow = 
          SPWorkflowAssociation.CreateListAssociation(template, 
          "My Basic Workflow", tasks, history);
        links.WorkflowAssociations.Add(workflow);

        // Start the workflow on document 1.
        site.WorkflowManager.StartWorkflow(links.GetItemById(1), workflow, 
          String.Empty, SPWorkflowRunOptions.Synchronous);

        // Start the workflow on list item 2 by using the 
        // SynchronousAllowPostpone start option.
        site.WorkflowManager.StartWorkflow(links.GetItemById(2), workflow, 
          String.Empty, SPWorkflowRunOptions.SynchronousAllowPostpone);

        // Start the workflow on list item 3 by using the 
        // Asynchronous start option.
        site.WorkflowManager.StartWorkflow(links.GetItemById(3), workflow, 
          String.Empty, SPWorkflowRunOptions.Asynchronous);
    } 
}

Starting the SharePoint 2010 Approval Workflow with Initiation Data

This example creates an association of the SharePoint 2010 approval workflow, and then starts it on the first item in the Shared Documents library. Because the association is created programmatically and without association data, we need to construct the workflow settings XML string and pass it to StartWorkflow as event data. The string gives the workflow the information it needs to assign approval tasks.

using (SPSite site = new SPSite("https://server"))
{
    using (SPWeb web = site.OpenWeb())
    {
        // Create new association.
        SPList docs = web.Lists["Shared Documents"];
        SPList tasks = web.Lists["Tasks"];
        SPList history = web.Lists["Workflow History"];
        SPWorkflowTemplate template = 
          web.WorkflowTemplates.GetTemplateByBaseID(new 
          Guid("3BC0C1E1-B7D5-4e82-AFD7-9F7E59B60409"));
        SPWorkflowAssociation workflow = 
          SPWorkflowAssociation.CreateListAssociation(template, 
          "Document Approval", tasks, history);
        docs.WorkflowAssociations.Add(workflow);

        // Start the workflow on the first document.
        String data = String.Format("<dfs:myFields 
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" 
xmlns:dms=\"https://schemas.microsoft.com/office/2009/documentManagement/types\" 
xmlns:dfs=\"https://schemas.microsoft.com/office/infopath/2003/dataFormSolution\" 
xmlns:q=\"https://schemas.microsoft.com/office/infopath/2009/WSSList/queryFields\" 
xmlns:d=\"https://schemas.microsoft.com/office/infopath/2009/WSSList/dataFields\" 
xmlns:ma=\"https://schemas.microsoft.com/office/2009/metadata/properties/metaAttributes\" 
xmlns:pc=\"https://schemas.microsoft.com/office/infopath/2007/PartnerControls\" 
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">
<dfs:queryFields></dfs:queryFields><dfs:dataFields>
<d:SharePointListItem_RW><d:Approvers><d:Assignment><d:Assignee>
<pc:Person><pc:DisplayName>{0}</pc:DisplayName><pc:AccountId>{1}</pc:AccountId>
<pc:AccountType>User</pc:AccountType></pc:Person></d:Assignee>
<d:Stage xsi:nil=\"true\" /><d:AssignmentType>Serial</d:AssignmentType>
</d:Assignment></d:Approvers><d:ExpandGroups>true</d:ExpandGroups>
<d:NotificationMessage /><d:DueDateforAllTasks xsi:nil=\"true\" />
<d:DurationforSerialTasks xsi:nil=\"true\" /><d:DurationUnits>Day</d:DurationUnits>
<d:CC /><d:CancelonRejection>false</d:CancelonRejection>
<d:CancelonChange>false</d:CancelonChange>
<d:EnableContentApproval>false</d:EnableContentApproval>
</d:SharePointListItem_RW></dfs:dataFields></dfs:myFields>", 
          "User Display Name", "DOMAIN\\User");
        site.WorkflowManager.StartWorkflow(docs.GetItemById(1), docs, data);
    }
}

Conclusion

Workflow development can become complex as it adapts to various business processes, but the samples and direction provided in this article give a beginning workflow developer most of the basic components needed for building workflows.

Additional Resources

For more information, see the following resources: