6

When using COM Interop with Office (usually Excel), I always carefully ensure I call Marshal.ReleaseComObject on every reference, to avoid the problem where Excel doesn't quit as described in this KB article.

How can I ensure Excel quits when I use Interop from an OOB Silverlight application (with AutomationFactory.CreateObject)?

Silverlight doesn't have a Marshal.ReleaseComObject method and even calling GC.Collect and GC.WaitForPendingFinalizers doesn't help.

Surely Microsoft hasn't added this feature to Silverlight without a mechanism to release COM references? This seems to me to be a showstopper for automating out-of-process COM servers such as Excel.

A surprising ommission, all the more so as Pete Brown in section 5.5 of his book "Silverlight 4 in Action" goes as far as to say about AutomationFactory.CreateObject, that:

The primary intent of this feature is to allow automation of other applications, including Microsoft Office.

UPDATE in response to Hans' comments.

I'm not convinced the "silent assassin" problem exists in typical automation of Office apps. A common use might look something like the following, which I've seen used repeatedly in WinForms applications without ever coming across the "poisoned RCW" described in the article linked by Hans:

  • Create an Excel.Application instance
  • Open or create a workbook
  • Write data to the workbook
  • Show Excel if all went well, close the workbook and call Application.Quit if not.
  • Call Marshal.ReleaseComObject to release all Excel object references.

Failing to call Marshal.ReleaseComObject as recommended by Hans will leave multiple copies of Excel.exe running, as described in the KB article mentioned above - highly undesirable.

UPDATE 2

The sample I'm using to repro this is a sample from the source code for Pete Brown's book Silverlight 4 in action, there's a download link on this page. The sample solution AutomatingExcel is in Ch05.zip / 5.03. To repro:

  • Make sure no instances of Excel are running
  • Run AutomatingExcel sample
  • An Excel workbook is opened
  • Close Excel
  • Observe with Task Manager that Excel is still running.

Setting all the dynamic variables to null and calling GC.Collect() seems to work as pointed out in AnthonyWJones's answer.

UPDATE 2

Otaku's answer is what I was looking for - by wrapping references in a using statement the COM references are released without the need to call GC.Collect. A bit of experimentation shows that it's more tolerant of failing to dispose every single reference, unlike the standard Marshal.ReleaseComObject solution described in the KB article referenced above.

It would be interesting to have an authoritative take on exactly what must be disposed to ensure that all Excel references are released.

Joe
  • 122,218
  • 32
  • 205
  • 338
  • possible duplicate of [When to use RelaseComObject vs FinalReleaseComObject?](http://stackoverflow.com/questions/3937181/when-to-use-relasecomobject-vs-finalreleasecomobject) – Hans Passant Apr 09 '11 at 13:28
  • @Hans: Silverlight has neither of these methods on the `Marshal` class. – AnthonyWJones Apr 09 '11 at 13:57
  • @Anthony - it explains why neither of these methods is a good idea. The "silent assassin" link is very relevant. – Hans Passant Apr 09 '11 at 14:01
  • @Hans - "it explains why neither of these methods is a good idea" - can you propose an alternative? See update to question. Also this is not a duplicate of the question you linked. – Joe Apr 09 '11 at 15:03
  • Find the real problem, why is the finalizer thread not running anymore? The usual reason is the "deer in the headlight" reason, you make your program stop doing anything interesting after finishing debugging the interop code. – Hans Passant Apr 09 '11 at 15:40
  • @Hans, what makes you think the finalizer thread "isn't running"? I'm not talking about a specific application, I'm trying to understand generally how I will have to address this issue when migrating current working code to Silverlight. – Joe Apr 09 '11 at 16:11
  • Because the finalizer thread releases the RCWs and makes Excel quit. – Hans Passant Apr 09 '11 at 16:16
  • @Hans "Because the finalizer thread releases the RCWs" - but surely at an unpredictable time? Hence the problem described in the KB article. – Joe Apr 09 '11 at 17:26
  • Why does it have to be predictable? It is implicit in the design, it decides by itself and doesn't care much that you stopped using Excel. You already know how to make it predictable. – Hans Passant Apr 09 '11 at 17:58

4 Answers4

2

You could implement the IDisposable interface. The best example of this I've seen is at http://csfun.blog49.fc2.com/blog-entry-79.html. The blog entry is in Japanese, but open in Chrome and let Google do the page translation for you if you don't read Japanese.

Also, if you just want the source code example of the COM wrapper directly you can download the sample app it comes in: SilverOffice.

Todd Main
  • 28,951
  • 11
  • 82
  • 146
  • +1 thanks - this is the answer I was looking for - how to release the COM objects without calling GC.Collect. It appears that the references created with the dynamic keyword implement IDisposable, and that this releases the COM references. A pity MSDN doesn't provide more explicit info on this topic: http://msdn.microsoft.com/en-us/library/dd264733.aspx – Joe May 13 '11 at 19:52
  • Thats very interesting I hadn't thought of testing for and using `IDisposable`, unfortunately disposing doesn't release the COM object. – AnthonyWJones May 13 '11 at 20:15
  • @Joe: I just tested that by adding Dispose to my code. Without the GC.Collect I still end up with multiple instances of Excel. – AnthonyWJones May 13 '11 at 20:17
  • @AnthonyWJones: Have you tried the referenced sample project, SilverOffice? The code has an `InstanceManager` class that ensures only one instance of Excel is being used (and disposed). – Todd Main May 13 '11 at 20:39
  • No I haven't I was just trying to see if IDisposable used raw solved the problem. Certainly the use of an Instance manager along with plenty of other wrapper work makes a great deal of sense for extensive office work. However the underlying problem appears to remain; the COM object only seems to be actually released when finalised regardless of whether Dispose is called. – AnthonyWJones May 13 '11 at 20:51
1

Take look at this code:-

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        dynamic app = AutomationFactory.CreateObject("Excel.Application");
        dynamic book = app.Workbooks.Add();
        dynamic sheet = app.ActiveSheet();

        sheet = null;
        book.Close(false);
        book = null;
        app.Quit();
        app = null;

        GC.Collect();
    }

