8

I am creating an excel file using interop.excel and the process is not closing. This is the code i am trying to use.

 Private Sub converToExcel(fileLoc As String, ds As DataSet)
    Dim xlApp As Excel.Application
    Dim xlWorkBook As Excel.Workbook
    Dim xlWorkBooks As Excel.Workbooks
    Dim xlWorkSheet As Excel.Worksheet
    Dim misValue As Object = System.Reflection.Missing.Value
    Dim i As Integer
    Dim j As Integer

    xlApp = New Excel.Application
    xlWorkBooks = xlApp.Workbooks
    xlWorkBook = xlWorkBooks.Add(misValue)
    xlWorkSheet = xlWorkBook.Sheets("sheet1")

    For i = 0 To ds.Tables(0).Rows.Count - 1
        For j = 0 To ds.Tables(0).Columns.Count - 1
            xlWorkSheet.Columns.NumberFormat = "@"
            xlWorkSheet.Cells(i + 1, j + 1) = String.Format("{0}", ds.Tables(0).Rows(i).Item(j).ToString())
        Next
    Next

    xlWorkSheet.SaveAs(fileLoc)
    xlWorkBook.Close()
    xlApp.Quit()

    releaseObject(xlWorkSheet)
    releaseObject(xlWorkBook)
    releaseObject(xlWorkBooks)
    releaseObject(xlApp)

End Sub
Private Sub releaseObject(ByVal obj As Object)
    Try
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj)
        obj = Nothing
    Catch ex As Exception
        obj = Nothing
    Finally
        GC.Collect()
    End Try
End Sub

I think i am missing a COM object but cant seem to find a solution. Also as a note, this is running on 64-bit Windows 8. Any help would be great! Thanks

Joe Smith
  • 101
  • 1
  • 2
  • 4
  • And you are sure that this is not an instance left from previous testing? – Fionnuala Jan 08 '13 at 19:47
  • yes, I check Task Manager after each test and remove all instances of excel. – Joe Smith Jan 08 '13 at 19:52
  • I don't think this is the cause of your problem, but `releaseObject` may not do all you think it does. Since you're passing `obj` `ByVal`, the `obj = Nothing` doesn't have any effect on the `xlWorkSheet`, `xlWorkBook` etc. variables. – prprcupofcoffee Jan 08 '13 at 19:58
  • Changed releaseObject to byref instead of byval, same result. – Joe Smith Jan 08 '13 at 20:11
  • Does adding xlWorkbooks.Close remove the process? If not, I would suggest stepping through the code, seeing when processes are being created, and then determining what gets removed towards the end of your Sub. – APrough Jan 08 '13 at 20:12
  • ...and is the final Excel file actually being generated? Is there a possibility that your Sub is erroring and kicking out before it releases references? I plugged your code into an editor, and it did that if I supplied a dataset with no tables. If I supply a dataset with at least one table, then your code ran fine and released all references. – APrough Jan 08 '13 at 20:28
  • The sub isn't erroring out before the release, if I step through it everything executes correctly. Also the sub call is in error handling so it would kick out to that. The only other thing I can think of is that it is running on Windows 8. – Joe Smith Jan 08 '13 at 20:30
  • I know this is a dumb question, but your calling Sub doesn't set references to Excel, correct? – APrough Jan 08 '13 at 20:38
  • The only other time in the application I reference excel it is accessed via an oleDB connection not interop. I did throw the original code into an console app in VS running on windows 7 and it worked correctly. On windows 8 the process stays open. – Joe Smith Jan 08 '13 at 20:44
  • Do they close once the application is exited? – msmucker0527 Jan 08 '13 at 21:23

9 Answers9

19

Manual memory management like this just never works. This is a problem that's been known for very a long time and the core reason that garbage collectors were invented. Programmers just forever forget to release memory.

It gets extra hard when you can't see the memory being used. Which is certainly the case in your code, the xlWorkSheet.Cells(i + 1, j + 1) expression uses no less than three references. One for the range object returned by the Cells property, one for a sub-range object selected by i+1 and another for the sub-range object selected by j+1. Very nice syntax sugar provided by the VB.NET language, writing COM code without it is pretty doggone painful. But not helpful to let you see the references. Not only can't you see it in your source code, there is absolutely nothing the debugger can do to help you see them either.

This is very much a solved problem in .NET, it has a garbage collector and it can see everything. The most basic problem is that you don't give it a chance to solve your problem. The mistake you made is that you stopped. Probably by setting a breakpoint on the last statement and then looking in Task Manager and seeing Excel.exe still running. Yes, that's normal. Garbage collection is not instant.

