VB.Net - Background Excel process REOPENS after closing file - excel

I have been searching all over for this problem, but I can't seem to find one that quite matches my issue.
I am creating a new excel file, filling it with data, and showing it to user. While monitoring the task manager i can see that once the file has been created and open for the user, the background process disappears as it is supposed to. (The main process is still running up top because the file is open.)
The problem I have is that once the user closes the file, Excel background process pops back up in list and won't go away until program (that generated the file) is closed.
This is the clean up that I am using;
Dim xlObject As New Excel.Application
Dim xlBook As Excel.Workbook = Nothing
Dim xlSheet As Excel.Worksheet = Nothing
xlBook = xlObject.Workbooks.Add
xlSheet = xlBook.Worksheets(1)
'Fill data and do some formatting
xlBook.SaveAs("FileName")
xlObject.Visible = True
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet)
xlSheet = Nothing
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook)
xlBook = Nothing
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlObject)
xlObject = Nothing
Is there something that I am missing? Why is the background process continuing and only go away once the creating program is closed?
This is using Visual Studios 2013,
Office 365 Excel,
and Windows 10 Pro

You see, in Interop, you should avoid accessing an object through another object such as
xlBook = xlObject.Workbooks.Add
This is called the two dot rule. The reasoning is that the Workbooks object is then referenced in .NET and orphaned (and should also be killed as you have the other three)
So make a reference to the Workbooks, then kill that as well.
Dim xlApp As New Excel.Application()
Dim xlBooks = xlApp.Workbooks
Dim xlBook = xlBooks.Add
Dim xlSheet = DirectCast(xlBook.Worksheets(1), Excel.Worksheet)
Try
xlBook.SaveAs("FileName.xlsx")
xlApp.Visible = True
Finally
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet)
xlSheet = Nothing
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook)
xlBook = Nothing
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBooks)
xlBooks = Nothing
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp)
xlApp = Nothing
End Try
And, I always struggle with this, because according to the preeminent expert in vb.net on StackOverflow, HansPassant, the two dot rule is just superstition, as he said "Feel free to continue to ignore [the two dot rule], it is nonsense and causes nothing but grief" but in the case of your question, I can only make it work by following the two dot rule, and NOT by using Hans' solution:
GC.Collect()
GC.WaitForPendingFinalizers()
Perhaps Hans will see this and set us straight!

Related

Save changes in an Excel spreadsheet that were made using MS Access VBA

In my database, I have a VBA script that opens an Excel spreadsheet and removes the first row so the new first row is the header row.
When I save the file in Access VBA and open the file in Excel, nothing is there.
If I reimport the spreadsheet, the data is visible.
I changed my script around and if I don't save the changes the file is ok. If I save the changes then this problem appears.
dim sheetpath as string
dim xl as excel.application
dim xlbook as excel.workbook
dim xlsheet as excel.worksheet
sheetpath = "c:\users\me\export.xlsx"
set xl = createobject("Excel.Application")
set xlbook = GetObject(sheetpath)
xl.visible = true
set xlsheet = xlbook.Worksheets(1)
If xlsheet.Range("a1").mergecells = true then
xlsheet.cells.unmerge
end if
if xlsheet.range("a1") = "Values" then
xlsheet.rows(1).delete
end if
xlbook.close savechanges:=true
xl.application.qit
set xl = nothing
set xlbook = nothing
set xlsheet = nothing
Try a reboot.
But before you do that, check in Task Manager / Process Explorer, if you have multiple Excel processes still running.
Instead of
set xl = createobject("Excel.Application")
set xlbook = GetObject(sheetpath)
do
Set xlbook = GetObject(sheetpath)
Set xl = xlbook.Application
What's this?
xl.application.qit
It should be
xl.Quit
And clear your references in the correct order, from bottom to top:
set xlsheet = nothing
set xlbook = nothing
set xl = nothing

Disable excel save changes prompt

I have tried all the ways I normally do this and what I can find searching. oxl.DisplayAlerts = False is not working. I still get asked if I want to save changes.
I am essentially trying to use the excel sheet as a template. The full script exports to pdf, but this is enough to re-create the problem. BTW I tried saving it as a xltx file (template) and still get the save promt.
Dim oxl As New Excel.Application
Dim apppath2 As String = My.Application.Info.DirectoryPath.ToString
Dim mywb As Excel.Workbook = oxl.Workbooks.Open(Filename:=apppath2 & "\fuse template.xlsx", [ReadOnly]:=True)
oxl.Visible = False
Dim mysheet As Excel.Worksheet = mywb.Sheets(1)
mysheet.Cells(10, 5) = l_region.Text
mysheet.Cells(11, 5) = comb_emc_name.Text
oxl.DisplayAlerts = False
mywb.Close(False)
mysheet = Nothing
mywb = Nothing
oxl = Nothing
GC.Collect()
I was missing mywb.Saved = True. I have never had to do that before.

Excel process running after closing VB.NET [duplicate]

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
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.
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.
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).
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.
'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
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
It's just as simple as adding this line in your code, just after opening the Workbook:
oExcel.displayalerts = False
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.
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

