TFS11 API: Managing the Team Iteration Schedule

Retrieving and Updating the Iteration Schedule

Time for another TFS 11 blog post, this time about retrieving and modifying the iteration schedule for a team project. You may be wondering what I mean exactly when I say iteration schedule. I’m talking about the duration of iterations, which is determined by a start date and a finish date. What is going to make this post different from the last several TFS 11 API blog posts is that we are actually going to change some settings and not only retrieve them. Additionally, what makes things interesting this time is the fact that it will be easy to set up the iteration dates, but it will take some effort to read them from the server in the first place.

Setting the iteration dates

Like I’ve said, setting the iteration dates is simple. To do this, we will use the ICommonStructureService4 TFS service. If you’ve read my blog post about querying teams and team members, you might recall that I’ve mentioned that the common structure service can be used for retrieving information about team projects, as well as iteration and area info. Here’s a simple snippet that will set the dates for a specified iteration:

static void Main(string[] args)
{
	Uri serverUri = new Uri("<server_uri>");
	var collection = GetServer("<project_name>");
	int iterationId = <iteration_id>;
	var css4 = collection.GetService<ICommonStructureService4>();

	DateTime startDate = DateTime.Today;
	DateTime finishDate = DateTime.Today.AddDays(14);
	string iterationUri = GetIterationURI(css4, iterationId);
	
	css4.SetIterationDates(iterationUri, startDate, finishDate);
}

The SetIterationDates() method accepts an iteration URI (string) and two DateTime? objects. Because both the start date and the finish date can be null, you use the SetIterationDates() method to also clear the iteration dates, like this:

css4.SetIterationDates(iterationUri, null, null);

If you try to set only a single date, you’ll get a nice ArgumentException:

ArgumentException was unhandled
TF297000: You must specify both start and finish dates or neither date.

You’ll get pretty much the same if you mess up the dates by setting the start date that’s after the finish date:

ArgumentException was unhandled
TF297003: You cannot specify a start date greater than the finish date.

Getting the iteration dates

I must admit that I have spent a lot of time searching for a way to retrieve the iteration dates. I’ve found it very strange that there is no GetIterationDates() method defined alongside the SetIterationDates() method. I have already found a very nasty workaround when a colleague of mine, Toni from ToniCodes.net told me that he had noticed StartDate and FinishDate attributes while working with some XML retrieved from the common structure service. In fact, Toni had also blogged about working with iteration dates, but I’d like to include my own notes here for completeness. Nevertheless, many thanks to Toni!

Because I thought the date retrieval method was missing in the ICommonStructureService4 interface, I’ve chosen to implement it as an extension method:

static IEnumerable<ScheduleInfo> GetIterationDates(this ICommonStructureService4 css, string projectUri)
{
	NodeInfo[] structures = css.ListStructures(projectUri);
	NodeInfo iterations = structures.FirstOrDefault(n => n.StructureType.Equals("ProjectLifecycle"));
	List<ScheduleInfo> schedule = null;

	if (iterations != null)
	{
		string projectName = css.GetProject(projectUri).Name;
	
		XmlElement iterationsTree = css.GetNodesXml(new[] { iterations.Uri }, true);
		GetIterationDates(iterationsTree.ChildNodes[0], projectName, ref schedule);
	}

	return schedule;
}

The ListStructures() method (notice the “detailed” documentation) will return an array of two NodeInfo objects, one of which will be a tree structure representing the Iteration nodes and the other will represent the Area nodes. The iterations tree has the value “ProjectLifecycle” as the StructureType, so we filter out only this tree (again, not much help from the MSDN documentation with this stuff). To get the actual XML tree, we use the GetNodesXml() method and pass in the iterations tree URI. If you try to output the OuterXml property of the iterationsTree object to the console, you’ll see an XML structure much like this one:

<Nodes xmlns="">
	<Node StructureType="ProjectLifecycle" NodeID="..." Name="Iteration" Path="\Test\Iteration" ProjectID="...">
		<Children>
			<Node Name="Release 1" ParentID="..." Path="\Test\Iteration\Release 1" ProjectID="..." StructureType="ProjectLifecycle">
				<Children>
					<Node NodeID="..." Name="Sprint 1" ParentID="..." Path="\Test\Iteration\Release 1\Sprint 1" ProjectID="..." StructureType="ProjectLifecycle" StartDate="2012-03-16T00:00:00.000" FinishDate="2012-03-31T00:00:00.000" />
					<Node NodeID="..." Name="Sprint 2" ParentID="..." Path="\Test\Iteration\Release 1\Sprint 2" ProjectID="..." StructureType="ProjectLifecycle" StartDate="2012-03-01T00:00:00.000" FinishDate="2012-03-15T00:00:00.000" />
					<Node NodeID="..." Name="Sprint 3" ParentID="..." Path="\Test\Iteration\Release 1\Sprint 3" ProjectID="..." StructureType="ProjectLifecycle" />
				</Children>
			</Node>			
			<Node Name="Release 2" ParentID="..." Path="\Test\Iteration\Release 2" ProjectID="..." StructureType="ProjectLifecycle">
				<Children>
					<Node NodeID="..." Name="Sprint 1" ParentID="..." Path="\Test\Iteration\Release 2\Sprint 1" ProjectID="..." StructureType="ProjectLifecycle" />
					<Node NodeID="..." Name="Sprint 2" ParentID="..." Path="\Test\Iteration\Release 2\Sprint 2" ProjectID="..." StructureType="ProjectLifecycle" />			
				</Children>
			</Node>
		</Children>
	</Node>
