October 2009

Volume 24 Number 10

Team System - Customizing Work Items

By Brian A. Randell | October 2009

Team Foundation Server’s work item tracking system provides a number of advanced customization options. If you’re new to work item customization and the work item object model, you can review my earlier articles (see msdn.microsoft.com/en-us/magazine/cc301186.aspx) as well as the documentation. The area I’ll cover in this article is custom control support. To start, why would you want to use a custom control in your work items? Well, you might want a better user experience, you might want to link data from another external system (like a help desk application) or you might want to present existing work item data in chart format. Whatever the reason, custom controls can help.

Custom Controls

In the 2005 SP1 release of Team System, Microsoft added support for custom controls when you use the standard work item renderer within Visual Studio or your own custom Windows applications. In the 2008 release, Microsoft added support for Web-based custom controls for Team System Web Access. You write a custom control as a combination of user interface and logic that interacts with the work item API. Custom controls hosted inside the standard work item renderer are classes that inherit from System.Windows.Forms.Control and implement the IWorkItemControl interface. For Team System Web Access, you create a class that inherits from System.Web.UI.Control and implements IWorkItemWebControl.

To get started, you need Visual Studio 2008 Professional or higher, the Visual Studio Team System 2008 Team Explorer, and access to a Team Foundation server. As I’ve mentioned in previous columns, I encourage you to use a test environment like one of the virtual machine images provided by Microsoft. Inside Visual Studio, create a class library project, and then rename the default class added by the template to Rank. This custom control will provide a combo-box interface for the built-in Rank field used in the built-in Task work item type. The Microsoft-provided process guidance states: “Required. The rank field is a subjective importance rating. If the rank field is set to 1, the task is a must-have task and should be completed as soon as possible. If the rank is set to 2, the task is a should-have task, to be completed following all tasks ranked 1. If the rank is set to 3, it’s a could-have task, to be completed after tasks ranked 1 and 2.” However, the Rank field when rendered uses an open text field that allows the user to enter random data. Using a combo box allows you to restrict the values entered by a user.

You need to add references to a number of assemblies before you can write any code. First, you need a reference to System.Windows.Forms. Second, you need to reference a couple of Team System assemblies. To start, add a reference to Microsoft.TeamFoundation.WorkItemTracking.Controls.dll, located in %Program Files%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies.

At the top of the class file, add the following Imports statements:

Imports System.Windows.Forms
Imports Microsoft.TeamFoundation.WorkItemTracking.Controls

Because you’re going to replace the existing text box control with a combo box, you want the Rank class to inherit from ComboBox. Next, in order for the work item renderer to host your control, you need to implement the IWorkItemControl interface. The interface, shown in Figure 1, consists of two events, four methods and four properties. Figure 2 provides a breakdown of each member and its purpose.

Figure 1 The IWorkItemControl Interface

Public Interface IWorkItemControl
    Event AfterUpdateDatasource As EventHandler
    Event BeforeUpdateDatasource As EventHandler
    Sub Clear()
    Sub FlushToDatasource()
    Sub InvalidateDatasource()
    Sub SetSite(ByVal serviceProvider As IServiceProvider)
    Property Properties As StringDictionary
    Property [ReadOnly] As Boolean
    Property WorkItemDatasource As Object
    Property WorkItemFieldName As String
End Interface
Member Description
AfterUpdateDatasource Raises this event after you’ve made changes to the work item value.
BeforeUpdateDatasource Raises this event before you make any changes to the work item value.
Clear Clears your display elements of any data.
FlushToDatasource Saves any data displayed in the control to the work item; usually occurs as part of a work item save operation.
InvalidateDatasource Reloads and displays current data from the work item.
SetSite Provides a reference to the IServiceProvider if you need to access services within Visual Studio; its value can be null.
Properties A property bag containing relevant attributes for the particular field from the XML definition of the hosting work item type.
ReadOnly Indicates that the control should be displayed in read-only mode.
WorkItemDatasource A reference to the active work item instance; you need to cast it to a WorkItem instance.
WorkItemFieldName The name of field to which the control is bound.

