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

Close a Work Item Only if Child Work Items are Closed - WebAccess

5.00/5 (2 votes)
26 Feb 2014CPOL8 min read 27.9K   224  
Custom controls over WebAccess of TFS

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 APIs which enable users/developers to extend the application as per our needs. Some may be simple configuration changes while others would be extending the functionality using the APIs 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 over web. Look for my other papers here for Visual Studio.

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 as 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. To provide flexibility to the team members, the ALM can be operations through various platforms, out of which web is prominent, which can be accessed from any device.

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 Web Interface

NOTE: We will be developing a JQuery plugin which needs to be installed on to the TFS web server.

Create a .js file and create a TFS module to register as FF.WITEventHandler. Declare dependencies on TFS.WorkItemTracking.Controls, TFS.WorkItemTracking and TFS.Core modules.

JavaScript
TFS.module("FujiFilm.WITEventHandler",
   [
       "TFS.WorkItemTracking.Controls",
       "TFS.WorkItemTracking",
       "TFS.Core"
   ]

Create a constructor and inherit it from TFS.WorkItemTracking.Controls.WorkItemControl

JavaScript
function () {
// module content
var WITOM = TFS.WorkItemTracking,
    WITCONTROLS = TFS.WorkItemTracking.Controls,
    delegate = TFS.Core.delegate;
// Constructor for WITEventHandler
function WITEventHandler(container, options, workItemType)
{
    this.baseConstructor.call(this, container, options, workItemType);
}
// WITEventHandler inherits from WorkItemControl
WITEventHandler.inherit(WITCONTROLS.WorkItemControl, {
    _control: null,
    _status: null,
WITCONTROLS.registerWorkItemControl("WITEventHandler", WITEventHandler);
}

Initialize the control UI. There is where all the global variables go.

JavaScript
_init:function () {
     this._base();
     var oldStateValue;
    },

Update control data when work item is bound to a specific item.

JavaScript
invalidate: function (flushing) {
    },

Clear the control data. Framework calls this method when the control needs to reset its state to "blank", such as when work item form is unbound from a specific work item. 

JavaScript
 clear: function() {
    this._workItem= null;
},

Bind a function to the work item where we handle all the logic.

JavaScript
bind:function (workItem) {
}

Within the above function, create a function delegate and attach it to the workitem changed event in bind above.

JavaScript
bind:function (workItem) {
            this._workItemChangeDelegate = function (sender, args) {
            }
    workItem.attachWorkItemChanged(this._workItemChangeDelegate);
    }

So when a workitem changes, this._workItemChangeDelegate gets fired. This method contains the logic we need to implement for our requirement.

Logic

Logic is simple,

  • Identify if the work item is changed
    • If changed then check if it is the state control
    • If its state control and its changed to Close
      • Check if the child work items are open
        • If child work items are open then
        • Raise an alert to the user
        • And revert back the change

Identify the Control Clicked

If you closely observe, the controls generated on the TFS work item web form are dynamic controls. We need a mechanism to identify the control clicked.

Image 1

So create an event for every dropdown. (In our case, we are working only with state, so I selected dropdown.)

Image 2

We are writing an event to the dropdown button highlighted above.

JavaScript
$('.drop').bind('click', function () {
    if (this.id == "") 
        stateCtrl = $("#" + this.parentElement.id + "_txt")[0];
    else
        stateCtrl = $("#" + this.id + "_txt")[0];
});

When the button is clicked, create a variable to load the control’s parent as that is the actual dropdown control.

Store the previous value in a variable called

oldStateValue = workItem.fieldData[2];

Now identify which work item you want to check, Epic, User Story, Task, etc.

JavaScript
if (workItem.workItemType.name == "Epic")
    wiClosed = "Resolved";
if (workItem.workItemType.name == "Task")
    wiClosed = "Closed";
if (workItem.workItemType.name == "User Story")
    wiClosed = "Closed";

Now create a work item changed delegate function.

Check if the change is a ‘field-change’.

JavaScript
if (args.change === "field-change") 

There will be multiple auto field changes once a field is changed in a work item, like changed date, changed by, etc. For each changed value, we are interested in change of state field which is changed to close.

JavaScript
for (var i in args.changedFields) {
    if (args.changedFields[i].fieldDefinition.name === "State" && stateCtrl.value === wiClosed) {

Get the links for the work item. Links could be children, parent, etc., we need to check only for children.

JavaScript
var links = parentWorkItem.getLinks();

for (var i in links) {
    var child = null;

if (links[i].baseLinkType === "WorkItemLink" && links[i].getLinkTypeEnd().name == "Child") {

If children are available, then check the state of the child. If the child state is not closed, then show an alert to the user and reset the state value to the previous value.

JavaScript
state = child.fieldMap.STATE.getValue();
if (state != wiClosed) {
    alert("You must close all the open child work items to close this work item.");
    stateCtrl.value = oldStateValue;

Here we are displaying only a basic alert to the user, but to the details of the open children. So break out of the loop if atleast one open child is found.

The final code looks as below:

JavaScript
// Register this module as "FFFilm.WITEventHandler" and declare 
// dependencies on TFS.WorkItemTracking.Controls, TFS.WorkItemTracking and TFS.Core modules
TFS.module("FFFilm.WITEventHandler",
    [
        "TFS.WorkItemTracking.Controls",
        "TFS.WorkItemTracking",
        "TFS.Core"
    ],
    function () {
 
        // module content
 
        var WITOM = TFS.WorkItemTracking,
            WITCONTROLS = TFS.WorkItemTracking.Controls,
            delegate = TFS.Core.delegate;
 
        // Constructor for WITEventHandler
        function WITEventHandler(container, options, workItemType) {
            this.baseConstructor.call(this, container, options, workItemType);
        }

        // WITEventHandler inherits from WorkItemControl
        WITEventHandler.inherit(WITCONTROLS.WorkItemControl, {
            _control: null,
            _status: null,
 
            // Initialize the control UI without data (in "blank" state).
            // Framework calls this method when the control needs to render its initial UI
            // Notes: 
            // - The work item data is NOT available at this point
            // - Keep in mind that work item form is reused for multiple work items 
            // by binding/unbinding the form to work item data
            _init: function () {
                this._base();
                var oldStateValue;
                var stateCtrl;
                var wiClosed;
            },
 
            // Update the control data
            // Framework calls this method when the control needs to update itself, such as when:
            // - work item form is bound to a specific work item
            // - underlying field value has changed due to rules or another control logic
            invalidate: function (flushing) {
            },
 
            // Clear the control data
            // Framework calls this method when the control needs to reset its state to "blank", such as when:
            // - work item form is unbound from a specific work item
            clear: function () {
                this._workItem = null;
            },
 
            bind: function (workItem) {
                this._base(workItem);

                $('.drop').bind('click', function () {
                    if (this.id == "") 
                        stateCtrl = $("#" + this.parentElement.id + "_txt")[0];
                    else
                        stateCtrl = $("#" + this.id + "_txt")[0];
                });
 
                oldStateValue = workItem.fieldData[2];
                if (workItem.workItemType.name == "Epic")
                    wiClosed = "Resolved";
                if (workItem.workItemType.name == "Task")
                    wiClosed = "Closed";
                if (workItem.workItemType.name == "User Story")
                    wiClosed = "Closed";
 
                this._workItemChangeDelegate = function (sender, args) {
 
                    if (args.change === "field-change") {
                        // find a label field, with class workitemcontrol-label and
                        // title contans helptext of args.changedFields[i].fieldDefinition
                        // get that for value which is the state control
 
                        for (var i in args.changedFields) {
                            if (typeof stateCtrl != 'undefined') {
                                if (args.changedFields[i].fieldDefinition.name === "State" && stateCtrl.value === wiClosed) {
                                    var parentWorkItem = workItem;
                                    var links = parentWorkItem.getLinks();
 
                                    for (var i in links) {
                                        var child = null;
                                        var state = "";
 
                                        if (links[i].baseLinkType === "WorkItemLink" && links[i].getLinkTypeEnd().name == "Child") {
                                            parentWorkItem.store.beginGetWorkItem(links[i].getTargetId(), function (child, state) {
                                                state = child.fieldMap.STATE.getValue();
 
                                                // if state is not closed, then return true
                                                if (state != wiClosed) {
                                                    // if the previous state is already the same, then do not display any thing to the user.
                                                    if (stateCtrl.value != oldStateValue) {
                                                        alert("You must close all the open child work items to close this work item.");
                                                        stateCtrl.value = oldStateValue;
                                                    }
                                                }
                                            });
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                workItem.attachWorkItemChanged(this._workItemChangeDelegate);
            },
 
            unbind: function (workItem) {
                if (this._workItemChangeDelegate) {
                    this._workItem.detachWorkItemChanged(this._workItemChangeDelegate);
                    delete this._workItemChangeDelegate;
                }
            }
        });
 
        // Register this module as a work item custom control called "WITEventHandler"
        WITCONTROLS.registerWorkItemControl("WITEventHandler", WITEventHandler);
 
        return {
            WITEventHandler: WITEventHandler
        };
    });

Creating the JQuery class is not sufficient to use the functionality. We need a manifest file stating so.

Create a manifest.xml and copy the below content into it:

XML
<WebAccess version="11.0">
  <plugin name="Work Item Template Event Handler" vendor="FFFilm" moreinfo="http://www.FFmed.com" version="1.0.0" >
    <modules>
      <module namespace="FFFilm.WITEventHandler" kind="TFS.WorkItem.CustomControl"/>
    </modules>
  </plugin>
</WebAccess>

Packaging

We need only server side deployment in this case. The user needs admin permissions on TFS to get this done. 

Before we deploy, we need to package the files we created.

Name the JS file as FFFilm.WITEventHandler.min.js. You can minify the file if required.

Make a copy of the file are rename is as FujiFilm.WITEventHandler.debug.js

Zip the above three files including Manifest.xml. Do not place them in a folder before zipping. The name of the zip file doesn’t matter.

This creates our package.

Deployment

Open TFS WebAccess.

Go to Control Panel. Click on Extensions tab. You should see all the previous plugins installed here.

Click in the install button to install the plugin you created now. If this button is not visible, then you do not have the required permissions.

Image 3

Choose file and point to the zip file we created. And click ok to install the plugin.

By default, the installed plugin will be disabled. Click on enable to enable it.

Now navigate to the work item and perform your testing.

Modifying a Work Item Type

Installing the files 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.

image004

image006

Click New to add a new field.

image008

Fill in the following details: 

image009

Go to the layout section and add the control.

image011

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.

Getting both Visual Studio and WebAccess to Work

To get both Visual Studio and Web access to work, you need not make any more modifications to the work item type. The modifications made above will suffice the Visual Studio also. Make sure the control name matches. 

Debugging

Debugging is simple in this case. In Chrome, press F12 key to open the developer tools window.

Select the source tab, and expand the folders on the left. Go to the folder as specified below where you will find the .js file you created. Click on the line number to set a break point, and refresh the browser to hit the break point.

Image 9

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 multiple alert to display the child details
    Displayed only one generic message.
  • The handler created should be detached and removed from the work item. Else on every change to the next work item, the handler stays and repeated multiple alert boxes comes up annoying the user. 
    Navigated to the TFS.Controls.js to identify exactly how to detach the handler.
  • Frequently copying the assembly files to the specified location.
    Created a batch file which copies .dll, .pdb and .wicc files

References

Work Item Custom Control Development in TF Web Access 2012 – Deployment

http://blogs.msdn.com/b/serkani/archive/2012/06/22/work-item-custom-control-development-in-tf-web-access-2012-deployment.aspx

TFS 2013 PowerTools

http://visualstudiogallery.msdn.microsoft.com/f017b10c-02b4-4d6d-9845-58a06545627f

License

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