December 2009

Volume 24 Number 12

Team System - Building a Visual Studio Team Explorer Extension

By Brian A. Randell, Marcel de | December 2009

The main user interface you use to interact with Team Foundation Server (TFS) is the Team Explorer client. Team Explorer provides a way to access and view information on one or more TFS servers. By default, Team Explorer provides access to TFS features like work items, SharePoint document libraries, reports, builds, and source control.

Marcel’s development teams use a number of separate Visual Studio solutions, each consisting of five to ten projects. In each of these solutions, there is a single point of deployment, maintenance, and design. Sometimes there are cross-solution dependencies. The team manages those dependencies via a special folder inside the TFS version control repository. Each solution writes its output to a well-known location and the team checks in the output after each build. A solution that depends on the output of another solution can reference the assembly when it is retrieved from TFS.

Unfortunately, Marcel’s team faced a problem: Visual Studio stores file references using a relative path to the current solution, but developers want the flexibility of using their own directory structures and workspace mappings. This desirable flexibility results in frustration when Visual Studio can’t build solutions because it can’t find referenced files.

One way to fix this problem is to map the location that contains the binary references as a substituted drive using the subst.exe command. By referencing the assemblies on the substituted drive, Visual Studio is able to find the files in a consistent location. Developers can put the files in the locations they prefer, then use subst.exe to map their locations to the standard mapping. Because the files reside on a different drive, Visual Studio stores a full path rather than a relative path. An additional benefit is that a developer can test a different version by simply changing the mapping.

While this technique works, even better would be a Team Explorer extension that allows a developer to define a mapping between the version control location and a mapped drive. Marcel’s team implemented this functionality in a Team Explorer extension called Subst Explorer. You can see the menu for the Subst Explorer extension in see Figure 1.

The Subst Explorer Loaded in the Team Explorer Window

Figure 1 The Subst Explorer Loaded in the Team Explorer Window

Getting Started

To build your own Team Explorer plug-in, you’ll need Visual Studio 2008 Standard Edition or higher, Team Explorer, and the Visual Studio 2008 SDK, which you can get from the Visual Studio Extensibility Developer Center.

Before creating a Team Explorer plug-in, you need to create a VSPackage. The package supplies a class that implements the Microsoft.TeamFoundation.Common.ITeamExplorerPlugin interface defined in Microsoft.VisualStudio.TeamFoundation.Client.dll.

In Visual Studio parlance, your plug-in becomes a part of the hierarchy. One feature that differentiates the Team Explorer hierarchy from other hierarchy implementations in Visual Studio is that Team Explorer supports asynchronous loading of the hierarchy, essentially because loading the hierarchy for things like work items and documents often requires remote queries to TFS. These calls would otherwise block Visual Studio during those queries, resulting in a poor user experience. The ITeamExplorerPlugin interface implemented by your VSPackage provides the mechanism that Team Explorer uses to load the contents of each node asynchronously.

Create a VSPackage by selecting File | New | Project. In the New Project dialog, expand the Other Project Types node and select the Extensibility node. Over in the Templates pane, select the Visual Studio Integration Package template.

After you fill out the New Project dialog and click OK, Visual Studio launches the Visual Studio Integration Package wizard. First, choose your preferred language—C++, C#, or Visual Basic (C# is used in this article). When choosing Visual Basic or C#, you need to select the option to generate a new strong name key file or specify an existing file. On the next page, fill in information for your company and some details about the package, including the Package Name, icon, and so on. Much of this information is shown in the Help | About dialog in Visual Studio.

On the next page, select how you want Visual Studio to expose your package. For this particular example, you want to have a MenuCommand only. The plug-in will use this to handle the context menus. On the next page, you provide a Command Name and Command ID. Just accept the defaults since you’ll change them later. On the next page you can add a support for test projects. We won’t be covering them in this article, so feel free to deselect them and then finish the wizard. The wizard will generate the basic classes and resources you need for implementing the VSPackage.

Next, you need to add the following references, which provide access to the Team Explorer base classes used to create the Team Explorer plug-in:

Microsoft.VisualStudio.TeamFoundation.Client.(9.0.0)
Microsoft.VisualStudio.TeamFoundation (9.0.0)
Microsoft.VisualStudio.Shell (2.0.0)

