Spawn tasks to implement newly added ‘Requirement’ or ‘User Story’ in Team Foundation Server (TFS) 2010
In this article let us delve into the case of workitems spawning. We will check on how we can create a set of children work items through an automated process when a workitem of certain type is added by the user. For example, when a Requirement or User Story is created, we will find out how the Tasks to fulfill that Requirement or to implement that User Story can be created in our program. Many organizations would like that kind of automation since it ensures that we are ensuring that we will implement that Requirement or User Story and avoid oversight.
We begin with conceptualizing the workflow.
· A workitem is added by the user.
· An event handler kicks in and checks that workitem is of the type “User Story” and it is newly created.
· The event handler reads the details of the workitems to be spawned from the configuration file.
· New workitems are spawned with the titles as read from configuration file.
· Parent – Child links are created between the “User Story” and spawned “Tasks”
· Workitems are saved.
To implement this workflow we will require a webservice to be created as an event handler and create a subscription for that webservice. I have handled the issue of ‘how to subscribe to the TFS2010 events’ in my earlier article (https://www.dotnetcurry.com/ShowArticle.aspx?ID=330).
The webservice that we create will work as a subscriber to the “WorkItemChangedEvent”. We begin by creating a webservice project.
Note: We should select .NET framework 2.0 since an indirectly referenced assembly Microsoft.TeamFoundation.WorkItemTracking.Client.DataStore.dll cannot work in .NET 4.0 environment (at least in VSTS 2010 Beta 1 and .NET 4.0 Beta 1).
Provide references to:
· Microsoft.TeamFoundation.dll
· Microsoft.TeamFoundation.Client.dll
· Microsoft.TeamFoundation.WorkItemTracking.Client.dll
These assemblies are located in <system drive>\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies folder.
Next we add the ‘using’ or ‘Import’ statements as the selected language may be. We will require the namespaces:
· System.Xml
· System.Web.Configuration
· System.Web.Services.Protocols
· Microsoft.TeamFoundation.dll
· Microsoft.TeamFoundation.Client.dll
· Microsoft.TeamFoundation.WorkItemTracking.Client.dll.
We may change the class name and the name of the .asmx file. If you change the name of the .asmx file remember to change that in the CodeBehind tag in the file. We now will add the Notify method which will be our webmethod that will execute on event notification. The Notify method will take two parameters. The first one will store the event related data which will be sent by TFS with event notification and the second one will pass the TFS identity data.
Notify method will also have two attributes and the entire signature of the method is as follows:
C#
[SoapDocumentMethod(Action = "http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify", RequestNamespace = "http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03")]
[WebMethod(MessageName = "Notify")]
public void Notify(string eventXml, string tfsIdentityXml)
{
}
VB.NET
<SoapDocumentMethod(Action:=_
"http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03/Notify", RequestNamespace:="_
http://schemas.microsoft.com/TeamFoundation/2005/06/Services/Notification/03"), _
WebMethod(MessageName:="Notify")> _
Public Sub Notify(ByVal eventXml As String, ByVal tfsIdentityXml As String)
End Sub
We will now convert the event related data which is XML data converted into string, back to XML in a xml document
C#
XmlDocument eventDoc = new XmlDocument();
eventDoc.LoadXml(eventXml);
VB.NET
Dim eventDoc As New XmlDocument()
eventDoc.LoadXml(eventXml)
Our first task is to check that the notification is for the event that is for “addition of a new workitem of the type ‘User Story’. For this we will make use of the XPath query
· To get the ‘NewValue’ node of the ‘Field’ node for which ‘Name’ node has a value of ‘Work Item Type’ and
· To get the ‘OldValue’ node of the in the ‘Field’ node for which ‘Name’ node has a value of ‘ID’
We will check that the InnerText value of ‘NewValue’ is ‘User Story’ and that of the ‘OldValue’ node is 0.
C#
XmlNode node1 = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Work Item Type']/NewValue");
XmlNode node2 = eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/OldValue");
if (node1.InnerText == "User Story" && Int32.Parse(node2.InnerText) == 0)
VB.NET
Dim node1 As XmlNode = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Work Item Type']/NewValue")
Dim node2 As XmlNode = eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/OldValue")
If node1.InnerText = "User Story" AndAlso Int32.Parse(node2.InnerText) = 0 Then
Now that we are sure that we have the correct notification, we will start the code to create tasks.
We will first connect to the Team Foundation Server. We can use either the GetServer method of TeamFoundationServerFactory class or we can use the default constructor of TeamFoundationServer class. Notice that now in TFS 2010, we have to provide the Project Collection name while creating the object of TFS. We then get the WorkItemStore and using that get the collection of all workitem types supported in our project.
C#
TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer("http://TfsName:8080/Tfs/DefaultCollection");
WorkItemStore WIStore = new WorkItemStore(tfs);
WorkItemTypeCollection WITCollection = WIStore.Projects["Marketing Management"].WorkItemTypes;
VB.NET
Dim tfs As TeamFoundationServer = TeamFoundationServerFactory.GetServer("http://TfsName:8080/Tfs/DefaultCollection")
Dim WIStore As New WorkItemStore(tfs)
Dim WITCollection As WorkItemTypeCollection = WIStore.Projects("Marketing Management").WorkItemTypes
We now can get the tasks (titles only) that we are going to create as workitems. These tasks may be pre-configured in Web.Config file and if not, will be provided by the event handling code.
C#
List<String> tasks = ReadTasks();
VB.NET
Dim tasks As List(Of String) = ReadTasks()
Code of the method ReadTasks is as follows:
C#
private List<string> ReadTasks()
{
List<string> tasks = new List<string>();
Configuration appSettingsRoot = WebConfigurationManager.OpenWebConfiguration(null);
if (0 < appSettingsRoot.AppSettings.Settings.Count)
{
tasks.Add("Architect Solution");
tasks.Add("Develop Solution");
tasks.Add("Test Solution");
tasks.Add("Deploy Solution");
}
else
{
tasks.Add(ConfigurationManager.AppSettings.Get("ArchTask"));
tasks.Add(ConfigurationManager.AppSettings.Get("DevTask"));
tasks.Add(ConfigurationManager.AppSettings.Get("TestTask"));
tasks.Add(ConfigurationManager.AppSettings.Get("DeployTask"));
}
return tasks;
}
VB.NET
Private Function ReadTasks() As List(Of String)
Dim tasks As New List(Of String)()
Dim appSettingsRoot As Configuration = WebConfigurationManager.OpenWebConfiguration(Nothing)
If 0 < appSettingsRoot.AppSettings.Settings.Count Then
tasks.Add("Architect Solution")
tasks.Add("Develop Solution")
tasks.Add("Test Solution")
tasks.Add("Deploy Solution")
Else
tasks.Add(ConfigurationManager.AppSettings.Get("ArchTask"))
tasks.Add(ConfigurationManager.AppSettings.Get("DevTask"))
tasks.Add(ConfigurationManager.AppSettings.Get("TestTask"))
tasks.Add(ConfigurationManager.AppSettings.Get("DeployTask"))
End If
Return tasks
End Function
We will now take one task title at a time from the list and create workitems of the type task for those. We will assign those tasks right now to the person who has created the User Story. Those can be reassigned to other developers latter on. We will also set the title of each task with the suffix of the User Story for implementing which we are creating these tasks. Finally, we will link these tasks to the “User Story” where that user story will be “parent” and the tasks will be children.
C#
foreach (string task in tasks)
{
WorkItem wi = new WorkItem(WITCollection["Task"]);
wi.Title = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Title']/NewValue").InnerText + task;
wi.Fields["System.AssignedTo"].Value = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Assigned To']/NewValue").InnerText;
wi.Fields["System.AssignedTo"].Value.ToString());
WorkItemLinkTypeEnd linkTypeEnd = WIStore.WorkItemLinkTypes.LinkTypeEnds["Parent"];
wi.Links.Add(new RelatedLink(linkTypeEnd, Int32.Parse(eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/NewValue").InnerText)));
wi.Save();
}
VB.NET
For Each task As String In tasks
Dim wi As New WorkItem(WITCollection("Task"))
wi.Title = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Title']/NewValue").InnerText & task
wi.Fields("System.AssignedTo").Value = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Assigned To']/NewValue").InnerText
wi.Fields("System.AssignedTo").Value.ToString())
Dim linkTypeEnd As WorkItemLinkTypeEnd = WIStore.WorkItemLinkTypes.LinkTypeEnds("Parent")
wi.Links.Add(New RelatedLink(linkTypeEnd, Int32.Parse(eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/NewValue").InnerText)))
wi.Save()
Next task
This webservice should be hosted within its own application pool and the identity of the pool should be same as the TFS application pool or any other normal account. Use <identity impersonate=”true” /> tag with username and password of the user who has permissions to create workitems in the selected team project. Now you can add a subscription to the event using BisSubscribe tool.
This boilerplate code has given you idea about how the tasks can be spawned when a “User Story” or “Requirement” is added by the user. Taking this as a base you can do number of modifications to improve the reliability of the code with additional customizations to suite your requirements.
While doing so please remember:
1. Create the subscriber in .NET 2.0 only! I spent 4 hours playing with assembly created with .NET 4.0 to realize that there is an incompatibility.
2. Assemblies to be reference are located in ReferenceAssemblies folder and not in PrivateAssemblies folder as was in TFS 2008.
3. While creating the instance of TeamFoundationServer provide the Uri consisting of Project Collection.
The entire source code of this article can be downloaded from here.