46

In my company the common way to release Excel Interop Objects is to use IDisposable the following way:

Public Sub Dispose() Implements IDisposable.Dispose
    If Not bolDisposed Then
        Finalize()
        System.GC.SuppressFinalize(Me)
    End If
End Sub

Protected Overrides Sub Finalize()
    _xlApp = Nothing
    bolDisposed = True
    MyBase.Finalize()
End Sub

where _xlApp was created in the constructor the following way:

Try
    _xlApp = CType(GetObject(, "Excel.Application"), Excel.Application)
Catch e As Exception
    _xlApp = CType(CreateObject("Excel.Application"), Excel.Application) 
End Try

And the client uses the using-statement to execute code concerning excel interop objects.

We completely avoid to use the two dot rule. Now I started researching how to release (Excel) Interop Objects and almost all discussions I found about it like How to properly clean up excel interop objects or Release Excel Objects are using mostly Marshal.ReleaseComObject(), none of them using the IDisposable interface.

My questions is: Are there any disadvantages using the IDisposable interace for releasing excel interop objects? If so, what are these disadvantages.

ruedi
  • 5,365
  • 15
  • 52
  • 88

2 Answers2

144

Are there any disadvantages using the IDisposable Interace

Sure, it accomplishes absolutely nothing. Using Using or calling Dispose() is never an appropriate way to set a variable to Nothing. Which is all that your code does.

We completely avoid to use the two dot rule.

Feel free to continue to ignore it, it is nonsense and causes nothing but grief. The blog author's implied assertion is that doing so would force the programmer to use a variable to store the value of xlApp.Workbooks. So he'd have a fighting chance, later, to not forget to call releaseObject(). But there are many more statements that produce an interface reference that don't use dots. Something like Range(x,y), there's a hidden Range object reference there that you'll never see. Having to store them as well just produces incredibly convoluted code.

And overlooking just one is enough to completely fail to get the job done. Utterly impossible to debug. This is the kind of code that C programmers have to write. And often failed at miserably, large C programs often leak memory and their programmers spend a great deal of time finding those leaks. Not the .NET way of course, it has a garbage collector to do this automatically. It never gets it wrong.

Trouble is, it is a bit slow at taking care of the job. Very much by design. Nobody ever notices this, except in this kind of code. You can see that the garbage collector didn't run, you still see the Office program running. It didn't quit when you wrote xlapp.Quit(), it is still present in the Processes tab of Task Manager. What they want to happen is for it to quit when they say so.

That's very possible in .NET, you can certainly force the GC to get the job done:

GC.Collect()
GC.WaitForPendingFinalizers()

Boom, every Excel object reference gets released automatically. There is no need to store these object references yourself and explicitly call Marshal.ReleaseComObject(), the CLR does it for you. And it never gets it wrong, it doesn't use or need a "two dot rule" and it has no trouble finding those hidden interface references back.


What matters a great deal however is exactly where you put this code. And most programmers put it in the wrong place, in the same method that used those Excel interfaces. Which is fine, but does not work when you debug the code, a quirk that's explained in this answer. The proper way to do it in the blog author's code is to move the code into a little helper method, let's call it DoExcelThing(). Like this:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    DoExcelThing()
    GC.Collect()
    GC.WaitForPendingFinalizers()
    '' Excel.exe no longer running anymore at this point
End Sub

