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.
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:
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.
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.