You also need to remove the default reference to Microsoft.VisualStudio.Shell.9.0, since Microsoft built the Team Foundation assemblies against the 2.0 version of the assemblies instead of the 9.0 version. In addition, as generated, the project assumes it can use the regpkg.exe tool to register the package in the registry after compile. However, regpkg.exe depends on the Shell.9.0 assembly. To make the project build in Visual Studio 2008, you must change the project’s .proj file. You need to unload the project file, and then add the following properties to the file under the RegisterOutputPackage property:

<!-- We are 2005 compatible, and don't rely on RegPkg.exe
of VS2008 which uses Microsoft.VisualStudio.Shell.9.0 --> 
<UseVS2005MPF>true</UseVS2005MPF> 
<!-- Don't try to run as a normal user (RANA), 
create experimental hive in HKEY_LOCAL_MACHINE --> 
<RegisterWithRanu>false</RegisterWithRanu>.

The Microsoft.VisualStudio.TeamFoundation.Client assembly provides a Microsoft.TeamFoundation.Common namespace that contains a base class called PluginHostPackage. Use this as the base class for your package. It also contains a base class called BasicAsyncPlugin that implements the required ITeamExplorerPlugin interface. You’ll need to delete the default implementation of the generated Package class, then inherit from PluginHostPackage instead of the default Package class.

Because the class now inherits from PluginHostPackage, you only need to override the method OnCreateService. This method returns a new instance of a BasicAsyncPlugin derived class that manages the actual plug-in implementation. You can see the implementation of the HostPackage for the Subst Explorer in Figure 2. You’ll also need to register your Team Explorer plug-in by hand, a task we’ll return to later in the article.

Figure 2 SubstExplorerPackage Implementation

...

[ProvideService(typeof(SubstExplorer))]
[PluginRegistration(Catalogs.TeamProject, "Subst explorer", typeof(SubstExplorer))]
public sealed class SubstExplorerPackage: PluginHostPackage, IVsInstalledProduct {
  private static SubstExplorerPackage _instance;
  public static SubstExplorerPackage Instance {
    get { return _instance; }
  }

  public SubstExplorerPackage () : base() {
    _instance = this;
  }
        
  protected override object OnCreateService(
    IServiceContainer container, Type serviceType) {

    if (serviceType == typeof(SubstExplorer)) {
      return new SubstExplorer();
    }
    throw new ArgumentException(serviceType.ToString());
  }
}

In Figure 2, there are two attributes that are of special interest for the Team Explorer plug-in. ProvideService indicates this package provides a service, and the ServiceType is SubstExplorer. PluginRegistration indicates that the package provides a Team Explorer plug-in and that additional registration is required. This attribute derives from RegistrationAttribute and regpkg.exe normally processes it.

Nodes and Hierarchies

As you can see in Figure 2, the implementation of OnCreateService is straightforward. It returns a new instance of the SubstExplorer class that provides the implementation of the BasicAsyncPlugin class. The SubstExplorer class is responsible for managing a part of the Team Explorer hierarchy. A hierarchy in Visual Studio is a tree of nodes where each node has a set of associated properties. Examples of other hierarchies in Visual Studio are the Solution Explorer, the Server Explorer, and the Performance Explorer.

The SubstExplorer manages the plug-in hierarchy by overriding two methods called CreateNewTree and GetNewUIHierarchy**.** In Figure 3, you can see the implementation of the SubstExplorer class that derives from BasicAsyncPlugin.

Figure 3 SubstExplorer implementation

[Guid("97CE787C-DE2D-4b5c-AF6D-79E254D83111")]
public class SubstExplorer : BasicAsyncPlugin {
  public SubstExplorer() : 
    base(MSDNMagazine.TFSPlugins.SubstExplorerHostPackage.Instance) {}

  public override String Name
  { get { return "Subst drive mappings"; } }

  public override int DisplayPriority {
    get { 
      // After team explorer build, but before any installed power tools
      // power tools start at 450
      return 400; 
    }
  }

  public override IntPtr OpenFolderIconHandle
  { get { return IconHandle; }}

  public override IntPtr IconHandle
  { get { return new Bitmap(
    SubstConfigurationFile.GetCommandImages().Images[2]).GetHicon(); } }

  protected override BaseUIHierarchy GetNewUIHierarchy(
    IVsUIHierarchy parentHierarchy, uint itemId) { 

    SubstExplorerUIHierarchy uiHierarchy = 
      new SubstExplorerUIHierarchy(parentHierarchy, itemId, this);
    return uiHierarchy;
  }

