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:
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.
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:
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.
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.
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.
This is one of the features we had to cut for v1 that could be implemented as an extension.
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.
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.
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.
There a few v1 limitations that can make some scenarios challenging:
The WorkItemFormControl can only host the following set of controls (all contained within Microsoft.TeamFoundation.WorkItemTracking.Controls.dll) in v1:
This control was not exposed for v1 and hence cannot be hosted.
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.
The following diagram gives a very course-grained depiction of the Work Item Tracking client components.
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:
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 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 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).
The work item object model provides a rich API for working with work item tracking servers and the artifacts they contain, including:
The object model enforces the same work item data integrity rules that are imposed on the server.
This section describes the basic UI elements that are exposed by the Work Item Tracking package.
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 provides a grid-based query editor and displays query results in a grid (WorkItemResultGrid control) in the lower half of the window.
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.
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.
Two of the Work Item tracking document windows have corresponding toolbars that appear when they have focus.
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:
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.
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.
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.
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:
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);
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:
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)
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:
private void MyMethod(TeamFoundationServer tfsServer, int id)
IWorkItemDocument wiDoc =
(IWorkItemDocument)_docService.GetWorkItem(tfsServer, id, this);
// make a another modification here…
// error! ModifyItem already released our reference!!!
if (wiDoc.Item.Fields[CoreField.State].Value == “Closed”)
private void ModifyItem(TeamFoundationServer tfs, int id)
(IWorkItemDocument)_docService.GetWorkItem(tfs, id, this);
// do something …
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).
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.
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…
IWorkItemTrackingDocument events are described in another section of the document.
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:
// obtain the document service …
private const string cServiceName =
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 =
if (doc != null)
if (doc is IWorkItemDocument)
// work item processing
else if (doc is IQueryDocument)
// it’s a query document
// it’s a results document
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:
private void ShowWorkItem (TeamFoundationServer tfsServer, int workItemId)
wiDoc = (IWorkItemDocument)_docService.GetWorkItem(tfsServer, workItemId, this);
// Note: This is only a sample. In actuality ShowWorkItem
// will call Load if the document has not been loaded yet.
// 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.
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.
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:
// this assumes the DocumentAdded event has already been hooked
void _docService_DocumentAdded(object sender, DocumentService.DocumentServiceEventArgs e)
// if loaded, we can go ahead and hook field changed events.
// otherwise we need to hook the loaded event and hook them there.
void workItemDoc_Loaded(object sender, EventArgs e)
// hook the field changed event ...
IWorkItemDocument wiDoc = (IWorkItemDocument)sender;
// unhook the Loaded event
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 is fired when a document window is releasing its lock on the document as it is closing.
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 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 is fired after a document has been successfully saved.
Loading is fired just before initiating the loading of a document.
Loaded is fired when a document has completed loading.
Reloaded is fired when a document has been reloaded.
SelectionChanged is fired when selection in the document window associated with the document has changed.
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:
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.
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.
The following code illustrates displaying an existing work item:
private void GoToWorkItem(TeamFoundationServer tfsServer, int id)
IWorkItemDocument wiDoc =
(IWorkItemDocument) _docService.GetWorkItem(tfsServer, id, this);
// we are done – the document window holds its own reference
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:
// 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) _docService.CreateWorkItem(item, this);
// Here we let the DocumentService create the item.
private void LaunchTask(TeamFoundationServer tfs,
// 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”;