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.
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.Why has my button stopped working" problem described in Chapter 1, "An Introduction to Office Programming."
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(); } } }
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 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 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 24-1. 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.
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 24-2. COM add-ins that are not shimmed are loaded into the default application domain by default.
Given Figure 24-1 and Figure 24-2, 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 24-3 shows the ideal situation for RCWs. With each add-in loaded into its own application domain, each add-in has its own RCWs. Figure 24-3 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.
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 24-4.
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 24-5 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.
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.
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.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.