Team System

Work Items and Undo Support

Brian A. Randell

Code download available at: TeamSystem2007_09.exe(197 KB)

Contents

Changes
Work Item Support
Returning Work Item Queries
Updating the Check-In Dialog
Final Details

In the January 2007 installment of this column (msdn.microsoft.com/msdnmag/issues/07/01/TeamSystem), I described how to build a Microsoft® Word 2003 add-in to work with the Team Foundation Server version control subsystem. In the April 2007 column (msdn.microsoft.com/msdnmag/issues/07/04/TeamSystem), I drilled down into the work item tracking subsystem. In this month's column, I'll describe how you can add support for work items to the add-in. In addition, you'll learn how to add a feature that should have been in the first version of the add-in—undo support.

Changes

In the first column describing the add-in, I used the beta release of Microsoft Visual Studio® 2005 Tools for the 2007 Microsoft Office system (Visual Studio 2005 Tools for Office Second Edition, or VSTO 2005 SE for short). Since that time, Microsoft has released the final version, which supports building application add-ins for both Office 2003 and the 2007 Office system. Thus, if you're working along with the article, you need to upgrade the first "release" of the add-in to the RTM version. To do this, simply open the solution and recompile on a machine with VSTO 2005 SE installed. Once you've verified the new version works, the next step is to add undo pending changes support. This feature requires you modify the tfsvcUtil class and the ThisAddin class.

In tfsvcUtil, add a new shared method UndoPendingChanges that accepts the full path to the document currently being modified, and have it return a Boolean. It turns out the core functionality of this new method mimics the existing CheckInDocument method. The method checks to ensure a valid connection has been made to the Team Foundation Server and that a valid workspace is loaded. Once this is accomplished, it gets an array of PendingChanges objects for the passed-in document. Assuming a pending change is returned, the Undo method of the user's workspace is called, passing in the PendingChanges array. This method should return an integer value of 1. If it does, the method returns true, otherwise a tfsUtilException is thrown using the newly defined MSG_UNDO_NOT_ZERO constant. Figure 1 provides the full method listing.

Figure 1 UndoPendingChanges Method in tfsvcUtil

Public Shared Function UndoPendingChanges( _
  ByVal docPath As String) As Boolean
  If serverValidated Then
    If m_userWorkspace Is Nothing Then
      Dim userWorkstation As Workstation = Workstation.Current
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim pc As PendingChange() = _
      m_userWorkspace.GetPendingChanges(docPath)

    If pc IsNot Nothing AndAlso pc.Length > 0 Then
      Dim retval As Integer = m_userWorkspace.Undo(pc)
      ' Retval should equal the number of items 'undone'
      If retval = 1 Then
        Return True
      Else
        Throw New tfsUtilException(String.Format( _
          MSG_UNDO_NOT_ZERO, docPath, retval))
      End If
    Else
      Return False
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

The changes you need to make to the ThisAddIn class are pretty straightforward. As before, due to space restrictions, I won't go into any great detail on the Word add-in specific code (you can compare this column's download to the previous version to see the changes). Needless to say, you will have to modify the class to add a new Undo Pending Changes button to the toolbar, add a click handler for the new button, and add additional code to handle state changes to control whether the button is enabled. Basically, the Undo Pending Changes button's enabled state should mirror that of the existing Check-in button.

Work Item Support

Adding support for associating work items with a check-in requires three major sets of changes to take place. First, you need to modify the tfsvcUtil class to support connecting to the work item store. Second, you need to modify the CheckInDocument method to do the actual association of work items. Finally, you need to modify the existing frmCheckIn dialog to support listing work items for the user to select as part of check-in.

To program work items, you add a reference to the Microsoft.TeamFoundation.WorkItemTracking.Client.dll assembly from the TFSUtil project. As mentioned in earlier columns, Team Explorer installs its supporting assemblies in the Global Assembly Cache (GAC) by default. However, the installer does not register them to show up in the Visual Studio 2005 Add References dialog. You'll need to either modify your registry so the Add References dialog sees the assemblies or manually browse for the assemblies. You'll find the assemblies at %Program Files%\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies\.