Calling GC.Collect() is supposed to make it instant, but that doesn't work in the specific case of running the Debug build of your project. The lifetime of local variables gets then extended to the end of the method, help you see them in the Autos/Locals/Watch window. In other words, GC.Collect() doesn't actually collect any of the interface references. More about that behavior in this post.

The simple workaround is to not stop. Keep doing useful things to give the garbage collector a reason to run. Or letting your program terminate since it is done, Excel terminates when the finalizer thread runs for the last time. Which works because the local variables that had the references are not in scope anymore.

But everybody wants the instant fix anyway. You get it by deleting all the releaseObject() calls. And doing it like this instead:

converToExcel(path, dset)
GC.Collect()
GC.WaitForPendingFinalizers()

Or in other words, force a collection after the method has returned. The local variables are no longer in scope so they can't hold on to an Excel reference. It will now also work when you debug it, like it already did when you ran the Release build without a debugger.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • While your solution is generally the way to go it doesn't always work. When you need to be compatible with older Office versions there are many documented cases where you need to do manual reference counting, in particular when writing addins and handling events. Also some COM libraries (from Microsoft, but luckily not Office) don't like their objects being released in the wrong order by GC. – Zarat Jan 09 '13 at 09:04
  • That is pretty widespread claptrap. Common when an arbitrary small change has big side effects, making it all look like black magic. These "many documented cases" of course need a citation. Here is mine: http://blogs.msdn.com/b/visualstudio/archive/2010/03/01/marshal-releasecomobject-considered-dangerous.aspx – Hans Passant Jan 09 '13 at 12:48
  • Sorry, can't find them from quick google searches, but when I researched this topic a few years ago there were plenty user reports of office not closing with managed addins (most common cause was subscribing to events), or even some (very rare) crashes due to GC releasing references at times office couldn't deal with. Probably all fixed in modern versions of office (and .NET 4 helps with the event subscribing), so only relevant if you must be backward compatible. There were also a few Microsoft KB articles about it, but they are really hard to find. – Zarat Jan 09 '13 at 14:58
  • You are right (with your posted link) that ReleaseComObject will cause trouble once office starts implementing their interfaces in .NET, but the link is wrong in that “COM object that has been separated from its underlying RCW cannot be used” is a problem. If you get that then you did your reference counting wrong (didn't increase the RCW refcount when you split ownership of the reference to multiple owners). If you do manual reference counting then you must go all the way and do it correctly. – Zarat Jan 09 '13 at 14:59
  • And don't get me wrong, you are absolutely right that being GC compatible is the future. Should only bother with ReleaseComObject when forced to be compatible with old stuff like Office 2000-2003 and/or .NET 2 - and like I said in my answer I think this is not the case for this question, he wouldn't run into any of the problems *requiring* ReleaseComObject – Zarat Jan 09 '13 at 15:07
1

Try System.Runtime.InteropServices.Marshal.FinalReleaseComObject, that should help... also you should call xlWorkBook.Close() and xlapp.quit, if I recall correctly. First call them and then set them to nothing.

Christian Sauer
  • 10,351
  • 10
  • 53
  • 85
  • I do call xlWorkBook.Close() and xlApp.Quit() before calling releaseObject. Tried using finalRelease, same result. – Joe Smith Jan 08 '13 at 20:11
  • I think you order is wrong with the quit: I think you have to set the seehts to nothing, then quit the workbook then set it to nothing, then quot the app and quit it. But at further Investigatio I have seen tht you are calling gc collect only once - call it twice. If not, the objects are marked for release, but are not released. – Christian Sauer Jan 09 '13 at 08:20
1

The GC.Collect makes not much sense where you placed it, if anything you should call it after you return from converToExcel. Also you may need to wait for finalizers to run. Personally I think Hans' answer is the way to go, but I know from personal experience writing office addins in C# that sometimes its necessary to do manual reference counting, in particular when you need to be compatible with older office versions. (There are many documented problems, in particular when handling events from office, which can only be reliably solved by manual reference counting. Also some COM libraries don't like at all when released in the wrong order by GC, but thats not the case with office.)

So on to the actual problem in your code: there are three intermediate COM objects not released here:

  • xlWorkBook.Sheets returns a collection of type Excel.Sheets
  • xlWorkSheet.Columns returns a COM object of type Excel.Range
  • xlWorkSheet.Cells also returns an Excel.Range object

Besides this, if Marshal.ReleaseComObject throws an exception you did something wrong in your manual reference counting, therefore I wouldn't wrap it in an exception handler. When doing manual reference counting you must release every COM object once for every time it crosses the COM->NET boundary, meaning the Excel.Range objects need to be released in every iteration of the loop.

Here's code which properly terminates Excel for me:

Imports Microsoft.Office.Interop
Imports System.Runtime.InteropServices

