The IDTExtensibility2 Interface
As you now know, an implementation of IDTExtensibility2 lies at the core of every add-in. Visual Studio .NET calls the methods on this interface whenever it needs to apprise an add-in of important events, such as when another add-in is loaded or unloaded, or when Visual Studio .NET is about to shut down. The communication isn't just one-way, either: through the IDTExtensibility2 interface, the add-in has access to and control over the entire Visual Studio .NET automation object model.
The EnvDTE Namespace
Before examining the individual IDTExtensibility2 methods, we need to take a quick look at the real objective of add-inscontrolling the objects in the EnvDTE namespace. The name EnvDTE stands for Environment Development Tools Extensibility, which pretty much describes its purpose: it defines the Visual Studio .NET automation object model. The Visual Studio .NET documentation includes a chart of the automation object model that displays a hierarchy of over 140 objects defined by the EnvDTE namespace. The add-ins in this book will make use of most of those objects, but a few of the objects are of special interest to add-ins:
DTE
The root object of the automation object model
DTE.AddIn
An object that represents an add-in
DTE.AddIns
A collection of AddIn objects that includes all add-ins registered with the Visual Studio .NET IDE
DTE.Solution.AddIns
A collection of AddIn objects associated with a solution
The next several examples will focus on the DTE, DTE.AddIn, and DTE.AddIns objects, which collectively give you control over your own add-in and others. We'll cover the DTE.Solution.AddIns object in Chapter 8.NoteThe main purpose of an add-in class is to provide an implementation of IDTExtensibility2, but that doesn't have to be its only purpose. An add-in class is a class, after all, and it can define any number of non-IDTExtensibility2-related methods, properties, and events. The automation object model provides access to your add-in class through the AddIn.Object property, which returns the add-in's IDispatch interface. The following macro code shows how you would call a public method named DisplayMessage on the MyAddIn.Connect add-in class:
Dim dispObj As Object = DTE.AddIns.Item("MyAddIn.Connect").Object
dispObj.DisplayMessage("IDispatch a message to you.")
OnConnection
By far the most important of the IDTExtensibility2 methods, OnConnection provides an add-in with the main object reference it needs to communicate directly with the IDE. The OnConnection method has the following prototype:
public void OnConnection(object application,
ext_ConnectMode connectMode,
object addInInst,
ref Array custom);
The application parameter holds a reference to an instance of EnvDTE.DTE, which is the root object of the automation object model. Technically, application holds a reference to an instance of EnvDTE.DTEClass, which implements the EnvDTE.DTE interface, which in turn derives from the EnvDTE._DTE interface. This last interface contains the types you want. To get at the _DTE interface types, you can cast application to DTE or _DTE, according to your taste. (See the upcoming sidebar "Underscoring the Obvious" to find out where all those EnvDTE underscores come from.) Almost every add-in that does something useful has need of the DTE object, so the first statements in OnConnection typically cache the DTE object in a global variable.
Underscoring the ObviousStaring at the underscores that litter the EnvDTE namespace, you might begin to wonder what the Visual Studio .NET programmers were smoking when they designed it. Most of the type names in the EnvDTE namespace bear little resemblance to type names found elsewhere in the .NET Framework; the name EnvDTE itself violates the .NET Framework's Pascal-casing rule. As it turns out, there's a legitimate reason for EnvDTE's strange names (which implies that the programmers' smoking material probably was legitimate also): that reason is COM.The original extensibility object model, Design Time Extensibility (DTE), began life as a COM component in previous versions of Visual Studio. Rather than rewrite the component as managed code, the Visual Studio .NET team chose to offer the component's functionality via COM interoperability. The EnvDTE assembly that shows up in the Add Reference dialog box was generated mechanically by running the extensibility component's type library (dte.olb) through the Type Library Importer utility (TlbImp). As it churns through a type library, TlbImp preserves the type library names exactly as it finds themin the case of EnvDTE, the result is a namespace that carries its COM heritage in every underscore.Of course, the Visual Studio team made the right decision not to rewrite the extensibility component for the earliest versions of Visual Studio .NET, so for now we'll just have to live with the funny names and hope our pinkies don't give out from typing Shift+<Underscore> all day. |
Constant | Value (Int32) | Description |
---|---|---|
ext_cm_AfterStartup | 0x00000000 | Loaded after Visual Studio .NET started. |
ext_cm_Startup | 0x00000001 | Loaded when Visual Studio .NET started. |
ext_cm_External | 0x00000002 | Loaded by an external client. (No longer used by Visual Studio .NET.) |
ext_cm_CommandLine | 0x00000003 | Loaded from the command line. |
ext_cm_Solution | 0x00000004 | Loaded with a solution. |
ext_cm_UISetup | 0x00000005 | Loaded for user interface setup. |
OnStartupComplete
The OnStartupComplete event fires only in add-ins that load when Visual Studio .NET starts. The OnStartupComplete prototype looks like this:
public void OnStartupComplete(ref Array custom);
An add-in that loads at startup can't always rely on OnConnection for its initializationif the add-in arrives too early, it will fail when it tries to access a Visual Studio .NET component that hasn't yet loaded. In such cases, the add-in can use OnStartupComplete to guarantee that Visual Studio .NET is up and running first.
OnAddInsUpdate
The OnAddInsUpdate event fires when an add-in joins or leaves the Visual Studio .NET environment. An add-in can use this event to enforce dependencies on other add-ins. Here's the OnAddInsUpdate prototype:
public void OnAddInsUpdate(ref Array custom);
The lack of useful parameters reveals OnAddInsUpdate's passive-aggressive natureit interrupts your add-in to tell it that the state of some add-in has changed, but it withholds information about which add-in triggered the event and why. If you need to know the add-in responsible for the event, you have to discover its identity on your own. Fortunately, you have the DTE.AddIns collection to aid you in your investigation. This collection holds a list of AddIn objects (one for each registered add-in), and each AddIn object has a Connected property that exposes its connection status. You retrieve a specific add-in from the AddIns collection by passing the AddIns.Item method a ProgID or a 1-based index; if the requested index doesn't exist in the collection, the Item method throws an "invalid index" COMException; otherwise, it returns an AddIn reference. Here's one way to check InsideVSNET.AddIns.LifeCycle's connection status:
public void OnAddInsUpdate(ref Array custom)
{
try
{
AddIn addIn =
this.dte.AddIns.Item("InsideVSNET.AddIns.LifeCycle");
if (addIn.Connected == true)
{
// InsideVSNET.AddIns.LifeCycle is connected
}
else
{
// InsideVSNET.AddIns.LifeCycle isn't connected
}
}
catch (COMException)
{
// InsideVSNET.AddIns.LifeCycle isn't a
// registered add-in
}
}
Of course, whether InsideVSNET.AddIns.LifeCycle caused the event remains a mystery. The LoadUnload add-in, shown in Listing 6-3, does what the previous sample cannot: it deduces which add-in triggers the OnAddInsUpdate event.
Listing 6-3 The LoadUnload source code
LoadUnload.cs
namespace InsideVSNET
{
namespace AddIns
{
using EnvDTE;
using Extensibility;
using InsideVSNET.Utilities;
using Microsoft.Office.Core;
using System;
using System.Collections;
using System.Runtime.InteropServices;
[GuidAttribute("B2FCDEBF-1536-4EA2-9F1A-81878A9C028D"),
ProgId("LoadUnload.Connect")]
public class Connect : Object, IDTExtensibility2, IDTCommandTarget
{
private DTE dte;
private AddIn addInInstance;
private SortedList addInsList = new SortedList();
private AddIns addInsCollection;
private OutputWindowPaneEx output;
private string title = "LoadUnload";
public Connect()
{
}
public void OnConnection(object application,
ext_ConnectMode connectMode,
object addInInst,
ref Array custom)
{
this.dte = (DTE)application;
this.addInInstance = (AddIn)addInInst;
this.addInsCollection = this.dte.AddIns;
foreach (AddIn addIn in this.addInsCollection)
{
this.addInsList[addIn.ProgID] = addIn.Connected;
}
this.output = new OutputWindowPaneEx(this.dte, this.title);
}
public void OnDisconnection(ext_DisconnectMode disconnectMode,
ref Array custom)
{
}
public void OnAddInsUpdate(ref Array custom)
{
this.addInsCollection.Update();
foreach (AddIn addIn in this.addInsCollection)
{
if (this.addInsList.Contains(addIn.ProgID))
{
if (addIn.Connected !=
(bool)this.addInsList[addIn.ProgID])
{
string action = addIn.Connected ?
"loaded" : "unloaded";
this.output.WriteLine(addIn.ProgID +
" was " + action, this.title);
}
}
else
{
string action = addIn.Connected ?
" and loaded" : String.Empty;
this.output.WriteLine(addIn.ProgID +
" was added" + action, this.title);
}
this.addInsList[addIn.ProgID] = addIn.Connected;
}
}
public void OnStartupComplete(ref Array custom)
{
}
public void OnBeginShutdown(ref Array custom)
{
}
}
}
}
LoadUnload maintains a running list of add-ins and their connection statuses in its addInsList variable, which is declared as type SortedList. When OnAddInsUpdate fires, LoadUnload compares the connection statuses of the add-ins in its internal list with the connection statuses of the add-ins in the DTE.AddIns collectionif it finds a discrepancy, it knows which add-in to blame for the event. Here's the first part of the main loop from Listing 6-3:
this.addInsCollection.Update();
foreach (AddIn addIn in this.addInsCollection)
{
if (this.addInsList.Contains(addIn.ProgID))
{
if (addIn.Connected !=
(bool)this.addInsList[addIn.ProgID])
{
string action = addIn.Connected ?
"loaded" : "unloaded";
this.output.WriteLine(addIn.ProgID +
" was " + action, this.title);
}
}
The addInsCollection variable holds a reference to the DTE.AddIns collection, and the call to Update synchs up the collection with the registry so that any newly created add-ins are included. (The Add-in Manager performs the equivalent of Update each time it runs.) After the call to Update, the main loop iterates through the current add-ins in addInsCollection and checks whether each add-in already exists in its internal list. If so, the Connected property of the add-in is compared with the corresponding value stored in the internal list; if they differ, the Connected property determines whether the add-in was just loaded (true) or unloaded (false).If the current add-in doesn't exist in addInsList, the add-in was registered sometime between the previous OnAddInsUpdate event and this OnAddInsUpdate event. Here's the second part of the main loop, which handles new add-ins:
else
{
string action = addIn.Connected ?
" and loaded" : String.Empty;
this.output.WriteLine(addIn.ProgID +
" was added" + action, this.title);
}
this.addInsList[addIn.ProgID] = addIn.Connected;
}
The last statement either writes the current Connected value to an existing entry or creates a fresh entry for a newly registered add-in.LoadUnload isn't foolprooffor example, add-ins loaded by commands arrive and leave unannouncedbut it works well enough for demonstration purposes.
OnBeginShutdown
Here's the prototype for OnBeginShutdown:
public void OnBeginShutdown(ref Array custom);
This event fires only when the IDE shuts down while an add-in is running. Although an IDE shutdown might get canceled along the way, OnBeginShutdown doesn't provide a cancellation mechanism, so an add-in should assume that shutdown is inevitable and perform its cleanup routines accordingly. An add-in that manipulates IDE state might use this event to restore the original IDE settings.
OnDisconnection
This event is similar to OnBeginShutdown in that it signals the end of an add-in's life; it differs from OnBeginShutdown in that the IDE isn't necessarily about to shut down. OnDisconnection also provides more information to an add-in than OnBeginShutdown does. OnDisconnection's prototype looks like this:
public void OnDisconnection(ext_DisconnectMode removeMode,
ref Array custom);
The removeMode parameter passes in an IDTExtensibility2.ext_Disconnect
Mode enumeration value that tells an add-in why it was unloaded. Table 6-2 lists the ext_DisconnectMode values.
Constant | Value (Int32) | Description |
---|---|---|
ext_dm_HostShutdown | 0x00000000 | Unloaded when Visual Studio .NET shut down |
ext_dm_UserClosed | 0x00000001 | Unloaded while Visual Studio was running |
ext_dm_UISetupComplete | 0x00000002 | Unloaded after user interface setup |
ext_dm_SolutionClosed | 0x00000003 | Unloaded when solution closed |