Scripting the setup of a backlog of tasks in SharePoint Online

29 Apr 2015

The first part of this post describes the backlog format I typically use with clients and its rationale. On SharePoint projects, I tend to implement the backlog as a SharePoint list. If for no other reason, storing it in SharePoint may help drive adoption of both the backlog and SharePoint. The second part of this post outlines automating backlog setup in a new environment. I use the backlog across multiple clients and prefer scripting its setup using SharePoint's client side object model.

Motivation

In many cases, clients don't have a process and tooling in place for tracking a backlog of tasks. The default tool is mail, which is anything but transparent and structured. In today's world of free tools, a SharePoint list (or spreadsheet for that matter) may seem overly simplistic. But when all you need is a simple way to track tasks and communicate their state, I find a SharePoint list to be sufficient. The goal isn't to adhere to a formal process, such as Scrum, Kanban, or ITIL, but to minimizing lead time to business impact by striving for predictability. The bells and whistles and knobs and dials of specialized tools can easily cause one to lose sight of this goal.

Here's a screenshot of what an item in my backlog looks like:

A couple of remarks on the design and intended use of the backlog:

  • It's based on the build-in GenericList template rather than the build-in IssueTracking one. I prefer extending the basic list over trying to figure out how the advanced one is configured and how to remove unused functionality.
  • I strive for as few additions to the GenericList as possible. Just enough fields and views to satisfy the basic needs of a backlog. I don't want to leave anyone with the impression that it's a fully-fledged task tracker. Thus, for cases not fitting the basic format, I default to less-structured append-only Comments.
  • For Description and Acceptance criteria, I like to follow the Behavior-driven development (BDD)-style and elaborate with regular prose. Following BDD, perhaps in concert with the Five Whys technique, I find there's a higher propability of asking the right questions upfront to help balance business and technical aspects.
  • Business priority is an integer intended for business responsibles to prioritize tasks within their Area relative to each other. Thus, overall task priority may be determined by combining Business priority with Area.
  • Only I may add new tasks to the backlog to prevent it from evolving into an unmanageable to-do list with unqualified items added in the spur of the moment. In practice, I write task proposals based on mail or verbal dialog with business responsibles and ask for confirmation before going ahead. A business responsible should only have to modify Business priority, Status, and Comments.

Scripting the backlog

Relying on a point and click approach to backlog setup makes remembering the steps involved hard, even with access to an existing backlog. One alternative would be saving the original backlog as a template and importing in into a new environment. I dislike this approach because steps is equally opaque. Another alternative would be to document the setup with screenshots and prose, but that's a duplicate effort, usually resulting in stale documentation. Instead, I prefer scripting. It makes setup transparent and makes for easier local adjustments.

Let's start scripting by creating a container for the main scripting logic to go into. This script relies on the SharePoint client side object model and is intended for SharePoint Online, but can be adapted for on-prem by replacing the SetupContext logic.

// add references to:
//   C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll
//   C:\Program Files\Common Files\microsoft shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll

using Microsoft.SharePoint.Client;
using System;
using System.Linq;
using System.Security;
using System.Text;
using System.Xml.Linq;
using E = System.Xml.Linq.XElement;
using A = System.Xml.Linq.XAttribute;

namespace BugfreeConsultingBacklog {
    class Program {
        static ClientContext SetupContext(Uri siteCollection) {
            var user = "ronnieholm@bugfree.onmicrosoft.com";
            var password = "password";
            var securePassword = new SecureString();
            password.ToCharArray().ToList().ForEach(securePassword.AppendChar);
            var credentials = new SharePointOnlineCredentials(user, securePassword);
            return new ClientContext(siteCollection) { Credentials = credentials };
        }

        static XElement CreateNoteField(string internalName, string displayName, bool appendOnly = false) {
            return
                new E("Field",
                    new A("Type", "Note"),
                    new A("DisplayName", displayName),
                    new A("Required", "FALSE"),
                    new A("EnforceUniqueValues", "FALSE"),
                    new A("Indexed", "FALSE"),
                    new A("NumLines", "6"),
                    new A("RichText", "FALSE"),
                    new A("Sortable", "FALSE"),
                    new A("ID", Guid.NewGuid()),
                    new A("StaticName", internalName),
                    new A("Name", internalName),
                    new A("AppendOnly", appendOnly));
        }

        static XElement CreateTextField(string internalName, string displayName) {
            return
                new E("Field",
                    new A("Type", "Text"),
                    new A("DisplayName", displayName),
                    new A("Description", ""),
                    new A("Required", "FALSE"),
                    new A("EnforceUniqueValues", "FALSE"),
                    new A("Indexed", "FALSE"),
                    new A("MaxLength", "255"),
                    new A("ID", Guid.NewGuid()),
                    new A("StaticName", internalName),
                    new A("Name", internalName));
        }