Access VBA: working with an existing excel workbook (Run-Time error 9, if file is already open)

I'm writing a macro in Access that (hopefully) will:
create an Excel worksheet
set up and format it based on information in the Access database
after user input, will feed entered data into an existing Excel master file
Opening the blank sheet etc. is working absolutely fine, but I'm stuck trying to set the existing master file up as a variable:
Sub XLData_EnterSurvey()
Dim appXL As Excel.Application
Dim wbXLnew, wbXLcore As Excel.Workbook
Dim wsXL As Excel.Worksheet
Dim wbXLname As String
Set appXL = CreateObject("Excel.Application")
appXL.Visible = True
wbXLname = "G:\[*full reference to file*].xlsm"
IsWBOpen = fnIsWBOpen(wbXLname)
'separate function (Boolean), using 'attempt to open file and lock it' method
'from Microsoft site.
If IsWBOpen = False Then
Set wbXLcore = appXL.Workbooks.Open(wbXLname, True, False)
'open file and set as variable.
ElseIf IsWBOpen = True Then
wbXLcore = appXL.Workbooks("ResultsOverall.xlsm") 'ERROR HERE.
'file is already open, so just set as variable.
End If
Debug.Print wbXLcore.Name
Debug.Print IsWBOpen
Set appXL = Nothing
End Sub
When the file is closed, this works perfectly. However, when it's open I get:
Run-Time error '9':
Subscript out of range
I'm only just starting to teach myself VBA (very trial and error!) and nothing else I've seen in answers here / Google quite seems to fit the problem, so I'm a bit lost...
Considering that it works fine when the file is closed, I suspect I've just made some silly error in referring to the file - perhaps something to do with the 'createobject' bit and different excel instances??
Any suggestions would be much appreciated! Thanks
Thank you #StevenWalker
Here's the working code:
Sub XLData_EnterSurvey()
Dim appXL As Excel.Application
Dim wbXLnew As Excel.Workbook, wbXLcore As Excel.Workbook
Dim wsXL As Excel.Worksheet
On Error GoTo Handler
Set appXL = GetObject(, "Excel.Application")
appXL.Visible = True
Dim wbXLname As String
wbXLname = "G:\ [...] .xlsm"
IsWBOpen = fnIsWBOpen(wbXLname)
If IsWBOpen = False Then
Set wbXLcore = appXL.Workbooks.Open(wbXLname, True, False)
ElseIf IsWBOpen = True Then
Set wbXLcore = appXL.Workbooks("ResultsOverall.xlsm")
End If
Set appXL = Nothing
'-------------------Error handling------------------
Exit Sub
' For if excel is not yet open.
Handler:
Set appXL = CreateObject("Excel.Application")
Err.Clear
Resume Next
End Sub
Sorry I'm on my phone so I can't go in to too much detail or do much with the code but at a glance I think you might need to add an error handler so that if the file is already open, a different line of code is executed.
Add 'On error go to handler' (before creating the excel object) and at the bottom
Of your code add 'handler:'. In the error handler, use get object rather than create object.
You will have to ensure you use exit sub before the error handler or it will run the handler every time you run the code.
You can see an example of what I mean here: How to insert chart or graph into body of Outlook mail
Although please note in this example it's the other way round (if error 'getting' outlook, then create it).
Example in link:
Set myOutlook = GetObject(, "Outlook.Application")
Set myMessage = myOutlook.CreateItem(olMailItem)
rest of code here
Exit Sub
'If Outlook is not open, open it
Handler:
Set myOutlook = CreateObject("Outlook.Application")
Err.Clear
Resume Next
End sub
If you move the appXL.Workbooks statement to the debugging window, you will find that the names of the items in that collection are without extension.
So in your case, I'm guessing the line should read:
wbXLcore = appXL.Workbooks("ResultsOverall")

How to Properly pull data from Excel into Powerpoint

I have a powerpoint presentation with a slide that has several labels on it. I need the value of the labels to be pulled from an excel-sheet that is located in the same folder as the powerpoint presentation file.
The code I use to populate the labels so far is this:
Public Sub OnSlideShowPageChange(ByVal Wn As SlideShowWindow)
If Wn.View.CurrentShowPosition = 1 Then
'load variable values?
Dim xlApp As Object
Dim xlWorkBook As Object
Set xlApp = CreateObject("Excel.Application")
xlApp.Visible = False
Set xlWorkBook = xlApp.Workbooks.Open(ActivePresentation.Path & "\QSheet.xlsx", True, False)
BrownRetail = xlWorkBook.Sheets(1).Range("B3").Value
lblBrownTruckRetail.Caption = "$" & CCur(BrownRetail)
Set xlApp = Nothing
Set xlWorkBook = Nothing
End If
End Sub
This works, except it is quite slow loading (10-15 seconds to start the presentation) and the labels show with a white background instead of being transparent. Also, it seems odd to me that the label values stay persistent even in design view.
Is there some better way to pull a value to populate a label from an excel-sheet?

Resources