Microsoft .NET Development for Microsoft Office

 

This article is an excerpt from Microsoft .NET Development for Microsoft Office, from Microsoft Press (ISBN 0-7356-2132-2, copyright Microsoft Press 2005, all rights reserved).

The author, Andrew Whitechapel, spent several years on the .NET Enterprise Applications Development team at Microsoft Consulting Services in the United Kingdom. He has extensive experience developing Microsoft Officebased applications with Microsoft .NET managed code. Andrew is now a technical program manager on the Microsoft Visual Studio Tools for Office team.

No part of this chapter may be reproduced, stored in a retrieval system, or transmitted in any form or by any meanselectronic, electrostatic, mechanical, photocopying, recording, or otherwisewithout the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

Download sample code.

Chapter 2: Basics of Office Interoperability (Part 2 of 3)

Contents

2.3 Visual Studio .NET Office Interop Wizards
2.4 Interface/Class Ambiguity
    C# Version
    Visual Basic .NET Version
2.5 Releasing COM Objects
    Simple Garbage Collection
    Using ReleaseCOMObject
    AppDomain Unloading

2.3 Visual Studio .NET Office Interop Wizards

Note   This section applies to Office XP and Office 2003.

The previous section demonstrated how to generate Visual Studio .NET projects that interoperate with Office, using either the PIAs that ship with Office XP and Office 2003 or Visual Studio–generated IAs for Office 97 and Office 2000.

Accompanying this book is a complete set of sample source projects for all the walk-through exercises (for each version of Office where workable). In addition, there is a set of simple tools that you can use to make managed Office development simpler and quicker. Among these tools is a Microsoft Installer file (MSI) for a set of Visual Studio .NET Office Interop Wizards. These are standard wizard extensions to Visual Studio .NET 2003 (Framework 1.1) that generate projects with predefined skeleton code and project properties. These properties include references to the Office 2003 or Office XP PIAs for the particular target Office application you're working with. The wizard-generated code includes a simple fragment that launches the target Office application and puts some simple data into it.

To install these wizards, first make sure you don't have any instances of Visual Studio currently running. Then simply run the MSI. Once you've installed the wizards, start Visual Studio and select File | New | Project. In the New Project dialog box, expand the Visual C# Projects node, and you will see a new child node labeled Office Interop. Expand this node, and you will see two further child nodes, for Office 2003 and Office XP, respectively (Figure 2-9).

Office Interop Wizard in Visual Studio

Figure 2-9. Office Interop Wizard in Visual Studio

As you can see, there are four different project types for each version of Office, covering Excel, Outlook, PowerPoint, and Word XP and Word 2003.

The default project name is based on the name of the target Office application and the version. As normal with Visual Studio projects, a numeric suffix is used, which is automatically incremented and allows for any other similarly named project that might already exist in the target location.

When you run one of the wizards, the code generated includes a simple test that launches the target Office application and inserts some dummy data. For example, the code generated for an Excel 2003 project includes code to launch Excel, add a Workbook to the Workbooks collection, and insert some dummy text into cell A1 in the ActiveSheet:

public Excel2003InteropApplication1Form() 
{ 
    InitializeComponent(); 
 
 
    // TODO 
    // This sample code demonstrates how to launch Excel. 
    // After you have tested that this works, you can safely 
    // remove all this code. 
    try 
    { 
        Excel.Application xl = new Excel.Application(); 
        xl.Visible = true; 
 
        Excel.Workbooks books = (Excel.Workbooks)xl.Workbooks; 
        Excel.Workbook book = books.Add(Excel.XlSheetType.xlWorksheet); 
        Excel.Worksheet sheet = (Excel.Worksheet)book.ActiveSheet; 
 
        Excel.Range r = (Excel.Range)sheet.Cells[1,"A"]; 
        r.Value2 = "Hello Excel2003InteropApplication1Form"; 
 
        r = null; 
        sheet = null; 
        book = null; 
        books = null; 
        xl = null; 
        GC.Collect(); 
        GC.WaitForPendingFinalizers(); 
        GC.Collect(); 
        GC.WaitForPendingFinalizers(); 
    } 
    catch (Exception ex) 
    { 
        Debug.WriteLine(ex.Message); 
    } 
    // ODOT 
}

Each of the eight wizards relies on the appropriate PIA for the target Office application being registered and deployed to the GAC. If you attempt to generate a project for an application for which the PIA is either not registered or not GAC-deployed, the wizard will fail with an explanatory message. You don't need all of the PIAs installed—just the one you're targeting.

When you've generated the skeleton project, it's a good idea to build it and execute it to make sure that the environment is set up correctly. Then you can safely delete the test code (or comment it out, if you want to use it for reference later).

