VB.NET Releasing Excel file after reading so it can be overwritten - vb.net

I am having the same issue that many before me have had, and I have found several threads on the issue but my application of their fixes have not yielded any change.
I am reading an excel file and populating a DataGridView with it. Very simply the user can modify it and then save it. The problem, like the other threads, is that the program has such a tight grip on the file I can't overwrite it.
Among the many fixes offered, Garbage Collection was the most frequently mentioned, but my results are unchanged. I have tried disposing of the OLEDB connection, commands, adapters and datasets also without success.
I've been running through several different examples and tutorials to write this program and so if these fixes are required, I've obviously implemented them in the wrong spot.
Here is the reading block which happens on the form load (I've removed all my attempts to fix it to unclutter the code):
Private Sub Inventory_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Using MyConnection = New System.Data.OleDb.OleDbConnection("provider=Microsoft.Jet.OLEDB.4.0;Data Source='c:\AllTrade\Inventory.xlsx';Extended Properties=Excel 8.0;")
Using MyCommand = New System.Data.OleDb.OleDbDataAdapter("select * from [InventorySheet$]", MyConnection)
DtSet = New System.Data.DataSet
MyCommand.Fill(DtSet)
InventoryGridView.DataSource = DtSet.Tables(0)
MyConnection.Close()
End Using
End Using
End Sub
Here is the saving block (This is the code that generates the "File in Use" error):
Private Sub UpdateInventory_Click(sender As Object, e As EventArgs) Handles UpdateInventory.Click
Dim ExcelApp As New Excel.Application()
ExcelApp.Application.Workbooks.Add(Type.Missing)
For i As Integer = 0 To InventoryGridView.Rows.Count - 1
Dim row As DataGridViewRow = InventoryGridView.Rows(i)
For j As Integer = 0 To row.Cells.Count - 1
ExcelApp.Cells(i + 1, j + 1) = row.Cells(j).Value
Next
Next
ExcelApp.ActiveWorkbook.SaveAs("C:\Alltrade\Inventory.xlsx")
ExcelApp.ActiveWorkbook.Saved = True
ExcelApp.Quit()
End Sub
What's the trick to let go of the file? If the GC.Collect() and GC.WaitForPendingFinalizers() are required, where would they go?
I appreciate any help and apologize since I haven't been able to successfully implement the other answers from similar threads.

You need to marshal to release the COM object: ExcelApp...
Private Sub ReleaseObject(ByVal obj As Object)
Try
Dim intRel As Integer = 0
Do
intRel = System.Runtime.InteropServices.Marshal.ReleaseComObject(obj)
Loop While intRel > 0
Catch ex As Exception
MsgBox("Error releasing object" & ex.ToString)
obj = Nothing
Finally
GC.Collect()
End Try
End Sub
You can call this method like this at the end of your sub.
ReleaseObject(ExcelApp)
You can get more here Excel application not quitting after calling quit
EDIT
I also noticed your connection string is wrong for the newer .xlxs file extention...
Normal ConnectionString : (work for xls files)
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Extended Properties=\"Excel 8.0;HDR=YES;\""
Office 2007 ConnectionString : (work for xlsx files)
"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties=\"Excel 12.0;HDR=YES;\""

After much research and frustration, I found a solution.
The connection string of my OleDB connection needs the tidbit added: OLE DB Services = -4 which disables connection pooling. As it happens, the connection is not disposed of (which was the initial hypothesis) when the connection is closed, but instead dumps the connection back into the pool.
Another forum had suggested OLE DB Services = -2 which was supposed to do the same thing, but had no effect in my instance. It's possible it varies between versions but I haven't researched this to conclude that for certain (or someone typoed in one of the forums)
No further code was required in my program to dump the OleDB connection's hold on the file. MyConnection = Nothing and the line that drains the pool followed by a GC.Collect were also recommended but did not resolve my symptoms.
Thanks everyone for the input

Related

Why is my invoke adding 10 seconds onto grid loading time?

I've attempted to add a method invoker to stop my error log being spammed with "Bounds cannot be changed while locked."
This has solved my issue, however...It has added an extra 10 seconds onto the loading time of my RadGridView.
I looked at https://www.telerik.com/forums/bounds-cannot-be-changed-while-locked to setup my invoker but there isn't much else that I can see to help with my issue.
I've attached a sample of my code below, any help would be appreciated.
Private Sub bgw_initialLoad_DoWork(sender As Object, e As DoWorkEventArgs)
Try
liveDS = New DataSet
Dim dsholder As DataSet = GetDataFromSQL("LoadData")
Dim dt1 As DataTable = dsholder.Tables(0)
Dim dt_1 As DataTable = dt1.Copy()
dt_1.TableName = "Customer"
liveDS.Tables.Add(dt_1)
Dim dt2 As DataTable = dsholder.Tables(1)
Dim dt_2 As DataTable = dt2.Copy()
dt_2.TableName = "Orders"
liveDS.Tables.Add(dt_2)
Dim dt3 As DataTable = dsholder.Tables(2)
Dim dt_3 As DataTable = dt3.Copy()
dt_3.TableName = "OrderLine"
liveDS.Tables.Add(dt_3)
If RadGridView.InvokeRequired Then
RadGridView.Invoke(New MethodInvoker(AddressOf SetupDataSources))
Else
SetupDataSources()
End If
Catch ex As Exception
sendCaughtError(ex)
End Try
End Sub
Private Sub SetupDataSources()
If liveDS.Tables.Count > 1 Then
RadGridView.DataSource = liveDS.Tables("Customer")
liveOrdersTemplate.DataSource = liveDS.Tables("Orders")
liveOrdersTemplate2.DataSource = liveDS.Tables("OrderLine")
End If
End Sub
Private Sub bgw_initialLoad_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs)
Try
RadGridView.DataSource = liveDS.Tables("Customer")
Dim template As New GridViewTemplate()
template.DataSource = liveDS.Tables("Orders")
RadGridView.MasterTemplate.Templates.Add(template)
Dim template2 As New GridViewTemplate()
template2.DataSource = liveDS.Tables("OrderLine")
RadGridView.Templates(0).Templates.Add(template2)
Dim relation As New GridViewRelation(RadGridView.MasterTemplate)
relation.ChildTemplate = template
relation.ParentColumnNames.Add("Invoice Customer")
relation.ChildColumnNames.Add("InvoiceCode")
RadGridView.Relations.Add(relation)
Dim relation2 As New GridViewRelation(RadGridView.Templates(0))
relation2.ChildTemplate = template2
relation2.ParentColumnNames.Add("OrderNo")
relation2.ChildColumnNames.Add("OrderNo")
RadGridView.Relations.Add(relation2)
FormatGrid()
SplitContainer2.Panel1.Enabled = True
SplitContainer1.Panel2.Enabled = True
refreshMainGrid()
HideLoadingGif()
Catch ex As Exception
sendCaughtError(ex)
End Try
End Sub
Debugging threads can be hard, trust me. This isn't a "real" answer, but a bunch of tips which may help - which is what I hope will happen.
There are dedicated windows in the debug menu which may help. I started with this webpage when I was wondering what was happening to my application and why it wasn't obvious why it was happening.
Also, while your parallel thread is running, it may "silent crash" if your IDE isn't set to pause on every crash, in which case it won't return a value but will just stay silent. Make sure at least these options are set:
And don't forget to show this window while debugging: (previous image showed Threads and Call stack instead, while they are good to have around while debugging it's the parallel stacks which I was going for)
One last thing: such a big delay may be database related. I'm not saying that it is, but you should be aware of the possibility.
Now the following isn't part of the answer per se, but is more of a friendly advice: put your invoke logic in SetupDataSources() instead, this way wherever it's called you'll be thread safe. Like this:
Private Sub SetupDataSources()
If RadGridView.InvokeRequired Then
RadGridView.Invoke(Sub() SetupDataSources())
End If
If liveDS.Tables.Count > 1 Then
RadGridView.DataSource = liveDS.Tables("Customer")
liveOrdersTemplate.DataSource = liveDS.Tables("Orders")
liveOrdersTemplate2.DataSource = liveDS.Tables("OrderLine")
End If
End Sub
Best of luck... you might need some ;)

