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.
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.
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
|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.|
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.
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.
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="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://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&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.
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.
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.
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|
|User Stories Without Test Cases|
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
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.