Figure 2 Members of IWorkItemControl

For each of the interface properties, you need to create a corresponding backing field. To store the WorkItemDatasource as a strongly typed reference, you need to add a reference to the Microsoft.TeamFoundation.WorkItemTracking.Client.dll assembly stored at %Program Files%\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies. Once you’ve added this reference, you should also import the Microsoft.TeamFoundation.WorkItemTracking.Client namespace. After you’ve linked the properties with their backing fields, you can implement the interface methods.

However, before you do, you need to define the data elements for the combo box and address the question whether you want to easily adjust the values used. If so, you should implement a mechanism such as a Web service that allows the control to retrieve the most current values. In addition, if you’re going to display string values, you should consider localization issues. For this example, however, we’ll just add a default constructor that hardcodes the values, shown in the following. Note that the work item runtime supports only controls that have a default, no arguments constructor.

Public Sub New()
  MyBase.New()

  Me.Items.Add("1 - Must Have")
  Me.Items.Add("2 - Should Have")
  Me.Items.Add("3 - Could Have")
End Sub

You should also provide an implementation for the Clear method. By default, when the work item runtime loads the control, the displayed value is blank. To return to this state, you need to set the SelectedIndex property to -1 in the Clear method. The last two methods you need to implement relate to loading the value of the bound work item field to and from the work item instance and then persisting the value if the user saves the work item. You use the InvalidateDataSource method to refresh your control’s display data from the current work item or clear out previously loaded data. The work item runtime calls FlushToDataSource when it’s saving the active work item. Figure 3 provides an implementation for both methods for the Rank custom control.

Figure 3 Implementation of FlushToDatasource and InvalidateDataSource

Public Sub FlushToDatasource() _
  Implements IWorkItemControl.FlushToDatasource
 
  If SelectedIndex > -1 And workItemDS IsNot Nothing Then
    RaiseEvent BeforeUpdateDatasource(Me, EventArgs.Empty)
    workItemDS.Fields(fieldName).Value = CStr(SelectedIndex + 1)
    RaiseEvent AfterUpdateDatasource(Me, EventArgs.Empty)
  End If
End Sub

Public Sub InvalidateDatasource() _
  Implements IWorkItemControl.InvalidateDatasource
  If workItemDS IsNot Nothing AndAlso _
    Not String.IsNullOrEmpty(workItemDS.Fields(fieldName).Value) Then

    Dim current As Integer = CInt(workItemDS.Fields(fieldName).Value)
    ' Make sure the value returned does not exceed our valid range
    ' If it does, set it to our most important value
    If current > Me.Items.Count Then
      current = 1
    End If
    SelectedIndex = (current - 1)
  Else
    Clear()
  End If
End Sub

FlushToDataSource checks whether the user has selected a value and that the reference to the active work item is still valid. If so, it fires the BeforeUpdateDataSource event, writes the current value to the work item, and follows this by a call to AfterUpdateDataSource. InvalidateDataSource checks whether there’s a valid reference to an active work item. If so, it checks to see if there’s a value currently stored in the bound field, retrieves the value if there is and casts it to an integer because it’s stored as a string. Finally, it does a range check to ensure that the value doesn’t exceed the supported range of the control—in this example, 1 to 3. Here, the code simply sets the value to 1 if it’s outside the supported range. As an alternative, you could provide some user interface to allow the user to pick a supported value or some other experience. Finally, if there’s no active work item or if the user has not made a choice, the code calls the Clear method to reset the control. Figure 4 provides the complete implementation of the Rank control.

Figure 4 The Rank Control

Imports System.Windows.Forms
Imports Microsoft.TeamFoundation.WorkItemTracking.Controls
Imports Microsoft.TeamFoundation.WorkItemTracking.Client
Imports System.Collections.Specialized