Once you've added the reference, you need to modify the tfsvcUtil class and add an imports statement at the top of the source file as follows:

Imports Microsoft.TeamFoundation.WorkItemTracking.Client

Currently when the add-in connects to the Team Foundation Server box, it only connects to the version control service. You could modify the existing connect method to also connect to the work item store. However, there might be times when the user of the add-in is not going to associate work items, and thus this would add unnecessary overhead to the connect method. Instead, you'll add a new method that is called separately to connect to the work item store. You'll make this method public; however, most of the time existing methods will call the method before doing any work item-related operations. If the connection to the work item store is already in place, then it effectively becomes a no-op.

You need to add a new shared method ConnectWIS to the tfsvcUtil class. This method accepts no parameters and returns nothing. The method body is simple. First, it checks to make sure a valid Team Foundation Server instance exists. If not, it throws an exception since the user needs to initiate the connection before this method is called. That said, you could modify the architecture of the add-in to support attempting to login to the user's default Team Foundation Server installation. If the server is available, a connection is made to the work item store using the GetServiceMethod of the m_tfs instance, typed as a TeamFoundationServer object:

Public Shared Sub ConnectWIS()
  If Not m_tfs Is Nothing Then
    If tfsvcUtil.m_tfsWIS Is Nothing Then
      tfsvcUtil.m_tfsWIS = CType( _
        m_tfs.GetService(GetType(WorkItemStore)), WorkItemStore)
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Sub

You'll note the work item store reference is cached in a class-level variable m_tfsWIS, typed as a WorkItemStore object. You'll need to add this variable to the class like this:

Private Shared m_tfsWIS As WorkItemStore = Nothing

Once you've done this, you need to create an overloaded version of the CheckInDocument method so that it matches the existing version with an additional input parameter that accepts an array of WorkItemCheckinInfo instances. Next, cut the body from the existing method and paste it into your new method. Modify the original method so that it calls the new version, passing Nothing for the WorkItemCheckinInfo array. The modified version is now one line of code:

Return tfsvcUtil.CheckInDocument(docPath, comment, Nothing)

Now you need to modify the code in CheckInDocument to see if any WorkItemCheckinInfo objects were passed into the method. If this is the case, the code calls the ConnectWIS method to ensure a connection to the work item store has been made. Once you've done that, you call the overloaded version of CheckIn that accepts an array of WorkItemCheckinInfo instances, passing Nothing for check-in notes and policy overrides. Figure 2 provides the complete listing.

Figure 2 Check-In with Support for Work Items

Public Shared Function CheckInDocument( _
  ByVal docPath As String, ByVal comment As String, _
  ByVal SelectedWorkItems() As WorkItemCheckinInfo) _
  As Integer

  If serverValidated Then
    If m_userWorkspace Is Nothing Then
      Dim userWorkstation As Workstation = Workstation.Current
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim pc As PendingChange() = _
      m_userWorkspace.GetPendingChanges(docPath)

    If pc IsNot Nothing AndAlso pc.Length > 0 Then
      If SelectedWorkItems IsNot Nothing _
        AndAlso SelectedWorkItems.Length > 0 Then

        tfsvcUtil.ConnectWIS()

        Return m_userWorkspace.CheckIn( _
           pc, comment, Nothing, SelectedWorkItems, Nothing)
       Else
         Return m_userWorkspace.CheckIn(pc, comment)
      End If
    Else
      Return -1
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

Returning Work Item Queries

At this point, you've added work item association support to the check-in process. However, you need to enhance the tfsvcUtil class to support returning a list of work item queries for the current Team Project. In addition, you'll need a method that runs a query and returns the work items to the caller so that they can be displayed to the user as part of the integrated check-in experience.

To do this, add two methods. The first method, GetWIQ, returns a StoredQueriesCollection. The second method, RunWIQ, accepts a query name as string parameter and returns a WorkItemsCollection object. I detailed how to do this in the April 2007 edition of this column (msdn.microsoft.com/msdnmag/issues/07/04/TeamSystem). Figure 3 provides the code you need.