The wizards were designed to work only with Office 2003 and Office XP because at the time of writing these are the only versions of Office for which Microsoft ships PIAs. While it would be easy enough to create wizards that cover Office 97 and Office 2000, the wizards do have to make certain assumptions about where the IAs are to be found. With Office 97 and 2000, it's not possible to make these assumptions. It would be possible, of course, to simply regenerate IAs based on the registered Office type library. However, regenerating IAs for each and every project is clearly inefficient. For these reasons, then, the wizards support only Office 2003 and Office XP. The wizards also generate only C# code. Again, it would be simple enough to generate Visual Basic .NET code, but the majority of the code in this book is written in C#, so I've kept things simple.

Note   If you ever need to uninstall the wizards, you can do so either by running the MSI again (and choosing the Remove option) or by going to Control Panel | Add Or Remove Programs and clicking the Remove button for the Visual Studio .NET Office Interop Wizards in the normal way.

2.4 Interface/Class Ambiguity

Note   This section applies to Office 97, Office 2000, Office XP and Office 2003.

The IAs, including the Office PIAs, include several types with the same root name, with or without the suffix "Class." You should use only objects that do not end with the suffix. For example, the Excel.ApplicationClass type is a class that implements the Excel.Application, Excel._Application, and Excel.AppEvents_Event interfaces. Confusion can arise because tools such as the Visual Studio Object Browser and ILDASM show the _Application and AppEvents_Event interfaces, but the only relevant class they show is ApplicationClass. In fact, there will also be an Application class. Therefore, the following statements, which declare instances of the Word and Excel Application classes, are correct, despite the listings in the Object Browser:

    Word.Application wd = new Word.Application(); 
    Excel.Application xl = new Excel.Application();

The (somewhat invisible) Application class gives us access to all the methods, events, and properties exposed by the "real" Excel Application COM object. So when we ask for a new Application, the real Application COM class object is actually instantiated on our behalf.

One reason why the exposed object services have been split into multiple interfaces is because .NET cannot resolve the difference between overloaded statements across member types. For example, the Word.Application object exposes both a Quit event and a Quit method, and the .NET CLR cannot resolve the ambiguity.

Another reason is an attempt to rationalize the behavior that is expected by developers who have a C/C++ background or a Visual Basic background. Traditional Visual Basic developers are used to dealing with classes, not interfaces. C/C++ developers are used to dealing with both classes and interfaces. Part of the behavior of traditional Visual Basic was to streamline development and effectively hide interfaces as classes. Now that many developers are transitioning from both traditional Visual Basic and C/C++ to Visual Basic .NET or C#, the distinction between classes and interfaces has to be more explicit.

In the following simple example, we'll develop a .NET Windows Forms application to interoperate with Word. We'll offer the user two buttons: one to launch Word, the other to quit Word. A common dilemma with Office interop is that if you make the application visible, you have to allow for the user to interact with it. So although we're launching Word, there is a possibility that the user will quit Word. We don't want to be in the situation where we try to quit an instance of Word that has already closed. Fortunately, Word fires a Quit event when it closes, so we can intercept this event and take appropriate action.

Note   The sample solution for this topic can be found in the sample code at <install location>\Code\Office<n>\AmbiguousMember.

