TFS 2010 - WorkItems Linking and Automation Workflow implementation for Closing Linked Workitems

Posted by: Subodh Sohoni , on 6/22/2009, in Category Visual Studio, VSTS & TFS
Views: 40241
Abstract: In my earlier article on “Spawn ‘Tasks’ in response to creation of ‘User Story’” we had seen how to use the Team Foundation Object Model in general and WorkItem Tracking APIs of TFS 2010 in particular.
TFS 2010 - WorkItems Linking and Automation Workflow implementation for Closing Linked Workitems
 
In my earlier article on “Spawn ‘Tasks’ in response to creation of ‘User Story’” we had seen how to use the Team Foundation Object Model in general and WorkItem Tracking APIs of TFS 2010 in particular. I cannot help in writing the other end of the workflow which was started in that article. We dealt with the creation and linking of workitems in that article. Let us now see how to close the workitems which are linked with parent – child links.
Taking a case which I feel is very interesting one, we will close the child workitems as well as set the state of parent to ‘Resolved’ under certain conditions. It is very natural that if a ‘Requirement’ or ‘User Story’ is dropped due to any reason then the orphaned tasks should also be closed. If they remain in the ‘Active’ state then some work may get done against them without the requirement being present, which is to say the least is waste of efforts. Another perspective to it is if all the ‘Tasks’ which implement a ‘Requirement’ or ‘User Story’ are closed then the parent workitem should reflect the actual state it is in which is ‘Resolved’. These activities can be done manually but that may lead to chance of omission. Not to leave anything to chance, I would like to do these things automatically as soon as some events take place. Now, what is better way of taking action on some events than to create some event handler!
We will implement two workflows in the same event handler since both are to be executed against the same event, ‘WorkItemChanged’. The first workflow is:
·         User closes a ‘Task’
·         Event handler gets the parent ‘User Story’ of the task – I firmly believe that there should not be any tasks lying around which cannot be linked to a ‘Requirement’ or ‘User Story’.
·         Event Handler now gets all the children ‘Tasks’ of this ‘User Story’
·         State of these children is checked and if all are closed then state of the parent ‘User Story’ is set to ‘Resolved’
Let us create some code for this in the ‘Notify’ method of the subscribing webservice. We will begin with converting the event related data passed as parameter of the type string into a XML document which can be queried. The XPath Queries that we will make first will filter out all events which do not have ‘Task’ being closed.
 
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)
{
                  if (! (EventLog.SourceExists("TfsSource")))
                  {
                        EventLog.CreateEventSource("TfsSource", "TfsLogs");
                  }
                  EventLog ThisLog = new EventLog();
                  ThisLog.Source = "TfsSource";
                  try
                  {
                        XmlDocument eventDoc = new XmlDocument();
                        eventDoc.LoadXml(eventXml);
                        eventDoc.Save("D:\\XmlText\\eventXml.xml");
                        XmlNode node1 = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Work Item Type']/NewValue");
                        XmlNode node2 = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='State']/NewValue");
 if (node1.InnerText == "Task" && node2.InnerText == "Closed")
 {
 
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)
                  If (Not EventLog.SourceExists("TfsSource")) Then
                        EventLog.CreateEventSource("TfsSource", "TfsLogs")
                  End If
                  Dim ThisLog As New EventLog()
                  ThisLog.Source = "TfsSource"
                  Try
                        Dim eventDoc As New XmlDocument()
                        eventDoc.LoadXml(eventXml)
                        eventDoc.Save("D:\XmlText\eventXml.xml")
                        Dim node1 As XmlNode = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='Work Item Type']/NewValue")
                        Dim node2 As XmlNode = eventDoc.DocumentElement.SelectSingleNode("//StringFields/Field[Name='State']/NewValue")
 If node1.InnerText = "Task" AndAlso node2.InnerText = "Closed" Then
      
Now that we have the desired event data, we will create the objects for Team Foundation Server, WorkItem Store and the workitem which is passed to us that is the task being closed.
 