Figure 3 GetStoredQueries and RunWIQ Methods

Public Shared Function GetStoredQueries(ByVal docPath As String) _
  As StoredQueryCollection

  If IsServerReady Then
    tfsvcUtil.ConnectWIS()

    If m_userWorkspace Is Nothing Then
      Dim userWorkstation As Workstation = Workstation.Current
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim vcTeamProject As TeamProject = _
      m_userWorkspace.GetTeamProjectForLocalPath(docPath)

    m_wisProject = m_tfsWIS.Projects(vcTeamProject.Name)

    Return m_wisProject.StoredQueries()
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

Public Shared Function RunWIQ(ByVal docPath As String, _
  ByVal WIQ As StoredQuery) As WorkItemCollection

  Dim wiqlToExecute As String = WIQ.QueryText.ToLower()
  Dim params As Hashtable = Nothing
  Dim wic As WorkItemCollection = Nothing

  If wiqlToExecute.Contains(MACRO_PROJECT) Then
    If params Is Nothing Then
      params = New Hashtable
    End If
    params.Add(MACRO_PROJECT.Substring(1), WIQ.Project.Name)
  End If
  If wiqlToExecute.Contains(MACRO_ME) Then
    If params Is Nothing Then params = New Hashtable
    params.Add(MACRO_ME.Substring(1), _
      m_tfsVCS.AuthenticatedUser.Substring( _
      m_tfsVCS.AuthenticatedUser.IndexOf("\"c) + 1))
  End If

  If params IsNot Nothing Then
    wic = m_tfsWIS.Query(wiqlToExecute, params)
  Else
    wic = m_tfsWIS.Query(wiqlToExecute)
  End If

  If wic.Count = 0 Then
    Return Nothing
  Else
    Return wic
  End If
End Function

Updating the Check-In Dialog

Now that you've got the main code written to handle associating work items at check-in, you need to modify the existing check-in dialog to support work item association. Start by making the existing form larger, adjusting the size property to something like 640×480 to give you room to adjust the layout. Next, add a panel control to the form and name it pnlComments. Then cut the existing textbox txtComment from the form and paste it into the new panel, setting the Dock property to Fill. You'll need to re-add a handles clause to the txtComment_TextChanged event handler because, when you cut the textbox, the existing clause is removed.

You'll need a GroupBox control called grpOptions added to the form on the left. Add two RadioButton controls into the group box and name them rdoComments and rdoWorkItems. Change their AutoSize property to False and their Appearance property to Button. Set the rdoComments radio button's Checked property to True. Finally, rearrange the form so that it resembles Figure 4.

Figure 4 Modified Check-In Dialog with Comments Page

Figure 4** Modified Check-In Dialog with Comments Page **(Click the image for a larger view)

With the basic UI changes in place, you need to add support for showing work items in a grid when the Work Items toggle button is clicked. Add a panel control to the form called pnlWorkItems. Inside that panel, add two panels: pnlSQCombo and pnlGridHolder. Set pnlSQCombo's Dock property to Top and pnlGridHolder's to Fill. You then need to add a label and a combobox control to pnlSQCombo and a DataGridView control to pnlGridHolder. Name the combobox cboStoredQueries and the grid dgvWorkItems. Dock the grid within the panel control using the smart tag. Adjust the size and location of pnlWorkItems to match pnlComments and set its Visible property to False.

When the user clicks the Work Items toggle button, the click event handler will perform the work necessary to make the work items available. There's no point in adding the extra overhead to the form load time unless you know you'll always want work items associated with a document check-in. There's quite a bit of code that makes things happen, much of which is generic Windows® Forms code necessary to set the UI correctly.

The order of operations when the Work Items toggle button is clicked is as follows: load the check-in action combobox data sources, define the grid's columns, load the stored queries, and then run the My Work Items query, binding the results of the query to the grid control. Figure 5 lists the code that gets called when the user clicks the Work Items toggle button.

Figure 5 rdoWorkItems_Click Event Handler

Private Sub rdoWorkItems_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles rdoWorkItems.Click
  ' Load ComboBox Data Sources
  LoadCheckInActionComboBox()

  ' Define Grid Layout
  DefineWorkItemGrid()

  ' Load WIQs
  LoadStoredQueries()
  For Each sq As StoredQuery In msq
    If sq.Name = "My Work Items" Then
      Me.cboStoredQueries.SelectedItem = sq
      Exit For
    End If
  Next

  ' Run WIQ
  LoadGridData()

  Me.pnlComments.Visible = False
  Me.pnlWorkItems.Visible = True
End Sub

The rdoWorkItems_Click event handler calls a number of supporting procedures to do its work. LoadCheckInActionComboBox initializes the data sources for the check-in action combobox that will appear in the grid. If you examine the Microsoft check-in dialog, you'll note that, depending upon the type of work item being selected, the check-in action combobox will either show only the word Associate or it will show Associate and Resolve. Mimicking this behavior requires a bit of behind-the-scenes work in the form.

In order to make this work, your first step is to define two class-level arrays:

Private listAssociate(0) As String
Private listAssociateResolve(1) As String

The next thing you'll do is swap the check-in action combobox's data source between these two arrays depending upon the type of work item selected. LoadCheckInActionComboBox loads the string values into the arrays.

The event handler then runs DefineWorkItemGrid to initialize the grid control. This method adds an unbound checkbox control, four data-bound text columns (Work Item Type, Work Item ID, Title, and State), and an unbound combobox control for the check-in action. Next, rdoWorkItems_Click calls LoadStoredQueries. This method calls the GetStoredQueries method you created earlier. It then data binds the results to the cboStoredQueries combobox control.

Back in the rdoWorkItems_Click method, the code walks the list of stored queries until it finds the My Work Items query. Once it finds this, the code sets that to be the currently selected query in the cboStoredQueries combobox control. Then the method executes LoadGridData, which retrieves the work items returned by the My Work Items query and binds the results to the grid. Finally, rdoWorkItems_Click hides the comments panel and shows the work items panel.

To make the grid work in a similar fashion to the Microsoft version, you need to write event handlers for two grid-related events. The first event is the CellFormatting. One of the columns displayed by the grid is the Type property from the WorkItem object. This property is typed as WorkItemType object. You want to display the Name property of this object in the grid. Unfortunately, when the ToString method is executed, the object returns its fully qualified type name, not its Name property. To display the Name property, you need to change the cell's value in the CellFormatting event handler. This code checks to see whether the column to format is the Work Item Type column:

Private Sub dgvWorkItems_CellFormatting(ByVal sender As Object, _
  ByVal e As DataGridViewCellFormattingEventArgs) _
  Handles dgvWorkItems.CellFormatting

  If e.ColumnIndex = typeColumnIndex Then
    If e.Value IsNot Nothing Then
      Dim wit As WorkItemType = CType(e.Value, WorkItemType)
      e.Value = wit.Name
      e.FormattingApplied = True
    End If
  End If
End Sub

If it is and the cell's value is not null, the code retrieves the WorkItemType instance from the cell and changes the value of the cell to the value of the Name property.

Next, you need to implement an event handler for the Current-CellDirtyStateChanged event. In this handler you're changing the data source of the check-in action combobox based upon the type of work item selected. Write the code so that it only executes when the checkbox column's state is changed.

To know what the data source should be, you need to access the WorkItem object that is bound to the current row. Once you have it, you call its GetNextState method passing in a string value of Microsoft.VSTS.Actions.Checkin. The code is checking to see if there's a valid state transition from a check-in. If there is, the code receives a valid string back that tells it that the combobox should display both Associate and Resolve. If a valid string is not retrieved, then only Associate should be available. In this way, the code sets the correct data source and then commits the current edit, changing the state of the checkbox.

The code then checks the value of the checkbox and, if it has been set to checked (True), the check-in action combobox is enabled and made the active control, and an edit is started. If the checkbox is returning to an unchecked state, the code resets the combobox. Figure 6 provides the details.

Figure 6 Complete CurrentCellDirtyStateChanged Event Handler

Private Sub dgvWorkItems_CurrentCellDirtyStateChanged( _
  ByVal sender As Object, ByVal e As System.EventArgs) _
  Handles dgvWorkItems.CurrentCellDirtyStateChanged

  Dim c As DataGridViewCell = dgvWorkItems.CurrentCell

  If c.ColumnIndex = checkedColumnIndex Then
    Dim dcb As DataGridViewComboBoxCell = _
      CType(dgvWorkItems(comboBoxColumnIndex, _
      c.RowIndex), DataGridViewComboBoxCell)

    Dim currentRow As DataGridViewRow = dgvWorkItems.Rows(c.RowIndex)
    Dim wi As WorkItem = CType(currentRow.DataBoundItem, WorkItem)

    Dim nextState As String = _
      wi.GetNextState("Microsoft.VSTS.Actions.Checkin")

    If String.IsNullOrEmpty(nextState) Then
      dcb.DataSource = listAssociate
    Else
      dcb.DataSource = listAssociateResolve
    End If

    dgvWorkItems.CommitEdit( _
      DataGridViewDataErrorContexts.CurrentCellChange)

    If CBool(c.Value) Then
      dcb.ReadOnly = False
      dcb.Value = dcb.Items.Item(0)

      dgvWorkItems.CurrentCell = dcb
      dgvWorkItems.BeginEdit(True)
    Else
      dcb.Value = Nothing
      dcb.ReadOnly = True
    End If
  End If
End Sub

The last bit of code you need to add to process work items is developed by creating an array of WorkItemCheckinInfo objects in the FormClosing event (see Figure 7). You create this array by walking the rows of the grid, creating a WorkItemCheckinInfo instance if the checkbox column is checked. When you create a WorkItemCheckinInfo instance, you specify the check-in action. In addition to this method, you need to add a public property, WorkItems, to the form so that the array can be retrieved once the dialog is closed. You set this property at the end of the FormClosing method.

Figure 7 FormClosing Event Handler in the frmCheckIn Dialog

Private Sub frmCheckIn_FormClosing( _
  ByVal sender As Object, ByVal e As FormClosingEventArgs) _
  Handles Me.FormClosing

  Dim chkCol As DataGridViewCell
  Dim cboCol As DataGridViewCell

  Dim wiciList As New List(Of WorkItemCheckinInfo)
  Dim cia As WorkItemCheckinAction = WorkItemCheckinAction.None

  For Each row As DataGridViewRow In Me.dgvWorkItems.Rows
    chkCol = row.Cells(checkedColumnIndex)
    If CBool(chkCol.Value) Then
      ' Item is selected
      cboCol = row.Cells(comboBoxColumnIndex)
      Select Case cboCol.Value.ToString()
        Case "Associate"
          cia = WorkItemCheckinAction.Associate
        Case "Resolve"
          cia = WorkItemCheckinAction.Resolve
        Case Else
          cia = WorkItemCheckinAction.None
      End Select

      wiciList.Add(New WorkItemCheckinInfo( _
        CType(row.DataBoundItem, WorkItem), cia))
    End If
  Next

  If wiciList.Count > 0 Then
    Me.WorkItems = wiciList.ToArray()
  Else
    Me.WorkItems = Nothing
  End If
End Sub

Final Details

You'll find some additional code in the form to handle the stored queries combobox's SelectedValueChanged event. The only thing left to do for the add-in to support work items is to modify the cbbCheckInDoc_Click method of the ThisAddIn class in the Word add-in. The modified version simply checks to see if the new WorkItems property of the dialog has data. If it does, the new version of the CheckInDocument method is called. Otherwise, the old version is executed. Figure 8 displays the completed check-in experience working with work items.

Figure 8 Completed Check-In Dialog with Work Item Support

Figure 8** Completed Check-In Dialog with Work Item Support **(Click the image for a larger view)

At this point, if you run the add-in, you can add a document to source control, check it in with comments and associate work items, check it out, and even undo pending changes. By this time, a lot of work has been accomplished, but there's even more that can be done. In the next edition of this column, I'll look at adding check-in notes, policy support, and possibly other interesting features.

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. Contact Brian via his blog at mcwtech.com/cs/blogs/brianr.