Work Item Tracking v1 Client Extensibility in Visual Studio Team System

This document was written by Tony Edwards, who owned Work Item Tracking VSIP client and is very helpful especially with things related to DocumentService. Since the blog entry seems to remove images, find the attached document version for viewing with images.

 

Introduction

When you install VSTS you get a lot of out of the box functionality in the form of pre-defined Process Templates that are used to create your project.  When you create a project using one of these templates, it includes a set of work item types, each containing:

 

      • The set of fields for the type
      • Rule definitions that govern workflow and define how items of the type can be modified
      • A form definition that specifies the layout and controls used to edit the type
      • A set of pre-defined queries
      • A set of initial work items

 

After your project has been created, you can continue to evolve your work item types to match your team’s requirements.  These customizations are essentially declarative and do not involve writing code in a traditional programming language.

 

This document describes how work item tracking can be extended through writing code that runs on the client, and mostly focuses on writing code that runs within Visual Studio in the form of Add-ins or packages.  Some of the APIs can also be used to create command-line utilities, stand alone Windows Forms applications, and so on.

Examples of useful extensions

The public interfaces available in v1 can be used to provide many useful extensions.  This section describes a number of possible extensions.  Here are just a few of the many possibilities:

Bulk Editing

The first version of the Team Foundation Work Item Tracking client provides a Results View (see screenshot in the section on UI elements) that allows the user to step through and edit the results as a collection of work items.  You can also bulk-edit work items within an Excel spreadsheet. 

 

There are times, however, when it would be convenient to apply a set of changes to selected work items within Visual Studio.  The architecture makes it relatively simple to provide this kind of functionality in an Add-in or a package.

Extra Validation

The work item type definition language provides rich declarative constructs for expressing data integrity rules.  There may be situations where additional control is needed.  The Work Item Tracking package provides events that allow work item changes to be tracked and to prevent saving items that do not conform to your validation requirements.

Calculated Fields

There are cases when the value of one field should depend on the values of one or more other fields.  Simple cases can be expressed directly in the work item type definition.  More complex functionality can be achieved by responding to appropriate Work Item Tracking events.

Emailing work items

This is one of the features we had to cut for v1 that could be implemented as an extension.

Creating offline views

Another useful extension would be the ability to select work items (or the results of a query) and then generate an archive for offline viewing.

Creating editable pre-populated work items

There are cases where you will want to generate work items (e.g. for failed test cases) and then allow the user to edit them before saving.  The exposed interfaces make this quite simple.

Hosting work item query results

You may want to integrate work item query results into your extensions.  One example of this is the Version Control Pending Changes window.  This tool window hosts the WorkItemResultGrid and allows the user to select a set of work items to resolve or associate with a check-in.

Limitations

There a few v1 limitations that can make some scenarios challenging:

Custom controls on the work item form are not supported

The WorkItemFormControl can only host the following set of controls (all contained within Microsoft.TeamFoundation.WorkItemTracking.Controls.dll) in v1:

  • FieldControl
  • DateTimeControl
  • HtmlFieldControl
  • LinksControl
  • AttachmentsControl
  • WorkItemClassificationControl
  • WorkItemLogControl

The query builder control is not public and cannot be hosted

This control was not exposed for v1 and hence cannot be hosted.

There is no mechanism for creating Work Item Tracking child nodes in the Team Explorer

The Team Foundation SDK provides information for creating Team Explorer plug-ins.  These plug-ins appear as children of the project node (as and siblings of Work Item Tracking).  There is no officially supported way for v1 to provide a Work Item Tracking child node.

Work Item Tracking Client Components

The following diagram gives a very course-grained depiction of the Work Item Tracking client components. 

 

Work Item Tracking Package

Visual Studio provides a number of mechanisms for extending its functionality.  The Work Item Tracking client functionality is implemented as a Visual Studio Package.  Packages can expose new project types, commands, document windows, tool windows, language extensions, services, and more.  At a high level our package provides:

 

  • Work Item View, Query View, and Results View document windows
  • Integration in the Team Explorer (Work Item Tracking child node within each project)
  • A set of commands that appear on various menus (Team, Edit, View, various context menus) and toolbars
  • A service (Microsoft..VisualStudio.TeamFoundation.WorkItemTracking.DocumentService) that should be used when creating your add-ins and packages.