C#
 
TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer("http://to-intra:8080/Tfs/DefaultCollection");
 WorkItemStore WIStore = new WorkItemStore(tfs);
 WorkItem ClosingTask = WIStore.GetWorkItem(Int32.Parse(eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/OldValue").InnerText));
 
VB.NET
 
Dim tfs As TeamFoundationServer = TeamFoundationServerFactory.GetServer("http://to-intra:8080/Tfs/DefaultCollection")
Dim WIStore As New WorkItemStore(tfs)
Dim ClosingTask As WorkItem = WIStore.GetWorkItem(Int32.Parse(eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/OldValue").InnerText))
 
Now we will get a collection of all the links from this task to other workitems. Workitems can have links to other workitems as well as to other artefacts. In the earlier versions of TFS we used to work with Linking Service which was not easy to manoeuvre. We now have a link type for workitems and another link type for all other artefacts. We will off course create the collection of workitem links. Once we have that collection then we can go through all the links to find the link to the parent workitem.
 
C#
 
WorkItemLinkCollection AllLinksToClosingTask = ClosingTask.WorkItemLinks;WorkItemLink ParentLink = null;
 foreach (WorkItemLink link in AllLinksToClosingTask)
 {
          if (link.LinkTypeEnd.Name == "Parent")
          {
                   ParentLink = link;
                   break;
          }
}
 
VB.NET
 
Dim AllLinksToClosingTask As WorkItemLinkCollection = ClosingTask.WorkItemLinks
Dim ParentLink As WorkItemLink = Nothing
For Each link As WorkItemLink In AllLinksToClosingTask
             If link.LinkTypeEnd.Name = "Parent" Then
                           ParentLink = link
                           Exit For
             End If
Next link
 
We will now create the instance of the parent workitem and get the collection of links from it in turn. These links are to the children which are other tasks for the implementation of parent. We will then go through all of those to check the status of each of them and if we find that status of all of them is set to ‘Closed’ then we will set the status of the parent to ‘Resolved’ and save it.
 
C#
 
WorkItem Parent = WIStore.GetWorkItem(ParentLink.TargetId);
WorkItemLinkCollection AllLinksToParent = Parent.WorkItemLinks;
bool AllChildrenWorkItemsClosed = true;
foreach (WorkItemLink child in AllLinksToParent)
 {
      if (WIStore.GetWorkItem(child.TargetId).State != "Closed")
         AllChildrenWorkItemsClosed = false;
}
if (AllChildrenWorkItemsClosed)
 {
        Parent.State = "Resolved";
        Parent.Save();
}
 }
 
VB.NET
 
Dim Parent As WorkItem = WIStore.GetWorkItem(ParentLink.TargetId)
Dim AllLinksToParent As WorkItemLinkCollection = Parent.WorkItemLinks
Dim AllChildrenWorkItemsClosed As Boolean = True
For Each child As WorkItemLink In AllLinksToParent
       If WIStore.GetWorkItem(child.TargetId).State <> "Closed" Then
             AllChildrenWorkItemsClosed = False
       End If
Next child
If AllChildrenWorkItemsClosed Then
            Parent.State = "Resolved"
            Parent.Save()
End If
 }
 
After creating code for first workflow let us now work on the second workflow.
·         User sets the status of the ‘User Story’ to ‘Closed’, may be because the requirement is dropped or is found to be not a requirement at all.
·         Event handler finds out the tasks which were created to implement the ‘User Story’
·         Status of all those tasks is set to ‘Closed’
This is a much simpler workflow to implement. We will first filter out all the events where event is not for ‘User Story’ with the status ‘Closed’. Then we will create the instance of Team Foundation Server, WorkItem Store and the workitem of the type ‘User Story’ which is passed in the parameter to the event handler.
 
C#
 
else if (node1.InnerText == "User Story" && node2.InnerText == "Closed")
 {  
            TeamFoundationServer tfs = TeamFoundationServerFactory.GetServer("http://to-intra:8080/Tfs/DefaultCollection");
            WorkItemStore WIStore = new WorkItemStore(tfs); //This is neat compared to GetService()
            WorkItem ClosingStory =   WIStore.GetWorkItem(Int32.Parse(eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/OldValue").InnerText));
Now we will find out all the links to children and one by one set the status of each of them to ‘Closed’.
WorkItemLinkCollection AllLinksToClosingStory = ClosingStory.WorkItemLinks;
 foreach (WorkItemLink link in AllLinksToClosingStory)
                    {
                        WorkItem RelatedTask = WIStore.GetWorkItem(link.TargetId);
                        RelatedTask.State = "Closed";
                        RelatedTask.Save();
                    }
                }
            }
            catch (Exception ex)
           {
               ThisLog.WriteEntry(ex.Message + ex.InnerException.Message);
            }
        }
 
VB.NET
 
ElseIf node1.InnerText = "User Story" AndAlso node2.InnerText = "Closed" Then
                  Dim tfs As TeamFoundationServer = TeamFoundationServerFactory.GetServer("http://to-intra:8080/Tfs/DefaultCollection")
                  Dim WIStore As New WorkItemStore(tfs) 'This is neat compared to GetService()
                  Dim ClosingStory As WorkItem = WIStore.GetWorkItem(Int32.Parse(eventDoc.DocumentElement.SelectSingleNode("//IntegerFields/Field[Name='ID']/OldValue").InnerText))
Now we will find all the links to children and one by one set the status of each of them to ‘Closed’. WorkItemLinkCollection AllLinksToClosingStory = ClosingStory.WorkItemLinks
 For Each link As WorkItemLink In AllLinksToClosingStory
                                    Dim RelatedTask As WorkItem = WIStore.GetWorkItem(link.TargetId)
                                    RelatedTask.State = "Closed"
                                    RelatedTask.Save()
 Next link
End If
                  }
                  Catch ex As Exception
                     ThisLog.WriteEntry(ex.Message + ex.InnerException.Message)
                  End Try
            }
