Visual Studio Automation Object Model. EnvDTE interfaces

16
Visual Studio Automation Object Model . EnvDTE interfaces. Author: Paul Eremeev Date: 18.10.2012 Abstract This article contains an overview of Visual Studio Automation Object Model. Model's overall structure and the means of obtaining access to its interfaces through DTE/DTE2 top level objects are examined. Several examples of utilizing elements of the model are provided. Also discussed are the issues of using model's interfaces within multithreaded applications; an example of implementing such mechanism for multithreaded interaction with COM interfaces in managed code is provided as well. Introduction Visual Studio development environment is built upon the principles of automation and extensibility, providing the developers using it with the ability of integrating almost any custom element into the IDE and allowing for an easy interaction with its default and user-created components. As the means of implementing these tasks, Visual Studio users are provided with several cross-complementing toolsets, the most basic and versatile among these is the Visual Studio Automation Object Model. Automation Object Model is represented by a series of libraries containing a vast and well-structured API set which covers all aspects of IDE automation and the majority of its extensibility capabilities. Although, in comparison to other IDE extensibility tools, this model does not provide access to some portions of Visual Studio (this applies mostly to the extension of some IDE's features), it is nonetheless the most flexible and versatile among them. The majority of the model's interfaces are accessible from within every type of IDE extension module, which allows interacting with the environment even from an external independent process. Moreover, the model itself could be extended along with the extension of Visual Studio IDE, providing other third- party developers with an access to user-created custom components. Automation Object Model structure Visual Studio automation model is composed of several interconnected functional object groups covering all aspects of the development environment; it also provides capabilities for controlling and extending these groups. Accessing any of them is possible through the top-level global DTE interface (Development Tools Environment). Figure 1 shows the overall structure of the automation model and how it is divided among functionality groups.

description

This article contains an overview of Visual Studio Automation Object Model. Model's overall structure and the means of obtaining access to its interfaces through DTE/DTE2 top level objects are examined. Several examples of utilizing elements of the model are provided. Also discussed are the issues of using model's interfaces within multithreaded applications; an example of implementing such mechanism for multithreaded interaction with COM interfaces in managed code is provided as well.

Transcript of Visual Studio Automation Object Model. EnvDTE interfaces

Page 1: Visual Studio Automation Object Model. EnvDTE interfaces

VisualStudioAutomationObjectModel.

EnvDTEinterfaces.

Author: Paul Eremeev

Date: 18.10.2012

Abstract This article contains an overview of Visual Studio Automation Object Model. Model's overall structure

and the means of obtaining access to its interfaces through DTE/DTE2 top level objects are examined.

Several examples of utilizing elements of the model are provided. Also discussed are the issues of using

model's interfaces within multithreaded applications; an example of implementing such mechanism for

multithreaded interaction with COM interfaces in managed code is provided as well.

Introduction Visual Studio development environment is built upon the principles of automation and extensibility,

providing the developers using it with the ability of integrating almost any custom element into the IDE

and allowing for an easy interaction with its default and user-created components. As the means of

implementing these tasks, Visual Studio users are provided with several cross-complementing toolsets,

the most basic and versatile among these is the Visual Studio Automation Object Model.

Automation Object Model is represented by a series of libraries containing a vast and well-structured

API set which covers all aspects of IDE automation and the majority of its extensibility capabilities.

Although, in comparison to other IDE extensibility tools, this model does not provide access to some

portions of Visual Studio (this applies mostly to the extension of some IDE's features), it is nonetheless

the most flexible and versatile among them.

The majority of the model's interfaces are accessible from within every type of IDE extension module,

which allows interacting with the environment even from an external independent process. Moreover,

the model itself could be extended along with the extension of Visual Studio IDE, providing other third-

party developers with an access to user-created custom components.

Automation Object Model structure Visual Studio automation model is composed of several interconnected functional object groups

covering all aspects of the development environment; it also provides capabilities for controlling and

extending these groups. Accessing any of them is possible through the top-level global DTE interface

(Development Tools Environment). Figure 1 shows the overall structure of the automation model and

how it is divided among functionality groups.

Page 2: Visual Studio Automation Object Model. EnvDTE interfaces

Figure 1 — Visual Studio Automation Object Model (click the picture to zoom in)

Page 3: Visual Studio Automation Object Model. EnvDTE interfaces

The model itself could be extended by user in one of the following groups:

• project models (implementing new project types, support for new languages);

• document models (implementing new document types and document editors)

• code editor level models (support for specific language constructs)

• project build-level models

Automation model could be extended from plug-ins of VSPackage type only.

Despite the model's versatility, not every group belonging to the model could be equally utilized from all

the types of IDE extensions. For instance, some of the model's capabilities are inaccessible to external

processes; these capabilities are tied to specific extension types, such as Add-In or VSPackage.

Therefore, when selecting the type for the extension to be developed, it is important to consider the

functionality that this extension will require.

Obtaining references to DTE/DTE2 objects. In order to create a Visual Studio automation application it is necessary to obtain access to the

automation objects themselves in the first place. To accomplish this, first of all it is necessary to hook up

the correct versions of libraries containing the required managed API wrappers in the EnvDTE

namespace. Secondly, the reference to the automation model top-level object, that is the DTE2

interface, should be obtained.

In the course of Visual Studio evolution, several of its automation objects had been modified or received

some additional functionality. So, to maintain a backward compatibility with existing extension

packages, new EnvDTE80, EnvDTE90, EnvDTE100 etc. namespaces were created instead of updating the

interfaces from the original EnvDTE namespace. The majority of such updated interfaces from these

new namespaces do maintain the same names as in the original ones, but with addition of an ordinal

number at the end of the name, for example Solution and Solution2. It is advised that these updated

interfaces should be utilized when creating a new project, as they do contain the most recent

functionality. It's worth noting that properties and methods of DTE2 interface usually return object

references with types corresponding to the original DTE, i.e. accessing dte2.Solution will return Solution

and not the Solution2 as it would seem.

Although these new EnvDTE80, EnvDTE90, EnvDTE100 namespaces do contain some of the updated

functionality as mentioned above, still it is the EnvDTE interface that contains the majority of

automation objects. Therefore, in order to possess access to all of the existing interfaces, it is necessary

to link all versions of the managed COM wrapper libraries to the project, as well as to obtain the

references to DTE and also to DTE2.

The way of obtaining top-level EnvDTE object reference is dependent upon the type of IDE extension

being developed. Let's examine 3 of such extension types: Add-In, VSPackage and an MSVS-independent

external process.

Add-In extension

In the case of an Add-In extension, access to the DTE interface can be obtained inside the OnConnection

method which should be implemented for the IDTExtensibility interface that provides access to the

extension-environment interaction events. The OnConnection method is called at the moment when the

module is loaded by the IDE; it can happen either when the environment is being loaded itself or after

Page 4: Visual Studio Automation Object Model. EnvDTE interfaces

the extension was called for the first time in the IDE session. The example of obtaining the reference

follows:

public void OnConnection(object application,

ext_ConnectMode connectMode, object addInInst, ref Array custom)

{

_dte2 = (DTE2)application;

...

}

An Add-In module can be initialized either at the moment of IDE start-up, or when it is called for the first

time in current IDE session. So, the connectMode can be used to correctly determine the moment of

initialization inside the OnConnection method.

switch(connectMode)

{

case ext_ConnectMode.ext_cm_UISetup:

...

break;

case ext_ConnectMode.ext_cm_Startup:

...

break;

case ext_ConnectMode.ext_cm_AfterStartup:

...

break;

case ext_ConnectMode.ext_cm_CommandLine:

...

break;

}

As in the example above, add-In could be loaded either simultaneously with the IDE itself (if the startup

option in the Add-In manager is checked), when it is called the first time or when it is called through the

command line. The ext_ConnectMode.ext_cm_UISetup option is invoked only for a single time in the

plug-in's overall lifetime, which is during its first initialization. This case should be used for initializing

user UI elements which are to be integrated into the environment (more on this later on).

If an Add-In is being loaded during Visual Studio start-up (ext_ConnectMode.ext_cm_Startup), then at

the moment OnConnect method receives control for the first time, it is possible that the IDE still is not

fully initialized itself. In such a case, it is advised to postpone the acquisition of the DTE reference until

the environment is fully loaded. The OnStartupComplete handler provided by the IDTExtensibility can be

used for this.

public void OnStartupComplete(ref Array custom)

{

...

}

Page 5: Visual Studio Automation Object Model. EnvDTE interfaces

VSPackage extension

For VSPackage type of extension, the DTE could be obtained through the global Visual Studio service

with the help of GetService method of a Package subclass:

DTE dte = MyPackage.GetService(typeof(DTE)) as DTE;

Please note that the GetService method could potentially return null in case Visual Studio is not fully

loaded or initialized at the moment of such access, i.e. it is in the so called "zombie" state. To correctly

handle this situation, it is advised that the acquisition of DTE reference should be postponed until this

interface is inquired. But in case the DTE reference is required inside the Initialize method itself, the

IVsShellPropertyEvents interface can be utilized (also by deriving our Package subclass from it) and then

the reference could be safely obtained inside the OnShellPropertyChange handler.

DTE dte;

uint cookie;

protected override void Initialize()

{

base.Initialize();

IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell;

if (shellService != null)

ErrorHandler.ThrowOnFailure(

shellService.AdviseShellPropertyChanges(this,out cookie));

...

}

public int OnShellPropertyChange(int propid, object var)

{

// when zombie state changes to false, finish package initialization

if ((int)__VSSPROPID.VSSPROPID_Zombie == propid)

{

if ((bool)var == false)

{

this.dte = GetService(typeof(SDTE)) as DTE;

IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell;

if (shellService != null)

ErrorHandler.ThrowOnFailure(

shellService.UnadviseShellPropertyChanges(this.cookie) );

this.cookie = 0;

}

}

return VSConstants.S_OK;

}

It should be noted that the process of VSPackage module initialization at IDE startup could vary for

different Visual Studio versions. For instance, in case of VS2005 and VS2008, an attempt at accessing

DTE during IDE startup will almost always result in null being returned, owning to the relative fast

loading times of these versions. But, one does not simply obtain access into DTE. In Visual Studio 2010

case, it mistakenly appears that one could simply obtain an access to the DTE from inside the Initialize()

Page 6: Visual Studio Automation Object Model. EnvDTE interfaces

method. In fact, this impression is a false one, as such method of DTE acquisition could potentially cause

the occasional appearance of "floating" errors which are hard to identify and debug, and even the DTE

itself may be still uninitialized when the reference is acquired. Because of these disparities, the

aforementioned acquisition method for handling IDE loading states should not be ignored on any

version of Visual Studio.

Independent external process

The DTE interface is a top-level abstraction for Visual Studio environment in the automation model. In

order to acquire a reference to this interface from an external application, its ProgID COM identifier

could be utilized; for instance, it will be "VisualStudio.DTE.10.0" for Visual Studio 2010. Consider this

example of initializing a new IDE instance and when obtaining a reference to the DTE interface.

// Get the ProgID for DTE 8.0.

System.Type t = System.Type.GetTypeFromProgID(

"VisualStudio.DTE.10.0", true);

// Create a new instance of the IDE.

object obj = System.Activator.CreateInstance(t, true);

// Cast the instance to DTE2 and assign to variable dte.

EnvDTE80.DTE2 dte = (EnvDTE80.DTE2)obj;

// Show IDE Main Window

dte.MainWindow.Activate();

In the example above we've actually created a new DTE object, starting deven.exe process by the

CreateInstance method. But at the same time, the GUI window of the environment will be displayed

only after the Activate method is called.

Next, let's review a simple example of obtaining the DTE reference from an already running Visual

Studio Instance:

EnvDTE80.DTE2 dte2;

dte2 = (EnvDTE80.DTE2)

System.Runtime.InteropServices.Marshal.GetActiveObject(

"VisualStudio.DTE.10.0");

However, in case several instances of the Visual Studio are executing at the moment of our inquiry, the

GetActiveObject method will return a reference to the IDE instance that was started the earliest. Let's

examine a possible way of obtaining the reference to DTE from a running Visual Studio instance by the

PID of its process.

using EnvDTE80;

using System.Diagnostics;

using System.Runtime.InteropServices;

using System.Runtime.InteropServices.ComTypes;

[DllImport("ole32.dll")]

private static extern void CreateBindCtx(int reserved,

out IBindCtx ppbc);

[DllImport("ole32.dll")]

private static extern void GetRunningObjectTable(int reserved,

out IRunningObjectTable prot);

Page 7: Visual Studio Automation Object Model. EnvDTE interfaces

public static DTE2 GetByID(int ID)

{

//rot entry for visual studio running under current process.

string rotEntry = String.Format("!VisualStudio.DTE.10.0:{0}", ID);

IRunningObjectTable rot;

GetRunningObjectTable(0, out rot);

IEnumMoniker enumMoniker;

rot.EnumRunning(out enumMoniker);

enumMoniker.Reset();

IntPtr fetched = IntPtr.Zero;

IMoniker[] moniker = new IMoniker[1];

while (enumMoniker.Next(1, moniker, fetched) == 0)

{

IBindCtx bindCtx;

CreateBindCtx(0, out bindCtx);

string displayName;

moniker[0].GetDisplayName(bindCtx, null, out displayName);

if (displayName == rotEntry)

{

object comObject;

rot.GetObject(moniker[0], out comObject);

return (EnvDTE80.DTE2)comObject;

}

}

return null;

}

Here we've acquired the DTE interface by identifying the required instance of the IDE in the table of

running COM objects (ROT, Running Object Table) by its process identifier. Now we can access the DTE

for every of the executing instances of Visual Studio, for example:

Process Devenv;

...

//Get DTE by Process ID

EnvDTE80.DTE2 dte2 = GetByID(Devenv.Id);

Additionally, to acquire any project-specific interface (including custom model extensions), for example

the CSharpProjects model, through a valid DTE interface, the GetObject method should be utilized:

Projects projects = (Projects)dte.GetObject("CSharpProjects");

The GetObject method will return a Projects collection of regular Project objects, and each one of them

will contain a reference to our project-specific properties, among other regular ones.

Visual Studio text editor documents Automation model represents Visual Studio text documents through the TextDocument interface. For

example, C/C++ source code files are opened by the environment as text documents. TextDocument is

based upon the common automation model document interface (the Document interface), which

represents file of any type opened in Visual Studio editor or designer. A reference to the text document

Page 8: Visual Studio Automation Object Model. EnvDTE interfaces

object can be obtained through the 'Object' field of the Document object. Let's acquire a text document

for the currently active (i.e. the one possessing focus) document from IDE's text editor.

EnvDTE.TextDocument objTextDoc =

(TextDocument)PVSStudio.DTE.ActiveDocument.Object("TextDocument");

Modifying documents

The TextSelection document allows controlling text selection or to modify it. The methods of this

interface represent the functionality of Visual Studio text editor, i.e. they allow the interaction with the

text as it presented directly by the UI.

EnvDTE.TextDocument Doc =

(TextDocument)PVSStudio.DTE.ActiveDocument.Object(string.Empty);

Doc.Selection.SelectLine();

TextSelection Sel = Doc.Selection;

int CurLine = Sel.TopPoint.Line;

String Text = Sel.Text;

Sel.Insert("test\r\n");

In this example we selected a text line under the cursor, read the selected text and replaced it with a

'test' string.

TextDocument interface also allows text modification through the EditPoint interface. This interface is

somewhat similar to the TextSelection, but instead of operating with the text through the editor UI, it

directly manipulates text buffer data. The difference between them is that the text buffer is not

influenced by such editor-specific notions as WordWrap and Virtual Spaces. It should be noted that both

of these editing methods are not able to modify read-only text blocks.

Let's examine the example of modifying text with EditPoint by placing additional lines at the end of

current line with a cursor.

objEditPt = objTextDoc.StartPoint.CreateEditPoint();

int lineNumber = objTextDoc.Selection.CurrentLine;

objEditPt.LineDown(lineNumber - 1);

EditPoint objEditPt2 = objTextDoc.StartPoint.CreateEditPoint();

objEditPt2.LineDown(lineNumber - 1);

objEditPt2.CharRight(objEditPt2.LineLength);

String line = objEditPt.GetText(objEditPt.LineLength);

String newLine = line + "test";

objEditPt.ReplaceText(objEditPt2, newLine,

(int)vsEPReplaceTextOptions.vsEPReplaceTextKeepMarkers);

Navigating the documents

VSPackage modules are able to obtain access to a series of global services which could be used for

opening and handling environment documents. These services could be acquired by the

Package.GetGlobalService() method. It should be noted that the services described here are not part of

the DTE model and are accessible only from a Package-type extension, and therefore they could not be

utilized in other types of Visual Studio extensions. Nonetheless, they can be quite useful for handling IDE

Page 9: Visual Studio Automation Object Model. EnvDTE interfaces

documents when they are utilized in addition to the Documents interface described earlier. Next, we'll

examine these services in more detail.

The IVsUIShellOpenDocument interface controls the state of documents opened in the environment.

Following is the example that uses this interface to open a document through path to a file which this

document will represent.

String path = "C:\Test\test.cpp";

IVsUIShellOpenDocument openDoc =

Package.GetGlobalService(typeof(IVsUIShellOpenDocument))

as IVsUIShellOpenDocument;

IVsWindowFrame frame;

Microsoft.VisualStudio.OLE.Interop.IServiceProvider sp;

IVsUIHierarchy hier;

uint itemid;

Guid logicalView = VSConstants.LOGVIEWID_Code;

if (ErrorHandler.Failed(

openDoc.OpenDocumentViaProject(path, ref logicalView, out sp,

out hier, out itemid, out frame))

|| frame == null)

{

return;

}

object docData;

frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocData, out docData);

The file will be opened in a new editor or will receive focus in case it already has been opened earlier.

Next, let's read a VsTextBuffer text buffer from this document we opened:

// Get the VsTextBuffer

VsTextBuffer buffer = docData as VsTextBuffer;

if (buffer == null)

{

IVsTextBufferProvider bufferProvider = docData as

IVsTextBufferProvider;

if (bufferProvider != null)

{

IVsTextLines lines;

ErrorHandler.ThrowOnFailure(bufferProvider.GetTextBuffer(

out lines));

buffer = lines as VsTextBuffer;

Debug.Assert(buffer != null,

"IVsTextLines does not implement IVsTextBuffer");

if (buffer == null)

{

return;

}

}

}

Page 10: Visual Studio Automation Object Model. EnvDTE interfaces

The IVsTextManager interface controls all of the active text buffers in the environment. For example we

can navigate a text document using the NavigateToLineAndColumn method of this manager on a buffer

we've acquired earlier:

IVsTextManager mgr =

Package.GetGlobalService(typeof(VsTextManagerClass))

as IVsTextManager;

mgr.NavigateToLineAndColumn(buffer, ref logicalView, line,

column, line, column);

Subscribing and handling events Automation objects events are represented by the DTE.Events property. This element references all of

the common IDE events (such as CommandEvents, SolutionEvents), as well as the events of separate

environment components (project types, editors, tools etc.), also including the ones designed by third-

party developers. To acquire a reference for this automation object, the GetObject method could be

utilized.

When subscribing to the DTE events one should remember that this interface could be still unavailable

at the moment of extension being initialized. So it is always important to consider the sequence of your

extension initialization process if the access to DTE.Events is required in the Initialize() method of your

extension package. The correct handling of initialization sequence will vary for different extension types,

as it was described earlier.

Let's acquire a reference for an events object of Visual C++ project model defined by the

VCProjectEngineEvents interface and assign a handler for the removal of an element from the Solution

Explorer tree:

VCProjectEngineEvents m_ProjectItemsEvents =

PVSStudio.DTE.Events.GetObject("VCProjectEngineEventsObject")

as VCProjectEngineEvents;

m_ProjectItemsEvents.ItemRemoved +=

new _dispVCProjectEngineEvents_ItemRemovedEventHandler(

m_ProjectItemsEvents_ItemRemoved);

MDI windows events

The Events.WindowEvents property could be utilized to handle regular events of an environment MDI

window. This interface permits the assignment of a separate handler for a single window (defined

through the EnvDTE.Window interface) or the assignment of a common handler for all of the

environment's windows. Following example contains the assignment of a handler for the event of

switching between IDE windows:

WindowEvents WE = PVSStudio.DTE.Events.WindowEvents;

WE.WindowActivated +=

new _dispWindowEvents_WindowActivatedEventHandler(

Package.WE_WindowActivated);

Next example is the assignment of a handler for window switching to the currently active MDI window

through WindowEvents indexer:

Page 11: Visual Studio Automation Object Model. EnvDTE interfaces

WindowEvents WE =

m_dte.Events.WindowEvents[MyPackage.DTE.ActiveWindow];

WE.WindowActivated += new

_dispWindowEvents_WindowActivatedEventHandler(

MyPackage.WE_WindowActivated);

IDE commands events

The actual handling of environment's commands and their extension through the automation model is

covered in a separate article of this series. In this section we will examine the handling of the events

related to these commands (and not of the execution of the commands themselves). Assigning the

handlers to these events is possible through the Events.CommandEvents interface. The CommandEvents

property, as in the case of MDI windows events, also permits the assignment of a handler either for all

of the commands or for a single one through the indexer.

Let's examine the assignment of a handler for the event of a command execution being complete (i.e.

when the command finishes its execution):