The Excel process appears and then disappears. Remove the GC and the Excel process will continue. Do you get the same if you copy this code verbatim? If so then it would suggest that somewhere in your code a reference to an excel object remains reachable from one of the thread stacks or static fields.

Do you ever hold an excel object in a field (as opposed to a local variable)?

Do you hold an excel object in what appears to be variable but is referenced from a dynamic delegate or lambda that is used as an event handler?

Are you attaching event handlers to long-lived objects from an object that has a short lifespan? If so are you ensuring you detach from those handlers properly?

Many of these things can catch devs out into leaving what they think are objects ready for GC but the GC finds them accessible and therefore not candidates for collection.

If the code above doesn't behave the same then we a looking for another issue entirely. I'm using Office 2007 on Server 2008 R2 from the latest SL 4 runtime. However if we are having a varition because of setup then we're on very shakey ground.

Edit

With some testing this appears to be effective:-

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        using (dynamic app = AutomationFactory.CreateObject("Excel.Application"))
        {
            using (dynamic book = app.Workbooks.Add())
            {
                using (dynamic sheet = app.ActiveSheet())
                {

                }
                book.Close();
            }
            app.Quit();
        };

        GC.Collect();
    }

However leave off the GC and you will end up with unwanted Excel processes left running eventually.

AnthonyWJones
  • 187,081
  • 35
  • 232
  • 306
  • Looks promising. My target environment is Excel 2003, but I've just tried your code on Excel 2007 at home and it works. Setting the variables to null seems to be required, not sure why as they should be unreachable anyway. Nevertheless GC.Collect() does seem like heavy artillery just to release COM references. – Joe May 13 '11 at 16:16
  • @Joe: Not necessarily, if you remove the `x= null` from my code then at the time the GC.Collect is called you could see the Excel process continues despite the call to `GC.Collect`. This is because when Collect is called all threads are suspended and the GC will inspect their stacks where it may find the references held by the local variables. I use the word "may" because optimisations could make them unreachable. If you move most to the code into another function and call it from the Click event then call GC.Collect after the assigning to null would not be needed. – AnthonyWJones May 13 '11 at 17:01
  • +1 and thanks for the continued investigation. Your code potentially has an implicit instantiation (the "double-dot" problem). Have you tried "using(dynamic workbooks = app.Workbooks)" followed by "workbooks.Add()" to ensure all references are explicitly instantiated and disposed. Carefully doing this is necessary with standard COM Interop, and failing to do so may explain why you get processes running eventually. – Joe May 14 '11 at 08:08
  • @Joe: Nice one! Yes ensuring the object returned by `Workbooks` is also disposed does solve the problem. No GC.Collect is necessary. – AnthonyWJones May 14 '11 at 11:07
0

by wrapping references in a using statement the COM references are released

Please note: using statements are just syntactic sugar for try/catch/finally with Dispose() in the finally clause.

Also, most applications don't allow for using statements in this case, because creation and cleanup of COM objects are distributed among various places/methods.

The critical line which is needed here reads:

((IDisposable)_excel).Dispose();  // Release COM Interface

This assumes:

dynamic _excel = AutomationFactory.CreateObject("Excel.Application");
gnat
  • 6,213
  • 108
  • 53
  • 73
0

I would consider building the Excel file in a WCF service. You can do all the clean up there. Send to bytes to your Silverlight app and use a SaveFileDialog to send them to the user.

Silverlight has a restriction on access to the client file system. Manipulating an Excel file on the client's system, or even assuming the client has Excel installed seems like a violation of this.

cadrell0
  • 17,109
  • 5
  • 51
  • 69
  • while your approach is valid, it doesn't answer the question. And there may be legitimate reasons for wanting to automate Excel on the client side in elevated trust OOB apps. – Joe May 13 '11 at 08:36