</Nodes>

Please note that I’ve edited out all the IDs and some of the structure for brevity. Also note that the NodeID, ParentID and ProjectID attributes are URIs and not GUIDs or integers. Also, here’s the code for the other GetIterationDates() overload, which parses the XML data:

private static void GetIterationDates(XmlNode node, string projectName, ref List<ScheduleInfo> schedule)
{
	if (schedule == null)
		schedule = new List<ScheduleInfo>();

	if (node != null)
	{
		string iterationPath = node.Attributes["Path"].Value;
		if (!string.IsNullOrEmpty(iterationPath))
		{
			// Attempt to read the start and end dates if they exist.
			string strStartDate = (node.Attributes["StartDate"] != null) ? node.Attributes["StartDate"].Value : null;
			string strEndDate = (node.Attributes["FinishDate"] != null) ? node.Attributes["FinishDate"].Value : null;

			DateTime? startDate = null, endDate = null;
			
			if (!string.IsNullOrEmpty(strStartDate) && !string.IsNullOrEmpty(strEndDate))
			{
				bool datesValid = true;

				// Both dates should be valid.
				datesValid &= DateTime.TryParse(strStartDate, out startDate);
				datesValid &= DateTime.TryParse(strEndDate, out endDate);

				// Clear the dates unless both are valid.
				if (!datesValid)
				{
					startDate = null,
					endDate = null;
				}
			}
			
			schedule.Add(new ScheduleInfo
			{
				Path = iterationPath.Replace(string.Concat("\\", projectName, "\\Iteration"), projectName),
				StartDate = startDate,
				EndDate = endDate						
			});
		}

		// Visit any child nodes (sub-iterations).
		if (node.FirstChild != null)
		{
			// The first child node is the <Children> tag, which we'll skip.
			for (int nChild = 0; nChild < node.ChildNodes[0].ChildNodes.Count; nChild++)
				GetIterationDates(node.ChildNodes[0].ChildNodes[nChild], projectName, ref schedule);
		}
	}
}

Like I’ve said, it is a bit more work, but I think the code is still pretty straightforward. Basically, the idea is to retrieve the current node’s path, then try to parse both the start date and the finish date, then create an object to hold the schedule information, and recursively continue traversing down the tree structure. Only one thing to note here, and that’s the Path attribute values. This value is not the iteration path value, as you would see it in a work item edit form, for example. That’s why we need to perform a simple String.Replace() call to convert a value from “\Test\Iteration\Release 1\Sprint 3″ to “Test\Release 1\Sprint 3″, which would be a valid iteration path assigned to a work item (or rather, the other way round).

Conclusion

So much for today. Don’t forget to check out the other TFS 11 API blog posts, for more Team Foundation Server goodness. Happy coding!

Be Sociable, Share!
  • http://tonicodes.net/blog/ Toni

    Just a small typo, it should be:
    string projectName

    Otherwise, handy extensions.

    • Ivan Popek

      Thanks for letting me know. Fixed. :)

  • hexate

    your posts have been a great help in getting at my team’s tfs data.

    a simple modification i made that has been helpful for our purposes was to add the tree relationship to the ScheduleInfo object, and then tweaking the recursive function:

    1. add children to ScheduleInfo object

    public class ScheduleInfo
    {

    public List Children { get; set; }
    }

    2. add parent ScheduleInfo to recursive function signature

    private static void GetIterationDates(XmlNode node, string projectName, ScheduleInfo parent, ref List schedule)

    3. add relationship if parent exists

    iteration = new ScheduleInfo
    {
    Path = iterationPath.Replace(string.Concat(“\”, projectName, “\Iteration”), projectName),
    StartDate = startDate,
    EndDate = endDate
    };

    schedule.Add(iteration);

    // link child to parent
    if (parent != null)
    {
    if (parent.Children == null)
    {
    parent.Children = new List();
    }
    parent.Children.Add(iteration);
    }

  • rza

    i would like to know what are the properties of the

    • http://blog.johnsworkshop.net/ Ivan Popek

      The ScheduleInfo i just a custom data transfer object:

      public class ScheduleInfo
      {
      public string Path { get; set; }
      public DateTime StartDate { get; set; }
      public DateTime EndDate { get; set; }
      }