Work Item Custom Control Development in TF Web Access 2012 - Development
One of the biggest investments we have made in the new version of Web Access is on the Work Item Tracking area. In TF Web Access 2010, we used to rely on WIT Client Object Model running on the server and it had been causing a number of issues (especially around shared cache) since it was initially designed to work for a single user through Visual Studio Team Explorer.
TF Web Access 2012 has a completely new architecture for Work Item Tracking where the rule processing logic is moved from the server to the browser. WIT Client Object Model is not used anymore and there is a thin Object Model written in JavaScript runs in the browser. The rules are executed in the browser and a direct communication is made to the server via XHR for only a few operations like save. Because there is no need for a roundtrip to the server to process the rules, performance significantly improved.
These changes impacted our Work Item Custom Control Development story significantly as you might have expected. We can categorize the changes in 2 areas: development and deployment. This post covers the development only and deployment is going to be a subject of another blog post.
In TF Web Access 2010, you needed to work on both server and client side when you were developing a custom control. Server side code was needed to interact with the work item object where you needed to inherit your custom control from IWorkItemWebControl. At the same time, JavaScript code needed in order to handle the UI interaction.
In the new version of Web Access, there is no server side development involved. Everything happens in the browser meaning that custom controls are written completely in JavaScript. Along with Web Access framework, jQuery and jQuery UI is also available for custom control developers.
You can start implementing your custom control with an empty JavaScript file. When specifying a name for your .js file, you'll need to have 2 separate versions of your JavaScript file for different flavors which is debug and min. If you chose a name like Acme.VoteButton for your JavaScript file, you'll need to have two files named Acme.VoteButton.debug.js and Acme.VoteButton.min.js. Web Access module loading system will then decide which file to load. We can ignore minified version for now, we’ll talk about more on this in the deployment section.
The next thing you are going to do is define a module for your custom control(s). This is necessary to make your .js file integrate with the Web Access module loader so that it can take the advantage of on-demand loading and automatic flavor selection.
TFS.module("Acme.VoteButton",
[
"TFS.WorkItemTracking.Controls",
"TFS.WorkItemTracking",
"TFS.Core"
],
function () {
// custom control implementation
}
);
Let’s get into details of this expression.
TFS is a global variable belonging to Web Access framework which provides a utility method to define your module.
The first parameter of the module is a string which specifies a namespace for the module. Please note that this namespace should match the filename you specified for your .js file (the part before flavor).
The second parameter is an array of strings which specifies the dependencies of your module. The list in the above line is a typical list for the custom control development. Web Access module loader makes sure that the dependent modules are first loaded before your module gets executed.
The last parameter is a function which gets executed when the module is loaded and the actual module implementation lives in here. Optionally you can expose anything you want from your module by return an object.
The next step is adding shortcuts for common framework objects and functions to the top of main function. This step is optional but it makes the code cleaner and more readable.
var WITOM = TFS.WorkItemTracking,
WITCONTROLS = TFS.WorkItemTracking.Controls,
delegate = TFS.Core.delegate;
Then you can start implementing your custom control. The custom control must to be inherited from WorkItemControl which is provided by TFS.WorkItemTracking.Controls module. You’ll also leverage the inheritance support of Web Access framework by using inherit utility function.
First, you define the constructor of your custom control. You never instantiate your custom control directly. It is going to be instantiated by Web Access. The only thing you need to do is register your custom control and we will get to that soon.
// Constructor for VoteButton
function VoteButton(container, options, workItemType) {
this.baseConstructor.call(this, container, options, workItemType);
}
And then your control implementation takes place by inheriting it from WorkItemControl. WorkItemControl provides a number of functions to be overridden by the custom control which is called during the life cycle of a custom control like bind, unbind, invalidate and flush.
// VoteButton inherits from WorkItemControl
VoteButton.inherit(WITCONTROLS.WorkItemControl, {
_control: 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();
// Initialize your control by creating some UI elements and attaching to events
}
});
Finally, the custom control must be registered using a control name. The control name is the name which is used in the work item type definition. When a work item form is rendered, Web Access looks for a registered control for the specified control name and if exists, it creates an instance of the registered control by providing a container, options and the work item type.
// Register a work item custom control called "VoteButton"
WITCONTROLS.registerWorkItemControl("VoteButton", VoteButton);
Here is a list of most commonly used functions for a work item control:
_init()
This is called when a control is created. At this point, there is no work item bound to the control yet. When you think of Work Items View in Web Access, there is a possibility that a work item can be visible and invisible multiple times (especially when navigating through result of a query using keyboard). A control is not created every time it is displayed. Instead, it is created at first appearance and bound to the current work item. When another work item is displayed, control is unbound from the previous work item and bound to the new one (if the work items are of the same type).
bind(workItem)
This method is called when the control is being bound to a new work item.
unbind(workItem)
This method is called when the control is no longer bound to the specified work item
invalidate(flushing)
This method is called when the control needs to display with the current value (which is a field change caused by work item refresh, revert or save). If flushing is true then the value is being written to the work item field.
getControlValue()
This method is called to get the value of the control to write to the corresponding Work Item Tracking field
cleanup()
This method is called to allow the control to release reference to work item, detach from events, set members to null to free memory which is called after unbind.
clear()
This method is called to set control value to empty which is called after cleanup.
_container
This property is a jQuery object (DIV) which contains the control and sub elements are placed.
_getField()
This method gets the work item form field that corresponds to this control. _getField().getValue() and _getField().setValue(value) are used to read and modify the underlying work item fields. This method returns null if no field is associated with the control in the work item type definition.
And below is the complete content of the sample custom control.
// Register this module as "ACME.VoteButton" and declare
// dependencies on TFS.WorkItemTracking.Controls, TFS.WorkItemTracking and TFS.Core modules
TFS.module("Acme.VoteButton",
[
"TFS.WorkItemTracking.Controls",
"TFS.WorkItemTracking",
"TFS.Core"
],
function () {
// module content
var WITOM = TFS.WorkItemTracking,
WITCONTROLS = TFS.WorkItemTracking.Controls,
delegate = TFS.Core.delegate;
// Constructor for VoteButton
function VoteButton(container, options, workItemType) {
this.baseConstructor.call(this, container, options, workItemType);
}
// VoteButton inherits from WorkItemControl
VoteButton.inherit(WITCONTROLS.WorkItemControl, {
_control: 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();
this._control = $("<button type='submit'>Vote</button>").appendTo(this._container).bind("click", delegate(this, this._onClick));
},
// 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) {
// Get the vote count from the underlying field
var voteCount = this._getField().getValue() || 0;
// Display the number of votes if any
if (voteCount > 1) {
this._control.text(voteCount + " votes");
} else if (voteCount == 1) {
this._control.text(voteCount + " vote");
} else {
this._control.text("Vote");
}
},
// 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._control.text("Vote");
},
// Handle the click event on the vote button
// Note: This is a private method for this control
_onClick: function () {
// Get the vote count from the underlying field
var voteCount = this._getField().getValue() || 0;
// Increment vote count
voteCount++;
// Store new vote count in the underlying field
this._getField().setValue(voteCount);
}
});
// Register this module as a work item custom control called "VoteButton"
WITCONTROLS.registerWorkItemControl("VoteButton", VoteButton);
return {
VoteButton: VoteButton
};
});
In the next post, we are going to talk about the deployment of a Work Item Custom Control.
Let us know if you have any questions or feedback.
Comments
Anonymous
September 04, 2012
Great post! Thanks for the update - the TWA is the single biggest reason for me to want to migrate to 2012, and the ability to customize it will greatly influence my ability to convince my company to do so. Thanks for the very clear and descriptive example!Anonymous
September 09, 2012
Can you please the source code...Anonymous
September 11, 2012
@Pradeep, The whole source exists actually in the last block. Please let me know if you have any specific question.Anonymous
October 04, 2012
Is there already any official documentation available on the client side object model of Web Access? I'm anxious to start working with it, but apart from a good blog post from Tiago Pascoal: pascoal.net/.../team-foundation-task-board-enhancer-version-0-3-released, there's nothing I can find.Anonymous
October 07, 2012
@Jeffry Unfortunately, there is no official documentation available yet for client side OM. I can try to help you offline on your scenario, if you contact me at serkani [at] microsoft [dot] com.Anonymous
January 06, 2013
nice post, I want to ask if there is a way to debug the code written in the js file?? am trying to create a custom control that is using XmlHttpRequest and I'm not able to know why it does not work.Anonymous
January 09, 2013
Does anybody know how we can enable debug javascript? i.e. instead of the minified javascript showing up in the browser, we would see the debug.js version.Anonymous
January 09, 2013
The comment has been removedAnonymous
January 22, 2013
I need to be able to hook into the "save" and "Save and close" event. How do I do this?Anonymous
August 06, 2013
Thanks for the development post and deployment post. I have everything working perfectly, but there is one anomally. I have created radio Yes No buttons that are established on the control. When the new form opens, not a problem the controls function perfectly. When you recall from the main project search page, renders perfectly. When you open in the query preview, no problem. When you open from the query, the rendering process must hit an additional function and unselects both buttons on the control. I have added an alert function to the invalidate process and confirms it is hitting it which should take the value from the work item and post it correctly. When it appears, it is blank, but the old value is in the form and work item. If there is something I can send you to show you what is happening I will.Anonymous
August 21, 2013
The comment has been removedAnonymous
November 10, 2013
Hi! I need to know the same as Rupinder. I have some custom controls on TWA 2010 which run queries. Will I be able to do it on this new Object Model? I mean... Is it possible to run a query on TWA 2012/2013? tksAnonymous
March 05, 2014
I have written articles to get this working on visual studio as well as web refer to www.codeproject.com/.../Close-a-Work-Item-only-if-Child-Work-items-are-c-2 www.codeproject.com/.../Close-a-Work-Item-Only-if-Child-Work-Items-are-CAnonymous
April 03, 2015
Hi How can I have background color (for highlight text) in description toolbar