And do keep in mind that this is truly all just a debugging artifact. Programmers just hate to have to use Task Manager to kill the zombie Excel.exe instances. Zombified when they stopped the debugger, preventing the program from exiting normally and collect garbage. This is normal. It will also happen when your program dies in production for any kind of reason. Put your energy where it belongs, getting the bugs out of your code so your program won't die. The GC doesn't need more help than that.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 25
    Amen! Please keep fighting the whole two-dot, ReleaseComObject voodoo crap that has infected the internet (and +1 for pointing to the debug quirk too). – Govert Aug 21 '14 at 22:50
  • 3
    Thank you for clarifying that the no-two-dot, Marshal.ReleaseComObject style is not necessary to do Excel (or I think most other COM) Interop and still release references. I have been hearing so much about it, I was afraid that I would have to rewrite large portions of my codebase . . . – Mike Oct 24 '14 at 19:21
  • 3
    The whole [RCW design](http://blogs.msdn.com/b/yvesdolc/archive/2004/07/21/190691.aspx) is already a [dead horse](https://en.wikipedia.org/wiki/Flogging_a_dead_horse). The future is going to be an automatically generated C++/CLI wrapper assembly (enhanced interop assembly) that will release correctly whether you `Dispose` it or leave it to the GC. The misdesign is what causes every C# office automation unstable, cause everyone to runaway from COM, and eventually cause everyone to runaway from the Office suite itself. Too late, already happened. – rwong Jul 15 '15 at 22:36
  • 4
    On my first read of this post, I missed the part about WHERE I should put the `GC.Collect()` command. After putting it in the correct place it worked perfectly. – Cornelius Jul 22 '15 at 21:37
  • 1
    I'm a tad confused by all this...I never have any zombie instances of Excel lying around. My boss cam in to show me, when I told him I was writing out to Excel, and they weren't there, checked both in processes and in applications inside of the Task Manager. My question is...is the GC needing additional help to cleanup COM objects this still an issue, as of VS2015 and .NET 4.6? – jpcguy89 Oct 06 '15 at 19:46
  • 1
    Thanks, that's a very insightful answer. My only improvement suggestion would be to put the GC statements into a `Finally` block, to cover the case where `DoExcelThing` exits by exception. – Heinzi Aug 28 '19 at 12:55
  • This is an amazing and very helpful answer. I would, however, like to clarify whether it is acceptable to include the ``DoExcelThing()`` under a ``try`` without the "little helper method" when the ``GC.Collect()`` and ``GC.WaitForPendingFinalizers()`` are in a ``finally``? – gktscrk May 20 '20 at 06:35
  • This answer lives forever it seems. One thing that wasn't mentioned in your answer was the COM threading requirements and message pumping. I see many people trying to use office COM instances from a background thread that has no message pump. This can cause office apps to not shutdown properly. – MikeJ Nov 18 '20 at 20:47
  • 1
    I've been struggling with these Excel.exe leftovers (even came up with the idea of process termination), but the cure was simple - another location! Lots of kudos! :) – JohnyL Aug 07 '21 at 20:44
  • 1
    @gktscrk Your suggested method seems to work well. So instead of a `DoExcelStuff()` helper, everything is wrapped in a try/catch/finally block. For example ```lang-c# public static DataTable GetDataTableFromExcel(string path, string sheetName) { DataTable tbl = new DataTable(); try { // Do things in Excel and extract data to tbl } catch (Exception e) { // Handle exceptions here however you want } finally { GC.Collect(); GC.WaitForPendingFinalizers(); } return tbl; } ``` – Nixxen Aug 10 '23 at 08:39
  • 1
    @Nixxen: Yep, that's the way I've kept on using this methodology as well. It seems to work well in normal circumstances, but in debug if you kill the program while Excel is open, the instance is left open (as the code did not pass through finally). – gktscrk Aug 23 '23 at 07:44
1

If you're looking for a cleaner way, you can use Koogra. It's not much extra overhead (just two DLL's you have to include in your references) and you don't have to deal with implicit garbage collection.

The code below is all that's needed to read 10 rows of 10 columns from an excel file with no EXCEL32 or Excel.exe processes left behind. I had this issue a month ago and wrote instructions on how to do it. So much easier and cleaner than dealing with Excel Interop directly.

Koogra.IWorkbook workbook = Koogra.WorkbookFactory.GetExcel2007Reader("MyExcelFile.xlsx");
Net.SourceForge.Koogra.IWorksheet worksheet = workbook.Worksheets.GetWorksheetByName("Sheet1");

//This will invididually print out to the Console the columns A-J (10 columns) for rows 1-10.
for (uint rowIndex = 1; rowIndex <= 10; rowIndex++)
{
    for (uint columnIndex = 1; columnIndex <= 10; columnIndex++)
    {
        Console.WriteLine(worksheet.Rows.GetRow(rowIndex).GetCell(columnIndex).GetFormattedValue());
    }
}
Community
  • 1
  • 1
Antoine Dahan
  • 574
  • 2
  • 9
  • 23