  protected override BaseHierarchyNode CreateNewTree(
    BaseUIHierarchy hierarchy) {

    SubstExplorerRoot root = 
      new SubstExplorerRoot(hierarchy.ProjectName + 
      '/' + "SubstExplorerRoot");
    PopulateTree(root);
    // add the tree to the UIHierarchy so it can handle the commands
    if (hierarchy.HierarchyNode == null)
      { hierarchy.AddTreeToHierarchy(root, true); }
    return root;
  }

  public static void PopulateTree(BaseHierarchyNode teNode) {
    string projectName = 
      teNode.CanonicalName.Substring(0, 
      teNode.CanonicalName.IndexOf("/"));
    var substNodes = 
      SubstConfigurationFile.GetMappingsForProject(projectName);
    if (substNodes != null) {
      foreach (var substNode in substNodes) {
        SubstExplorerLeaf leafNode = 
          new SubstExplorerLeaf(substNode.name, substNode.drive, 
          substNode.versionControlPath);
        teNode.AddChild(leafNode);
      }
      // (bug workaround) force refresh of icon that changed 
      // during add, to force icon refresh
      if (teNode.IsExpanded) {
        teNode.Expand(false);
        teNode.Expand(true);
      }
    }
  }
}

The SubstExplorer class manages the creation of a set of hierarchy nodes. For the SubstExplorer package, these nodes represent virtual folder locations that the plug-in can map as a drive. Each node contains the properties needed to map a drive using the subst.exe command. The package will track Name, Drive Letter, and Location (in the version control repository).

The package creates the tree in two steps. First, it creates the command handler class of all hierarchy nodes, better known as a UIHierarchy. The GetNewUIHierarchy method initiates this step. Second, the CreateNewTree method handles the creation of the tree of nodes that represent virtual drive mappings.

GetNewUIHierarchy is called from the UI thread and returns an instance of a class that derives from the base class BaseUIHierarchy. You’ll find the package’s implementation in the SubstExplorerUIHierarchy class. SubstExplorerUIHierarchy needs to handle all the Add, Delete, and Edit commands executed from any of the nodes the package adds to Team Explorer. The ExecCommand method handles these commands. But first you need to create the menus and commands in Visual Studio.

In the SubstExplorer class, you override the CreateNewTree method that is called from a non-UI thread and returns the tree of nodes that represent all the drive substitutions configured for a team project. The tree always starts with a root node, derived from the RootNode class. For each definition, you’ll add a child node to the root. The leaf node contains the properties you need to map a drive.

Commands and Properties

Now that you’ve seen the basic requirements to set up a Team Explorer plug-in, you need to add some functionality to it. The SubstExplorerRoot class derives from the RootNode class found in the Microsoft.TeamFoundation.Common assembly. Here you override the Icons, PropertiesClassName, and ContexMenu properties.

The Icons property returns an ImageList that contains the icons you want to use for displaying the nodes. In the constructor of the RootNode, you need to set the ImageIndex so that it points to the right image in the ImageList.

The PropertiesClassName returns a string that represents the name that Visual Studio displays in the properties grid window when you select a node. Any string you think is appropriate will suffice here.

The ContextMenu property returns a CommandID that represents the context menu you want to show. For the root node, you need a Context Menu with one option, called Add. Figure 4 shows the implementation of the SubstExplorerRoot.

Figure 4 SubstExplorerRoot

public class SubstExplorerRoot : RootNode {
  static private readonly CommandID command = 
    new CommandID(GuidList.guidPackageCmdSet, 
    CommandList.mnuAdd);

  public SubstExplorerRoot(string path) : base(path) {
    this.ImageIndex = 2;
    NodePriority  = (int)TeamExplorerNodePriority.Folder;
  }

  public override System.Windows.Forms.ImageList Icons
  { get { return SubstConfigurationFile.GetCommandImages();  } }

  public override string PropertiesClassName {
    //Name of the node to show in the properties window
    get { return "Subst Explorer Root"; }
  }

  public override 
    System.ComponentModel.Design.CommandID ContextMenu
  { get { return command; } }
}

The leaf node class SubstExplorerLeaf (see Figure 5) derives from BaseHierarchyNode, and here you need to override the properties ContextMenu, PropertiesClassName and PropertiesObject. Youl also need to provide a custom implementation of DoDefaultAction. Visual Studio calls this method when you double-click a leaf node. DoDefaultAction executes the code that performs the Subst command. If you’ve previously executed the Subst command, it removes the mapping.

Figure 5 SubstExplorerLeaf

public class SubstExplorerLeaf : BaseHierarchyNode {
  private enum SubstIconId {
    unsubsted = 1,
    substed = 2
  }