Public Class Rank
  Inherits ComboBox
  Implements IWorkItemControl
  ' Backing fields
  Private fieldName As String
  Private fReadOnly As Boolean
  Private serviceProvider As IServiceProvider
  Private workItemDS As WorkItem
  Private workItemProperties As StringDictionary

  Public Event AfterUpdateDatasource( _
    ByVal sender As Object, ByVal e As System.EventArgs) _
    Implements IWorkItemControl.AfterUpdateDatasource

  Public Event BeforeUpdateDatasource( _
    ByVal sender As Object, ByVal e As System.EventArgs) _
    Implements IWorkItemControl.BeforeUpdateDatasource

  Public Sub New()
    MyBase.New()
 
    Me.Items.Add("1 - Must Have")
    Me.Items.Add("2 - Should Have")
    Me.Items.Add("3 - Could Have")
  End Sub

  Public Sub Clear() Implements IWorkItemControl.Clear
    SelectedIndex = -1
  End Sub

  Public Sub FlushToDatasource() _
    Implements IWorkItemControl.FlushToDatasource

    If SelectedIndex > -1 And workItemDS IsNot Nothing Then
      RaiseEvent BeforeUpdateDatasource(Me, EventArgs.Empty)
      workItemDS.Fields(fieldName).Value = CStr(SelectedIndex + 1)
      RaiseEvent AfterUpdateDatasource(Me, EventArgs.Empty)
    End If
  End Sub

  Public Sub InvalidateDatasource() _
    Implements IWorkItemControl.InvalidateDatasource
    If workItemDS IsNot Nothing AndAlso _
      Not String.IsNullOrEmpty(workItemDS.Fields(fieldName).Value) Then
 
      Dim current As Integer = CInt(workItemDS.Fields(fieldName).Value)
      ' Make sure the value returned does not exceed our valid range
      ' If it does, set it to our most important value
      If current > Me.Items.Count Then
        current = 1
      End If
      SelectedIndex = (current - 1)
    Else
      Clear()
    End If
  End Sub

  Public Property Properties() As StringDictionary _
    Implements IWorkItemControl.Properties
    Get
      Return workItemProperties
    End Get
    Set(ByVal value As System.Collections.Specialized.StringDictionary)
      workItemProperties = value
    End Set
  End Property

  Public Property IsReadOnly() As Boolean _
    Implements IWorkItemControl.ReadOnly
    Get
      Return fReadOnly
    End Get
    Set(ByVal value As Boolean)
      fReadOnly = value
      Enabled = (Not fReadOnly)
    End Set
  End Property

  Public Sub SetSite(ByVal serviceProvider As IServiceProvider) _
    Implements IWorkItemControl.SetSite
    Me.serviceProvider = serviceProvider
  End Sub

  Public Property WorkItemDatasource() As Object _
    Implements IWorkItemControl.WorkItemDatasource
    Get
      Return workItemDS
    End Get
    Set(ByVal value As Object)
      workItemDS = TryCast(value, WorkItem)
    End Set
  End Property

  Public Property WorkItemFieldName() As String _
    Implements IWorkItemControl.WorkItemFieldName
    Get
      Return fieldName
    End Get
    Set(ByVal value As String)
      fieldName = value
    End Set
  End Property
End Class

Before you can try out the control, you need to take three additional steps. First, you need to provide a simple XML configuration file that defines the control for the work item runtime. Second, you need to deploy the control’s assembly and configuration file (as well as the PDB if you want to debug) to one of the valid locations supported by the runtime. Finally, you need to update the type definition for a work item that uses the Rank field and add the correct information to load the control.

Deploy and Debug

Each custom control you create needs to have a WICC file. You need to create a simple XML file, like the one shown here, that defines the host assembly and the class name of your control.

<?xml version="1.0" encoding="utf-8" ?>
<CustomControl xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
 xmlns:xsd="https://www.w3.org/2001/XMLSchema">
  <Assembly>BrianRandell.MSDNMag.WICC.Winforms.dll</Assembly>
  <FullClassName>BrianRandell.MSDNMag.WICC.Winforms.Rank</FullClassName>