ADODB Connection Timeout in VBA for Excel 2016 - how to check if a connection is still active?

I have developed a small Excel addin using VBA which connects directly to a database. I set up the connection via a DSN. The addin works wonderfully when opening it and going right at it. However, after a while the connection to the database seems to timeout. More precisely, a perfectly valid query returns an error when trying to open the recordset.
My code is something like this:
'Check Connection
If Not MakeConnectionToDB Then
'Connection failed
[do something]
Exit Function
End If
'Active connection - run the query!
If Rs Is Nothing Then Set Rs = New ADODB.Recordset 'make sure its an active object
If Not Rs.State = adStateClosed Then Rs.Close 'make sure its not full (throws error if a query was called before)
Rs.Open strSQLQuery, CON 'Run query
the rs.open statement fails if the application was open but not used for a while. This is despite the MakeConnectionToDB UDF, which looks something like this:
If Not ConIsActive Then 'If there is no active connection, make it so
If CON Is Nothing Then 'Connection may be inactive because the object dropped, or because it timed out, or any other reason - Only recreate the object if the former is the case
Set CON = New ADODB.Connection
End If
On Error Resume Next
CON.Open strCon 'Try to connect - on error resume statement in order to ignore a connection error, that will be caught below
On Error GoTo 0
Err.Clear
MakeConnectionToDB = ConIsActive 'This is where a connection error will be caught if it occurred
Else
MakeConnectionToDB = True 'connection is active already
End If
and ConIsActive looks like:
Private Function ConIsActive() As Boolean
'return TRUE if there is an active connection, false otherwise
Dim blnTemp As Boolean
blnTemp = False
If (Not (CON Is Nothing)) And (Not (CON = "")) Then If CON.State = adStateOpen Then blnTemp = True
ConIsActive = blnTemp
End Function
Basically, I check if the connection is open. My problem: All these checks return TRUE, but the connection isn't open at all. If I connect, then leave the application for a while, then get back to it, all the above will return that the connection is active, but when trying to open the recordset with a new query it will fail, presumably because the server closed the connection or something. I need to find a way to check if the connection is actually able to open a recordset.
Can I ping the server or something? How can I check if the database actually returns a result to my queries? Is there a way that has a higher performance than just sending a test query to the server combined with error handling on the recordset? I suppose that would work, but I need a high performance solution and I don't think doubling the number of queries for a simple connection check is a superior solution...
Any help is appreciated!
Your CON object seems to be globally-scoped, opened once, and then used everywhere in your code, and possibly closed at some point... or not.
Like every single object in any code base written in any language that supports objects, a database connection should be as short-lived as possible.
You open it, you do what you need to do with it, and then you close it. If you don't know what the next command is going to be executed against it and when, then the connection has no business remaining open.
Delete your global-scope CON. Kill it, with fire. A connection should be local to the function or procedure that uses it - it begins in that scope, and ends in that scope.
Or you can encapsulate it in your own object, if that makes things easier for you.
'#Folder("Data.SqlConnection")
Option Explicit
Private Const CONNECTION_STRING As String = "{CONNECTION STRING}"
Private mConnection As ADODB.Connection
Private Sub Class_Initialize()
Set mConnection = New ADODB.Connection
mConnection.Open
End Sub
Private Sub Class_Terminate()
mConnection.Close
Set mConnection = Nothing
End Sub
Public Sub ExecuteNonQuery(ByVal sql As String, ParamArray params())
With New ADODB.Command
Set .ActiveConnection = mConnection
Dim p As ADODB.Parameter
For Each p In params
.Paramaters.Append p
Next
.Execute
End With
End Sub
'...
An instance of that SqlConnection class should also be as short-lived as possible, but now most of the plumbing is abstracted away so your calling code can look like this:
Const sql As String = "exec dbo.LogUserIn #userName=?, #password=?;"
With New SqlConnection
Dim userName As ADODB.Parameter
Set userName = .CreateStringParameter(Environ$("USERNAME"))
Dim password As ADODB.Parameter
Set password = .CreateStringParameter(PromptForPassword)
.ExecuteNonQuery sql, userName, password
End With
The connection begins at New SqlConnection, cleanly ends at End With, and you can tweak that SqlClass as you need, to support transactions, and/or as illustrated above, to abstract away the parameter-creating boilerplate.
But the idea remains: you don't create a database connection and leave it dangling in global scope, not knowing whether some code somewhere might have set it to Nothing, or closed it, or started a transaction that was never committed, or God knows what.
Create
Open
Execute
Close
Always. As tightly-scoped as possible. Then you won't have any object lifetime issues.