CommandEvents CEvents = DTE.Events.CommandEvents;

CEvents.AfterExecute += new

_dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);

But in order to assign such a handler for an individual command, it is necessary to identify this command

in the first place. Each command of the environment is identified by a pair of GUID:ID, and in case of a

user-created commands these values are specified directly by the developer during their integration, for

example through the VSCT table. Visual Studio possesses a special debug mode which allows identifying

any of the environment's comamnds. To activate this mode, it is required that the following key is to be

added to the system registry (an example for Visual Studio 2010):

[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0\General]

"EnableVSIPLogging"=dword:00000001

Now, after restarting the IDE, hovering your mouse over menu or toolbar elements with CTRL+SHIFT

being simultaneously pressed (though sometime it will not work until you left-click it) will display a

dialog window containing all of the command's internal identifiers. We are interested in the values of

Guid and CmdID. Let's examine the handling of events for the File.NewFile command:

CommandEvents CEvents = DTE.Events.CommandEvents[

"{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 221];

CEvents.AfterExecute += new

_dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);

The handler obtained in this way will receive control only after the command execution is finished.

void C_AfterExecute(string Guid, int ID, object CustomIn,

object CustomOut)

{

...

}

Page 12: Visual Studio Automation Object Model. EnvDTE interfaces

This handler should not be confused with an immediate handler for the execution of the command itself