</CustomControl>

Once you’ve created this file, you should copy the WICC file, your DLL and its PDB to one of two locations. If you want the control to be visible only to your account, copy it to %Local Application Data%\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\9.0. To make the control visible to all users on a particular workstation, copy the files to %Common Application Data%\Microsoft\Team Foundation\Work Item Tracking\Custom Controls\9.0. You’ll most likely need to create the folder structure under the Microsoft folder. You can use System.Environment.GetFolderPath with either the Environment.SpecialFolder.LocalApplicationData value or Environment.SpecialFolder.CommonApplicationData to figure out the paths on your local workstation. Once you’ve copied the files to one of these directories, you need to modify the type definition for a work item—in this example, the Task work item from the MSF Agile template—to use the control. (See my article at  msdn.microsoft.com/en-us/magazine/dd221363.aspx for more information.)

You can modify the form layout information for the Rank field as shown in the following code. At run time, the work item plumbing passes all the attributes, listed as part of the field’s layout definition, to your control via the IWorkItemControl.Properties property. The only change you need to make is to change the Type attribute from the default FieldControl to Rank, the name of the newly created control. Once you’ve made the change and imported the new work item type definition into a Team Project, you can try things out.

<Control Type="Rank" FieldName="Microsoft.VSTS.Common.Rank" 
Label="Ran&amp;k:" LabelPosition="Left" 
NumberFormat="WholeNumbers" MaxLength="10"/>

To debug, you need to have copied the most current bits to the correct deployment folder and imported the modified type definition into a Team Project. Then, you should modify the Debug settings for your project to use the “Start external program” option and point it to devenv.exe (see Figure 5). Now you can set some breakpoints and start debugging.

Figure 5: Choose the Set External Program Option
Figure 5 Choose the Set External Program Option.

Production Deployment

After you’ve worked out any kinks in your control, you need to have it deployed to workstations for everyone who will use the affected work item. You can do this a number of ways. You can build an installation program using the built-in Visual Studio template, or you can create a simple script that runs as part of a user’s login script and xcopy the files into place. Finally, if you’ve already deployed the October 2008 release of the Team Foundation Server Power Tools, you can use the deployment feature the Power Tools provide. To use this feature, you need to check in your assembly and WICC file to a folder %Team Project Name%/TeamProjectConfig/CustomControls. Then, you just need to access the Personal Settings option on the Team Members node and select the “Install downloaded custom components” option. Note that you’re encouraged to strongly name your assemblies if you use this feature. Microsoft provides more information on this feature in the Power Tools help files.

Caveats

At this point, you’ve seen the advantages of custom controls. Now a few caveats. The first big issue is host support. We’ve created a custom control that runs only within Visual Studio 2008 or an application that hosts the Microsoft provided custom control. If you want to support Visual Studio 2005, you need to build a version against the 2005 version of Team Explorer and deploy the assembly and WICC files separately. And what about Team System Web Access? In theory, if you use only Internet Explorer as your Web browser, this could work, but it doesn’t. What you see is similar to Figure 6.


Figure 6  Support for Visual Studio 2005 Requires Extra Steps

The good news is that the 2008 release of Team System Web Access supports its own custom control model. You’ll find documentation with samples under %Program Files%\Microsoft Visual Studio 2008 Team System Web Access\Sdk on the machine where you installed Team System Web Access. However, this works only if you’re running the 2008 version; the 2005 version doesn’t support custom controls. In addition, a third-party product like Teamprise doesn’t either. The best solution is to define multiple LAYOUT elements in your work item’s type definition. You then need to duplicate the definition for each unsupported or different host. You’ll then want to decorate the LAYOUT element with a Target attribute. For Windows, use WinForms for the value. For Team System Web Access, use Web. For Teamprise, use Teamprise. See msdn.microsoft.com/en-us/library/aa337635(VS.80).aspx for more information.