  CommandID command = 
    new CommandID(GuidList.guidPackageCmdSet, 
    CommandList.mnuDelete);
  bool IsDriveSubsted { get; set; }

  public string VersionControlPath { get; set; }
  public string SubstDriveLetter { get; set; }

  public SubstExplorerLeaf(string path,  
    string substDriveLetter, string versionControlPath)
    : base(path, path + " (" + substDriveLetter + ":)") {

    this.ImageIndex = (int)SubstIconId.unsubsted;
    this.NodePriority = (int)TeamExplorerNodePriority.Leaf;
          
    this.VersionControlPath = versionControlPath;
    this.SubstDriveLetter = substDriveLetter;
    this.IsDriveSubsted = false;
  }

  public override void DoDefaultAction() {
    if (!IsDriveSubsted) {
      SubstDrive();
    }
    else {
      UnsubstDrive(SubstDriveLetter);
    }
  }

  public override CommandID ContextMenu
  { get { return command;  } }

  public override string PropertiesClassName
  { get { return "Subst Leaf Node"; }}

  public override ICustomTypeDescriptor PropertiesObject {
    get {
      return new SubstExplorerProperties(this);
    }
  }

  private void SubstDrive() {
    if (IsDriveAlreadySubsted(SubstDriveLetter)) {
      UnsubstDrive(SubstDriveLetter);
    }
    string substresponse = 
      SubstHelper.Subst(SubstDriveLetter, GetLocalFolder());
 
    if (string.IsNullOrEmpty(substresponse)) {
      IsDriveSubsted = true;
      this.ImageIndex = (int)SubstIconId.substed;
    }
    else {
      MessageBox.Show(string.Format(
        "Unable to make subst mapping. Message:\n {0}", 
        substresponse));
    }
  }

  private bool IsDriveAlreadySubsted(string driveLetter) {
    bool IsdrivePhysicalyMaped = 
      SubstHelper.SubstedDrives().Where(
      d => d.Contains(driveLetter + ":\\")).Count() != 0;
    bool IsdriveKnownToBeMaped = 
      (from substedNode in _substedNodes
      where substedNode.SubstDriveLetter == driveLetter
      select substedNode).ToArray<SubstExplorerLeaf>().Length > 0;
    return IsdriveKnownToBeMaped || IsdrivePhysicalyMaped;
  }

  public void UnsubstDrive(string substDriveLetter) {
    string substResponse = SubstHelper.DeleteSubst(substDriveLetter);
    IsDriveSubsted = false;
    this.ImageIndex = (int)SubstIconId.unsubsted;
  }

  public string localPath {
    get { return VersionControlPath; }
  }
}

The ContextMenu property represents the context menu you want to show at the leaf node. The context menu exposes two commands: Properties and Delete. In the class, the PropertiesClassName has the same purpose as in the root node. You use the PropertiesObject property to get back an object that you can use to display the properties of the selected node in the properties window. For the leaf node, the properties exposed will be Name, DriveLetter, and VersionControlPath.

You return a new instance of the type SubstExplorerProperties (see Figure 6). You use this object to display the properties of the leaf node. SubstExplorerProperties provides an implementation of the ICustomTypeDescriptor interface that returns information on which properties you want to show and how you want to show them. BaseHierarchyNode has a default properties object that shows things like the URL, ServerName, and ProjectName, but that did not seem useful for our leaf node.

Figure 6 SubstExplorerProperties

public class SubstExplorerProperties 
  : ICustomTypeDescriptor, IVsProvideUserContext {

  private BaseHierarchyNode m_node = null;
  public SubstExplorerProperties(BaseHierarchyNode node)
    { m_node = node; }

  public string GetClassName()
    { return m_node.PropertiesClassName;}

  public string GetComponentName()
    { return m_node.Name; }
  public PropertyDescriptorCollection 
    GetProperties(Attribute[] attributes) { 

    // create for each of our properties the 
    // appropriate PropertyDescriptor
    List<PropertyDescriptor> list = new List<PropertyDescriptor>();
    PropertyDescriptorCollection descriptors = 
      TypeDescriptor.GetProperties(this, attributes, true);

    for (int i = 0; i < descriptors.Count; i++) {
      list.Add(new DesignPropertyDescriptor(descriptors[i]));
    }
    return new PropertyDescriptorCollection(list.ToArray());
  }

  public object GetPropertyOwner(PropertyDescriptor pd) {  
    // return the object implementing the properties
    return this;
  }

  // rest of ICustomTypeDescriptor methods are not 
  // shown since they are returning defaults 
  // actual properties start here
  [Category("Drive mapping")]
  [Description("...")]
  [DisplayName("Version Control Path")]
  public string VersionControlPath
    {  get { return ((SubstExplorerLeaf)m_node).VersionControlPath; } }

  [Category("Drive mapping")]
  [Description("...")]
  [DisplayName("Subst drive letter")]
  public SubstDriveEnum SubstDriveLetter {
    get { return 
      (SubstDriveEnum)Enum.Parse(typeof(SubstDriveEnum),
      ((SubstExplorerLeaf)m_node).SubstDriveLetter); }
  }

  [Category("Drive mapping")]
  [Description("...")]
  [DisplayName("Mapping name")]
  public string MappingName
    {  get { return ((SubstExplorerLeaf)m_node).Name; } }
}

