Team System
Work Item Tracking
Brian A. Randell
Code download available at: Team System 2007_04.exe(179 KB)
Contents
Team Foundation Servers, Team Projects, and Work Items
Getting Connected
Accessing the WIT Store
Working with Queries
Presenting and Editing Work Items
Conclusion
In my previous column, I started to describe how you can build a source-code control add-in for Microsoft® Word 2003 using the APIs exposed in Team System. If you examine the check-in dialog exposed by Team Explorer in Visual Studio® 2005, you'll notice that the integrated check-in experience is quite rich. Not only can you check in source files, but you can also associate your check-in with work items, add check-in notes, and validate your check-in against policy. Figure 1 shows the standard check-in dialog with the Work Items option selected.
Figure 1** Team Explorer Integrated Check-In Dialog **(Click the image for a larger view)
This is deceptively simple on the surface-in reality, this part of the check-in experience exposes a great number of work item features. For example, you can change the query used to display the work item list. You can drill down into a particular work item and manipulate it using the standard work item UI by double-clicking on it. And you can perform a check-in action, which you use to either associate the work item with the check-in's changeset or perform another action, such as marking the work item as complete.
Before you can implement these features in the add-in, you'll need a good understanding of the work item API. In this column, I'll explain how to build a simple Work Item Explorer (see Figure 2). This sample demonstrates the core operations needed to add work item support to the add-in. Due to space constraints, I'll only cover the core code needed to access the work item services of Visual Studio 2005 Team Foundation Server. I will not detail the sample's Windows® Forms-related code. (All the code is, however, provided in the download.) In the next installment of the Team System column, I'll show you how to add work item support to the add-in.
Figure 2** Simple Work Item Explorer **(Click the image for a larger view)
Team Foundation Servers, Team Projects, and Work Items
The work item tracking subsystem is a core feature of a Team Foundation Server installation. In order to use work items, you need at least one team project, which you create using the New Team Project wizard. When using this wizard, you specify the process template to use (a blueprint that describes the structure your team project will be based upon).
Of particular interest to this discussion, the process template you choose defines the default types of work items your team project will initially support. You can choose to customize the existing work item types or add your own. Work items provide a way for your team members to track just about anything related to the team project, including, but not limited to, bugs and tasks.
A default Team Foundation Server installation includes two process templates: Microsoft Solutions Framework for Agile Software Development version 4.0 (MSF Agile for short), and MSF for CMMI Process Improvement version 4.0 (MSF CMMI for short). Figure 3 provides an overview of the default types of work items, what each is for, and with which templates each is included.
Figure 3 Default Work Item Types
Work Item Type | Description | MSF Agile | MSF CMMI |
---|---|---|---|
Bug | Communicates that a potential problem exists or has existed in the system. | ||
Change Request | Identifies a proposed change to some part of the product or baseline. | ||
Issue | Documents an event or situation that may block work or is currently blocking work on the product. | ||
Quality of Service Requirement | Documents characteristics of the system, such as performance, load, availability, stress, accessibility, serviceability, and maintainability. | ||
Requirement | Captures and tracks what the product needs to do to solve the customer problem. | ||
Review | Documents the results of a design or code review. | ||
Risk | Identifies any probable event or condition that can have a potentially negative outcome on the project in the future. | ||
Scenario | Records a single path of user interaction through the system. | ||
Task | Communicates the need to do some work. |
Each work item is composed of a set of fields. Team Foundation Server scopes fields to the entire server installation. You can create your own custom process template and thus your own custom work items either from scratch or by modifying the templates provided by Microsoft. When defining your own custom work items, you can also define custom field definitions. This, however, is a discussion for another article.
Getting Connected
The add-in currently connects to the Team Foundation Server installation and its version control service via the Connect method in the tfsvcUtil class. In addition, it uses a custom Windows Forms dialog to get the server connection string from the user. You can use that same code with a few slight modifications to include a connection to the work item tracking (WIT) store. First, you need to add a reference to the Microsoft.TeamFoundation.WorkItemTracking.Client.dll assembly. Then, in the tfsvcUtil class, you need add the following Imports statement:
Imports Microsoft.TeamFoundation.WorkItem-Tracking.Client
You must also provide a class-level variable defined to hold a reference to the WIT store:
Private m_tfsWIT As WorkItemStore
And finally, you need to add an additional line of code to the Connect method so it can connect to the WIT store (see Figure 4).
Figure 4 Connecting to the WIT Store
Public Shared Sub Connect( _
ByVal fqServerName As String, ByVal showLoginUI As Boolean)
Try
m_serverName = fqServerName
If showLoginUI Then
Dim icp As ICredentialsProvider = New UICredentialsProvider
m_tfs = TeamFoundationServerFactory.GetServer( _
m_serverName, icp)
Else
m_tfs = TeamFoundationServerFactory.GetServer(m_serverName)
End If
m_tfs.Authenticate()
' Version Control Server
m_tfsVCS = CType(m_tfs.GetService( _
GetType(VersionControlServer)), _
VersionControlServer)
' Work Item Store
m_tfsWIT = CType(m_tfs.GetService( _
GetType(WorkItemStore)), WorkItemStore)
serverValidated = True
Catch ex As Exception
Throw New tfsUtilException( _
String.Format(MSG_UNEXPECTED, "Connect"), ex, m_serverName)
End Try
End Sub
Assuming a valid TeamFoundationServer reference exists, all it takes to connect to the work item store is to call the TeamFoundationServer's GetService method and pass the type object for the WorkItemStore. If you dig deeper into the Team Foundation Server APIs, you'll discover the DomainProjectPicker class-a hidden gem in the Proxy namespace within the Microsoft.TeamFoundation.dll assembly. This class exposes a GUI that lets you define Team Foundation Server connections and optionally access team projects defined on a server. The sample application uses this class rather than the aforementioned code. The sample app needs references to the following Team Foundation Server assemblies to perform its tasks:
- Microsoft.TeamFoundation.dll
- Microsoft.TeamFoundation.Client.dll
- Microsoft.TeamFoundation.Common.Library.dll
- Microsoft.TeamFoundation.WorkItemTracking.Client.dll
- Microsoft.TeamFoundation.WorkItemTracking.Controls.dll
The main form (frmMain.vb) of the sample application has six buttons, a combobox to list the selected team projects, a combobox to list the available work item types for the active team project, and a few labels to provide status information. You need to add the following Imports statements to the top of frmMain.vb:
Imports Microsoft.TeamFoundation.Client
Imports Microsoft.TeamFoundation.Common
Imports Microsoft.TeamFoundation.Proxy
Imports Microsoft.TeamFoundation.Server
Imports Microsoft.TeamFoundation.WorkItemTracking.Client
You must also add code to the click event handler in the Pick Team Foundation Server button, as shown in Figure 5 (note that the GUI management-related code has been left out for space purposes). This code displays the default Connect to Team Foundation Server dialog. The constructor of the DomainProjectPicker class provides an enumeration to control the style of the dialog. For example, you can choose to show the dialog with team project selection turned off. However, for this sample the default behavior is what you want. Figure 6 displays the results of the call to ShowDialog using the default constructor.
Figure 5 Using the DomainProjectPicker Class to Connect
Private Sub btnPickTFS_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles btnPickTFS.Click
Dim dpp As New DomainProjectPicker()
Select Case dpp.ShowDialog(Me)
Case Windows.Forms.DialogResult.OK
mwis = Nothing
mprj = Nothing
mtfs = dpp.SelectedServer()
Me.lblTFS.Text = mtfs.Name
Me.cboTeamProjects.DataSource = dpp.SelectedProjects()
Me.cboTeamProjects.DisplayMember = "Name"
If dpp.SelectedProjects.Length > 0 Then
Me.cboTeamProjects.Enabled = True
Me.btnConnectWIS.Enabled = True
End If
End Select
End Sub
Figure 6** Connect Dialog Box **
When you click the Servers button, the dialog displays the built-in Add/Remove Team Foundation Server dialog. If you select one or more team projects and then click OK, the code binds the list of selected projects to the Team projects combobox control.
Accessing the WIT Store
Once you have a valid connection to the server, you can connect to the WIT store. In the click event handler of the Connect to Work Item Store button, you need to use the GetService method as discussed earlier to connect to the WIT store. Once connected, the code initializes a class-level Project reference using the currently selected team project name:
If mtfs IsNot Nothing Then
mwis = CType(mtfs.GetService( _
GetType(WorkItemStore)), WorkItemStore)
End If
mprj = mwis.Projects.Item(Me.cboTeamProjects.Text)
Once you have a connection to the WIT store, you can get information from the server. The private method UpdateInfoData, shown in Figure 7, performs this task and displays the data on the main form. The code first gets the selected project's list of work item types as an array of WorkItemTypes objects. It binds the array to the Type combobox, and then the code uses the WIT store's FieldDefinitions collection to get a count of the server's field definitions. Next, the code retrieves the count of stored queries, both public and private, available for the identity connecting to the server. Finally, the code constructs a dynamic Work Item Query Language (WIQL) query to get a list of work items available to the identity connecting. (More details on WIQL in a moment.) Note that the code gets the list of field definitions and the work item count via the WIT store reference, whereas it uses the active team project reference to retrieve the work item types and stored queries.
Figure 7 UpdateInfoData Listing
Private Sub UpdateInfoData()
If mprj IsNot Nothing Then
Me.cboWIType.DataSource = mprj.WorkItemTypes
Me.cboWIType.DisplayMember = "Name"
Me.cboWIType.Enabled = True
Me.lblFieldCount.Text = mwis.FieldDefinitions.Count.ToString()
Me.lblSQCount.Text = mprj.StoredQueries.Count.ToString()
Dim wiql As String = String.Format( _
"SELECT [System.Id] FROM WorkItems " & vbCrLf & _
"WHERE [System.TeamProject] = '{0}'", mprj.Name)
Me.lblWICount.Text = mwis.QueryCount(wiql).ToString()
End If
WIQL is a SQL-like language. Full syntax reference is available online at msdn2.microsoft.com/bb130155.aspx. You can execute WIQL queries either as a dynamic WIQL statement or from a stored query. You can publish queries to your Team Foundation Server, making them available as public queries for your entire team or as private queries for your use only.
A valid query returns a collection of WorkItems. The WIT only returns those work items that match the query request and are visible to the identity executing the query based upon any security restrictions. Note is that the WIT does not throw an exception if the resultset is restricted due to security. Each WorkItem type has a collection of Field objects; each field in turn maps to a FieldDefinition object that provides the metadata for that particular field.
Constructing a valid WIQL query requires a valid list of field names using either the field name or its reference name. The field name is the friendly version and, when composed of multiple words, usually has spaces. When used in a WIQL query, a field name with spaces needs to be enclosed in square brackets. The reference name is a fully qualified name of the field that resembles the format used for .NET types. For example, a work item's Title uses the friendly name of Title and the reference name of System.Title. As another example, the Integration Build field's name is Integration Build, and its reference name is Microsoft.VSTS.Build.IntegrationBuild. Its label on the CMMI templates Change Request work item form is Integrated In. You can examine all the field definitions for a particular server accessing the FieldDefinitions of a valid WorkItemStore reference. If you run the sample application and click the Show Fields List, you can view a list of all the fields defined and drill down into each item. You could use this information to help you construct WIQL queries or even build your own query-build UI.
Working with Queries
Each of the two process templates included in the default Team Foundation Server installation contains a set of predefined WIQL queries. The MSF Agile template includes 11, while the MSF CMMI template includes 16. You access stored queries on a per-project basis. Clicking the Show Stored Queries button in the sample application loads a form listing all the stored queries for the active team project. A listbox on the form displays all the stored queries by name. Double-clicking on a query will open a detail form similar to the one shown in Figure 8. If you examine the queries provided by Microsoft, you'll see that they use the reference names when referring to work item fields.
Figure 8** Stored Query Details for the All Tasks Query **(Click the image for a larger view)
The detail form has two buttons: one that executes the query and returns the results in a grid and one that returns the results in a list view. The WIT API supports numerous ways to execute a WIQL query. The detail form shown in Figure 8 uses two different techniques to execute the query. If you click the Execute Query (Grid) button, the code preps the WIQL statement and passes it to the frmWIList that actually executes the query. When you click the Execute Query (List) button, the query is prepared differently and executed using the Query method of the stored query's project's store reference (see Figure 9).
Figure 9 Executing a WIQL Query
Private Sub btnExecuteGrid_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles btnExecuteGrid.Click
Dim wiqlToExecute As String = mSQ.QueryText.ToLower()
If wiqlToExecute.Contains(MACRO_PROJECT) Then
wiqlToExecute = wiqlToExecute.Replace( _
MACRO_PROJECT, "'" & mSQ.Project.Name & "'")
End If
Using f As New frmWIList(mSQ.Project.Store, wiqlToExecute)
f.ShowDialog(Me)
End Using
End Sub
Private Sub btnExecuteList_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles btnExecuteList.Click
Dim wiqlToExecute As String = mSQ.QueryText.ToLower()
Dim params As New Hashtable
Dim wic As WorkItemCollection = Nothing
If wiqlToExecute.Contains(MACRO_PROJECT) Then
params.Add(MACRO_PROJECT.Substring(1), mSQ.Project.Name)
wic = mSQ.Project.Store.Query(wiqlToExecute, params)
Else
wic = mSQ.Project.Store.Query(wiqlToExecute)
End If
If wic.Count = 0 Then
MessageBox.Show("The query didn't return any Work Items.", _
"Warning", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Else
Dim wi As WorkItem = wic.Item(0)
Dim d As New frmAWorkItem(Nothing)
Dim title As String = _
String.Format("{0}'s Work Items: {1}", _
mSQ.Name, wic.Count)
Using f As New frmListBox(title, wi, wic, "Title", d)
f.ShowDialog(Me)
End Using
End If
End Sub
Both pieces of code prepare the WIQL by checking for a predefined query macro: @project. (Any string in a WIQL statement that is prefixed with an @ sign is considered to be a macro.) The WIT plumbing automatically expands certain built-in macros, such as @today. Your own macros and the @project macro require manual substitution.
Depending on how you execute the query, you can have the WIT API perform macro substitution or you can do it yourself. The code in btnExecuteGrid performs simple manual substitution looking only for the @project macro. However, btnExecuteList lets the WIT API do the heavy lifting. For production code, using the API is recommended because, with the substitution approach, you might inadvertently do an incorrect substitution (such as if you were substituting for @project, but there was an occurrence of a superstring, such as @projected).
If you look at the results of each button click and the APIs used, you'll see that the results you're looking for often force you to go down a particular path. In the sample application, the grid detail form uses a WorkItemResultGrid control (more on this in just a bit) that only supports a WIQL string as input limiting the API choices. The list view, on the other hand, uses standard data binding to hook the WorkItemCollection to a standard Windows Forms ListBox control. Still, the end result is the same-executing the query returns a collection of WorkItem objects.
Presenting and Editing Work Items
If you can execute a WIQL query-either dynamically or from a stored query-you have the ability to view and manipulate work items using the work item object model. At some point, however, you might need to present a UI so the user can enter, edit, or view work item information. In fact, if you perform a check-in using the Team Explorer integrated check-in experience, you'll find that double-clicking on a work item brings up the standard work item editor. This is exactly the feature you want to have in the add-in that you're building.
Microsoft has exposed the main work item experience via user controls within the Microsoft.TeamFoundation.WorkItemTracking.Controls.dll assembly. The sample application uses three of the controls: WorkItemResultGrid (which is visible when clicking the btnExecuteGrid button on the frmStoredQueryDetails.vb form), PickWorkItemsControl (which lets you execute stored queries and searches), and WorkItemFormControl (which is visible when you double-click on a work item from the grid view or list view forms). The WorkItemFormControl control is shown in Figure 10.
Figure 10** WorkItemFormControl **(Click the image for a larger view)
These three controls do all the heavy lifting when it comes to searching, viewing, and editing work items. While you can add the controls to your toolbox, the sample application sets up the controls dynamically. There isn't any real design-time experience. The sample application uses the PickWorkItemsControl when you click the Search for Work Items button on the main form.
The control needs a reference to a valid WIT store object and a reference to the active project. From there it does all the hard work. The sample form that hosts the control hooks the control's PickWorkItemsListViewDoubleClicked event to show the work item using the WorkItemFormControl, as shown in Figure 10.
WorkItemFormControl exposes an Item property where you set the active work item that should be displayed. You have full access to each field and its dirty state. You can also reset the work item's values to their original queried values by using the Reset method. But keep in mind that it is up to you to implement your own save and cancel logic.
Figure 11 provides the relevant bits of code necessary to do just that. Note that if the user cancels the editing experience via the Cancel button or by using the window adornments, the code resets the work item. This is an important point if you're using a collection of WorkItems. The control takes changes in the UI and transfers them to the work item immediately. If you don't reset, the work item is dirty, and if it is reopened by the user, it will still have that information.
Figure 11 Handling Save and Cancel
Imports Microsoft.TeamFoundation.WorkItemTracking.Client
Imports Microsoft.TeamFoundation.WorkItemTracking.Controls
Friend Class frmAWorkItem
Implements IProcessDoubleClick
Private IsNewWI As Boolean = False
Private IsCanceled As Boolean = False
Private wifCtl As WorkItemFormControl = Nothing
Private Sub frmAWorkItem_FormClosing( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.FormClosingEventArgs) _
Handles Me.FormClosing
If wifCtl.Item.IsDirty AndAlso (Not IsCanceled) Then
Select Case MessageBox.Show( _
"You made changes to the work item." & vbCrLf & _
"Would you like to save your changes?", "Save Changes", _
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question)
Case Windows.Forms.DialogResult.Yes
e.Cancel = (Not Me.Save())
Case Windows.Forms.DialogResult.No
If wifCtl.Item.IsDirty OrElse _
(Not wifCtl.Item.IsValid) Then
wifCtl.Item.Reset()
End If
Case Windows.Forms.DialogResult.Cancel
e.Cancel = True
End Select
End If
End Sub
Private Sub btnCancel_Click( _
ByVal sender As Object, ByVal e As System.EventArgs) _
Handles btnCancel.Click
If wifCtl.Item.IsDirty OrElse (Not wifCtl.Item.IsValid) Then
wifCtl.Item.Reset()
End If
IsCanceled = True
End Sub
Private Sub btnSave_Click( _
ByVal sender As Object, ByVal e As System.EventArgs) _
Handles btnSave.Click
If wifCtl.Item.IsDirty Then
Me.Save()
Me.btnSave.Enabled = False
End If
End Sub
Private Function Save() As Boolean
Dim badFields As ArrayList = wifCtl.Item.Validate()
If Not wifCtl.Item.IsValid Then
Using sw As New IO.StringWriter
sw.WriteLine("The following fields are invalid: ")
For Each f As Field In badFields
If Not f.IsValid Then
sw.WriteLine(f.Name)
End If
Next
MessageBox.Show(sw.ToString(), "Invalid fields", _
MessageBoxButtons.OK, MessageBoxIcon.Error)
MsgBox(sw.ToString())
End Using
Return False
End If
wifCtl.Item.Save()
If IsNewWI Then
Me.Text = "Work Item: " & wifCtl.Item.Id
MessageBox.Show(String.Format( _
"New {0} saved. It's ID Is {1}", wifCtl.Item.Type.Name, _
wifCtl.Item.Id), "Save Work Item", _
MessageBoxButtons.OK, MessageBoxIcon.Information)
End If
Return True
End Function
End Class
When performing a save operation, you use the IsDirty and IsValid properties to determine if a save is necessary and possible. If the work item is not valid, you use the work item's Validate method to retrieve a list of the invalid fields. This gives you an opportunity to either correct the errors in code or return the user to the UI (which is what the code in Figure 11 does). If the work item is valid, you save the changes to the object and back to the Team Foundation Server using the Save method.
Finally, if you want to be able to create new work items in your application, you need one more piece of data. As part of the code shown earlier in Figure 7, a combobox on the main form is loaded with a list of work items supported by the active project. The combobox control lists the Name of each work item. This is all you really need to spin a work item and open the UI. The following code shows what you need to do:
Dim WIT As String = Me.cboWIType.Text
If WIT IsNot Nothing AndAlso WIT <> String.Empty Then
Dim wi As WorkItem = mprj.WorkItemTypes(WIT).NewWorkItem()
Using f As New frmAWorkItem(wi)
f.ShowDialog(Me)
End Using
End If
Basically, you pass the string name of the work item type to the WorkItemTypes collection object and call its NewWorkItem method. The code then passes the new work item instance to the frmAWorkItem object's constructor, which binds the new work item to the WorkItemForm control.
Conclusion
The Work Item Tracking subsystem offered by Team Foundation Server exposes a rich object model for integration with your own solutions. Accessing work item information from any application only requires a few lines of code. In addition, when building an application, you can let the built-in controls do the heavy lifting to provide a rich and consistent interface. In the next column, I'll show you how to integrate work item support into the add-in that I started in the previous column.
Send your questions and comments to mmvsts@microsoft.com.
Brian A. Randell is a senior consultant with MCW Technologies LLC. Brian spends his time speaking, teaching, and writing about Microsoft technologies. He is the author of Pluralsight's Applied Team System course and is a Microsoft MVP.