Work Item Controls

The Work Item Tracking package user interface is composed from UI elements that are packaged in the controls assembly.  This assembly contains components that can be hosted in other packages, add-ins, and stand-alone Windows Forms applications.  The most important of these are:

WorkItemFormControl

WorkItemFormControl can be used to display and edit work items.  This control reads an XML form definition which specifies the type and layout of the controls on the form.  Each work item type has an associated form definition (exposed in the object model as WorkItem.Type.DisplayForm) but the control can display any valid form definition.

WorkItemResultGrid

WorkItemResultGrid provides a simple work item grid for showing query results.  The grid makes use of a simple interface (IWorkItemGridValueProvider) to handle instance management (see section on work item instance management below).

Work Item Object Model

The work item object model provides a rich API for working with work item tracking servers and the artifacts they contain, including:

  • Work Item Type Definitions
  • Work Items
  • Queries
  • Field Definitions and values

 

The object model enforces the same work item data integrity rules that are imposed on the server.

Work Item Tracking UI Elements within Visual Studio

This section describes the basic UI elements that are exposed by the Work Item Tracking package.

Work Item View

The Work Item View is a document window that is used to edit a single work item.  This view can be invoked with a few lines of code using the DocumentService.  Note that it essentially a document window that hosts the WorkItemFormControl.

 

Query View

Query View provides a grid-based query editor and displays query results in a grid (WorkItemResultGrid control) in the lower half of the window.

Results View

The Results View displays the results of a query and allows the user to step through the results and edit each item in a work item form in the lower half of the window.  Saving in this view saves all of the dirty items contained in the results.

Team Explorer

The Team Explorer exposes a well defined plug-in mechanism for adding functionality to the project.  Each plug-in appears as a child node under the Project Node.  See the Team System SDK for more information.  Unfortunately, for v1 there is no officially supported mechanism for creating a child node plug-in under Work Item Tracking.  The Team Queries and My Queries nodes are the only nodes supported at this time.

Toolbars

Two of the Work Item tracking document windows have corresponding toolbars that appear when they have focus.

Query View Toolbar
Results View Toolbar

Work Item Instance Management

The Work Item Tracking package presents a model to the user that should be followed closely when developing extensions.  The model can be summarized as follows:

  • Wherever a work item appears in an editable state, all views of that item should refer to a single instance.
  • All modifications to existing work items should be made after the item has been locked via the Document Service.

 

This is best illustrated by an example.  Consider the case when a work item is being edited in a work item view.  As the user makes changes, those changes get reflected to all other views of the item within the environment (e.g. to query result windows or the Pending Changes tool window).  You can easily test this for yourself by double-clicking a query to launch results view and then modifying a field that is displayed within the grid.  You will immediately see the changes.

 

In order to make your Work Item Tracking extension play nicely with the environment and with other extensions, you need to use the DocumentService to acquire an edit lock on work items prior to modifying them, and equally important, you need to be careful to release the lock when you are done with the modifications.  Sample code in the last section illustrates the proper technique for doing this.

DocumentService

Visual Studio packages can expose services to other packages, thus enabling integration between features.  The Work Item Tracking package exposes Microsoft.VisualStudio.TeamFoundation.WorkItemTracking.DocumentService to allow other packages and add-ins to access Work Item Tracking functionality. 

 

The following is a summary of the events and methods that DocumentService exposes.  The Code Samples section illustrates use of the API.

 

// Events

public event DocumentServiceEventHandler DocumentAdded;

public event DocumentServiceEventHandler DocumentRemoved;

 

The DocumentAdded and DocumentRemoved events are fired when a document is added to or removed from the collection of Work Item Tracking documents maintained by the DocumentService.  Hooking these events is typically a pre-cursor to hooking other important events when developing an extension.  See the example code in the Code Samples section.

 

 // Work Item methods