C# Version

  1. Create a new Windows Forms application called AmbiguousMember. Add a reference to the appropriate Word IAs for your target version of Office. Put two Button controls on the form, and get Click event handlers for each one. Set the Enabled property of the QuitWord button to false—there's no point letting users think they can quit Word if we haven't launched it yet. See Figure 2-10.

    Office Interop Wizard in Visual Studio

    Figure 2-10. Office Interop Wizard in Visual Studio

  2. Add a reference to the appropriate Word IAs for your target version of Office. Declare a class field for the Word.Application object.

        private Word.Application wd;
    

    Bear in mind that the runtime will instantiate a real Word Application object on our behalf, and this object will implement multiple interfaces. The COM-heads among you will realize that when we ask for a C# Application object, what's happening is that the runtime performs a QueryInterface on the COM Application class object to return us an Application interface pointer.

  3. In the Click handler for the RunWord button, we'll instantiate the Word Application object and make it visible. For simplicity, we won't bother opening or adding any documents. Then we'll toggle the Enabled state of both buttons:

        private void btnRunWord_Click(object sender, System.EventArgs e) 
        { 
            wd = new Word.Application(); 
            wd.Visible = true; 
            btnQuitWord.Enabled = true; 
            btnRunWord.Enabled = false; 
        }
    
  4. In the QuitWord handler, we'll call the Word.Quit method, release the COM object, and force a garbage collection. Then we'll toggle the Enabled state of the buttons again:

        private void btnQuitWord_Click(object sender, System.EventArgs e) 
        { 
            if (wd != null) 
            { 
                object missing = Type.Missing; 
                wd.Quit(ref missing, ref missing, ref missing); 
                wd = null; 
                GC.Collect(); 
                GC.WaitForPendingFinalizers(); 
                GC.Collect(); 
                GC.WaitForPendingFinalizers(); 
            } 
            btnRunWord.Enabled = true; 
            btnQuitWord.Enabled = false; 
        }
    
  5. Build and test. So long as we do things in the right order and the user doesn't interfere, everything will be fine. However, see what happens if we launch Word, then the user quits Word interactively, and then we try to quit Word. Of course, the Word COM server will have stopped, so we'll get an RPC exception.

  6. To allow for the user quitting Word from underneath us, we should hook up Word's Quit event. Now we have a slight problem: if you try to access the Quit event member from our existing Application object, you'll find that you can't. This is because the Application interface doesn't have a Quit method. However, remember that behind the scenes, we're really accessing a real COM Word Application class object, and that Application is only one of the interfaces it implements.

    If you look in the Object Browser, you'll see that the ApplicationClass class implements multiple interfaces, including several event interfaces (Figure 2-11):

    Object browser showing ApplicationClass class

    Figure 2-11. Object browser showing ApplicationClass class

    So, should we have declared an ApplicationClass reference in the first place, instead of an Application reference? Well, you can try it, but it won't work. The ApplicationClass lists the Quit method, but it doesn't list the Quit event. If it did, the two Quit members would be ambiguous, and the runtime cannot resolve such ambiguity. In fact, the Quit event is listed in the event interfaces (Figure 2-12):

    Object browser showing event interfaces

    Figure 2-12. Object browser showing event interfaces

  7. To hook up the Quit event using our existing reference, we'll need to cast to the appropriate interface. Remember that this is really doing a QueryInterface for us behind the scenes to switch from one interface pointer to another interface pointer on the same object. Add this code to the RunWord button Click handler:

            Word.ApplicationEvents2_Event wdEvents2 =  
                (Word.ApplicationEvents2_Event)wd; 
            wdEvents2.Quit +=  
                new Word.ApplicationEvents2_QuitEventHandler(QuitHandler);
    
  8. When you hook up an event handler in this way, what you're saying is that when Word fires the Quit event, the runtime should call into our custom method, where we can do whatever we think is appropriate. In the code above, we've specified a custom method called QuitHandler, which we now need to write. We'll implement it to simply set the Application reference to null and toggle the Enabled state of our buttons.

        private void QuitHandler() 
        { 
            wd = null; 
            btnRunWord.Enabled = true; 
            btnQuitWord.Enabled = false; 
        }
    

    Note that, despite the somewhat confusing view we have of the Application class presented to us by the Object Browser and Visual Studio IntelliSense, we can quite happily instantiate classes using the Application type. We can also explicitly cast to any interface that this class implements. Although this seems confusing, we do end up coding correctly, using exactly the right type (class or interface) for each scenario.

  9. If you're paying close attention, you'll notice that we've set up a race condition here. In our QuitWord button handler, there's a window of opportunity between calling Quit and releasing the object reference. If our QuitHandler is invoked at that precise point, the reference will be set to null, so when we try to release it we'll fail. One simple way to eliminate this possibility is to unhook the event handler before we call Quit:

            if (wd != null) 
            { 
                wdEvents2.Quit -=  
                    new Word.ApplicationEvents2_QuitEventHandler(QuitHandler); 
                object missing = Type.Missing; 
                wd.Quit(ref missing, ref missing, ref missing); 
                wd = null; 
                GC.Collect(); 
                GC.WaitForPendingFinalizers(); 
                GC.Collect(); 
                GC.WaitForPendingFinalizers(); 
            }
    
  10. Build and test again.

You should find that we have dealt cleanly with the situation in which we launch a particular instance of Word but the user has closed it. For completeness, a listing of all the significant code—that is, all the code apart from the wizard- and designer-generated code—follows. Don't forget that the material accompanying this book includes all the sample solutions in full for all versions of Office where that topic is feasible.

using System; 
using System.Drawing; 
using System.Collections; 
using System.ComponentModel; 
using System.Windows.Forms; 
using System.Data; 
 
using Word = Microsoft.Office.Interop.Word; 
using System.Runtime.InteropServices; 
using System.Diagnostics; 
  
