Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / TFS

Close a Work Item only if Child Work items are closed

5.00/5 (6 votes)
20 Feb 2014CPOL8 min read 43.5K   279  
Team Foundation Server is an ALM tool. It can be extended as per the business requirement. Controls can be created so as to populate them on the work item form. We can add additional business functionality by creating the controls. The API provided by TFS makes this possible.

ABSTRACT

In the developing world, with the most flexible applications available, we have been provided an opportunity to extend the functionality of the software. Similar software in the market does not match the required functionality, but one would be more inclined to a particular application based on the features it is offering, like free, open source, extensibility, expertise etc., The application one has chosen may not have a specific functionality another application is providing or need a new feature as per the business requirement. To incorporate this functionality, we may not be able to contact the vendor due to various reasons like, the cost of support, time, frequent changes etc., To overcome this situation, few vendors are providing API’s which enables users/developers to extend the application as per our needs. Some may be simple configuration changes while others would be extending the functionality using eh API’s provided. This enables us to incorporate the functionality we need, and the control is in our hands.

Introduction

Team Foundation Server is an ALM tool. It can be extended as per the business requirement. Controls can be created so as to populate them on the work item form. We can add additional business functionality by creating the controls. The API provided by TFS makes this possible. Closing a work item only if child work items are closed, is not as part of TFS standard features. We need to extend the functionality to achieve this, by using the API provided by TFS. TFS can be used with the help of various interfaces. Visual studio, web are the most prominent. In this paper I would explain how to achieve the above described functionality in Visual Studio. Look for my other papers for web.

Background

TFS being a configuration management tool, which stores versions of code, later on moved to be an Application Life cycle management(ALM) tool. Since providing the additional feature, it was not so efficient as the other tools available in the market, could be due to the process templates we use. Because of this we need to develop our custom functionality into TFS, which is possible by the provided API from Microsoft for TFS.

Need for Restricting to close a work item if children are open

TFS being an ALM tool, user stories, tasks, issues, bugs etc., can be logged into it. We can apply proven agile practices to manage our applications lifecycle. Though the functionality described in this paper is not available as part of standard feature, we intend to develop one. Creating one such tool enables us not only follow the proven agile practice, but also ease for the developers, managers etc., to keep track of work.

Definitions

WIT Work Item Type
ALM Application Lifecycle Management
.WICC Work Item Custom Control

Developing the control for Visual Studio Interface

NOTE: We are developing a client side control for visual studio, so this control needs to be installed on every machine which accesses TFS through visual studio. If this control is not available on a client then this functionality will not work, but the existing functionality will be as it is.

Create a new project Windows Forms Control Library and inherit it from IWorkItemControl.

 Image 1

public partial class WITEventHandler : UserControl, IWorkItemControl
  {

  }

Our task here is, when the state of the work item is changed to closed we need to find if the child work items are open, and then disable the change of state. For this we need to capture the onChange event of the state dropdown.

Though the visual studio interface looks simple, we will not be able to capture the onChange event of the dropdown as we will not be able to identify the control.

To get such functionality we need to add an event handler for onChange of the work item. This means that any change on the work item is captured. Not only state, but also change in Priority, Rank etc.,

So add a method AddEventHandler() which adds an event to work item.

private void AddEventHandler()
        {
              workItem.FieldChanged += new WorkItemFieldChangeEventHandler (this.workItem_FieldChanged);
        }

Call this method in WorkItemDatasource property

object IWorkItemControl.WorkItemDatasource
        {
            get
            {
                return workItem;
            }
            set
            {
                workItem = (WorkItem)value;
                AddEventHandler();
            }
        }

Now perform the required functionality in <span>workItem_FieldChanged</span> method.

As per our requirement, we need to identify if the changed field is state, and if it is set to Closed. This can be achieved by checking the event args as below.

(e.Field.Name == "State" && e.Field.Value.ToString() == "Closed")

Once we ensure that we are capturing the right event, now find if there are any children to that work item.