public bool ContainsWorkItem(TeamFoundationServer tfsServer, int id);

public IWorkItemDocument CreateWorkItem(WorkItem workItem, object lockToken);

public IWorkItemDocument CreateWorkItem(TeamFoundationServer tfsServer, string teamProjectName, string workItemTypeName, object lockToken);

public IWorkItemDocument GetWorkItem(TeamFoundationServer tfsServer, int workItemId, object lockToken);

public void ShowWorkItem(IWorkItemDocument workItemDocument);

ContainsWorkItem can be used to check to see if a work item is being locked for edit. 

 

CreateWorkItem is used to create a work item document.  There are two forms.  The first form above is used for situations when you want to create a new work item and delegate that item to the document service.  The second form specifies a type to create so that the DocumentService can create the item on your behalf.

 

GetWorkItem is an essential method for placing an edit lock on an exiting work item.  If the item is already locked, then you will be given back a reference the one already in use.  Otherwise, an entry is created for the item, but the item may or may not have been loaded when GetWorkItem returns.  You need to check the IsLoaded property to see if the item is loaded before accessing it.  If the item is not loaded, you should call the IWorkItemDocument.Load method to load it.

 

The ShowWorkItem method is used to place a work item in a document window so that the user can edit it.  If the document has not been loaded, ShowWorkItem will call the IWorkItemDocument.Load method to load the document prior to showing it to the user.

// Query methods

public IQueryDocument CreateQuery(TeamFoundationServer tfsServer, string teamProjectName, bool publicQuery, string queryText, object lockToken);

public IQueryDocument GetQuery(TeamFoundationServer tfsServer, string queryId, object lockToken);

public void ShowQuery(IQueryDocument queryDocument);

CreateQuery creates a query document and locks it for edit.  If a document with the same canonical name already exists an exception is thrown, otherwise an entry is created and the document is locked.

 

GetQuery is used to lock an existing query for edit.  You must check the IsLoaded property before accessing the other properties.  If the query has not been loaded you can call the IWorkItemDocument.Load method to load it, or hook the Loaded event if the load has already been initiated.

 

The ShowQuery method places the specified document in a Query View document window, which allows the user to edit the query definition and run the query to view the results.

// Results Methods

public IResultsDocument CreateResults(IQueryDocument queryDocument, object lockToken);

public IResultsDocument GetResults(IQueryDocument queryDocument, object lockToken);

public void ShowResults(IResultsDocument resultsDocument);

CreateResults creates a new results document.  The results document represents a collection of work items returned by a query.  The Load method runs the query.  The Save method saves all work items in the collection that are dirty.  GetResults is used to lock an existing results document for edit.  If the document does not exist, an exception is thrown.  You must check the IsLoaded property before accessing any of the document properties.  ShowResults opens a Results View document window, executes the query, and prepares the first result for edit.

 

// General document methods

public IWorkItemTrackingDocument FindDocument(string canonicalId, object lockToken);

public List<IWorkItemTrackingDocument> GetDocuments();

FindDocument, checks if the document is open for edit, and if so places an edit lock on the document.  Otherwise it returns null.  GetDocument returns a collection of documents that are currently open.

Code Samples

Two techniques for obtaining the DocumentService from an Add-in

DocumentService is exposed both as a Visual Studio service and as an automation object (in the RC build and later). 

 

It you access it as a service (either through the managed package framework or through interop assemblies) you have to wait startup completion  GetGlobalService will always return null if you try to obtain the service in the OnConnection method.

 

To access the service as an automation object you can add the following code:

 

using Microsoft.VisualStudio.TeamFoundation.WorkItemTracking;

 

public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)

{

  _applicationObject = (DTE2)application;

  _addInInstance = (AddIn)addInInst;

  _docService = (DocumentService)_addInInstance.DTE.GetObject(cServiceName);

}

DocumentService _docService;

private const string cServiceName = “Microsoft.VisualStudio.TeamFoundation.WorkItemTracking.DocumentService”;

 

If you have the Visual Studio SDK you can use the managed package framework to access the DocumentService as a service as follows:

 