InvalidCastException on select XP machines

I am having a problem with a tool that I wrote in VB.NET running on certain XP machines. The tool runs fine and as expected on Windows 7 and XP machines that have .NET 4.0.30319.1022.
I've noticed the XP machines that I am having trouble on are those that did not have .NET 4 even on it, so I went and installed .NET 4 which puts on version .NET 4.0.30319.1.
Here is the error:
Here is the code at line 86 (86 is the For Each):
'_BIOSVer
Dim bios_query As String = "SELECT * FROM " & "Win32_BIOS"
Dim bios_searcher As New ManagementObjectSearcher(bios_query)
For Each info As ManagementObject In bios_searcher.Get()
_BIOSVer = info.Properties("BIOSVersion").Value.ToString()
Next info
I am running Windows Updates for .NET 4 Framework to see if that will fix the issue, but I was wondering if you guys might have a sense of what is happening, or if my code is just wrong.
Updates just finished, restarted. It is running off of .NET 4.0.30319.1022 but still throws that error. Must be my code then.
There are several problems, but mainly BIOSVersion is a string array. Rather than poking into that, you can just get the Name which returns the version info (so does Caption and Description). Since you are after one thing, you dont need a loop, but I left it in as a template:
Dim myver As String = ""
Using searcher As New ManagementObjectSearcher("Select Name From Win32_Bios")
For Each mo As ManagementObject In searcher .Get
' EXAMPLE
'DebugProperties(mo)
' no real need to Loop - just for illustration
For Each pd As System.Management.PropertyData In mo.Properties
' if you do SELECT * and want to find something,
' compare Pd.Name to the target property
' some things can be nothing. Such as when a property does not
' exist on older systems, so always test:
If pd.Value IsNot Nothing Then
myver = pd.Value.ToString
Exit For
End If
Next
Next
End Using
Return myver
This is a helpful WMI debug tool to display all the property names and values (the code above has the call to this commented out):
Private Shared Sub DebugProperties(mo As Management.ManagementObject)
For Each pd As PropertyData In mo.Properties
If pd.Value IsNot Nothing Then
Console.WriteLine("{0} {1}", pd.Name,
If(pd.Value IsNot Nothing,
pd.Value.ToString,
"Nothing"))
End If
Next
End Sub
BIOS Version returns a string array (for more information check this link).
Try reading the data like this
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim BIOSSearcher As New ManagementObjectSearcher("SELECT BIOSVersion FROM Win32_BIOS")
Dim BIOSVersionInfo() As String = BIOSSearcher.Get(0).Properties("BIOSVersion").Value
Dim BIOSVersion As String = ""
If Not BIOSVersionInfo Is Nothing Then
BIOSVersion = String.Join(vbCrLf, BIOSVersionInfo)
End If
MsgBox(BIOSVersion)
End Sub

Kill Excel on Error

I am hoping you can help me here, in the past you all have been great. I have tried every variation of the kill script for killing excel from vb.net, to no avail.
First I can't post explicit code on here because it is my company's proprietary software, but I can tell you a few things. Also there are over 28,000 lines of code.
I am not using
Imports Excel = Microsoft.Office.Interop.Excel
due to the fact that we have to accommodate different variations of clients software. I am creating the new excel as an object as such
Dim XLObj As Object = CreateObject("Excel.Application")
I have seen this used on several other sites but the kill function they are using is when you save and then close it, which I'm not doing.
The error message I am getting says that "Com object that has been separated from its underlying RCW cannot be used". I'm not sure where this com object is because I have released the sheets, workbook and then the application.
Oh and I don't want to use the excel.kill() because if a client already has the excel open I don't want to kill it without saving it. I only want to kill the newly generated excel process that doesn't have a window open associated with it.
My questions are as follows
I need to be able to close the Excel application when/if the open fails. So say I am click a link and it opens the dialog box to select an Excel template to load but either the data from the database is corrupt or the sql statement is broken. The program throws and error and then Excel should close in the Task Manager. Unfortunately it doesn't close hence the problem.
is there a way to close only the newly created process id? I have tried to use the directions here but it doesn't work either. When I do that it gives me a different error "Value cannot be null Parameter name: o". The line that is throwing the error is on (from the link)
Marshal.FinalReleaseComObject(tempVar)
I only tried this because we are using the With on the XLObj. The With is in reference to the workbook itself so shouldn't it be released when I close the workbook? And being as I'm causing it to error on purpose at the moment it shouldn't reach the With statement anyway.
Is there a way to tell which com object is not closing?
Things I have tried:
This releaseObject that I found on the internet. (don't ask me where I've been through about 75 pages)
Private Sub releaseObject(ByRef obj As Object)
Try
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(obj)
If obj Is Nothing Then
Else
obj = Nothing
End If
Catch ex As Exception
If obj Is Nothing Then
Else
obj = Nothing
End If
Finally
GC.Collect()
GC.WaitForPendingFinalizers()
End Try
End Sub
This is used in conjunction with this function (which was pieced together from the many sites I have been on)
Public Sub CloseExcel(ByRef WorkBook As Object, ByRef Application As Object)
Dim xLSheet As Object = WorkBook.Sheets
For Each xLSheet In WorkBook.Sheets
If xLSheet IsNot Nothing Then
releaseObject(xLSheet)
End If
If xLSheet IsNot Nothing Then
Kill(xLSheet)
End If
Next
If WorkBook IsNot Nothing Then
WorkBook.Close(False)
End If
If WorkBook IsNot Nothing Then
Kill(WorkBook)
End If
releaseObject(WorkBook)
If Application IsNot Nothing Then
Application.Quit()
End If
If Application IsNot Nothing Then
Kill(Application)
End If
releaseObject(Application)
GC.Collect()
GC.WaitForPendingFinalizers()
Application.Quit()
End Sub
and because it is also referenced the Kill function
Public Sub Kill(ByRef obj As Object)
Try
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(obj)
Catch ex As Exception
MessageBox.Show("moduleExcel.Kill " & ex.Message)
Finally
obj = Nothing
End Try
End Sub
any help would be greatly appreciated.
Ok so for those of you having this exact same issue. I do have a solution for you. Yes the above code does work but for a few minor adjustments.
you need to take out all the code in the CloseExcel sub and place it EXACTLY where you want it to close. So if you want it to close if the program errors out, put after the catch statement. You cannot call a Sub and pass in your objects and expect it to kill the process.
you need a few bits above the opening of the new Excel process. and they are as follows.
'declare process for excel
Dim XLProc As Process
'loads the financials excel bookmarks
'this will be where you declare your new excel opbject
Dim XLObj As Object = CreateObject("Excel.Application")
'get window handle
Dim xlHWND As Integer = XLObj.hwnd
Dim ProcIDXL As Integer = 0
'get the process ID
GetWindowThreadProcessId(xlHWND, ProcIDXL)
XLProc = Process.GetProcessById(ProcIDXL)
and of course you will need the GetWindowThreadProcessId which I got from the link I included in the original question. I am posting it here so you don't have to search for it.
<System.Runtime.InteropServices.DllImport("user32.dll", SetLastError:=True)> _
Private Function GetWindowThreadProcessId(ByVal hWnd As IntPtr, ByRef lpdwProcessId As Integer) As Integer
End Function
This code will only close the single process you have it associated with, it will not close other open Excel files. Our clients sometimes will have multiple files open and we don't want to close them without telling them. This KILLS the Excel process that was created at run time when the system Errors out.

