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.