using Microsoft.VisualStudio.TeamFoundation.WorkItemTracking;

 

public void OnStartupComplete(ref Array custom)

{

  // Note: do not retrieve the document service in the OnConnection method

  // It is not ready in OnConnection and will always be null

  _docService = (DocumentService)Package.GetGlobalService(typeof(DocumentService));

  if (_docService != null)

  {

    HookWorkItemEvents();

  }

}

DocumentService _docService;

Lock Tokens

You may have noticed that a number of DocumentService methods take an object parameter called lockToken.  When you lock a document for edit you supply a lock token.  It really does not matter what object you pass, but typically you use an object that is scoped to your use of the document.  When you are finished you call Release method on the document itself to let DocumentService know that you no longer need the document.  When the last lock token on a document is released then the document is removed from the DocumentService.

 

Correctly managing document locks is critical.  DocumentService only keeps one reference to any given object for a document.  The first call to Release for an object will remove that token from the document.  Hence the following code is incorrect:

using Microsoft.TeamFoundation.Client;

using Microsoft.VisualStudio.TeamFoundation.WorkItemTracking;

 

private void MyMethod(TeamFoundationServer tfsServer, int id)

{

   IWorkItemDocument wiDoc =

      (IWorkItemDocument)_docService.GetWorkItem(tfsServer, id, this);

  

   ModifyItem(tfsServer, id);

   // make a another modification here…

   // error! ModifyItem already released our reference!!!

   if (wiDoc.Item.Fields[CoreField.State].Value == “Closed”)

   {

   }

  

   wiDoc.Release(this);

}

private void ModifyItem(TeamFoundationServer tfs, int id)

{

   IWorkItemDocument wiDoc =

   (IWorkItemDocument)_docService.GetWorkItem(tfs, id, this);

   // do something …

   wiDoc.Release(this);

}

DocumentService _docService;

 

In the contrived example above, MyMethod calls another method that uses the same lock token (ModifyItem).  When that method calls Release on the document, it causes the lock to be removed from the document.  If the lock is the last one on the document, then it is removed from the document service.  It is critical that you hold a valid lock token while you are operating on an existing document.  The example is contrived because you could have passed the document directly to the method, but there are more realistic scenarios you should watch out for (most involving event handling code).

Hooking DocumentService events

DocumentService exposes events to let you know when documents are being locked for edit.  DocumentAdded is fired whenever a document is locked for edit.  DocumentRemoved is called when the document is no longer being edited, and hence is a good place to unhook events.  DocumentService events are ordinary .Net events and can be hooked as follows:

 

private void HookWorkItemEvents()

{

  // Receive notification when documents are being edited

  // or are no longer being edited.

  _docService.DocumentAdded +=

    new DocumentServiceEventHandler(_docService_DocumentAdded);

  _docService.DocumentRemoved +=

    new DocumentServiceEventHandler(_docService_DocumentRemoved);

}

 

When you receive the DocumentAdded event you can hook interesting events on the document itself.  For example:

 

void _docService_DocumentAdded(object sender, DocumentServiceEventArgs e)

{

  IWorkItemDocument workItemDoc = e.Document as IWorkItemDocument;

  // There are three document types - we only care about work item documents

  // workItemDoc will be null if it is not a IWorkItemDocument

 

  if (workItemDoc != null)

  {

     // get notified when a work item is being saved…

   workItemDoc.Saving +=

        new WorkItemTrackingDocumentEventHandler(workItemDoc_Saving);

  }

}

 

IWorkItemTrackingDocument events are described in another section of the document.

Canonical Names

Each document in the DocumentService has an associated canonical name.  These names uniquely identify a document.  When you create Visual Studio Add-ins you will often want to know a) whether you command should be enabled or b) what Work Item Tracking document, if any, is associated with the currently active document. 

 

The following snippet shows how to:

  • Get the DocumentService from an Add-in
  • Get the canonical name from the active document
  • Find out if that document is a Work Item Tracking document
  • Determine what kind of document it is.

 

// obtain the document service …

private const string cServiceName =

  "Microsoft.VisualStudio.TeamFoundation.WorkItemTracking.DocumentService";