That was the skeleton of the code for very interesting two workflows. You may have to flesh in the validations, setting other properties of workitems and error checking etc. but this can be a jumping board for creating your customized applications for the purpose. Some salient points are:
1.    Hierarchical workitems and links of different types (Parent – Child, Predecessor – Successor) are a new addition in TFS 2010.
2.    In TFS 2010 Beta 1 there is a bug due to which you will need to create code in .NET 2.0 only. An indirectly referenced assembly Microsoft.TeamFoundation.WorkItemTracking.Client.DataStore.dll is incompatible with .NET 4.0 so you have to build and host your webservice in .NET 2.0. This is tricky and takes lots of efforts in Team Suite 2010. Easiest way out that I can suggest is to create the webservice in VS / Team Suite 2008, giving the reference to TFS 2010 assemblies.
3. TFS 2010 assemblies which are needed ( Microsoft.TeamFoundation.Client.dll and Microsoft.TeamFoundation.WorkItemTracking.Client.dll) are now in the C:\Program Files\Microsoft Visual Studio 10\Common7\IDE\RefereceAssemblies and not in the PrivateAssemblies folder.
The entire source code of this article can be downloaded from here
Give me a +1 if you think it was a good article. Thanks!
Recommended Articles
Subodh Sohoni, Team System MVP, is an MCTS – Microsoft Team Foundation Server – Configuration and Development and also is a Microsoft Certified Trainer(MCT) since 2004. Subodh has his own company and conducts a lot of corporate trainings. He is an M.Tech. in Aircraft Production from IIT Madras. He has over 20 years of experience working in sectors like Production, Marketing, Software development and now Software Training. Follow him on twitter @subodhsohoni


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by Priya on Friday, March 29, 2013 5:17 PM
Will this service be installed on the TFS server ? Can it be consumed during the workflow ? when the work item is being edited ?

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel