Moving Away from COM Add-Ins





Moving Away from COM Add-Ins

Chapter 23, "Developing COM Add-Ins for Word and Excel," examined several issues with building COM add-ins in C# for Office applications. In particular, the chapter considered several problems with using the default configuration of a COM add-in where mscoree.dll loads the COM add-in:

  • Mscoree.dll can be disabled, causing all managed COM add-ins to stop loading.

  • Mscoree.dll cannot be signed, which makes it so your COM add-in cannot be loaded when the Trust all installed add-ins and templates option is not checked.

  • Mscoree.dll loads all COM add-ins into the same application domain, which allows COM add-ins to affect one another adversely.

VSTO add-ins for Outlook solves these issues. VSTO also fixes some other issues in Outlook COM add-in development that we consider here to motivate you to use the VSTO add-in technology rather than the COM add-in technology described in Chapter 23. This chapter describes the problems with the COM add-in technology in enough detail so that if you are forced to use a COM add-in approach, you will know how to work around these issues.

Getting Outlook to Shut Down Properly with a COM Add-In

The most troublesome issue in Outlook COM add-in development is that the OnDisconnection method you implement in a COM add-in sometimes is not called if you have variables such as a class member variable that is holding an Outlook object. The result is that when Outlook exits, all the Outlook windows go away but Outlook does not shut downthe outlook.exe process will continue running, waiting for the COM add-in to release the Outlook objects it is holding.

To get Outlook to shut down and call the COM add-in's OnDisconnection method, you must use a trick that involves listening to Outlook events to determine when the last window has been closed. Outlook windows are represented by two object model objects. The Explorer object in the Outlook object model represents the main Outlook window that consists of a view showing folders and items in folders. It is possible to open additional Explorer views by right-clicking an Outlook folder and choosing Open in New Window. The Inspector object in the Outlook object model represents the Outlook window that appears when you double-click an individual item in a folder such as a mail item, contact item, or other Outlook item.

The secret to getting OnDisconnection called and your COM add-in to unload is to listen to Explorer and Inspector Close events as well as the Application object's Quit event. When the last Explorer or Inspector has closed or when the Application object's Quit event is raised, you must make sure you set all the variables that are holding Outlook objects to null. You should then force a garbage collection after setting the variables to null to ensure that your add-in will not hold on to Outlook objects because objects are waiting to be garbage collected.

Listing 24-1 shows a helper class that you can create and use from your main Connect class in a COM add-in. The class takes as a parameter an Outlook Application object as well as a delegate to a Shutdown method that you would declare in your Connect class. The Shutdown method you declare in your Connect class would set all the class member variables in the Connect class that are holding Outlook objects to null, similar to what this helper class does in its HandleShutdown method. Note that you do not have to use this approach or this class in VSTO Outlook add-insonly in COM add-ins. This is one of the strong arguments for switching to VSTO Outlook add-ins.

You might also notice that the helper class holds on to the Explorers and Inspectors collection objects, as well as an array of Explorer or Inspector objects. The helper class holds on to these things because if it does not, the event sinks it has established on these objects will not work. This is another variant of the classic "Why has my button stopped working" problem described in Chapter 1, "An Introduction to Office Programming."