Unable to diligently close the excel process running in memory

I have developed a VB.Net code for retrieving data from excel file .I load this data in one form and update it back in excel after making necessary modifications in data. This complete flow works fine but most of the times I have observed that even if I close the form; the already loaded excel process does not get closed properly.
I tried all possible ways to close it but could not be able to resolve the issue. Find below code which I am using for connecting to excel and let me know if any other approach I may need to follow to resolve this issue.
Note: I do not want to kill the excel process as it will kill other instances of the excel
Dim connectionString As String
connectionString = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=" & ExcelFilePath & ";
Extended Properties=excel 8.0; Persist Security Info=False"
excelSheetConnection = New ADODB.Connection
If excelSheetConnection.State = 1 Then excelSheetConnection.Close()
excelSheetConnection.Open(connectionString)
objRsExcelSheet = New ADODB.Recordset
If objRsExcelSheet.State = 1 Then objRsExcelSheet.Close()
Try
If TestID = "" Then
objRsExcelSheet.Open("Select * from [" & ActiveSheet & "$]", excelSheetConnection, 1, 1)
Else
objRsExcelSheet.Open("Select Test_ID,Test_Description,Expected_Result,Type,UI_Element,Action,Data,Risk from [" & ActiveSheet & "$] WHERE TEST_Id LIKE '" & TestID & ".%'", excelSheetConnection, 1, 1)
End If
getExcelData = objRsExcelSheet
Catch errObj As System.Runtime.InteropServices.COMException
MsgBox(errObj.Message, , errObj.Source)
Return Nothing
End Try
excelSheetConnection = Nothing
objRsExcelSheet = Nothing
You are using the old VB6 COM interfaces. There isn't any evidence in your code snippet of you ever calling Close() on the RecordSet. You can't call Close() on the Connection since you set it to Nothing.
As written, Excel won't exit until the garbage collector runs and the finalizer thread releases the reference counts on the COM interfaces. Which can take a long time if your program doesn't exit or goes dormant after processing the data. You really should consider uplifting this code to use the classes in System.Data.Oledb so you can properly Dispose() everything when you're done with the objects.
The Q&D solution is to force the finalizer thread to run:
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect()
Run this after you're done using the RecordSet. It isn't otherwise recommended.