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.
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()
{
}
void IWorkItemControl.FlushToDatasource()
{
}
void IWorkItemControl.InvalidateDatasource()
{
}
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;
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.
Click New to add a new field.
Fill in the following details
Go to the layout section and add the control.
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.
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.
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