Find out the links to the work item by WorkItem.WorkItemLinks. Also check if the link is a child.(there could be other links like parent etc.,

Once we find out the child links, we need to check their state.

Foreach (WorkItemLink link in parentWorkItem.WorkItemLinks)
            {
                WorkItem child = null;
                if (link.LinkTypeEnd.Name == "Child")
                {
                    child = parentWorkItem.Store.GetWorkItem(link.TargetId);
                    if (child.State != "Closed")
                    {
                        workItemDesc.Append(child.Id + "\t" + trimDescription(child.Type.Name, 10) + "\t" + trimDescription(child.Title, 18) + "\t\t" + child.Fields["priority"].Value + "\n");
                        isOpen = true;
                    }
                }
            }

If the child state is not closed, then we need to raise an alert stating the children are open, and then revert back the state change on the parent.

private void ResetState()
        {
            workItem.State = originalState;
            workItem.Fields["Assigned To"].Value = originalAssignedTo;
        }

You can even display the work item details to the user by adding an additional line of code to capture the details.

During this process, we see that the event is fired when something is changed on the work item. Initially when we change the state to closed on parent, it checks the children state and reverts back the change made on the parent, which is also a change, this again fires the onChange event, which will turn into an infinite loop. To prevent this we need to reset the thread.

System.Threading.Thread resetThread = new System.Threading.Thread(new   System.Threading.ThreadStart(ResetState));
resetThread.Start();

The whole source code is available below. It contains more refined and most reliable code which can be used.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Controls;
using System.Collections.Specialized;

namespace FF.TFSControls
{
    public partial class WITEventHandler : UserControl, IWorkItemControl
    {
        string originalState = string.Empty;
        string originalAssignedTo = string.Empty;
        bool isSettingFieldsManually = false;
        
        public WITEventHandler()
        {
            InitializeComponent();
        }

        private static object EventBeforeUpdateDatasource = new object();
        private static object EventAfterUpdateDatasource = new object();
        
        event EventHandler IWorkItemControl.AfterUpdateDatasource
        {
            add { Events.AddHandler(EventAfterUpdateDatasource, value); }
            remove { Events.RemoveHandler(EventAfterUpdateDatasource, value); }
        }

        event EventHandler IWorkItemControl.BeforeUpdateDatasource
        {
            add { Events.AddHandler(EventBeforeUpdateDatasource, value); }
            remove { Events.RemoveHandler(EventBeforeUpdateDatasource, value); }
        }

        void IWorkItemControl.Clear()
        {
            //throw new NotImplementedException();
        }

        void IWorkItemControl.FlushToDatasource()
        {
           // throw new NotImplementedException();
        }

        void IWorkItemControl.InvalidateDatasource()
        {
           // throw new NotImplementedException();
        }

        private StringDictionary properties;
        StringDictionary IWorkItemControl.Properties
        {
            get
            {
                return properties;
            }
            set
            {
                properties = value;
            }
        }

        private bool readOnly;
        bool IWorkItemControl.ReadOnly
        {
            get
            {
                return readOnly;
            }
            set
            {
                readOnly = value;
            }
        }

        private IServiceProvider sProvider;
        void IWorkItemControl.SetSite(IServiceProvider serviceProvider)
        {
            sProvider = serviceProvider;
        }

        private WorkItem workItem;
        // testing
        object IWorkItemControl.WorkItemDatasource
        {
            get
            {
                return workItem;
            }
            set
            {
                workItem = (WorkItem)value;
                AddEventHandler();
            }
        }

        private void AddEventHandler()
        {
            if (workItem != null && workItem.Id != 0 && workItem.State != "Closed")
            {
                workItem.FieldChanged += new WorkItemFieldChangeEventHandler(this.workItem_FieldChanged);
                originalState = workItem.Fields["State"].Value.ToString();
                originalAssignedTo = workItem.Fields["Assigned To"].Value.ToString();
            }
        }

        private void workItem_FieldChanged(object sender, WorkItemEventArgs e)
        {
            StringBuilder workItemDesc = new StringBuilder();
            if (!this.IsDisposed && e.Field != null)
            {
                if (isSettingFieldsManually == false && (e.Field.Name == "Assigned To" || (e.Field.Name == "State" && e.Field.Value.ToString() != "Closed")))
                {
                    originalState = workItem.Fields["State"].Value.ToString();
                    originalAssignedTo = workItem.Fields["Assigned To"].Value.ToString();
                }

                if (e.Field.Name == "State" && e.Field.OriginalValue.ToString() != "Closed" && e.Field.Value.ToString() == "Closed")
                {
                    if (isChildOpen(workItem,workItemDesc))
                    {
                        string message ="Below child workitems are not closed. \n\n"+
                            "ID\tType\tDescription\t\tPriority\n\n" +
                            workItemDesc.ToString() + "\n" +
                            "Bugs with priority 4 and 5 can be moved to backlog.\n" +
                            " \nClick on 'All Links' in the work item to see the Child items for this work item.";
                        
                        MessageBox.Show(message, "Dependency Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

                        System.Threading.Thread resetThread = new System.Threading.Thread(new System.Threading.ThreadStart(ResetState));
                        resetThread.Start();
                    }
                }
            }
        }



        private bool isChildOpen(WorkItem parentWorkItem, StringBuilder workItemDesc)
        {
            bool isOpen = false;

            foreach (WorkItemLink link in parentWorkItem.WorkItemLinks)
            {
                WorkItem child = null;
                if (link.LinkTypeEnd.Name == "Child")
                {
                    child = parentWorkItem.Store.GetWorkItem(link.TargetId);
                    if (child.State != "Closed")
                    {
                        workItemDesc.Append(child.Id + "\t" + trimDescription(child.Type.Name, 10) + "\t" + trimDescription(child.Title, 18) + "\t\t" + child.Fields["priority"].Value + "\n");
                        isOpen = true;
                    }
                }
            }

            return isOpen;
        }

        private string trimDescription(string description, int len)
        {
            if (description.Length > len)
                return description.Substring(0, len-3) + "...";
            else
                return description;
        }
        
       private void ResetState()
        {
            isSettingFieldsManually = true;
            workItem.State = originalState;
            workItem.Fields["Assigned To"].Value = originalAssignedTo;
            isSettingFieldsManually = false;
        }

     

        private string fieldName;
        string IWorkItemControl.WorkItemFieldName
        {
            get
            {
                return fieldName;
            }
            set
            {
                fieldName = value;
            }
        }
    }
}

Creating this assembly is not sufficient; we need a mechanism to consume this in visual studio.

Create a .wicc file with the name of the control. In this case WITEventHandler.wicc and paste the below content in it.

<?xml version="1.0"?>
<CustomControl xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Assembly>FF.TFSControls.dll</Assembly>
  <FullClassName>FF.TFSControls.WITEventHandler</FullClassName>
</CustomControl>

This states the name of the assembly and the class name.

Installation

Once we are ready with the two files, the assembly(.dll) and .wicc file, copy them to the below location.

C:\ProgramData\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\12.0

This could be different from machine to machine based on the operating system. Please check the environment section on which this code is developed. Minor modifications should enable the code to work on any environment.

Modifying a Work Item Type 

Copying the files to the specified location will not be sufficient, as we are trying to modify the work item, we need to tell work item to use this new control we developed. For this we need to modify the Work Item template.

To do this we have multiple third party tools, of which TFS Power tools is prominent.

From visual studio navigate to tools -> Process Editor -> Work Item Types -> Open WIT from server.

Select the project you prefer and select Task from the expansion.

 Image 2

 Image 3

Click New to add a new field.

 Image 4

Fill in the following details

 Image 5

Go to the layout section and add the control.

 Image 6

Make sure you don’t add any Label to the control, as we do not want the control to be displayed on the Work Item Form in visual studio.

Though the control we built doesn’t have any interface, adding the Label will display the Label text and also a default text box.

Once complete, click on save, which saves the Work Item to the server.

Testing

Since we have modified a Task Work Item, on the specific server, and specific project, open a Task from that specific location.

You see that there is no difference on the work item form, as we have not made any visual changes.

Open a task which is not closed and has children which are not closed.

Change the state to Closed, and see that it displays an alert box stating which child work items are open and once clicked OK, the state of the parent control moves back to the original state.

 Image 7

Debugging

Debugging the control is very simple, but you need to open two visual studio applications. One containing the project where the code is written, other containing a work item type where the functionality is installed.

Attach the devenv process of the first visual studio.

Put break points where ever necessary.

Do not forget to copy the .pdb file to the destination location.

 Image 8

Extending the control

The code and description in this paper is related to a Task work item and for change of State control. This can be extended to other controls with minor modifications.

This can also be extended to other work item types by just modifying the work item types. It’s like, install once and use if for many other work item types.

Taking this code as a baseline we can develop multiple controls which can satisfy Agile practices.

Challenges faced

  • Capturing on change event of a control
    • Written a handler to capture any event on the form.
  • Eliminating the infinite loop for onChange
    • Threading is applied to eliminate this.
  • Modifying and importing work item types in bulk
    • Created a batch file to import/export bulk work item types.
  • Frequently copying the assembly files to the specified location.
    • Created a batch file which copies .dll, .pdb and .wicc files

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)