DocumentService docService =

_applicationObject.ActiveDocument.DTE.GetObject(cServiceName) as DocumentService;

if (docService != null)

{

   // get the canonical name for the active document…

   string documentPath = _applicationObject.ActiveDocument.FullName;

   // see if this is a work item tracking document…

   IWorkItemTrackingDocument doc =

      docService.FindDocument(documentPath, this);

   if (doc != null)

   {

      try

      {

         if (doc is IWorkItemDocument)

         {

            // work item processing

         }

       else if (doc is IQueryDocument)

         {

            // it’s a query document

         }

         else

         {

            // it’s a results document

         }

      }

      finally

      {

   doc.Release(this);

      }

   }

}

 

Insuring a document has completed loading

Always check the IsLoaded property before accessing other properties on the document.  What you do when IsLoaded is false depends on the scenario.  Suppose you want to unconditionally do something with the work item.  In that case you will want to call Load before accessing the work item:

 

using Microsoft.TeamFoundation.Client;

using Microsoft.VisualStudio.TeamFoundation.WorkItemTracking;

private void ShowWorkItem (TeamFoundationServer tfsServer, int workItemId)

{

  IWorkItemDocument wiDoc;

  wiDoc = (IWorkItemDocument)_docService.GetWorkItem(tfsServer, workItemId, this);

  try

  {

  if (!wiDoc.IsLoaded)

  {

  wiDoc.Load();

  }

    // Note: This is only a sample. In actuality ShowWorkItem

    // will call Load if the document has not been loaded yet.

  wiDoc.ShowWorkItem(wiDoc);

  }

  finally

  {

    // Guarantee that we release the edit lock

    // In this case, we don’t care about the outcome

    // once the item is shown. The document window

    // has its own lock and therefore we can release ours.

    wiDoc.Release(this);

  }

}

 

In the above example, Load is synchronous so we just call it and then call ShowWorkItem when it completes.  Other situations demand a different approach. 

 

Monitoring changes to work items

Suppose you want to listen to field changed events on all work items as they are being modified.  The approach to doing this is as follows:

1. Hook the DocumentService DocumentAdded event.

2. When a document is added, check if it is a work item document, and if so

· If the document has been loaded hook the FieldChanged event

· If the document has not been loaded yet, hook the Loaded event and then hook the FieldChanged event when the Loaded event fires.

 

Here is sample code:

 

using Microsoft.VisualStudio.TeamFoundation.WorkItemTracking;

// this assumes the DocumentAdded event has already been hooked

void _docService_DocumentAdded(object sender, DocumentService.DocumentServiceEventArgs e)

{

  IWorkItemDocument workItemDoc = e.Document as IWorkItemDocument;

  // There are three document types - we only care about work item documents

  // workItemDoc will be null if it is not a IWorkItemDocument

  if (workItemDoc != null)

  {

  if (workItemDoc.IsLoaded)

  {

        // if loaded, we can go ahead and hook field changed events.

        workItemDoc.Item.FieldChanged +=

           new WorkItemFieldChangeEventHandler(Item_FieldChanged);

      }

      else

      {

         // otherwise we need to hook the loaded event and hook them there.

         workItemDoc.Loaded +=

           new WorkItemTrackingDocumentEventHandler(workItemDoc_Loaded);

      }

   }

}

void workItemDoc_Loaded(object sender, EventArgs e)

{

  // hook the field changed event ...

  IWorkItemDocument wiDoc = (IWorkItemDocument)sender;

  wiDoc.Item.FieldChanged +=

    new WorkItemFieldChangeEventHandler(Item_FieldChanged);

  // unhook the Loaded event

  wiDoc.Loaded -=

    new WorkItemTrackingDocumentEventHandler(workItemDoc_Loaded);

}

 

Hooking Document Events

IWorkItemTrackingDocument exposes a number of interesting events that can be monitored.  The general pattern is to hook the DocumentService.DocumentAdded event and then hook the events on the document that you are interested in.  The following document events are available:

 

Closing