-1. A Helper Class That Helps an Outlook COM Add-In Shut Down Properly (This class is not necessary for VSTO Outlook add-ins.)
using System;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace MyAddin
{
 public class EventListener
 {
  public delegate void Shutdown();

  private Outlook.Application application;
  private Outlook.Explorers explorers;
  private Outlook.Inspectors inspectors;
  private System.Collections.ArrayList eventSinks;
  private Shutdown shutdownHandlerDelegate;

  public EventListener(Outlook.Application application,
    Shutdown shutdownHandlerDelegate)
  {
   this.application = application;
   this.shutdownHandlerDelegate = shutdownHandlerDelegate;
   explorers = application.Explorers;
   inspectors = application.Inspectors;
   eventSinks = new System.Collections.ArrayList();


   explorers.NewExplorer += new
     Outlook.ExplorersEvents_NewExplorerEventHandler
    (Explorers_NewExplorer);

   inspectors.NewInspector += new
     Outlook.InspectorsEvents_NewInspectorEventHandler(
     Inspectors_NewInspector);

   ((Outlook.ApplicationEvents_10_Event)application).Quit +=
      new Outlook.ApplicationEvents_10_QuitEventHandler(
      Application_Quit);

   foreach (Outlook.Explorer e in application.Explorers)
   {
    Explorers_NewExplorer(e);
   }

   foreach (Outlook.Inspector i in application.Inspectors)
   {
    Inspectors_NewInspector(i);
   }
  }

  public void Explorers_NewExplorer(Outlook.Explorer explorer)
  {
   eventSinks.Add(explorer);
   Outlook.ExplorerEvents_Event explorerEvents =
    (Outlook.ExplorerEvents_Event)explorer;
   explorerEvents.Close += new
    Outlook.ExplorerEvents_CloseEventHandler(Explorer_Close);
  }

  public void Inspectors_NewInspector(Outlook.Inspector inspector)
  {
   eventSinks.Add(inspector);
   Outlook.InspectorEvents_Event inspectorEvents =
     (Outlook.InspectorEvents_Event)inspector;
   inspectorEvents.Close += new
     Outlook.InspectorEvents_CloseEventHandler(Inspector_Close);
  }

  public void Explorer_Close()
  {
   if (application.Explorers.Count <= 1 &&
     application.Inspectors.Count == 0)
   {
    HandleShutdown();
   }
  }

  public void Inspector_Close()
  {
   if (application.Explorers.Count == 0 &&
     application.Inspectors.Count <= 1)
   {
    HandleShutdown();
   }
  }

  public void Application_Quit()
  {
    HandleShutdown();
  }

  void HandleShutdown()
  {
   // Release any outlook objects this class is holding
   application = null;
   explorers = null;
   inspectors = null;
   eventSinks.Clear();
   eventSinks = null;

   // call client provided shutdown handler delegate
   shutdownHandlerDelegate();

   // Force a garbage collection
   GC.Collect();
   GC.WaitForPendingFinalizers();
   GC.Collect();
   GC.WaitForPendingFinalizers();
  }
 }
}

Understanding RCWs, Application Domains, and Why to Avoid Calling ReleaseComObject

Some Outlook developers have used ReleaseComObject on class member variables holding Outlook objects instead of setting these variables to null and forcing a garbage collection as shown in Listing 24-1. ReleaseComObject is a function in the CLR that has some additional side effects if you misuse it that can adversely affect your code. It can also affect other COM add-ins if you are not using a COM add-in shim as described in Chapter 23. For this reason, we recommend against using ReleaseComObject. Because it has been recommended in the past, it is important to describe in more detail why calling ReleaseComObject is not advised. This will eventually lead us to VSTO Outlook add-ins and a description of why they do not have to do any of the tricks shown in Listing 24-1.

To understand ReleaseComObject, it is necessary to understand more of what is really happening when your code runs inside the Outlook process. The first thing you need to understand is the concept of an application domain, or AppDomain. An application domain is an isolated environment in which your code runs within a processin this case within outlook.exe. You can think of an application domain as being a sort of process within a process. There can be one or more application domains running inside of a single process. There are several ways an application domain provides "process-like" isolation. An application domain can be stopped and unloaded without affecting another application domain. Individual application domains can be configured differently with different security policy, different settings for loading assemblies, and so on. Code running in one application domain cannot directly access code in another application domain. In addition, faults occurring in one application domain cannot affect other application domains.

With typical console applications or Windows Forms applications, you will usually have just one application domain where your code will run. There always is at least one application domain created automatically for any process running managed code. The application domain the CLR creates automatically is called the default application domain. The default application domain can only be unloaded when the process exits. This is often acceptable because you typically control all the code that loads into a console application or Windows Forms application that you have written.

In Office scenarios, you will want to have multiple application domains created in the same process where each add-in loads into its own application domain. This is desirable because if you load in the same application domain as another add-in, that add-in can adversely affect you, as discussed shortly. You also will not want to have an add-in or customization associated with a document load into the default application domain because the default application domain can only be unloaded and cleaned up when the process exits. A user might want to unload an add-in or close a document and the user will not want the customization to stick around in memory in the default application domain. Users will want the add-in to unload and free up that memory for other uses.

Figure shows the most desirable situation for Outlook COM add-ins (and Office COM add-ins in general). If each COM add-in is shimmed as described in Chapter 23, each add-in will load into its own application domain, providing isolation so that one add-in cannot affect another. Note that no add-ins load into the default application domain in Figure. If you do not shim a COM add-in, mscoree.dll will load it into the default application domain. If we could rule the add-in world, no add-ins would ever load into the default application domain. You should avoid loading into the default application domain because a tested COM add-in that works fine on your developer machine might conflict with some other add-in loading into the default application machine on a user's machine and chaos will ensue.

An ideal situation for add-inseach add-in loads in its own application domain. No add-ins load into the default application domain.


If you do not use a shim to load a COM add-in and instead let mscoree.dll load your add-in, you will end up with a situation such as the one shown in Figure. COM add-ins that are not shimmed are loaded into the default application domain by default.

The undesirable situation that occurs when add-insare not shimmed and loaded by mscoree.dll.


Given Figure and Figure, we now consider what happens when you use a COM object in your customization. When you use a COM object in your customization such as Outlook's Application object, the CLR creates an object called a Runtime Callable Wrapper (RCW) for that COM object that your managed code talks to. The RCW in turn talks to the actual COM object. Any time your code talks to Outlook's Application object, your code is actually talking through the RCW.

RCWs are scoped to an application domain. The CLR creates one RCW that all code in a given application domain will use to talk to Outlook's Application object. Figure shows the ideal situation for RCWs. With each add-in loaded into its own application domain, each add-in has its own RCWs. Figure also illustrates that when multiple variables are declared in a particular application domain that are set to an instance of Outlook's Application object, they share the same RCW object. Note that the RCW is shared because Outlook's Application object is a singleton COM object. For nonsingleton COM objects, the RCW is not shared and the situation described below does not have as great an impact.

An ideal situation for add-insadd-ins should not share RCWs.


Now we consider what ReleaseComObject does. Suppose you have a class variable in your add-in code called appObject1 that is set to an instance of Outlook's Application object. You also might have another class variable in another area of your add-in called appObject2 that is also set to an instance of Outlook's Application object. Even though you have two variables set to an instance of Outlook's Application object, these two variables will both share one RCW that is at the application domain level.

Now suppose that appObject1 gets set to an instance of Outlook's Application object first. This causes Outlook's Application RCW to be created. The RCW is reference countedthat is, a count is kept of each variable that is using the RCW. So the reference count of the RCW goes to 1. In addition, the RCW talks to the COM object for Outlook's Application object and adds a reference count to the COM object, too. Outlook now knows that there is some code that is "using" one of its objects. Later in the code, appObject2 gets set to an instance of Outlook's Application object. The CLR detects that an RCW is already available and so it increments the reference count on the RCW and has appObject2 share the RCW with appObject1. It does not increment the reference count on the COM object, howeverthe RCW will only take one reference count on the COM object and it will release that reference count when all the variables using the RCW are garbage collected.

Because Outlook is more strict about reference counts than the other Office applications, to get Outlook to shut down you need to release the reference count the RCW has made on any COM objects your managed code is using when the last Outlook window (either Explorer or Inspector) is closed or when Outlook's Application object raises the Quit event. The right way to do this is setting all the variables you have set to Outlook objects to null and then forcing two garbage collections. The quick and dirty way to do this is using ReleaseComObject. When you call ReleaseComObject on a variable, the CLR releases the reference count on the RCW associated with that variable type. So if you want to get rid of the RCW for Outlook's Application object and thereby release Outlook's COM object to get it to shut down properly, you could write the following code:

Runtime.InteropServices.Marshal.ReleaseComObject(appObject1);
Runtime.InteropServices.Marshal.ReleaseComObject(appObject2);

Note that this assumes that there are only two variables in the application domain that are using the RCW: appObject1 and appObject2. If you forgot about a variable that was set to Outlook's Application object or you are referencing a library that sets its own internal variables to Outlook's Application object, this code would not result in the RCW going away and releasing Outlook's COM object because the reference count on the RCW would be greater than 2.

ReleaseComObject also returns the number of reference counts left on the RCW. So armed with this knowledge, you could write this even scarier code:

int count;

do
{
  count = Runtime.InteropServices.Marshal.ReleaseComObject(appObject1);
}
while (count > 0);

This code keeps releasing the reference count on the RCW until it goes to 0, which then causes the RCW to be released and the COM object it is talking with to have its reference count released. This code would get rid of the RCW even in the case where you forgot about a variable that was set to Outlook's Application object or using a library that was using Outlook's Application object. The CLR also provides another method that is the equivalent to calling ReleaseComObject in a loop. This method is shown here:

Runtime.InteropServices.Marshal.FinalReleaseComObject(appObject1);

After the RCW has gone away because of calling ReleaseComObject, ReleaseComObject in a loop, or FinalReleaseComObjectif you attempt to use any of the properties or methods on any variables that were set to the Outlook Application object (for example you try to access appObject1.Name)you will get the error dialog shown in Figure.

The error that occurs when you try to talk to a variable whose RCW has been released.


So you can probably see that if you load into your own application domain and if you are not using any referenced libraries that talk to Outlook's application object and if you can avoid talking to any properties or methods of Outlook's Application object after you have called ReleaseComObject in a loop or FinalReleaseComObjectyou could get away with using this approach. This is only because you are in your own application domain and are presumably in control of all the code that might load there. If you shoot anyone in the foot by using ReleaseComObject, it will be yourself and not other developers.

Consider what happens if you are not using a shim and you load into the default application domain. Now you have great potential to adversely affect other add-ins that also are not shimmed and are loading into the default application domain. Figure shows this situation. Suppose that Add-in 1 calls FinalReleaseComObject on its appObject1 object. This will not only release the references that Add-in 1 has on the RCW, but because the RCW is shared at the application domain level and Add-in 2 is also loaded in the same application domain, it will release the references that Add-in 2 has on the RCW. Now, even if Add-in 1 is smart enough to not touch appObject1 anymore, Add-in 2 has no way of knowing that when it talks to appObject2 or appObject3 it will get an exception due to the RCW going away.

Worst-case situation for add-insadd-ins share RCWs and one add-in calls ReleaseComObject in a loop or FinalReleaseComObject.


If instead, Add-in 1 sets appObject1 to null and forces a garbage collection, .NET will make sure that the right number of reference counts are released on the RCW without affecting other users of the RCW. Also, with appObject1 set to null, it will be clearer in your code that you are no longer allowed to talk to appObject1.

The CLR does not clean up the reference counts on the RCW until the variable you have set to null is garbage collected. In Listing 24-1, where we are trying to clean up the reference count immediately after the last window is closed, we force a garbage collection immediately after setting the variables referring to Outlook objects to null. To force the garbage collection, we call GC.Collect() followed by GC.WaitForPendingFinalizers(). Note that we then call GC.Collect() and GC.WaitForPendingFinalizers() a second time to ensure that any RCWs that were stored as members of objects with finalizers are properly cleaned up.

How Outlook Add-In Development Should Bethe VSTO Outlook Add-In Project

Outlook COM add-in development requires you to track any variables set to Outlook objects, sink the Close events of the Inspector and Explorer objects, set your variables set to Outlook objects to null when the last Inspector or Explorer closes or the Application object's Quit event is raised, and force two garbage collections. This complexity is not required when building add-ins for other Office applications, so do not apply these techniques to Excel or Word. Excel and Word are more robust to reference counts on their COM objects being held during the shutting down of the application. This situation also never occurs in VSTO 2005 customizations because of VSTO's better model for loading and unloading code.

If you use the VSTO Outlook add-in project, you will not have to worry about any of these Outlook-specific shutdown problems or any of the problems that we said required a shim in Chapter 23. The VSTO Outlook add-in project uses the VSTO model for loading and unloading an add-in. The VSTO model always loads a customization into its own application domain. When the add-in is unloaded or the application exits, VSTO raises a Shutdown event into the customization. The developer does not have to set any objects to null or force a garbage collection to clean up RCWs because once the Shutdown event handler has been run, VSTO unloads the application domain associated with the customization. When the application domain is unloaded, all the RCWs used by that application domain and customization are cleaned up automatically and the references on COM objects are released appropriately. After the application domain has been unloaded, memory used by the customization is freed, and the process can continue to run. Because VSTO Outlook add-ins apply this approach to add-ins, you never have to worry about setting variables to null, RCWs, or any of the complexity discussed in this section.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows