The Automation Object Model
One look at the automation object model chart and you'll agree that the Visual Studio .NET designers are serious about letting developers customize their programming environment. With over 140 different objects, the automation object model gives you unprecedented control over the IDE, from the solution level all the way down to a single parameter in a function.
Automation Objects
Just as its name suggests, the automation object model exposes its functionality through objectsa programming paradigm familiar to most programmers today. Each object in the model encapsulates some small part of Visual Studio .NET and offers programmatic access to it through a set of methods, properties, and events. The objects are arranged in a hierarchy, with the methods and properties of one object allowing access to its child objects below and to its parent object above.The DTE object sits atop this automation object model hierarchy and serves as the entrance to its furthest recesses. Add-ins get their DTE reference from the application parameter of their OnConnection event; macros use the DTE reference in the predefined global variable named DTE. From this DTE reference, you can reach all the other objects in the automation object model. If you're wondering what objects you might want to reach, the following short list should get your imagination working: Solution, Project, and ProjectItem objects give you programmatic control over all aspects of project management. Document and TextDocument objects let you manipulate documents in the IDE. TextSelection and TextPoint objects let you edit text as viewed in an editor window; EditPoint objects let you make changes directly to the text buffer. FileCodeModel, CodeNamespace, CodeInterface, CodeVariable, CodeFunction, and similar objects let you manipulate code constructs at a level independent of the underlying programming language. CommandWindow, TaskList, OutputWindow, ToolBox, and other similar objects give you control of specific tool windows within the IDE. Command objects let you access environment commands. Debugger, Process, Program, Thread, StackFrame, and other related objects give you control over the Visual Studio .NET debugger.
Of course, the objects in the previous list are only the beginning. The remainder of the book covers these objects and more in exhaustive detail, teaching you everything you need to know to turn your copy of Visual Studio .NET into the coolest IDE on your block.
Object Model Guidelines
Whenever possible, the automation object model design follows a few simple guidelines, shown in the following list. (Keep these rules in mind while you program, and you won't waste time agonizing over why AddIns(0) throws a COMException or why Activate won't display your @#&%*! output window.)All objects use Automation types only.All objects have a DTE property that returns the DTE object.Collections have names that are the plural form of the objects they contain. For example, Documents is the collection of Document objects.Collections are accessed through properties that have the same name as the collection.Collections have an Item methodnot a propertyfor accessing their contained items.The Item method for a collection takes an object when you're using a managed language; it takes a VARIANT when you're using an unmanaged language.The Item method on a collection takes a numerical index or the name of a collection item.Numerical indexes into a collection are 1-based.Collections have a Count method that returns a System.Int32 when you're using a managed language or a long when you're using an unmanaged language.Collection items have a Collection property that returns the owning collection; all other objects have a Parent property that returns its parent object.You add an item to a collection by using the Add method on the collection; you remove an item from a collection by using the Remove or Delete method on the item. (Delete applies to user interface items, such as ToolBoxTab objects; Remove applies to nonuser interface items, such as AddIn objects.)The DTE.Events object provides access to all event interfaces.A general object can return one or more specific objects through its Object property, which takes the case-insensitive name of the specific object.Creating a window doesn't automatically make that window visible; instead, you make a window visible by setting its Visible property to True.Properties that return a complete path to a file on disk are called FullName.The Activate method sets the focus on a visible user interface item; if the item isn't visible, Activate won't necessarily make its container visible.
Automation Events
If automation objects tell you what you can do within Visual Studio .NET, automation events tell you when you can do it. Each of the functional groups within the automation object model defines events that allow you to listen in on Visual Studio .NET's activities and take action based on what you hear. Together with automation objects, automation events allow you to achieve hands-free control over every important aspect of Visual Studio .NET.
Connecting to Automation Events
Connecting to automation events is pretty simple, although the syntax for doing so differs dramatically between the languages supported by Visual Studio .NET. We'll begin our events overview with C# because the C# syntax reveals everything we care to know about the mechanics of events.The DTE.Events property is the first stop on the way to automation events. This property returns an EnvDTE.Events interface, which defines the read-only properties shown in Table 5-2. Each of the Events properties returns an event interface with the same name as the property; each event interface, in turn, defines a set of events to which you can subscribe.
Property | Events |
---|---|
BuildEvents | OnBuildBegin OnBuildDone OnBuildProjConfigBegin OnBuildProjConfigDone |
CommandEvents * | AfterExecute BeforeExecute |
DebuggerEvents | OnContextChanged OnEnterBreakMode OnEnterDesignMode OnEnterRunMode OnExceptionNotHandled OnExceptionThrown |
DocumentEvents * | DocumentClosing DocumentOpened DocumentOpening DocumentSaved |
DTEEvents | ModeChanged OnBeginShutdown OnMacrosRuntimeReset OnStartupComplete |
FindEvents | FindDone |
MiscFilesEvents (returns ProjectItemsEvents) | ItemAdded ItemRemoved ItemRenamed |
OutputWindowEvents * | PaneAdded PaneClearing PaneUpdated |
SelectionEvents | OnChange |
SolutionEvents | AfterClosing BeforeClosing Opened ProjectAdded ProjectRemoved ProjectRenamed QueryCloseSolution Renamed |
SolutionItemsEvents (returns ProjectItemsEvents) | ItemAdded ItemRemoved ItemRenamed |
TaskListEvents * | TaskAdded TaskModified TaskNavigated TaskRemoved |
TextEditorEvents * | LineChanged |
WindowEvents * | WindowActivated WindowClosing WindowCreated WindowMoved |
void OnBuildBegin(vsBuildScope scope, vsBuildAction action);
When this event fires, Visual Studio .NET passes along information about the impending build through the scope and action parameters: the scope parameter tells you the extent of the build that is about to begin (solution, batch, or project), and the action parameter tells you the kind of build that is about to begin (build, rebuild all, clean, or deploy). The event handler that you create must have the same signature as this event; you can name the handler anything you like, but the event's name is as good as any other:
public class Connect : Object, Extensibility.IDTExtensibility2
{
private void OnBuildBegin(vsBuildScope scope, vsBuildAction action)
{
// Log the builder's information
}
}
Now that the event handler is in place, it's time to wire it up to the event. From Table 5-2, you know that build events belong to the BuildEvents interface, so the delegate you need for the OnBeginBuild event has the name _dispBuildEvents_OnBeginBuildEventHandler. The following code wires up the event handler:
public class Connect : Object, Extensibility.IDTExtensibility2
{
public void OnConnection(object application,
Extensibility.ext_ConnectMode connectMode,
object addInInst,
ref System.Array custom)
{
applicationObject = (_DTE)application;
addInInstance = (AddIn)addInInst;
buildEvents = applicationObject.Events.BuildEvents;
buildEvents.OnBuildBegin +=
new _dispBuildEvents_OnBuildBeginEventHandler(
this.OnBuildBegin);
}
public void OnDisconnection(
Extensibility.ext_DisconnectMode disconnectMode,
ref System.Array custom)
{
buildEvents.OnBuildBegin -=
new _dispBuildEvents_OnBuildBeginEventHandler(
this.OnBuildBegin);
}
private void OnBuildBegin(vsBuildScope scope, vsBuildAction action)
{
// Log the builder's information
}
private _DTE applicationObject;
private AddIn addInInstance;
private EnvDTE.BuildEvents buildEvents;
}
The pattern is simple: OnConnection hooks up the OnBuildBegin event handler to the event when the add-in loads, and OnDisconnection unhooks the same event handler when the add-in unloads.ImportantIf you don't unsubscribe from all events before the add-in unloads, you're in for a good old-fashioned memory leak. Under normal circumstances, the add-in becomes fair game for the garbage collector when Visual Studio .NET unloads it. But when the add-in doesn't unsubscribe from its events, it never gets garbage collected because the events still hold references to it. And like the ghosts in the movie The Sixth Sense who "don't even know they're dead," the add-in will behave as though nothing is wrong and will continue to process events long after it has passed on from the realm of Visual Studio .NET.
Macro Event Handlers
Connecting to events from add-ins is easy, and connecting to events from macros is easier still. Every new macro project begins life with a module named EnvironmentEvents. This module defines an event variable for each of the event interfaces listed in Table 5-2, and these event variables are initialized automatically by the Macros IDE. When the EnvironmentEvents module is open in an editor window, you can add a new event handler by selecting the event variable from the Class Name drop-down list and then selecting the event you want to connect to from the Method Name drop-down list (as shown in Figure 5-7). The Macros IDE generates an empty event handlerall you have to do is add the code.
Figure 5-7. Adding a macro event handler

To unsubscribe from an event, you just delete its event handler from the EnvironmentEvents module. Be careful not to modify or delete the event variables located within the region marked "Automatically generated code, do not modify," because doing so might prevent the macro project from building.
Defining Your Own Event Variables
Although the EnvironmentEvents module is a convenient place to create and manage your macro event handlers, you might have reason to define some of your event handlers in other modules (if only to keep EnvironmentEvents from growing too large). To create an event handler in an arbitrary module, you first need to declare an event variable in that module. The following declaration creates the MyWindowEvents variable for handling window events:
<System.ContextStaticAttribute()> Public WithEvents _
MyWindowEvents As EnvDTE.WindowEvents
If you open the module containing this declaration in an editor window, you can generate an empty event handler just as you would in the EnvironmentEvents module by selecting the MyWindowEvents variable from the Class Name drop-down list and then selecting the event you want to handle from the Method Name drop-down list. Unlike the EnvironmentEvents module, however, the new event handler won't receive events automatically. That's because the Macros IDE initializes the event variables in EnvironmentEvents for youwhen you create your own event variables, you have to initialize them yourself.To initialize an event variable, you assign the appropriate DTE.Events property to it:
MyWindowEvents = DTE.Events.WindowEvents
You can put the assignment in a regular macro and run the macro by hand, which works well enough for some situations, but your handler will miss all events up to the time you run the macro. If the event handler has to intercept events right from startup, you need to automate the assignment somehow. The DTEEvents interface defines the two events you'll need for this purpose: OnStartupComplete, which fires when Visual Studio .NET finishes loading, and OnMacrosRuntimeReset, which fires when a macro project is reloaded into memory. By creating handlers in EnvironmentEvents for both of these events and performing the assignment within each handler, you achieve automatic initialization of your event variable:
Public Module EnvironmentEvents
Public Sub DTEEvents_OnMacrosRuntimeReset() _
Handles DTEEvents.OnMacrosRuntimeReset
MyModule.MyWindowEvents = DTE.Events.WindowEvents
End Sub
Public Sub DTEEvents_OnStartupComplete() _
Handles DTEEvents.OnStartupComplete
MyModule.MyWindowEvents = DTE.Events.WindowEvents
End Sub
End Module
Filtered Events
In general, you should use events sparingly because too many event handlers will degrade the performance of Visual Studio .NET. By creating a filtered event, however, you can have your event and eat it, too. Essentially, a filtered event lets you handle the events of some objects and ignore the events of others. For example, instead of receiving WindowClosing events for every window, you can choose to receive WindowClosing events for the Task List window only. The DTE.Events properties listed in Table 5-3 allow you to create filtered events.
DTE.Events Property | Filtered By |
---|---|
CommandEvents | Command name |
DocumentEvents | Document object |
OutputWindowEvents | Output window pane name |
TaskListEvents | Task List category |
TextEditorEvents | TextDocument object |
WindowEvents | Window object |
' Event variable declaration
<System.ContextStaticAttribute()> Public WithEvents _
MacroExplorerWindowEvents As EnvDTE.WindowEvents
Sub InitializeMacroExplorerFilter()
Dim macroExplorerWindow As EnvDTE.Window
macroExplorerWindow = _
DTE.Windows.Item(EnvDTE.Constants.vsWindowKindMacroExplorer)
MacroExplorerWindowEvents = _
DTE.Events.WindowEvents(macroExplorerWindow)
End Sub
Sub MacroExplorerWindowEvents_WindowActivated( _
ByVal GotFocus As EnvDTE.Window, _
ByVal LostFocus As EnvDTE.Window) _
Handles MacroExplorerWindowEvents.WindowActivated
' Description: Tracks each time the Macro Explorer window
' is activated
End Sub
Lab: Unfiltered and Filtered EventsWant to see the difference between unfiltered and filtered events? Then open the Output window and try the following experiment, which uses macros in the FilteredEvents module:Run the InitializeUnfilteredEvents macro, which initializes DocumentEvents and TextEditorEvents event variables without filters. FilteredEvents defines event handlers that process DocumentSaved and LineChanged events for these two event variables.Create two new text files.Type a line of text, and press Enter in both of the files you created. The Output window displays a "LineChanged fired" message for each new line.Save both of the files. As you save each file, the Output window displays a "DocumentSaved fired" message. Because the DocumentEvents and TextEditorEvents event variables were initialized without filters, the corresponding event handlers receive events for all documents. Now, leave open the two files you created and try a similar experiment using filtered events:Run the InitializeFilteredEvents macro. This macro creates a file named New File and initializes the DocumentSaved and LineChanged event variables so that they target events for this file only.In the New File file, enter a line of text and press Enter. The Output window displays a "LineChanged fired" message.Switch to each of the text files you created, and repeat the previous step. Notice that the Output window doesn't display "LineChanged fired" for either of these files.Save both of the text files. Again, notice that the Output window doesn't display "DocumentSaved fired" messages.Save New File. This time, you'll see "DocumentSaved fired" in the Output window. |
Late-Bound Events
Programming languages integrated into Visual Studio .NET can offer late-bound events that monitor the status of their own project typesyou can think of them as filtered events for projects. These project-specific events are shown in Table 5-4.
Language | Event Interface |
---|---|
Visual C# | CSharpBuildManagerEvents CSharpProjectsEvents CSharpProjectItemsEvents CSharpReferencesEvents |
Visual Basic | VBBuildManagerEvents VBImportsEvents VBProjectsEvents VBProjectItemsEvents VBReferencesEvents |
Visual C++ | CodeModelEvents VCProjectEngineEventsObject |
<System.ContextStaticAttribute()> Public WithEvents _
CSharpProjectsEvents As EnvDTE.ProjectsEvents
Sub InitializeCSharpProjectsEvents()
CSharpProjectsEvents = DTE.Events.GetObject("CSharpProjectsEvents")
End Sub
After the previous code executes, any event handlers you created for the CSharpProjectsEvents variable will receive C# project events only.