Our website uses cookies to enhance your browsing experience.
Accept
to the top
close form

Fill out the form in 2 simple steps below:

Your contact information:

Step 1
Congratulations! This is your promo code!

Desired license type:

Step 2
Team license
Enterprise license
** By clicking this button you agree to our Privacy Policy statement
close form
Request our prices
New License
License Renewal
--Select currency--
USD
EUR
* By clicking this button you agree to our Privacy Policy statement

close form
Free PVS‑Studio license for Microsoft MVP specialists
* By clicking this button you agree to our Privacy Policy statement

close form
To get the licence for your open-source project, please fill out this form
* By clicking this button you agree to our Privacy Policy statement

close form
I am interested to try it on the platforms:
* By clicking this button you agree to our Privacy Policy statement

close form
check circle
Message submitted.

Your message has been sent. We will email you at


If you do not see the email in your inbox, please check if it is filtered to one of the following folders:

  • Promotion
  • Updates
  • Spam

Webinar: Evaluation - 05.12

>
>
Visual Studio Automation Object Model. …

Visual Studio Automation Object Model. EnvDTE interfaces

Oct 18 2012
Author:

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.

This article is obsolete. You can read the new version of this article here.

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.

0169_VS_ext_2/image1.png

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

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 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)
{
    ...
}

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() 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);

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 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 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;
    }
  }
}

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:

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)
{
  ...
}

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();

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(
    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)
  {
    // 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

Other articles in this series

Popular related articles


Comments (0)

Next comments next comments
close comment form