        static XElement CreateNumberField(string internalName, string displayName) {
            return
                new E("Field",
                    new A("Type", "Number"),
                    new A("DisplayName", displayName),
                    new A("Required", "FALSE"),
                    new A("EnforceUniqueValues", "FALSE"),
                    new A("Indexed", "FALSE"),
                    new A("ID", Guid.NewGuid()),
                    new A("StaticName", internalName),
                    new A("Name", internalName));
        }

        static XElement CreateChoiceField(string internalName, string displayName, string[] choices, string defaultChoice) {
            var choicesElement = new E("CHOICES");
            choices.ToList().ForEach(c => choicesElement.Add(new E("CHOICE", c)));

            return 
                new E("Field",
                    new A("Type", "Choice"),
                    new A("DisplayName", displayName),
                    new A("Description", ""),
                    new A("Required", "FALSE"),
                    new A("EnforceUniqueValues", "FALSE"),
                    new A("Indexed", "FALSE"),
                    new A("Format", "Dropdown"),
                    new A("FillInChoice", "FALSE"),
                    new A("ID", Guid.NewGuid()),
                    new A("StaticName", internalName),
                    new A("Name", internalName),
                    new E("Default", defaultChoice),
                    choicesElement);
        }

        static XElement CreateUserField(string internalName, string displayName) {
            return
                new E("Field",
                    new A("Type", "User"),
                    new A("DisplayName", displayName),
                    new A("Description", ""),
                    new A("List", "UserInfo"),
                    new A("Required", "FALSE"),
                    new A("EnforceUniqueValues", "FALSE"),
                    new A("ShowField", "ImnName"),
                    new A("UserSelectionMode", "PeopleOnly"),
                    new A("UserSelectionScope", "0"),
                    new A("ID", Guid.NewGuid()),
                    new A("StaticName", internalName),
                    new A("Name", internalName));
        }

        static void Main(string[] args) {
            using (var ctx = SetupContext(new Uri("https://bugfree.sharepoint.com/sites/migration"))) {
                // code below goes here.
            }
        }
    }
}

While it may seem like a lot of code, it's mostly authentication logic and XML generation. The non-parameterized field XML was extracted from SharePoint by first configuring the field through the browser and then using SharePoint 2013 Client Browser to browse the list definition.

With the container in place, we can move on to the crux of the script:

var backlog = ctx.Web.Lists.Add(new ListCreationInformation {
    Title = "BugfreeConsultingBacklog",
    TemplateType = (int)ListTemplateType.GenericList
});

ctx.Load(backlog, l => l.Fields, l => l.DefaultView);
backlog.Title = "Bugfree Consulting Backlog";
backlog.EnableVersioning = true;
backlog.Update();
ctx.ExecuteQuery();

new[] {
    CreateNoteField("Description", "Description"),
    CreateNoteField("AcceptanceCriteria", "Acceptance criteria"),
    CreateNumberField("BusinessPriority", "Business priority"),
    CreateChoiceField("Classification", "Classification", new[] { "Change", "Defect" }, ""),
    CreateTextField("Area", "Area"),
    CreateUserField("AssignedTo", "Assigned to"),
    CreateUserField("BusinessContact", "Business contact"),
    CreateChoiceField(
        "Status", 
        "Status", 
        new[] { 
            "Received", 
            "Needs clarification", 
            "Ready for development", 
            "Under development", 
            "Development completed", 
            "Blocked", 
            "Ready for test", 
            "Test succeeded", 
            "Test failed", 
            "Completed", 
            "Closed" 
        },
        "Received"),
    CreateNumberField("OriginalEstimate", "Original estimate"),
    CreateNumberField("RemainingEstimate", "Remaining estimate"),
    CreateNoteField("Comments", "Comments", true),
}
.ToList()
.ForEach(schema => backlog.Fields.AddFieldAsXml(schema.ToString(), false, AddFieldOptions.DefaultValue));
ctx.ExecuteQuery();

var comments = ctx.CastTo(backlog.Fields.GetByTitle("Comments"));
ctx.Load(comments);
ctx.ExecuteQuery();
comments.AppendOnly = true;
comments.Update();
                
backlog.DefaultView.DeleteObject();
ctx.ExecuteQuery();

var newView = backlog.Views.Add(new ViewCreationInformation {
    Title = "AllItems",
    ViewFields = new[] { 
        "ID", "LinkTitle", "Area", "AssignedTo", "Classification", 
        "BusinessPriority", "Status", "OriginalEstimate", "RemainingEstimate"
    },
    SetAsDefaultView = true,
    Paged = true
});
newView.Title = "All Items";
newView.Update();
ctx.ExecuteQuery();

Notice how the script creates readable URLs for both the list and view as these tend to go into mails when communicating with business responsibles, e.g., including a link to a particular tasks.

Conclusion

As developers we should use the tools and notations we're most accustomed to to create an executable specification for setting up the backlog across different SharePoint environments -- in essence the client side object model provides an internal domain specific language for defining SharePoint artefacts. We should rarely have to fall back on opaque template definitions or ambiguous documentation outlining the setup. It would be akin to a SQL server administration not using SQL scripts to manipulate a database.