Using the custom layouts solves the rendering problem, but it also shines a light on a more subtle issue. Hosts like Microsoft Excel and Microsoft Project don’t support custom controls at all. They are unaffected by adjustments made to the LAYOUT element of a work item type. Thus, any host that doesn’t support your custom control will expose the raw data through its standard interface. In the case of the Rank field, that means a standard edit field. Each host will enforce the rules defined for the work item. However, custom logic you implement in your custom control will not. Thus, if you do go down the road of using custom controls, you need to educate your users on proper data entry when running outside supported hosts and be sure that your control performs proper data validation.

Team System 2010

The upcoming 2010 release of Team System promises to be full of client and server goodness. In May 2009, Microsoft released Beta 1 for review. As I write this column, Microsoft has promised a second beta sometime in the autumn of 2009. While there are still some changes to be made, the work item tracking feature is very mature. Arguably, the most requested feature provided by Microsoft is hierarchical work items. That said, I am using the Beta 1 release, so anything and everything I discuss here is subject to change between now and the final release.

Earlier releases of Team Foundation Server support the notion of linking work items. When you link a work item to another work item, you create a bidirectional relationship. Any work item can be linked to any other work item. While useful, this feature doesn’t provide parent-child relationships. In addition, you might also want to be able to define precedence, something very common when using Microsoft Project to manage tasks. Thankfully, both of these areas are addressed in the 2010 release. To support the new link semantics, Microsoft created two new work item query types: Work Items and Direct Links (Links query) and Tree of Work Items (Tree query). When you create a new work item query, you can choose one of these new types or choose Flat List of Work Items, which represents the standard type of query available prior to the 2010 release.

To get started, take a look at the new built-in work item types and work item queries. As it has with previous releases, Microsoft is currently providing two process templates. At this stage in the development cycle, most of their work is visible in the MSFT for Agile Software Development v5.0 template. Microsoft promises that the CMMI-based version will show significant updates in the Beta 2 release. Thus, I’ll be using the Agile template as my reference point. Once you’ve created a new Team Project and expanded the Work Items node, you’ll find the familiar My Queries and Team Queries nodes. You’ll notice two subtle enhancements in the Team Explorer window. First, Microsoft changed the sort order: My Queries sorts before Team Queries. Second, Microsoft customized the icons used for each node. If you open the Team Queries node, you’ll find the list of predefined queries has changed dramatically (see Figure 7). In addition, you’ll note that Microsoft has added a new folder, Workbook Queries. This folder contains queries that support the new Excel workbooks feature. You’re free to run the queries, but you should not modify them; otherwise, you could break one or more of the workbooks that depend on these particular queries.

TFS 2008 TFS 2010 Beta 1
Active Bugs Active Bugs
All Issues My Bugs
All Quality of Service Requirements My Tasks
All Scenarios My Test Cases
All Task My Work Items
All Work Items Open Issues
My Work Items for All Team Projects Open Tasks
Project Checklist Open Test Cases
Resolved Bugs Open User Stories
Untriaged Bugs Open Work Items
  P1 and P2 Active Bugs
  Resolved Bugs
  User Stories Without Test Cases

Figure 7 Predefined Queries in Team System 2010

You’ll see from the new names that Microsoft has made deep changes to the entire template. There are new work item types, like User Story (see Figure 8) and My Test Case. Microsoft has listened to the community and applied tons of feedback (like using more commonly adopted terms), made simplifications and improved reporting. New Excel-based workbooks, alluded to earlier, provide rich project tracking without needing SQL Server Reporting Services, with better estimating and resource tracking and new charts like burn-down charts. As Microsoft moves closer to release, I’ll dig further into the 2010 release, including how to upgrade your customized templates and your custom code.


Figure 8 The User Story Predefined Query

Wrapping Up

Work item custom controls in Team Foundation Server provide a nice mechanism to enhance the usability of work items in your teams. While this feature is not perfect, Microsoft continues to make it better. In fact, as I look forward to the 2010 release of Team System, I see a very bright future for Team System as a whole and especially work item tracking.