Private Sub converToExcel(fileLoc As String, ds As DataSet)
    Dim xlApp As New Excel.Application
    Dim xlWorkBooks As Excel.Workbooks = xlApp.Workbooks
    Dim xlWorkBook As Excel.Workbook = xlWorkBooks.Add(System.Reflection.Missing.Value)
    Dim xlWorkSheets As Excel.Sheets = xlWorkBook.Sheets
    ' accessing the sheet by index because name is localized and your code will fail in non-english office versions
    Dim xlWorkSheet As Excel.Worksheet = xlWorkSheets(1)

    For i = 0 To ds.Tables(0).Rows.Count - 1
        For j = 0 To ds.Tables(0).Columns.Count - 1
            ' couldn't this be moved outside the loop?
            Dim xlColumns As Excel.Range = xlWorkSheet.Columns
            xlColumns.NumberFormat = "@"
            Marshal.ReleaseComObject(xlColumns)

            Dim xlCells As Excel.Range = xlWorkSheet.Cells
            xlCells(i + 1, j + 1) = ds.Tables(0).Rows(i).Item(j).ToString()
            Marshal.ReleaseComObject(xlCells)
        Next
    Next

    xlWorkSheet.SaveAs(fileLoc)
    'xlWorkBook.Close() -- not really necessary
    xlApp.Quit()

    Marshal.ReleaseComObject(xlWorkSheet)
    Marshal.ReleaseComObject(xlWorkSheets)
    Marshal.ReleaseComObject(xlWorkBook)
    Marshal.ReleaseComObject(xlWorkBooks)
    Marshal.ReleaseComObject(xlApp)
End Sub

If you want to be extra careful you'd want to handle exceptions from the office API and call ReleaseComObject inside finally-clauses. It can be helpful to define a generic wrapper and write using-clauses instead of try-finally (make the wrapper a structure not a class so you don't allocate the wrappers on the heap).

Zarat
  • 2,584
  • 22
  • 40
1

Finally solved :)

Private Function useSomeExcel(ByVal Excelfilename As String) 
  Dim objExcel As Excel.Application
  Dim objWorkBook As Excel.Workbook
  Dim objWorkSheets As Excel.Worksheet

  Dim datestart As Date = Date.Now
  objExcel = CreateObject("Excel.Application") 'This opens...  
  objWorkBook = objExcel.Workbooks.Open(Excelfilename) ' ... excel process
  Dim dateEnd As Date = Date.Now
  End_Excel_App(datestart, dateEnd) ' This closes excel proces
End Function

use this method

  Private Sub End_Excel_App(datestart As Date, dateEnd As Date)
    Dim xlp() As Process = Process.GetProcessesByName("EXCEL")
    For Each Process As Process In xlp
     If Process.StartTime >= datestart And Process.StartTime <= dateEnd Then
       Process.Kill()
       Exit For
     End If
    Next
  End Sub

This method closes especific process opened.

Xtian11
  • 2,130
  • 1
  • 21
  • 13
1
'Get the PID from the wHnd and kill the process.
' open the spreadsheet
ImportFileName = OpenFileDialog1.FileName
excel = New Microsoft.Office.Interop.Excel.ApplicationClass
wBook = excel.Workbooks.Open(ImportFileName)
hWnd = excel.Hwnd
Dim id As Integer = GetWindowThreadProcessId(hWnd, ExcelPID)

Sub CloseExcelFile()
        Try
            ' first try this
            wBook.Saved = True
            wBook.Close()
            excel.Quit()

            ' then this.
            System.Runtime.InteropServices.Marshal.ReleaseComObject(excel)
            excel = Nothing

            ' This appears to be the only way to close excel!
            Dim oProcess As Process
            oProcess = Process.GetProcessById(ExcelPID)
            If oProcess IsNot Nothing Then
                oProcess.Kill()
            End If

        Catch ex As Exception
            excel = Nothing
        Finally
            GC.Collect()
        End Try
    End Sub
Paul Roub
  • 36,322
  • 27
  • 84
  • 93
Gretchen
  • 11
  • 1
  • 1
    Welcome to Stack Overflow! While this code may answer the question, it would be better to include some _context_, explaining _how_ it works and _when_ to use it. Code-only answers are not useful in the long run. – Benjamin W. May 09 '16 at 18:59
0

I did not see anyone properly address what was occuring and instead, tried to create work arounds for it.

What is happening here is that the workbook is prompting, in the background, to be saved. In your code, you're saving the worksheet and not the workbook. You can either trick it and set the saved state of the workbook to true or save the workbook before exiting the excel applicaiton.

I was also having this issue. The Excel process would run the entire time the application was open. By adding the xlWorkBook.Saved = True line, the process would end after the call to xlApp.Quit(). In my case, I did not need to save the excel file, only reference it for values.