Closing is fired when a document window is releasing its lock on the document as it is closing.

Closed

Closed is fired after a  document window has released it.  This event may fire after the document has been removed from DocumentService (in which case the document window was the last lock holder).

Saving

Saving is fired when a document is about to be saved.  If you would like to prevent the document from being saved you should throw an exception when processing this event.

Saved

Saved is fired after a document has been successfully saved.

Loading

Loading is fired just before initiating the loading of a document.

Loaded

Loaded is fired when a document has completed loading.

Reloaded

Reloaded is fired when a document has been reloaded.

SelectionChanged

SelectionChanged is fired when selection in the document window associated with the document has changed.

 

Hosting WorkItemFormControl and WorkItemResultGrid in your own user interface components

If launching our document windows does not suit your needs you can also host these Windows forms controls within your own UI.  Within Visual Studio you need to follow a few guidelines to insure that you remain consistent with the behavior that the user expects.  These guidelines are provided below:

Follow the proper instance management protocol

You must use DocumentService to obtain a proper edit lock on your document any time your code can modify the document (e.g. a work item or a query).  This code may be a command handler that modifies a work item without presenting UI, or it could involve displaying UI that modifies a work item.  In any case, the document should be locked so that all views refer correctly to the same document.

Use the value provider service when displaying read only views of work items

The IWorkItemGridValueProvider service provides a mechanism for obtaining the correct state of work items that you are displaying.  The WorkItemResultGrid has a property called ValueProvider that automatically keeps the grid in sync with any changes that have occurred to work items in other windows.  If you use the WorkItemResultGrid then you should obtain the IWorkItemValueProvider service from Visual Studio and set the ValueProvider service to the one returned.  If you are displaying work items in other UI you should also track changes – the value provider service provides an economical way to do so.

 

Displaying a work item in a document window

The following code illustrates displaying an existing work item:

using Microsoft.VisualStudio.TeamFoundation.WorkItemTracking;

private void GoToWorkItem(TeamFoundationServer tfsServer, int id)

{

  IWorkItemDocument wiDoc =

    (IWorkItemDocument) _docService.GetWorkItem(tfsServer, id, this);

  try

  {

    ShowWorkItem(wiDoc);

  }

  finally

  {

    // we are done – the document window holds its own reference

    wiDoc.Release(this);

  }

}

 

Creating a new work item for edit

A very common desire is to create a work item that is filled out in a pre-defined way and then allow the user to edit it before saving (e.g. generating a bug for a failed test).  This is simple to do using DocumentService:

 

using Microsoft.VisualStudio.TeamFoundation.WorkItemTracking;

// one way of doing it – create a new item and delegate it to the

// document service in the CreateWorkItem call

private void LaunchTask()

{

  WorkItem item = new WorkItem(“Task”);

  item.Fields[“MyNamespace.MyField1”].Value = “someValue”;

  item.Fields[“MyNamespace.MyField2”].Value = “anotherValue”;

 

  IWorkItemDocument wiDoc =

    (IWorkItemDocument) _docService.CreateWorkItem(item, this);

  try

  {

    ShowWorkItem(wiDoc);

  }

  finally

  {

    // we are done – the document window holds its own reference

    wiDoc.Release(this);

  }

}

// Here we let the DocumentService create the item.

private void LaunchTask(TeamFoundationServer tfs,

                        string projectName,

                        string workItemTypeName)

{

  IWorkItemDocument wiDoc =

    (IWorkItemDocument) _docService.CreateWorkItem(tfs,

                                                   projectName,

                                                   workItemTypeName,

                                                   this);

  try

  {

    // we don’t have to check IsLoaded here – new documents

    // are loaded as soon as they are returned.

  wiDoc.Item.Fields[“MyNamespace.MyField1”].Value = “someValue”;

  wiDoc.Item.Fields[“MyNamespace.MyField2”].Value = “anotherValue”;

 

    ShowWorkItem(wiDoc);

  }

  finally

  {

    // we are done – the document window holds its own reference

    wiDoc.Release(this);

  }

}

 

VSTS v1 WIT Client Extensibility.doc