which could be assigned during this command's initialization (from an extension package and in case the

command is user-created). Handling the IDE commands is described in a separate article that is entirely

devoted to IDE commands.

In conclusion to this section it should be mentioned that in the process of developing our own

VSPackage extension, we've encountered the necessity to store the references to interface objects

containing our handler delegates (such as CommandEvents, WindowEvents etc.) on the top-level fields

of our main Package subclass. The reason for this is that in case of the handler being assigned through a

function-level local variable, it is lost immediately after leaving the method. Such behavior could

probably be attributed to the .NET garbage collector, although we've obtained these references from

the DTE interface which definitely exists during the entire lifetime of our extension package.

Interacting with DTE2 COM interfaces from within a multithreaded

application Initially PVS-Studio extension package had not contained any specific thread-safety mechanisms for its

interaction with Visual Studio APIs. At the same time, we had been attempting to confine the

interactions with this APIs within a single background thread which was created and owned by our plug-

in. And such approach functioned flawlessly for quite a long period. However, several bug reports from

our users, each one containing a similar ComExeption error, prompted us to examine this issue in more

detail and to implement a threading safety mechanism for our COM Interop.

Although Visual Studio automation model is not a thread-safe one, it still provides a way for interacting

with multi-threaded applications. Visual Studio application is a COM (Component Object Mode) server.

For the task of handling calls from COM clients (in our case, this will be our extension package) to

thread-unsafe servers, COM provides a mechanism known as STA (single-threaded apartment) model. In

the terms of COM, an Apartment represents a logical container inside a process in which objects and

threads share the same thread access rules. STA can hold only a single thread, but an unlimited number

of objects, inside such container. Calls from other threads to such thread-unsafe objects inside STA are

converted into messages and posted to a message queue. Messages are retrieved from the message

queue and converted back into method calls one at a time by the thread running in the STA, so it

becomes possible for only a single thread to access these unsafe objects on the server.

Utilizing Apartment mechanism inside managed code

The .NET Framework does not utilize COM Apartment mechanics directly. Therefore, when a managed

application calls a COM object in the COM interoperation scenarios, CLR (Common Language Runtime)

creates and initializes apartment container. A managed thread is able to create and enter either an MTA

(multi-threaded apartment, a container that, contrary to STA, can host several threads at the same

time), or an STA, though a thread will be started as an MTA by default. The type of the apartment could

be specified before thread is launched:

Thread t = new Thread(ThreadProc);

t.SetApartmentState(ApartmentState.STA);

...

t.Start();

Page 13: Visual Studio Automation Object Model. EnvDTE interfaces

As an apartment type could not be changed once thread had been started, the STAThread attribute

should be used to specify the main thread of a managed application as an STA:

[STAThread]

static void Main(string[] args)

{...}

Implementing message filter for COM interoperation errors in a managed environment

As STA serializes all of calls to the COM server, one of the calling clients could potentially be blocked or

even rejected when the server is busy, processing different calls or another thread is already inside the

apartment container. In case COM server rejects its client, .NET COM interop will generate a

System.Runtime.InteropServices.COMException ("The message filter indicated that the application is

busy").

When working on a Visual Studio module (add-in, vspackage) or a macro, the execution control usually

passes into the module from the environment's main STA UI thread (such as in case of handling events

or environment state changes, etc.). Calling automation COM interfaces from this main IDE thread is

safe. But if other background threads are planned to be utilized and EnvDTE COM interfaces are to be

called from these background threads (as in case of long calculations that could potentially hang the

IDE's interface, if these are performed on the main UI thread), then it is advised to implement a

mechanism for handling calls rejected by a server.

While working on PVS-Studio plug-in we've often encountered these kinds of COM exceptions in

situations when other third-party extensions were active inside the IDE simultaneously with PVS-Studio

plug-in. Heavy user interaction with the UI also was the usual cause for such issues. It is quite logical that

these situations often resulted in simultaneous parallel calls to COM objects inside STA and

consequently to the rejection of some of them.

To selectively handle incoming and outgoing calls, COM provides the IMessageFilter interface. If it is

implemented by the server, all of the calls are passed to the HandleIncomingCall method, and the client

is informed on the rejected calls through the RetryRejectedCall method. This in turn allows the rejected

calls to be repeated, or at least to correctly present this rejection to a user (for example, by displaying a

dialog with a 'server is busy' message). Following is the example of implementing the rejected call

handling for a managed application.

[ComImport()]

[Guid("00000016-0000-0000-C000-000000000046")]

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

public interface IMessageFilter

{

[PreserveSig]

int HandleInComingCall(

int dwCallType,

IntPtr hTaskCaller,

int dwTickCount,

IntPtr lpInterfaceInfo);

[PreserveSig]

int RetryRejectedCall(

Page 14: Visual Studio Automation Object Model. EnvDTE interfaces

IntPtr hTaskCallee,

int dwTickCount,

int dwRejectType);

[PreserveSig]

int MessagePending(

IntPtr hTaskCallee,

int dwTickCount,

int dwPendingType);

}

class MessageFilter : MarshalByRefObject, IDisposable, IMessageFilter

{

[DllImport("ole32.dll")]

[PreserveSig]

private static extern int CoRegisterMessageFilter(

IMessageFilter lpMessageFilter,

out IMessageFilter lplpMessageFilter);

private IMessageFilter oldFilter;

private const int SERVERCALL_ISHANDLED = 0;

private const int PENDINGMSG_WAITNOPROCESS = 2;

private const int SERVERCALL_RETRYLATER = 2;

public MessageFilter()

{

//Starting IMessageFilter for COM objects

int hr =

MessageFilter.CoRegisterMessageFilter(

(IMessageFilter)this,

out this.oldFilter);

System.Diagnostics.Debug.Assert(hr >= 0,

"Registering COM IMessageFilter failed!");

}

public void Dispose()

{

//disabling IMessageFilter

IMessageFilter dummy;

int hr = MessageFilter.CoRegisterMessageFilter(this.oldFilter,

out dummy);

System.Diagnostics.Debug.Assert(hr >= 0,

"De-Registering COM IMessageFilter failed!")

System.GC.SuppressFinalize(this);

}

int IMessageFilter.HandleInComingCall(int dwCallType,

IntPtr threadIdCaller, int dwTickCount, IntPtr lpInterfaceInfo)

{

Page 15: Visual Studio Automation Object Model. EnvDTE interfaces

// Return the ole default (don't let the call through).

return MessageFilter.SERVERCALL_ISHANDLED;

}

int IMessageFilter.RetryRejectedCall(IntPtr threadIDCallee,

int dwTickCount, int dwRejectType)

{

if (dwRejectType == MessageFilter.SERVERCALL_RETRYLATER)

{

// Retry the thread call immediately if return >=0 &

// <100.

return 150; //waiting 150 mseconds until retry

}

// Too busy; cancel call. SERVERCALL_REJECTED

return -1;

//Call was rejected by callee.

//(Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))

}

int IMessageFilter.MessagePending(

IntPtr threadIDCallee, int dwTickCount, int dwPendingType)

{

// Perform default processing.

return MessageFilter.PENDINGMSG_WAITNOPROCESS;

}

}

Now we can utilize our MessageFilter while calling COM interfaces from a background thread:

using (new MessageFilter())

{

//COM-interface dependent code

...

}

References 1. MSDN. Referencing Automation Assemblies and the DTE2 Object.

2. MSDN. Functional Automation Groups.

3. MZ-Tools. HOWTO: Use correctly the OnConnection method of a Visual Studio add-in.

4. The Code Project. Understanding The COM Single-Threaded Apartment.

5. MZ-Tools. HOWTO: Add an event handler from a Visual Studio add-in.

6. Dr. eX's Blog. Using EnableVSIPLogging to identify menus and commands with VS 2005 + SP1.

Other articles in this series 0. Introduction.

1. Creating, debugging and deploying extension packages for Microsoft Visual Studio

2005/2008/2010/2012.

2. Visual Studio Automation Object Model. EnvDTE interfaces.

Page 16: Visual Studio Automation Object Model. EnvDTE interfaces

3. Visual Studio commands.

4. Visual Studio tool windows.

5. Integrating into Visual Studio settings.

6. Visual C++ project model.