Option #1 - Do not save the workbook:

xlWorkSheet.SaveAs(fileLoc)
xlWorkBook.Saved = True       ' Add this line here.
'xlWorkBook.Close()           ' This line shouldn't be necessary anymore.
xlApp.Quit()

Option #2 - Save the workbook to a new file:

'xlWorkSheet.SaveAs(fileLoc)  ' Not needed
xlWorkBook.SaveAs(fileLoc)    ' If the file doesn't exist
'xlWorkBook.Close()           ' This line shouldn't be necessary anymore.
xlApp.Quit()

Option #3 - Save the workbook to an existing file:

'xlWorkSheet.SaveAs(fileLoc)  ' Not needed
xlWorkBook.Save(fileLoc)      ' If the file does exist
'xlWorkBook.Close()           ' This line shouldn't be necessary anymore.
xlApp.Quit()

.Quit() Method:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.office.interop.excel._application.quit?view=excel-pia#Microsoft_Office_Interop_Excel__Application_Quit

.Saved() Method:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.office.interop.excel._workbook.saved?view=excel-pia#Microsoft_Office_Interop_Excel__Workbook_Saved

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
TinMan464
  • 56
  • 5
0

It's just as simple as adding this line in your code, just after opening the Workbook:

oExcel.displayalerts = False

char
  • 2,063
  • 3
  • 15
  • 26
0

Although many of the answers here did indeed result in EXCEL.EXE being closed, they either resulted in a prompt from AutoRecover upon manually opening the workbook or required redundant usage of Garbage Collection.

For my purposes, I'm creating workbooks with ClosedXML (because it's significantly faster for what I need) and using Excel Interop to set the workbook password (for reasons I won't get into here); however, Excel Interop doesn't actually close EXCEL.EXE for me when Application.Quit() is called, even after the method has returned and the objects are long out of scope.

Hans' answer beautifully explained how to get Excel to actually quit without forcefully killing the process. Using that explanation, I created a small class to help avoid redundant Garbage Collection calls as I have multiple areas of code that require this functionality.

Option Strict On
Option Explicit On

Imports Microsoft.Office.Interop.Excel

Public MustInherit Class ExcelInteropFunction
    Public Shared Sub SetWorkbookPassword(WorkbookPathAndName As String, NewPassword As String)
        SetPassword(WorkbookPathAndName, NewPassword)
        GC.Collect()
        GC.WaitForPendingFinalizers()
    End Sub

    Private Shared Sub SetPassword(WorkbookPathAndName As String, NewPassword As String)
        Dim xlWorkbook As Workbook = New Application() With {
            .Visible = False,
            .ScreenUpdating = False,
            .DisplayAlerts = False
        }.Workbooks.OpenXML(WorkbookPathAndName)

        xlWorkbook.Password = NewPassword
        xlWorkbook.Save()
        xlWorkbook.Application.Quit()
    End Sub
End Class

The reason the actual work is being done in a private method is to allow the method that the Excel Interop variables are in to return and go fully out of scope before garbage collection is manually triggered. Without it structured this way, I'd have to add GC calls in several areas of code, so this helps avoid some redundancy and ensures I don't forget a GC call after setting a workbook password.

Joshua Barker
  • 113
  • 1
  • 8
-2
Dim xlp() As Process = Process.GetProcessesByName("EXCEL")

 For Each Process As Process In xlp
   Process.Kill()
     If Process.GetProcessesByName("EXCEL").Count = 0 Then
       Exit For
     End If
 Next
Shivachandra
  • 93
  • 2
  • 8
  • 1
    Killing processes is rarely the right solution, it doesn't allow the target to do proper cleanup. For the posted question there exist clean solutions, so you should never have to fall back to killing excel. – Zarat Nov 20 '14 at 13:24
  • Sometimes it doesn't clean the Excel process and while generating excel repeatedly,it creates more then 1 excel process which supposed to slow up the process as well as the system.This was the problem which i was using and this solution worked fine in my case :) – Shivachandra Nov 21 '14 at 04:25
  • 4
    It might have worked in your own case, but it is not helpful to suggest it as a general purpose solution without explaining the consequences it has. For example it would kill *any* Excel process, including ones which the user opened. Most people don't want to install a program just to find out it'll kill their Excel instances just because the programmer couldn't think of a clean way to interact with Excel. – Zarat Nov 21 '14 at 08:25
  • Thanks @Zarat for the valuable information,Actually we never thought of such scenario,so now we need to change these line again.Thanks – Shivachandra Nov 26 '14 at 05:17
  • error `Count is not a member of System.Array` – john k Sep 30 '21 at 17:25