Sample Code: Refactoring to Handle Different Types of Work Items
When you write code that works with different team projects, your code may need to handle different types of work items that serve similar functions, but it different ways. For example, user stories in team projects that are based on MSF for Agile Software Development v5.0 are used to represent what the customer needs and values. In team projects that are based on MSF for CMMI Process Improvement v5.0, requirements serve that function. In team projects that are not based on one of the MSF process templates, a different type of work item may serve that function. Teams may also customize these work items. For example, user stories (as defined in MSF for Agile Software Development) are estimated in story points using the story points field. A team might customize it’s version of the user story work item so that it can use the baseline work to estimate the user story in hours. This topic provides a sample that works for a specific type of work item, and then refactors that sample to allow for certain types of customizations.
For more information about customizing types of work items, see Customizing Team Projects and Processes.
Print Trees of User Stories with Estimates in Story Points
This sample selects the trees of user stories in each team project on the server and prints them out, with estimates for the leaf nodes. To use this sample, create a console application, add references to the following assemblies, and then replace the contents of Program.cs with the following code.
- Microsoft.TeamFoundation.Client
- Microsoft.TeamFoundation.Common
- Microsoft.TeamFoundation.WorkItemTracking.Client
using System;
using System.Text;
using System.Collections.ObjectModel;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
namespace Microsoft.TeamFoundation.SDK
{
class Program
{
static void Main(string[] args)
{
try
{
// Connect to Team Foundation Server. The form of the url is https://server:port/vpath.
// server - the name of the server that is running the Team Foundation application-tier.
// port - the port that Team Foundation uses. The default is port is 8080.
// vpath - the virutal path to the Team Foundation application. The default path is tfs.
TfsConfigurationServer configurationServer =
TfsConfigurationServerFactory.GetConfigurationServer(new Uri("https://server:8080/tfs"));
// Get the catalog of team project collections
CatalogNode catalogNode = configurationServer.CatalogNode;
ReadOnlyCollection<CatalogNode> tpcNodes = catalogNode.QueryChildren(
new Guid[] { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None);
// Process each of the team project collections
foreach (CatalogNode tpcNode in tpcNodes)
{
// Use the InstanceId property to get the team project collection
Guid tpcId = new Guid(tpcNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection tpc = configurationServer.GetTeamProjectCollection(tpcId);
// Get the work item store
WorkItemStore wiStore = tpc.GetService<WorkItemStore>();
// Query for the trees of active user stories in the team project colleciton
StringBuilder queryString = new StringBuilder("SELECT [System.Id] FROM WorkItemLinks WHERE ");
queryString.Append("([Source].[System.WorkItemType] = 'User Story' AND [Source].[System.State] = 'Active') AND ");
queryString.Append("([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward') And ");
queryString.Append("([Target].[System.WorkItemType] = 'User Story' AND [Target].[System.State] = 'Active') ORDER BY [System.Id] mode(Recursive)");
Query wiQuery = new Query(wiStore, queryString.ToString());
WorkItemLinkInfo[] wiTrees = wiQuery.RunLinkQuery();
// Print the trees of user stories, with the estimated size of the leaves
PrintTrees(wiStore, wiTrees, " ", 0, 0);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
// Each WorkItemLinkInfo structure in the collection contains the IDs of the linked work items.
// In this case, the sourceId is the ID user story that is on the parent side of the link and
// the targetId is the ID of the user story that is on the child side of the link. The links
// are returned from in depth-first order. This function recursively traverses the collection
// and the title of each user story. If the user story has no children, it also prints its estimation.
static int PrintTrees(WorkItemStore wiStore, WorkItemLinkInfo[] wiTrees, string prefix, int sourceId, int iThis)
{
int iNext = 0;
// Get the parent of this user story, if there is one
WorkItem source = null;
if (sourceId != 0)
{
source = wiStore.GetWorkItem(wiTrees[iThis].SourceId);
}
// Process the items in the list that have the same parent as this user story
while (iThis < wiTrees.Length && wiTrees[iThis].SourceId == sourceId)
{
// Get this user story
WorkItem target = wiStore.GetWorkItem(wiTrees[iThis].TargetId);
Console.Write(prefix);
Console.Write(target.Type.Name);
Console.Write(": ");
Console.Write(target.Fields["Title"].Value);
if (iThis < wiTrees.Length - 1)
{
if (wiTrees[iThis].TargetId == wiTrees[iThis + 1].SourceId)
{
// The next item is this user story's child. Process the children
Console.WriteLine();
iNext = PrintTrees(wiStore, wiTrees, prefix + " ", wiTrees[iThis + 1].SourceId, iThis + 1);
}
else
{
// The next item is not this user story's child.
Console.Write("; estimate = ");
Console.WriteLine(target.Fields["Story Points"].Value);
iNext = iThis + 1;
}
}
else
{
// This is the last user story
iNext = iThis + 1;
}
iThis = iNext;
}
return iNext;
}
}
}
Print Trees of User Stories with Estimates in either Story Points or Baseline Work
This sample modifies the PrintTrees method to allow either the story points or baseline work field to be used for estimating user stories by checking to see which of the two fields is used by the work item. To use this sample, replace the line Console.WriteLine(target.Fields["Story Points"].Value); in the PrintTrees method with the following code.
// Determine which estimation field is present
string fieldName = "Story Points";
if (target.Type.FieldDefinitions.TryGetByName(fieldName) == null)
{
fieldName = "Baseline Work";
}
Console.WriteLine(target.Fields[fieldName].Value);
Print Trees of User Stories with Estimates in any Field
In many cases, you may not know exactly how the work items that your code deals with have been customized. In these cases, you need to provide some mechanism that allows the user of the code to describe how the work items in his team project interact with your code. For example, in the following sample, the fields used in each team project for estimation are specified in the file EstimateFields.xml.
static void Main(string[] args)
{
try
{
// Connect to Team Foundation Server. The form of the url is https://server:port/vpath.
// server - the name of the server that is running the Team Foundation application-tier.
// port - the port that Team Foundation uses. The default is port is 8080.
// vpath - the virutal path to the Team Foundation application. The default path is tfs.
TfsConfigurationServer configurationServer =
TfsConfigurationServerFactory.GetConfigurationServer(new Uri("https://server:8080/tfs"));
// Get the catalog of team project collections
CatalogNode catalogNode = configurationServer.CatalogNode;
ReadOnlyCollection<CatalogNode> tpcNodes = catalogNode.QueryChildren(
new Guid[] { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None);
// Load the estimate fields map
XmlDocument estimateFieldsDoc = new XmlDocument();
estimateFieldsDoc.Load("EstimateFields.xml");
// Process each of the team project collections
foreach (CatalogNode tpcNode in tpcNodes)
{
// Use the InstanceId property to get the team project collection
Guid tpcId = new Guid(tpcNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection tpc = configurationServer.GetTeamProjectCollection(tpcId);
// Get the work item store
WorkItemStore wiStore = tpc.GetService<WorkItemStore>();
foreach (Project project in wiStore.Projects)
{
Console.Write("Project: ");
Console.WriteLine(project.Name);
// Query for the trees of active user stories in the team project colleciton
StringBuilder queryString = new StringBuilder("SELECT [System.Id] FROM WorkItemLinks WHERE ");
queryString.Append("([Source].[System.WorkItemType] = 'User Story' AND ");
queryString.Append("[Source].[System.TeamProject] = '");
queryString.Append(project.Name);
queryString.Append("') AND ");
queryString.Append("([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward') And ");
queryString.Append("([Target].[System.WorkItemType] = 'User Story' AND ");
queryString.Append("[Target].[System.State] = 'Active') ORDER BY [System.Id] mode(Recursive)");
Query wiQuery = new Query(wiStore, queryString.ToString());
WorkItemLinkInfo[] wiTrees = wiQuery.RunLinkQuery();
// Print the trees of user stories, with the estimated size of the leaves
XmlElement ele = (XmlElement)estimateFieldsDoc.DocumentElement.SelectSingleNode(project.Name);
PrintTrees(wiStore, wiTrees, " ", 0, 0, ele.InnerText);
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
// Each WorkItemLinkInfo structure in the collection contains the IDs of the linked work items.
// In this case, the sourceId is the ID user story that is on the parent side of the link and
// the targetId is the ID of the user story that is on the child side of the link. The links
// are returned from in depth-first order. This function recursively traverses the collection
// and the title of each user story. If the user story has no children, it also prints its estimation.
static int PrintTrees(WorkItemStore wiStore, WorkItemLinkInfo[] wiTrees, string prefix, int sourceId, int iThis, string fieldName)
{
int iNext = 0;
// Get the parent of this user story, if there is one
WorkItem source = null;
if (sourceId != 0)
{
source = wiStore.GetWorkItem(wiTrees[iThis].SourceId);
}
// Process the items in the list that have the same parent as this user story
while (iThis < wiTrees.Length && wiTrees[iThis].SourceId == sourceId)
{
// Get this user story
WorkItem target = wiStore.GetWorkItem(wiTrees[iThis].TargetId);
Console.Write(prefix);
Console.Write(target.Type.Name);
Console.Write(": ");
Console.Write(target.Fields["Title"].Value);
if (iThis < wiTrees.Length - 1)
{
if (wiTrees[iThis].TargetId == wiTrees[iThis + 1].SourceId)
{
// The next item is this user story's child. Process the children
Console.WriteLine();
iNext = PrintTrees(wiStore, wiTrees, prefix + " ", wiTrees[iThis + 1].SourceId, iThis + 1, fieldName);
}
else
{
// The next item is not this user story's child.
Console.Write("; estimate = ");
// Determine which estimation field is present
Console.WriteLine(target.Fields[fieldName].Value);
iNext = iThis + 1;
}
}
else
{
// This is the last user story
iNext = iThis + 1;
}
iThis = iNext;
}
return iNext;
}
Print any Work Item used as Requirements by using Categories
Some team projects may use a different type of work item to represent backlog items. For example, projects that were created by using MSF for CMM Process Improvement v5.0 use requirements instead of user stories. Both of these types of work items belong to the same category. In this example, the code determines what type of work item belongs to the requirement category, and uses that work item. Once the type of work item is determined, this code assumes that user stories use story points and requirements use baseline hours. Of course, the techniques described in the previous examples can be used to generalize this code further. To use this sample, replace the code Main function from the previous sample with the following code.
static void Main(string[] args)
{
try
{
// Connect to Team Foundation Server. The form of the url is https://server:port/vpath.
// server - the name of the server that is running the Team Foundation application-tier.
// port - the port that Team Foundation uses. The default is port is 8080.
// vpath - the virutal path to the Team Foundation application. The default path is tfs.
TfsConfigurationServer configurationServer =
TfsConfigurationServerFactory.GetConfigurationServer(new Uri("https://server:8080/tfs"));
// Get the catalog of team project collections
CatalogNode catalogNode = configurationServer.CatalogNode;
ReadOnlyCollection<CatalogNode> tpcNodes = catalogNode.QueryChildren(
new Guid[] { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None);
// Load the estimate fields map
XmlDocument estimateFieldsDoc = new XmlDocument();
estimateFieldsDoc.Load("EstimateFields.xml");
// Process each of the team project collections
foreach (CatalogNode tpcNode in tpcNodes)
{
// Use the InstanceId property to get the team project collection
Guid tpcId = new Guid(tpcNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection tpc = configurationServer.GetTeamProjectCollection(tpcId);
// Get the work item store
WorkItemStore wiStore = tpc.GetService<WorkItemStore>();
foreach (Project project in wiStore.Projects)
{
Console.Write("Project: ");
Console.WriteLine(project.Name);
// Get the type of work item to use
CategoryCollection categories = wiStore.Projects[project.Name].Categories;
string wiType = categories["Requirement Category"].DefaultWorkItemType.Name;
// Query for the trees of active user stories in the team project colleciton
StringBuilder queryString = new StringBuilder("SELECT [System.Id] FROM WorkItemLinks WHERE ");
queryString.Append("([Source].[System.WorkItemType] = '");
queryString.Append(wiType);
queryString.Append("' AND [Source].[System.TeamProject] = '");
queryString.Append(project.Name);
queryString.Append("') AND ");
queryString.Append("([System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward') And ");
queryString.Append("([Target].[System.WorkItemType] = 'User Story' AND ");
queryString.Append("[Target].[System.State] = 'Active') ORDER BY [System.Id] mode(Recursive)");
Query wiQuery = new Query(wiStore, queryString.ToString());
WorkItemLinkInfo[] wiTrees = wiQuery.RunLinkQuery();
// Print the trees of user stories, with the estimated size of the leaves
XmlElement ele = (XmlElement)estimateFieldsDoc.DocumentElement.SelectSingleNode(project.Name);
PrintTrees(wiStore, wiTrees, " ", 0, 0, ele.InnerText);
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}