namespace AmbiguousMember 
{ 
    public class AmbiguousMemberForm : System.Windows.Forms.Form 
    { 
 
 
        // Wizard/Designer code omitted here for brevity. 
        private Word.Application wd; 
        private Word.ApplicationEvents2_Event wdEvents2; 
 
 
        private void btnRunWord_Click(object sender, System.EventArgs e) 
        { 
            wd = new Word.Application(); 
            wd.Visible = true; 
 
            wdEvents2 = (Word.ApplicationEvents2_Event)wd; 
            wdEvents2.Quit +=  
                new Word.ApplicationEvents2_QuitEventHandler(QuitHandler); 
 
            btnQuitWord.Enabled = true; 
            btnRunWord.Enabled = false; 
        } 
 
 
        private void btnQuitWord_Click(object sender, System.EventArgs e) 
        { 
            if (wd != null) 
            { 
                wdEvents2.Quit -=  
                    new Word.ApplicationEvents2_QuitEventHandler(QuitHandler); 
 
                object missing = Type.Missing; 
                wd.Quit(ref missing, ref missing, ref missing); 
 
                wd = null; 
                GC.Collect(); 
                GC.WaitForPendingFinalizers(); 
                GC.Collect(); 
                GC.WaitForPendingFinalizers(); 
            } 
            btnRunWord.Enabled = true; 
            btnQuitWord.Enabled = false; 
        } 
 
     
        private void QuitHandler() 
        { 
            wd = null; 
            btnRunWord.Enabled = true; 
            btnQuitWord.Enabled = false; 
        } 
 
    } 

Visual Basic .NET Version

If we're working in Visual Basic .NET, we suffer the same problem of ambiguity and the same confusion about the Application class and the Application interface.

We need to declare the Word Application COM object reference to be an Application interface reference. We also need a reference to the Word ApplicationEvents2_Event interface, even though this interface does not show up in Microsoft IntelliSense nor in the Object Browser.

Public wd As Word.Application 
Public wdEvents As Word.ApplicationEvents2_Event

Then, in our RunWord button handler, we instantiate the Application object and assign it to our Application reference. Then we can typecast from the Application interface to the ApplicationEvents2_Event interface (which, of course, performs a COM QueryInterface call behind the scenes). Then we can add a handler for the Quit event. We use the AddHandler statement instead of using the Handles keyword, because it allows us to dynamically remove the handler later. Note that IntelliSense does not offer the ApplicationEvents2_Event interface, nor the Quit member, so be careful with the spelling:

Private Sub btnRunWord_Click(ByVal sender As System.Object, _ 
ByVal e As System.EventArgs) Handles btnRunWord.Click 
wd = New Word.ApplicationClass() 
wd.Visible = True 
wdEvents = CType(wd, Word.ApplicationEvents2_Event) 
AddHandler wdEvents.Quit, _ 
AddressOf Me.wd_ApplicationEvents2_Event_Quit 
btnQuitWord.Enabled = True 
btnRunWord.Enabled = False 
End Sub

In our QuitWord button handler, we'll unhook the event handler, call Quit on Word, and clean up as usual:

Private Sub btnQuitWord_Click(ByVal sender As System.Object, _ 
ByVal e As System.EventArgs) Handles btnQuitWord.Click 
If Not wd Is Nothing Then 
RemoveHandler wdEvents.Quit, _ 
AddressOf Me.wd_ApplicationEvents2_Event_Quit 
wd.Quit() 
wd = Nothing 
GC.Collect() 
GC.WaitForPendingFinalizers() 
GC.Collect() 
GC.WaitForPendingFinalizers() 
End If 
btnQuitWord.Enabled = False 
btnRunWord.Enabled = True 
End Sub

Finally, our handler for Word's Quit event:

Private Sub wd_ApplicationEvents2_Event_Quit() 
wd = Nothing 
btnQuitWord.Enabled = False 
btnRunWord.Enabled = True 
End Sub

Version Notes   Note that Word 97 exposes only one event interface on the Application object, and in the IA this will be named ApplicationEvents_Event. So the C# code should be modified slightly, as indicated here:

private Word.ApplicationEvents_Event wdEvents; ... wdEvents = (Word.ApplicationEvents_Event)wd; wdEvents.Quit += new Word.ApplicationEvents_QuitEventHandler(QuitHandler); ... wdEvents.Quit -= new Word.ApplicationEvents_QuitEventHandler(QuitHandler);

Version Notes    And the Visual Basic .NET equivalent:

Public wdEvents As Word.ApplicationEvents2_Event ... wdEvents = CType(wd, Word.ApplicationEvents2_Event)

Version Notes    Also, Word 97 doesn't fire a Quit event unless at least one document is open. So for our exercise, we'll add some code to open a document when we open Word. The C# version is given here:

object missing = Type.Missing; wd.Documents.Add(ref missing, ref missing);

**Version Notes    **And the Visual Basic .NET version:

wd.Documents.Add()

2.5 Releasing COM Objects

Note   This section applies to Office 97, Office 2000, Office XP and Office 2003.

One area of interop between the managed world and the unmanaged world in which you need to be especially careful is in cleanly releasing COM objects when you're done with them. In the foregoing examples, we managed to achieve all the behavior we wanted using standard garbage collection. The only slight enhancement was to call GC.Collect twice to ensure that any memory that was available for collection but survived the first sweep was collected on the second sweep.

In this section, we'll examine two other strategies for releasing COM objects that are being used by managed code:

  • ReleaseComObject
  • AppDomain unloading

Before we look at the specifics of these strategies, we need to understand why they might be useful. In the case of ReleaseComObject, we also need to understand the dangers of misusing the strategy.

The Office applications are all based on COM technology, which is what allows us to develop against them. However, a basic mismatch exists between the way COM expects memory to be managed and the way .NET manages memory. Memory management and object lifetime in COM are deterministic. That is, you explicitly allocate memory and explicitly deallocate it. Equally, when you get a pointer to a COM object interface, an internal reference count on the object is incremented. In the COM world, when you're done with an object, you explicitly release it, which decrements the reference count. When the reference counts goes to zero, the object is destroyed and its memory is released. The point is, you get to choose—to a fine degree of granularity—exactly when the object is destroyed and the memory is released.

In the .NET world, however, we have the luxury of a run-time garbage collector that deallocates memory for us in the background. We don't explicitly free the memory or the objects. Instead, we can set a reference to null, and if there are no other references to the same memory, that marks the memory as available for collection. Then, at some indeterminate time later, the garbage collector deallocates the memory. The point here is that we can never be entirely sure exactly when the object will be destroyed and the memory will be released.

In both the COM world and the .NET world, memory is eventually cleaned up, so does it matter that there's a difference? Well, maybe, sometimes. Consider the following scenarios:

  • Your managed solution is referencing a COM object that's holding on to a lot of memory or other resource that you need to release eagerly (for performance or contention reasons).
  • You believe a circular reference has deadlocked (say we're done with object A but object A holds a reference to object B, and object B can't be released until object A is released), and you want to break the cycle.
  • If you leave everything to the CLR garbage collector, there is a very small window of opportunity where your managed object's cleanup code gets into a race with the CLR's shutdown. If your managed object holds a reference to a COM object, you might then be in danger of orphaning that COM object.

Recall that managed code does not make direct calls into COM objects. Instead, an RCW acts as a proxy between your managed code and the COM object. It's worth noting that when you have a .NET client talking through an RCW to a COM object, there is a reference count on the RCW, and this reference count is not the same as the COM object's internal reference count. The COM internal reference count is essentially a count of the number of clients. The RCW reference count is a count of the number of times the same interface pointer has entered the CLR—and this is almost always 1, regardless of the number of managed clients in the process.

If, in managed code, I declare a reference to a COM object (which will, of course, be a reference to an RCW at run time), what is the scope of the reference? Is it enough to declare the variable in, say, a try block, and force a garbage collection in the finally block? Well, that depends: in a debug build, the JIT compiler tracks local variables on a per-method basis. So even though an RCW reference might be nominally scoped to a try block, it's actually considered to be alive for the duration of the method. To complicate matters, this behavior is different in a release build. In a release build, forcing a garbage collection in the finally block cleans up all the RCWs and therefore all the COM objects. But in a debug build, you must force the garbage collection after the method returns.

So when we return from the method that's doing all this interop, we could force a garbage collection, and all our RCWs, including the implicit ones, should be cleaned up. But this approach has a couple of problems:

  • How do you know you've returned from a method? Is this a foolish question? Well, not necessarily—if it's a simple method, it might be inlined by the JITer, which means that when the source code indicates you're returning, maybe at run time you're not.
  • The CLR garbage collection is generational, so in some situations one or more of our RCWs might survive the first sweep and be promoted to generation 2.

Clearly, there is a basic mismatch between the COM world of deterministic object lifetime management and the .NET world of traced reference management. To reconcile this mismatch, the System.Marshal class exposes a ReleaseComObject method. You can call Marshal.ReleaseComObject on the RCW (that is, the managed proxy you have in your managed client) when you're done with it. ReleaseComObject will decrement the RCW reference count and return the updated reference count. There are a couple of points to note about this:

  • If you have multiple .NET clients using the same RCW and any one of them calls ReleaseComObject, the object is released as far as the whole runtime is concerned. RCWs are created on a per-AppDomain basis. Therefore the RCW is no longer usable by any of the managed clients in the AppDomain.
  • ReleaseComObject returns the updated reference count after the call, which is almost always 0. The RCW reference count on a live object is almost always 1, but not always. Therefore, to be sure you're releasing the object, you can call ReleaseComObject in a loop until it returns 0.

It's worth repeating that ReleaseComObject decrements the reference count on an RCW and that the RCW is scoped to the AppDomain. The reason I want to emphasize this is that in any one AppDomain it is possible for there to be more than one "application." Your application might not be the only one in the AppDomain, and if you call ReleaseComObject in a reckless manner, you risk causing damage to other applications in the AppDomain. We'll revisit this point when we consider isolating managed extensions in Chapter 8. For now, a general rule of thumb is that you should consider using ReleaseComObject only if you can guarantee that you own all the code in the AppDomain. This applies, for example, if you have created a Windows Forms application that automates an Office application. Your Windows Forms application will be given a default AppDomain, and you are in control of all the code that runs in that AppDomain. Indeed, you might choose to create additional AppDomains in your code, and you will remain in control of all those AppDomains. This might not be true if you develop a managed COM add-in.

For completeness, the Marshal class also exposes a Release method, which decrements the internal COM refcount on the interface. You should never use this unless you also use Marshal.AddRef and you've got an IntPtr that represents the raw COM IUnknown pointer (by calling Marshal.GetComInterfaceForObject or one of its siblings).

Another common point of confusion is that you might inadvertently instantiate a COM object without knowing it. For example, consider the following code:

Excel.Application xl = new Excel.Application(); 
Excel.Workbook book = xl.Workbooks.Add(Excel.XlSheetType.xlWorksheet); 
Excel.Worksheet sheet = (Excel.Worksheet)book.ActiveSheet;

How many COM objects have been instantiated? Answers generally range from three to five, and it depends on how much you know about the internal workings of the specific COM object model that you're interoperating with. In this case, we're likely instantiating these five objects:

  • the Application object
  • the Workbooks collection object
  • a Workbook object
  • the Sheets collection object
  • a Worksheet object

In the previous code we reference the Workbooks collection object and the Worksheets collection object. Both are COM objects that are implicitly created when we use the property. We don't keep any managed references to these objects ourselves, but that in itself confuses many developers. The CLR will have wrapped these objects for us, and because we don't have explicit references to them, we have no way of explicitly releasing them. Instead, we rely on the CLR to release them on our behalf.

In the following example, we'll explore a couple of ways of making our cleanup operations more deterministic—and more aggressive.

Note   The sample solution for this topic can be found in the sample code at <install location>\Code\Office<n>\CleanRelease. This solution includes three separate projects, corresponding to the three upcoming subsections.

Simple Garbage Collection

  1. Create a new Windows Forms application called SimpleCollection. Add a reference to the appropriate Excel IAs for your target version of Office. Put three Button controls on the form, and get Click event handlers for each one. See Figure 2-13.

    Simple garbage collection sample

    Figure 2-13. Simple garbage collection sample

  2. Declare fields in the form class to hold references to an Excel.Application object and an Excel.Workbook object. We want to use these objects across multiple method calls, hence the need for them to be class fields. However, this is also the first step in making life difficult—now that our managed COM object references are no longer scoped to single method calls, they'll be much more difficult to track and clean up:

        private Excel.Application xl; 
        private Excel.Workbook book;
    
  3. In the Click handler for the Run Excel button, launch a new instance of Excel and cache the Application object reference in one of our class fields. We'll also create a new Workbook and cache that reference:

        private void btnRunExcel_Click(object sender, System.EventArgs e) 
        { 
            xl = new Excel.Application(); 
            xl.Visible = true; 
            object missing = Type.Missing; 
            book = xl.Workbooks.Add(missing); 
        }
    
  4. In the Click handler for the New Sheet button, use the cached Workbook reference to create a new Worksheet object:

        private void btnNewSheet_Click(object sender, System.EventArgs e) 
        { 
            object missing = Type.Missing; 
            book.Worksheets.Add(missing, missing, missing, missing); 
        }
    

    Note that we're keeping things very simple here—for example, so as not to confuse the user, we should probably disable the NewSheet button until we've successfully instantiated the Excel.Application object.

  5. In the CleanUp button handler, perform simple cleanup, as before, by nulling the references and forcing two garbage collections:

        private void btnCleanUp_Click(object sender, System.EventArgs e) 
        { 
            book = null; 
            xl = null; 
            GC.Collect(); 
            GC.WaitForPendingFinalizers(); 
            GC.Collect(); 
            GC.WaitForPendingFinalizers(); 
        }
    
  6. Build and test.

We've set up our code to release the Excel Application COM object—assuming the user clicks the buttons in the right order. On the other hand, we haven't invoked the Excel.Quit command, so even though we release our reference, Excel will continue to run. This is fine—if we're making Excel visible, presumably we want the user to interact with it, and we'll leave it up to them when to quit. We could do it either way, but in our simple example this is not important. What is more relevant is that we are relying on the user to click the CleanUp button at the end. What happens if the user doesn't? When you release the COM object, there's no visual feedback, of course. So to test this scenario, run the application and keep Task Manager open so that you can watch when the Excel process stops.

If the user runs Excel and then directly quits Excel, you'll see that the Excel process remains running—this is because we're holding a reference to one of Excel's COM objects in our managed application. Of course, the CLR will clean up this reference on our behalf when the application stops. However, a very long period of time might pass while Excel is still running after the user has quit it. This is good, because we're holding a reference to an Excel object and we presumably want to use it again at some later point.

Also, this application is being deliberately reckless with its managed references to COM objects. We only have one Application and one Workbook reference, but the user can keep clicking the buttons, and each time we'll overwrite our reference. For example, if you click the button to run Excel three times, you'll get three instances of Excel. If the user then independently quits those three Excel instances, all she's doing is hiding the three Excel main windows; the three Excel processes will continue to run. You can verify the behavior in Task Manager or Process Explorer (freeware from www.sysinternals.com).

If we did this (that is, overwrite our references to make them refer to new instances) in unmanaged COM code, we'd be leaking memory. However, in the .NET world, we're actually better off. Recall that we're using RCWs and that RCWs are scoped to the AppDomain. If we then click the button once to clean up our references, all three instances will be cleaned up at once because there's only one RCW in the AppDomain, so forcing that one RCW to be collected will terminate all three instances.

Using ReleaseComObject

In almost all situations, nulling the RCW reference and forcing a garbage collection will clean up properly. If you also call GC.WaitForPendingFinalizers, garbage collection will be as deterministic as you can make it. That is, you'll be pretty sure exactly when the object has been cleaned up—on the return from the second call to WaitForPendingFinalizers. As an alternative, you can use Marshal.ReleaseComObject. However, note that you are very unlikely to ever need to use this method. The following code shows our simple example modified to use ReleaseComObject instead of GC.Collect:

    private void btnCleanUp_Click(object sender, System.EventArgs e) 
    { 
        // book = null; 
        // xl = null; 
        // GC.Collect(); 
        // GC.WaitForPendingFinalizers(); 
        // GC.Collect(); 
        // GC.WaitForPendingFinalizers(); 
 
        if (book != null) 
        { 
            Marshal.ReleaseComObject(book); 
            book = null; 
        } 
        if (xl != null) 
        { 
            Marshal.ReleaseComObject(xl); 
            xl = null; 
        } 
    }

If you try this code, you'll see that it behaves in essentially the same way as with the GC.Collect approach, in that the Excel Workbook and Application RCWs are released when the user clicks the button. However, ReleaseComObject relies on us passing a reference to the RCW we want to release, and we're then immediately nulling the reference. Therefore, we can only do this once. Recall that we've allowed the user to run multiple instances of Excel and create multiple workbooks. Using the ReleaseComObject approach, with the code as it stands, we'd only ever release the last instance. If we wanted to use ReleaseComObject, we'd have to keep all the Excel Application and Workbook references in some kind of list or collection so we could later walk the list, releasing each one.

Note also that ReleaseComObject doesn't directly release the RCW—it only decrements the RCW reference count. Normally, the RCW reference count is 1, so calling ReleaseComObject normally decrements it to 0, which releases the underlying object. However, the count is not always 1, and if you need to be absolutely sure that you're releasing the RCW and its underlying COM object, you have to call ReleaseComObject repeatedly. So, in our sample code, we could simply keep calling ReleaseComObject until the reference count returned is 0:

        if (book != null) 
        { 
            //Marshal.ReleaseComObject(book); 
            while (Marshal.ReleaseComObject(book) > 0) { } 
            book = null; 
        } 
        if (xl != null) 
        { 
            //Marshal.ReleaseComObject(xl); 
            while (Marshal.ReleaseComObject(xl) > 0) { } 
            xl = null; 
        }

Warning   If you call ReleaseComObject in a loop, the RCW will be unusable by any code in the AppDomain from that point on. Any attempt to use the released RCW will result in an InvalidComObjectException being thrown.

This risk is compounded when the COM component that is being used is a singleton because CoCreateInstance (which is how the CLR activates COM components under the covers) returns the same interface pointer every time it is called for singleton COM components. So separate and independent pieces of managed code in an AppDomain can be using the same RCW for a singleton COM component, and if either one calls ReleaseComObject on the COM component, the other will be broken.

As you can see, the standard .NET approach of relying on objects to go out of scope and be collected at some indeterminate point in the future is a little too relaxed when we're interoperating with COM objects. Fortunately, it takes little work to enhance this behavior by nulling the RCW references and forcing a double garbage collection sweep, coupled with waiting for the sweeps to complete. This approach gives us more deterministic behavior without running the risk of any unfortunate side effects. The only price we pay is a slight performance hit.

Best Practices   The option of using ReleaseComObject is safe only if you own all the code running in the AppDomain in which you call this method. It also has a couple of limitations that make it problematic to use. When we look at isolating managed extensions in Chapter 8, we'll examine the dangers more closely. For now, the best practice guideline is to not use ReleaseComObject.

AppDomain Unloading

An alternative strategy for ensuring that COM object references are properly released is to use AppDomains. A single .NET process can contain one or more AppDomains. By default, when a managed application starts, it is given an AppDomain by the runtime. Subsequently, the application can create as many new AppDomains as it likes. An AppDomain is programmatically represented by the System.AppDomain class. When you set up an AppDomain, you can load an assembly into it. If this assembly is an application, you can run it. This means that within a single Windows process, you can run multiple AppDomains, and each AppDomain can execute an application.

The ability to run multiple applications within a single process significantly increases scalability. Using AppDomains, you can stop individual applications without stopping the entire process—that is, you can unload a domain programmatically. Also, code running in one application cannot directly access code or resources from another application. The CLR enforces this isolation by preventing direct calls between objects in different AppDomains. Objects that pass between domains are either copied or accessed by proxy. Crucially, when an AppDomain is unloaded, the runtime releases all COM object references held in that domain.

The idea here is to create a new AppDomain and run all the COM-related code in it. Then we can unload the AppDomain. This ensures that any references to COM objects that we held in the AppDomain are cleaned up.

We'll implement this approach in a simple way—we'll create another application that creates a second thread. In the second thread, we'll create a second AppDomain. In the second AppDomain, we'll do all our Office interop. We need to run a second thread because otherwise, executing the second assembly on the same thread would block the first assembly.

  1. Add another Windows application project to the solution. We don't need any references to Office IAs in this project because we won't be doing any interop directly. Put two Button controls on the form, and get Click event handlers for them. Set the Enabled property of the Unload button to false initially. See Figure 2-14.

    AppDomain unloading sample

    Figure 2-14. AppDomain unloading sample

  2. Declare three class fields—for the second Thread, the second AppDomain, and the Form for the second application:

        private Thread excelThread; 
        private AppDomain excelDomain; 
        private Form appForm;
    
  3. In the Click handler for the New App button, create the second thread, and then toggle the Enabled state of the two buttons:

        private void btnNewApp_Click(object sender, System.EventArgs e) 
        { 
            excelThread = new Thread(new ThreadStart(ThreadProc)); 
            excelThread.Start(); 
            btnNewApp.Enabled = false; 
            btnUnload.Enabled = true; 
        }
    
  4. When you create a thread, you must specify a controlling method for that thread, as an argument to the ThreadStart constructor. In our thread controlling method, create a second AppDomain, using the static method AppDomain.CreateDomain. Then, from this second domain, instantiate the second application's Form object using CreateInstanceFromAndUnwrap. This method creates an instance of the specified type in the second domain and returns a proxy for use in our (first) domain:

        private void ThreadProc() 
        { 
            string assemblyName =  
                @"C:\Temp\SimpleRelease\bin\Debug\SimpleRelease.exe"; 
            string typeName = "SimpleRelease.SimpleReleaseForm"; 
            excelDomain = AppDomain.CreateDomain("ExcelDomain"); 
            appForm = (Form)excelDomain.CreateInstanceFromAndUnwrap( 
                assemblyName, typeName); 
            appForm.ShowDialog(); 
        }
    

    Note that in this code, you'll need to change the path for SimpleRelease.exe to wherever it actually is on your machine.

  5. Because we've created a second thread, we should be careful not to exit from the main application until the second thread has stopped. We can do several things to synchronize threads, some more sophisticated than others. In our simple example, we'll just wait for the second thread to terminate. (We're assuming here that the user will either unload the second assembly, or close it directly, before trying to close the first application.) So add this code to the Dispose method of the main application (that is, the new application that we're building to launch the original one):

            if (excelThread != null) 
            { 
                excelThread.Join(); 
            }
    
  6. In the Unload button handler, close the second application's window, and then unload the second AppDomain altogether:

        private void btnUnload_Click(object sender, System.EventArgs e) 
        { 
            if (appForm != null) 
            { 
                appForm.Close(); 
                appForm = null; 
                AppDomain.Unload(excelDomain); 
                excelDomain = null; 
            } 
            btnNewApp.Enabled = true; 
            btnUnload.Enabled = false; 
        }
    
  7. Build and test. Use Task Manager to check when the Excel process stops.

Best Practices   To summarize this section, we have three basic choices for cleaning up COM objects:

  • .NET garbage collection   This is controlled in a deterministic manner by using GC.Collect and GC.WaitForPendingFinalizers (twice).
  • ReleaseComObject   This is highly deterministic but somewhat problematic to use, and it is potentially dangerous because it affects the target RCW for the entire AppDomain.
  • AppDomain isolation   This means isolating our interop functionality in a separate AppDomain, which we can later unload altogether, thereby releasing all RCWs in that AppDomain.

We can also use either of the first two options in combination with the third. The general best practice guideline is to use .NET garbage collection wherever possible, to use AppDomain isolation in situations where you can (or must) cleanly isolate your interop code (such as in add-ins), and to avoid ReleaseComObject altogether if possible.

Read Chapter 2, Part 1

Read Chapter 2, Part 3