Commands and Menus

If you examine the root and leaf node implementations, you see that both need to display a context menu. The root node needs to have an Add menu item. A leaf node needs Delete and Properties menu items. Both node implementations return a CommandID instance as the implementation of their respective ContextMenu properties. In order for the CommandID class to function correctly, you need to define the menus and commands in the solution.

To add a menu and command to Visual Studio, you need to define the commands in a command table. You add command tables to the assembly as an embedded resource. In addition, you need to register the command table and the system registry during package registration. When you run devenv /setup, Visual Studio gathers all command resources from all registered packages and builds an internal representation of all commands in the development environment.

Starting with Visual Studio 2005, you could define command tables in an XML file with the extension .vsct. In this file, you define the menus, the command groups, and the buttons you want to show in the menu. A Visual Studio command is part of a command group. You place command groups on menus.

For the root node, you need an Add command, placed in a group contained by a menu. The leaf node needs Delete and Properties commands. You need to define a second menu that contains a different group that contains these two commands. (See the download accompanying this article for an example .vsct file.)

The .vsct file needs special treatment in the Visual Studio project. You must compile it into a resource and then embed the resource in the assembly. After you install the Visual Studio SDK, you can select a special build action for your command file called VSCTCompile. This action takes care of compiling and embedding the resource in the assembly.

In the command table XML, some symbols are used in the definition of the menus and commands. You add all menus, commands, and buttons to the same commandSet called GuidPackageCmdSet:

<Symbols>
  <!-- This is the package guid. -->
  <GuidSymbol name="GuidPackage" value=
"{9B024C14-2F6F-4e38-AA67-3791524A807E}"/>
  <GuidSymbol name="GuidPackageCmdSet" value=
"{D0C59149-AC1D-4257-A68E-789592381830}"/>
    <IDSymbol name="mnuAdd" value="0x1001" />
    <IDSymbol name="mnuDelete" value="0x1002" />

Everywhere you need to provide context menu information, you refer back to this symbol as the container of the menu. Thus, in the SubstExplorerRoot and SubstExplorerLeaf implementations, you create an instance of the CommandID type and use GuidPackageCmdSet as the first argument and the actual menu you want to display as the second argument:

CommandID command = new CommandID(
  GuidList.guidPackageCmdSet, 
  CommandList.mnuDelete);

In the .vsct file, there are three commands that the UIHierarchy needs to respond to. The ExecCommand method is called when you click one of the menu items. The method needs to select the action to execute based on the nCmdId passed to it. The basic implementation of the SubstExplorerUIHierarchy is shown in Figure 7.

Figure 7 SubstExplorerUIHierarchy

public class SubstExplorerUIHierarchy : BaseUIHierarchy, 
  IVsHierarchyDeleteHandler, IVsHierarchyDeleteHandler2 {

  public SubstExplorerUIHierarchy(IVsUIHierarchy parentHierarchy, 
    uint itemId, BasicAsyncPlugin plugin)
    : base(parentHierarchy, itemId, plugin, 
    MSDNMagazine.TFSPlugins.SubstExplorerHostPackage.Instance) { 
  }
        
  public override int ExecCommand(uint itemId, 
    ref Guid guidCmdGroup, uint nCmdId, 
    uint nCmdExecOpt, IntPtr pvain, IntPtr p) {

    if (guidCmdGroup == GuidList.guidPackageCmdSet) {
      switch (nCmdId) {
        case (uint)CommandList.cmdAdd:
             AddNewDefinition(this.ProjectName);
             return VSConstants.S_OK;
        case (uint)CommandList.cmdDelete:
             RemoveDefinition(itemId);
             return VSConstants.S_OK;
        case (uint)CommandList.cmdEdit:
             EditDefinition(itemId);
             return VSConstants.S_OK;
        default: return VSConstants.E_FAIL;
      }
    }

    return base.ExecCommand(itemId, ref guidCmdGroup, nCmdId, nCmdExecOpt, pvain, p);
  }
  ...
}

Add, Edit and Delete

Now you need to provide a way for the user to add, delete, or edit mappings on the root or leaf nodes. The code is in place to handle calls for Add on the root node and for the Edit and Delete commands on the leaf nodes. Adding a new mapping requires input from the user and you need to store the mapping data in a well-known location. This location is preferably in the user’s roaming profile. So let's take a closer look on how you can respond to the Add command.

The AddNewDefinition method in the SubstExplorerUIHierarchy class handles the Add command. AddNewDefinition shows a dialog allowing users to specify the mapping they want to create. A mapping needs to have a name and a drive letter for the Subst command. In addition, the mapping needs to point to a path in the version control repository. You want to allow the user to pick the location from version control rather than having to enter a complex path manually. You can enable this by using the TFS object model, specifically the GetServer method from the TeamFoundationServerFactory class. GetServer accepts a URL representing the server you want to use and a credentialsProvider in case the user is not in the same domain as the server and the server connection requires new authentication. After you have access to a valid TeamFoundationServer instance, you have access to the various services provided by TFS.

You need the VersionControlServer service to get information about the folder structure inside the current team project. In Brian’s January 2007 Team System column (msdn.microsoft.com/magazine/cc163498), he showed how you could use this service to create your own version control folder browser dialog. We’ve reused the dialog described in that article here (see Figure 8). The dialog returns the folder selected by the user in the Version Control repository as shown in Figure 9. You store the path returned in a configuration file.

When the user clicks OK, you can add a new node to the configuration file and a new child node to the hierarchy. You add a new node by calling the AddChild method on the HierarchyNode instance.

Adding a New Mapping Definition

Figure 8 Adding a New Mapping Definition

Choosing a Location in Version Control

Figure 9 Choosing a Location in Version Control

Executing the Default Command

The SubstExplorerUIHierarchy class is responsible for handling all commands fired by the menu options offered by the plug-in. One of the other commands you need to handle is when a user double-clicks on a node. The DoDefaultAction method processes this event. For the root node, the default action of either collapsing or expanding the nodes in the hierarchy is acceptable. However, for leaf nodes, you’ll provide a custom implementation.

You want to substitute the drive based on the properties set for that node. To subst a drive, you can issue a command-line action and provide the required parameters. For that purpose, we created a SubstHelper class that calls into the System.Diagnostics namespace to create a new process called subst.exe and provide it with the required parameters. The parameters needed are the drive letter and the local folder you want to map as the drive. You have the drive letter available. However, you need to map the version control path to local folder. Once again, you’ll use the TFS object model and get a reference to the VersionControlServer object. You can query this object for all available workspaces and try to get a mapping to a local folder based on the version control path you have. Figure 10 provides an implementation.

Figure 10 Mapping a Version Control Path to a Location on Disk

private string GetLocalFolder() {
  VersionControlServer vcs = 
    (VersionControlServer)((
    SubstExplorerUIHierarchy)ParentHierarchy).
    tfs.GetService(typeof(VersionControlServer));
  Workspace[] workspaces = 
    vcs.QueryWorkspaces(null, vcs.AuthenticatedUser, 
    Environment.MachineName);
  foreach (Workspace ws in workspaces) {
    WorkingFolder wf = 
      ws.TryGetWorkingFolderForServerItem(VersionControlPath);
    if (wf != null) {
      // We found a workspace that contains this versioncontrolled item
      // get the local location to map the drive to this location....
      return wf.LocalItem;
    }
  }
  return null;
}

Finishing Touches

Now you have all the logic in place to show a tree of nodes and handle drive mapping. However, you want your Team Explorer plug-in to stand out. You might want to add some additional features in terms of delete node handling and other professional touches, such as adding an icon in the splash screen of Visual Studio.

Adding the delete functionality requires you to implement an additional interface in the SubstExplorerUIHierarchy class. Visual Studio has a specific interface called IVsHierarchyDeleteHandler that you implement to show a default dialog when you press the delete key. For this plug-in, you’ll want to provide a custom dialog asking the user to confirm the deletion of the node that is selected. To make that work, you also need to implement the IVsHierarchyDeleteHandler2 interface for delete handling to work from the keyboard. Since you’ve already implemented the actual delete functionality, you need only to implement this interface and call the existing functions. In Figure 11 you can see the implementation of the interfaces.

Figure 11 IVsHierarchyDeleteHandler implementation

#region IVsHierarchyDeleteHandler2 Members

public int ShowMultiSelDeleteOrRemoveMessage(
  uint dwDelItemOp, uint cDelItems, 
  uint[] rgDelItems, out int pfCancelOperation) {

  pfCancelOperation = Convert.ToInt32(true);
  return VSConstants.S_OK;
}

public int ShowSpecificDeleteRemoveMessage(
  uint dwDelItemOps, uint cDelItems, uint[] rgDelItems, 
  out int pfShowStandardMessage, out uint pdwDelItemOp) {

  SubstExplorerLeaf nodeToDelete = 
    NodeFromItemId(rgDelItems[0]) as SubstExplorerLeaf;
  if (AreYouSureToDelete(nodeToDelete.Name)) { 
    pdwDelItemOp = 1; // == DELITEMOP_DeleteFromStorage; 
                      // DELITEMOP_RemoveFromProject==2; 
  }
  else { 
    pdwDelItemOp = 0; // NO delete, user selected NO option }

  pfShowStandardMessage = Convert.ToInt32(false);
  return VSConstants.S_OK;
}

#endregion
#region IVsHierarchyDeleteHandler Members

public int DeleteItem(uint dwDelItemOp, uint itemid) {
  SubstExplorerLeaf nodeToDelete = 
    NodeFromItemId(itemid) as SubstExplorerLeaf;
  if (nodeToDelete != null) {
    // remove from storage
    RemoveDefinitionFromFile(nodeToDelete);
    // remove from UI
    nodeToDelete.Remove();
  }
  return VSConstants.S_OK;
}

public int QueryDeleteItem(uint dwDelItemOp, uint itemid, 
  out int pfCanDelete) {

  pfCanDelete = Convert.ToInt32(NodeFromItemId(itemid) is SubstExplorerLeaf);
  return VSConstants.S_OK;
}
#endregion

It’s important to note that the plug-in does not support multiple selected nodes being deleted at once, hence pfCancelOperation is set to true in the ShowMultiSelDeleteOrRemoveMessage method. In the ShowSpecificDeleteRemoveMessage method implementation, you need to return the correct value of what you want to delete. You return a value of 1 to indicate you have removed it from storage. These flags are normally used in the Visual Studio project system and only a value of 1 produces the correct results.

You might also want to add support for splash screen integration. By default, each time you start Visual Studio, you’ll see a splash screen listing the products registered. You accomplish this by implementing the IVsInstalledProduct interface in the SubstExplorerHostPackage implementation class. The methods there require you register the resource IDs for the icon to use in the splash screen and the icon to use in the About box.

The implementation is nothing more than setting the out parameter to the correct integer value and embedding a 32x32 pixel icon as a resource in the assembly. In order to embed the resource correctly in your assembly, you need to open up the resources.resx file in the XML editor and add the following lines to the resource file:

<data name="500" 
  type="System.Resources.ResXFileRef, System.Windows.Forms">
  <value>..\Resources\SplashIcon.bmp;System.Drawing.Bitmap, 
    System.Drawing, Version=2.0.0.0, Culture=neutral, 
    PublicKeyToken=b03f5f7f11d50a3a</value> 
</data>

This adds the resource bitmap located in the Resources folder in the project to the resource and embeds it at the reference 500. In the method IdBmpSplash, you can now set pIdBmp to 500 and return the S_OK constant as a return value. To get the icon in the splash screen, you need to build the assembly and then run devenv /setup from the command line. This will get the information from the package you’ve created and it will cache the data. This ensures the package does not need to be loaded when Visual Studio shows the splash screen. You do this for the same reasons you needed to do so for the menu options you added: to speed up the load time of Visual Studio.

Package Registration

Now that you’ve finished the Team Explorer extension, it's time to package the product and get it running on other developer’s systems. So, how can you distribute the results?

First, Visual Studio behaves differently when you’ve installed the SDK. By default it will accept or load any VSPackage. This will not be the case on machines where you haven’t installed the SDK.

For a package to load correctly, you need to embed a package load key, which you obtain from Microsoft (see msdn.microsoft.com/vsx/cc655795). The most important part of this process is to ensure that you provide the exact same information when registering for your key as the information you provided in the attributes for the hostPackage class (in this case the SubstExplorerHostPackage class). Also, when the Web site asks you to enter the package name, you must provide the product name you used in the ProvideLoadKey attribute.

Once you get your load key, you can paste it into the resource file with the resource identifier you provided as last argument of the ProvideLoadKey attribute. Make sure you remove the carriage return/line feeds from the string when you copy it from the site so it is one consecutive string before you paste it in the resource file.

Now you can test if your plug-in works by specifying an additional debug parameter: /NoVsip. This parameter ensures that Visual Studio uses the normal loading behavior. If the key is not accepted, Visual Studio will display a load failure dialog. With the SDK installed, you’ll find under the Visual Studio Tools menu the Package Load Analyzer. You can point this at your assembly to help debug what is wrong. If it is only the package load key, then ensure you have typed exactly the same parameters at the Web site as in your attribute.

The last step that remains is the package registration for a production machine. Unfortunately, because the Team System assemblies use a different version of the shell assemblies, you cannot use regpkg.exe to register your package. Instead, you need to do it by hand using a registry file. In this file, you need to publish the package in the correct registry location. The registration script required is shown in Figure 12.

Figure 12 Package Registration Script

REGEDIT4
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\TeamSystemPlugins\Team Explorer Project Plugins\SubstExplorer]
@="97CE787C-DE2D-4b5c-AF6D-79E254D83111"
"Enabled"=dword:00000001
 
[HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\9.0\Services\{97ce787c-de2d-4b5c-af6d-79e254d83111}]
@="{9b024c14-2f6f-4e38-aa67-3791524a807e}"
"Name"="SubstExplorer"
 

[HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\9.0\Packages\{9b024c14-2f6f-4e38-aa67-3791524a807e}]
@="MSDNMagazine.TFSPlugins.SubstExplorerHostPackage, TFSSubstExplorer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=324c86b3b5813447"
"InprocServer32"="C:\\Windows\\system32\\mscoree.dll"
"Class"="MSDNMagazine.TFSPlugins.SubstExplorerHostPackage"
"CodeBase"="c:\\program files\\msdnsamples\\TFSSubstExplorer.dll"
"ID"=dword:00000065
"MinEdition"="Professional"
"ProductVersion"="1.0"
"ProductName"="SubstExplorer"
"CompanyName"="vriesmarcel@hotmail.com"

[HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\9.0\InstalledProducts\SubstExplorerHostPackage]
"Package"="{9b024c14-2f6f-4e38-aa67-3791524a807e}"
"UseInterface"=dword:00000001
 
[HKEY_LOCAL_MACHINE\Software\Microsoft\VisualStudio\9.0\Menus]
"{9b024c14-2f6f-4e38-aa67-3791524a807e}"=", 1000, 1"

In the registration script, you’ll see a number of entries. The first entry registers a new Team Explorer extension that Team Explorer should load as soon as it loads. Here you provide a registry value that refers to the service ID that provides an implementation of ITeamExplorerPlugin. The next entry provides the service registration where you see the previously referred-to service ID, as well as a registry value that points to the package that provides the plug-in.

The next entry is the package registration itself. There you use the package ID as a new key and provide the information where the assembly can be found, how it can be loaded using the COM infrastructure, and what Visual Studio version the package supports. The second to last entry is the registration of the installed products, used for the splash screen. Here the UseInterface key indicates Visual Studio must call the IVsInstalledProduct interface instead of relying on the InstalledProductRegistration attribute to provide an icon and product description that needs to be shown at startup.

The last entry is the registration of the context menus. Here you refer back to your package, but you also provide information about where you’ve embedded the resources in the assembly. These are the embedded resources you created before using the .vsct files and the custom build action on that file. With this script and the assembly you’ve built, you can deploy it on other machines. Just place the assembly on the file system, tweak the registry script to reflect the correct assembly location, and merge it into the registry. Then, the final step is to run devenv /setup on that machine. When you start Visual Studio, you will see the icon in the splash screen and when you load the Team Explorer, you will see the root node of the plug-in you’ve created.


Brian A. Randellis 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.

Marcel de Vriesis an IT architect at Info Support in the Netherlands, where he creates solutions for large bank and insurance companies across the country and teaches the Team System and Windows Workflow courses. Marcel is a frequent speaker at developer conferences in Europe, a Team System MVP since 2006, and a Microsoft regional director since January 2009.

Thanks to the following technical experts for reviewing this article: Dennis Habib and Buck Hodges