Memory leak Directory.GetFiles() VB.NET - vb.net

I have a launcher utility I wrote that uses Directory.GetFiles() on a Timer to keep track of shortcuts in the start menu.
It has a memory leak, however. I'm not doing anything strange, so I don't understand why it's leaking... I leave the program open and after a few days, it's at 300mb. I used the CLR Profiler to try to locate the leak and it says the memory leakage is coming from String instances allocated by Directory.GetFiles and Directory.GetFileNameWithoutExtension Here's the code I'm using:
Private Sub tmr_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles tmr.Tick
IndexStartMenu()
GC.Collect()
End Sub
Private Sub IndexStartMenu()
Dim startMenu As IO.DirectoryInfo
Dim shortcuts() As IO.FileInfo
'Enumerate current user's start menu
startMenu = New IO.DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu))
shortcuts = startMenu.GetFiles("*.lnk", IO.SearchOption.AllDirectories)
For Each lnk As IO.FileInfo In shortcuts
Dim newRow As DataRow = dtApps.NewRow
newRow("Application") = IO.Path.GetFileNameWithoutExtension(lnk.FullName)
newRow("Window") = "Launch"
newRow("Hwnd") = ""
newRow("IsShortcut") = True
newRow("ShortcutPath") = lnk.FullName
dtApps.LoadDataRow(newRow.ItemArray, LoadOption.Upsert)
newRow = Nothing
Next
'Enumerate all users' start menu
startMenu = New IO.DirectoryInfo(allUsersStartMenuPath)
shortcuts = startMenu.GetFiles("*.lnk", IO.SearchOption.AllDirectories)
For Each lnk As IO.FileInfo In shortcuts
Dim newRow As DataRow = dtApps.NewRow
newRow("Application") = IO.Path.GetFileNameWithoutExtension(lnk.FullName)
newRow("Window") = "Launch"
newRow("Hwnd") = ""
newRow("IsShortcut") = True
newRow("ShortcutPath") = lnk.FullName
dtApps.LoadDataRow(newRow.ItemArray, LoadOption.Upsert)
newRow = Nothing
Next
'Trying to fix memory usage
startMenu = Nothing
Array.Clear(shortcuts, 0, shortcuts.Length)
shortcuts = Nothing
End Sub

Based on the method you posted, wouldn't the timer just fire every interval and add the contents of those directories repeatedly? If dtApps is a DataTable field scoped to the class which persists for the duration of the application, you are just repeatedly adding the rows to the DataTable causing it to grow. It is not a memory leak, but a natural event. Check the row count of your dtApps. My guess is that you are intending to only add new rows.
Also, you could improve the solution above and eliminate the need to poll the two directories based on a timer by employing a FileSystemWatcher. The FileSystemWatcher will notify you by firing an event when there is a change to the file system.

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 ;)

Secondary thread causes "Application has stopped working" crashes even when invoking

I have an application which has a form with a DataGridView bound to a BindingSource, which is bound to a DataTable:
bsList.DataSource = dsData
bsList.DataMember = "List"
dgvList.DataSource = bsList
The underlying data which populates dsData.Tables("List") can change whilst the user is working so to combat this I have a background thread which routinely checks the database for changes and updates dsData.Tables("List"). It also changes the colour of any row where another user is currently working.
However, users report that when this background updating functionality is enabled the application routinely CTDs with no application error message. I have been unable to reproduce this and my attempt to log the crashes via writing to a log file in Private Sub MyApplication_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs) Handles Me.UnhandledException hasn't worked as the log file is never written to, suggesting this event is never triggered.
The thread is instantiated like this:
LiveUpdating = New Thread(AddressOf UpdateUserLocation) With {.IsBackground = True}
LiveUpdating.Start()
This is the UpdateUserLocation sub:
Public Sub UpdateUserLocation()
Do While My.Settings.customBackgroundUpdating = True And formLoaded = True
UserLocations.Clear()
dtUsers = CLS_USERS.GetUsersSequence(winUser)
dtProgress = DAC.GetProgress()
For Each CandRow As DataRow In dsHHData.Tables("List").Rows
Dim CandReadDate As Date
Dim CandRowNextRead As String = DBNull.Value.ToString
If Not (CandRow("NEXT READ").ToString = DBNull.Value.ToString) Then
If Date.TryParse(CandRow("NEXT READ").ToString, CandReadDate) Then
CandRowNextRead = CandReadDate.ToString("dd/MM/yyyy")
End If
End If
Dim CandRowSending As String = TryCast(CandRow("SENDING"), String)
Dim CandRowNotes As String = TryCast(CandRow("NOTES"), String)
For Each NewRow As DataRow In dtUsers.Rows
If CandRow("SQ").ToString = NewRow("SQ").ToString Then
UserLocations.Add(NewRow("SQ").ToString)
End If
Next
For Each ProgressRow As DataRow In dtProgress.Rows
If CandRow("SQ").ToString = ProgressRow("SQ").ToString Then
Dim NextReadDate As Date
Dim ProgressRowNextRead As String = DBNull.Value.ToString
If Not (ProgressRow("NEXT READ").ToString = DBNull.Value.ToString) Then
If Date.TryParse(ProgressRow("NEXT READ").ToString, NextReadDate) Then
ProgressRowNextRead = NextReadDate.ToString("dd/MM/yyyy")
End If
End If
Dim ProgressRowSending As String = TryCast(ProgressRow("SENDING"), String)
Dim ProgressRowNotes As String = TryCast(ProgressRow("NOTES"), String)
If CandRow("SQ").ToString = ProgressRow("SQ").ToString Then
If CandRowSending <> ProgressRowSending Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableSending), CandRow, ProgressRowSending)
End If
If CandRowNextRead <> ProgressRowNextRead Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableNextRead), CandRow, ProgressRowNextRead)
End If
If CandRowNotes <> ProgressRowNotes Then
BeginInvoke(New UpdateDataTableDelegate(AddressOf UpdateDataTableNotes), CandRow, ProgressRowNotes)
End If
End If
End If
Next
Next
dgv.BeginInvoke(
New MethodInvoker(
Sub()
For Each dgv_row As DataGridViewRow In dgv.Rows
If UserLocations.Contains(dgv_row.Cells("SQ").Value.ToString) Then
dgv.DefaultCellStyle.BackColor = My.Settings.customRowHighlight
Else
dgv.DefaultCellStyle.BackColor = Nothing
End If
Next
End Sub))
Thread.Sleep(My.Settings.customRefreshRate * 1000)
Loop
End Sub
The subs that do the DataTable update are like this:
Private Delegate Sub UpdateDataTableDelegate(ByVal CandRow As DataRow, ByVal ProgressRow As String)
Private Sub UpdateDataTableSending(ByVal CandRow As DataRow, ByVal ProgressRowSending As String)
CandRow("SENDING") = ProgressRowSending
End Sub
I know this is not the best way to handle a multi-user environment but the nature of this work requires that all people can access and see the same data. I could force them to refresh regularly but that seems very intrusive.
The crashes only occur when this thread is running and the crashes are regular (and not instant) but I cannot seem to reproduce them and the application is very stable otherwise.
There must be some cross-threading issue but I can't work how when all of the updates to the DataTable or DataGridView are done via a BeginInvoke on the main UI thread.
EDIT: I've just realised that even though I am doing the queries and most of the heavy lifting in the background thread, the updates are stilled called on the main UI thread which would lock the thread. This would be particularly noticeable if there were a lot of updates... Because each one is called individually.
If the UI lock up was long enough, and the user was clicking on stuff, would this cause Windows to treat the application as unresponsive and crash it? If so, is there a better way I could handle these updates?
Any help with resolving this would be enormously appreciated.

Using Filewatcher for a progress bar with subdirectories, Wont Update Properly

I'm trying to copy Files from a local Computer to a Network device. I'm trying to get a Progress Bar working for the File copy, and got it working for a Single Directory with no Subdirectory:
Private Sub CopyPictures()
Try
If Not Directory.Exists(DestinationPath) Then
My.Computer.FileSystem.CreateDirectory(DestinationPath)
End If
Dim counterLocalFiles = My.Computer.FileSystem.GetFiles(SourcePath)
UpdateProgressBarMaximum1(CInt(counterLocalFiles.Count))
UpdateLabelText2(CStr(counterLocalFiles.Count)) 'is a label which shows copied X files of Label2 Files
fsw1 = New IO.FileSystemWatcher(DestinationPath)
fsw1.EnableRaisingEvents = True
My.Computer.FileSystem.CopyDirectory(SourcePath, DestinationPath)
GetSettingsFromFile()
Catch Exec As System.IO.IOException
Dim dr As DialogResult = MessageBox.Show("Some Random Error Code", "Exception Title", MessageBoxButtons.OKCancel)
If (Not DialogResult.OK = dr) Then
Exit Sub
Return
End If
End Try
End Sub
Private Sub fsw1_Created(sender As Object, e As FileSystemEventArgs) Handles fsw1.Created
Dim counterRemoteFiles = My.Computer.FileSystem.GetFiles(DestinationPath)
UpdateProgressBar1(CInt(counterRemoteFiles.Count))
UpdateLabelText1(CStr(counterRemoteFiles.Count))
End Sub
The Update ObjectX Subs are just invoke Functions since the CopyPictures is raised by a backgroundworker as well looking all like this one for example
Private Sub UpdateProgressBar1(Value As Int32)
If ProgressBar1.InvokeRequired Then
ProgressBar1.Invoke(New Action(Of Integer)(AddressOf UpdateProgressBar1), Value)
Else
'We are on the UI thread so update the control.
ProgressBar1.Value = Value
End If
End Sub
This code works perfectly fine for me, but I have to deal with SubDirectories which contain the Images, and the names of the subs are random so i cant predetermine them so I came up with slight changes:
The Counter is looking now like this:
Dim counterLocalFiles = System.IO.Directory.GetFiles(SourcePath, "*.jpg*", SearchOption.AllDirectories).Length
UpdateProgressBarMaximum1(CInt(counterLocalFiles))
UpdateLabelText2(CStr(counterLocalFiles))
And this:
Dim counterRemoteFiles = IO.Directory.GetFiles(DestinationPath, "*.jpg", SearchOption.AllDirectories).Length
UpdateProgressBar1(CInt(counterRemoteFiles))
UpdateLabelText1(CStr(counterRemoteFiles))
And I added:
fsw1.IncludeSubdirectories = True
Now the weired Problems started: It would properly count the file in the source Directory setting label2 to the correct amount of files in all subdirectories and then start copying. It would NOT update the Progressbar though in real time. It just updated it once when it was done with the first directory and just adding the amount of files to it which it contained. After that it completly stoppedd nored the second directory and didn't add that at all to the progressbar. What am I doing wrong here? I hope my english is fine, If you have any question or If I was not clear enough, please let me know. Thank you
You don't have an event consumer that triggers your progressbar update routine - you call it once when your filesystemwatcher is instantiated.
You need to declare an event that handles the copy event and fires off your progress update code. Because Filesystemwatcher cannot monitor network drives, you may want to declare an event that fires off your progress update method when the counterRemoteFiles count increments.
Turns out I just made a mistake with correctly putting the
fsw1.IncludeSubdirectories = True
I was setting it to true in the Form Editor instead of doing it in the code. Once i actually put that in the code after initialising the fsw, it would work just fine

Unsure on proper use of Serial Port Data Received Event

I'm working on a VSTO add-in for Excel 2013 in VB.NET that will help me interface with an instrument via a serial connection. I currently have the COM connection set up correctly and it will allow me to send and receive one command at a time. I'd like to set it up so that I can push one button and have it collect two separate readings in different worksheet cells. Using the code below, the tools work great to collect a single reading, but when I enable the code to send a second command to the instrument the Data Received event stops working entirely until I send another single read command. I know that the instrument received and processed the second command, but it never appears in excel. Could anyone help with a way to modify this code?
Private Sub mySerialPort_DataReceived(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs)
'Handles serial port data received events
UpdateFormDeligate1 = New UpdateFormDeligate(AddressOf UpdateDisplay)
Dim n As Integer = mySerialPort.BytesToRead 'find number of bytes in buff
comBuffer = New Byte(n - 1) {} 're-dimension storage buffer (n - 1)
mySerialPort.Read(comBuffer, 0, n) 'read data from the buffer
comBuffer2 = mySerialPort.ReadTo(vbCr)
Me.Invoke(UpdateFormDeligate1) 'call the deligate
mySerialPort.Close()
End Sub
Private Sub Invoke(updateFormDeligate1 As UpdateFormDeligate)
lblReading.Label = processReading() 'write to a Current Reading lable on the ribbon
Dim myApp As Excel.Application = Globals.ThisAddIn.Application
Dim currentCell = myApp.ActiveCell
currentCell.Value = processReading() 'write data in the excel active cell
Try
advanceCell()
Catch ex As Exception
System.Windows.Forms.MessageBox.Show(ex.Message)
End Try
If measureNo = 2 Then 'this case is selected when I want to read 2 measurements with a single button push
cmdSent = 2
sendCommand(measureCmd)
End If
End Sub
Private Sub UpdateDisplay()
End Sub
Note that I did not include my sendCommand sub because this is a simple .write command to the instrument that appears to be working correctly in all cases. I'd much appreciate any help anyone could provide as I'm pretty new to using data received events.
OK, I tried to isolate only the relevant the part of the script that was having an issue and I created a completely new toolbar for testing. Below is the full code for this new toolbar that contains one connect/measure button and a label that displays the status/result. I tried to comment the code to make it readable, hopefully this helps.
This new toolbar does appear to be working correctly. I'm still a little unsure on my correct usage of the DataReceived event handler in conjunction with the Invoke method (which Visual Studio slightly changed for use with Excel2013). Could anyone please provide comment as to whether I'm still using these events in an unclear way and provide a suggestion on how I may make it better?
Thanks again in advance for any help. I really appreciate it.
Imports Microsoft.Office.Tools.Ribbon
Imports System.IO.Ports
Public Class Measure2x_COM
Dim mySerialPort As New SerialPort
Dim CMD As String = "M" & vbCr 'statement telling instrument to measure
Dim measureNo As Integer = 0 'counts the number of measure commands sent to the instrument
Private Delegate Sub UpdateFormDeligate()
Private UpdateFormDeligate1 As UpdateFormDeligate
Dim sngReading As Single 'this is the reading received from the instrument as a single data type
Private Sub setupConnectCOM()
'Open COM and send measure command - this part works correctly
'first, check if serial port is open
If mySerialPort.IsOpen Then 'send measure command
mySerialPort.Write(CMD) 'the instrument will generally take 15.1 sec to perform a measurement before sending the result back
Else
'if serial port is not open, set it up, then open, then send command
'Setup COM --this part works correctly
With mySerialPort
.PortName = "COM3"
.BaudRate = 1200
.DataBits = 7
.Parity = Parity.None
.StopBits = StopBits.Two
.Handshake = Handshake.None
.ReadTimeout = 16000
End With
Try
mySerialPort.Open()
Catch ex As Exception
System.Windows.Forms.MessageBox.Show(ex.Message)
Exit Sub 'exit sub if the connection fails
End Try
Threading.Thread.Sleep(200) 'wait 0.2 sec for port to open
mySerialPort.Write(CMD) 'send measure command after serial port is open
End If
measureNo = 1
lblResult.Label = "Measuring"
End Sub
Private Sub Measure2x_COM_Load(ByVal sender As System.Object, ByVal e As RibbonUIEventArgs) Handles MyBase.Load
AddHandler mySerialPort.DataReceived, AddressOf mySerialPort_DataReceived
End Sub
Private Sub mySerialPort_DataReceived(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs)
'Handles serial port data received events
UpdateFormDeligate1 = New UpdateFormDeligate(AddressOf UpdateDisplay)
'Read data as it comes back from serial port
'I had to do this in two steps because it, for some reason needs to read
'the +/- symbol as a Byte, then needs to read the ASCII measurement number
'the third part concatenates the data and converts it to a single type
'part 1 - read +/- symbol
Dim comBuffer As Byte()
Dim n As Integer = mySerialPort.BytesToRead 'find number of bytes in buff
comBuffer = New Byte(n - 1) {} 're-dimension storage buffer (n - 1)
mySerialPort.Read(comBuffer, 0, n) 'read data from the buffer
'part 2 - read ASCII measurement number
Dim comBuffer2 As String
comBuffer2 = mySerialPort.ReadTo(vbCr)
'part 3 - concatenate read data and convert to single type
Dim txtReading As String = Nothing
txtReading = System.Text.ASCIIEncoding.ASCII.GetString(comBuffer) & CStr(CInt(comBuffer2) / 10)
sngReading = CSng(txtReading)
'Call the update form deligate
'Visual Studio slightly changed this from the example on Microsoft's website that used a Windows Form
'I tried the code in a windows form and I get the same results
Me.Invoke(UpdateFormDeligate1) 'call the deligate
End Sub
Private Sub Invoke(updateFormDeligate1 As UpdateFormDeligate)
lblResult.Label = sngReading 'set the Result label in the ribbon to equal the received data value
'now place the data received in the active cell in the worksheet
Dim myApp As Excel.Application = Globals.ThisAddIn.Application
Dim currentCell = myApp.ActiveCell
currentCell.Value = sngReading
'advance cell to the next cell
Dim newCell = currentCell
newCell = myApp.ActiveCell.Offset(1, 0)
newCell.Select()
currentCell = newCell
'check if this was the first reading from the instrument
'if it was the first reading, then send a second read command
If measureNo = 1 Then
measureNo = 2 'make sure to change measurement number to 2 to avoid infinite loop
mySerialPort.Write(CMD) 'send command to measure to instrument
End If
End Sub
'the usage of this section changed from the Microsoft Windows Form example
'in function, the mySerialPort_DataREceived(), Invoke(), and UpdateDisplay() functions do appear to be
'working with the same results and same hangups
Private Sub UpdateDisplay()
End Sub
Private Sub btnMeasure_Click(sender As Object, e As RibbonControlEventArgs) Handles btnMeasure.Click
setupConnectCOM() 'connect to COM and send first measure command
End Sub
End Class

Cannot Close Down MS Word From .NET

I have the following snippet of code. It works (opens all the Word documents in a directory and then closes them down)...but it doesn't clean up after itself when I totally exit the program.
By this I mean that if I look at the TaskManager once I exit the VB.NET application I see the WINWORD.EXE even though it did not exist before I opened the application.
Here's the declarations I have:
Dim WordApp As Microsoft.Office.Interop.Word.Application
Dim aDoc As Microsoft.Office.Interop.Word.Document
Dim missing As Object = System.Reflection.Missing.Value
Dim nullobj As Object = System.Reflection.Missing.Value
Dim MYreadOnly As Object = False
Dim isVisible As Object = False
And here's the code:
Private Sub cmdGenerate_Click(sender As System.Object, e As System.EventArgs) Handles cmdGenerateKeywords.Click
Dim xmldoc As New XmlDataDocument()
Dim xmlnode As XmlNodeList
Dim i As Integer
Dim fs As FileStream
WordApp = New Microsoft.Office.Interop.Word.Application
WordApp.Visible = False
For Each f As FileInfo In New DirectoryInfo(txtFolderName.Text).GetFiles("*.docx")
' Open the document that was chosen by the dialog
aDoc = WordApp.Documents.Open(f.FullName, missing, [MYreadOnly], _
missing, missing, missing, missing, missing, missing, missing, _
missing, isVisible)
'aDoc.Close()
aDoc = Nothing
Next
'Close the Word Document
'aDoc.Close(nullobj, nullobj, nullobj)
WordApp.Application.Quit()
WordApp = Nothing
End Sub
As you can tell I've commented and uncommented various statements in regards to closing down Word documents and the Word Application itself. Nothing I have tried seems to be able to get rid of that pesky WINWORD.EXE
Something seems to have a lock and will not let it close down? Is that it?
Run explicitly Garbage Collector, as is shown in this article:
// Clean up the unmanaged Word COM resources by forcing a garbage
// collection as soon as the calling function is off the stack (at
// which point these objects are no longer rooted).
GC.Collect();
GC.WaitForPendingFinalizers();
// GC needs to be called twice in order to get the Finalizers called
// - the first time in, it simply makes a list of what is to be
// finalized, the second time in, it actually is finalizing. Only
// then will the object do its automatic ReleaseComObject.
GC.Collect();
GC.WaitForPendingFinalizers();
Despite link above, my experience is that run once is enough. But second call doesn't throw an error, so do it this way.
fs.Close()
fs.Dispose()
http://msdn.microsoft.com/en-us/library/system.io.filestream.dispose.aspx
This releases your resources. Might allow other close methods to work. I read through the code twice and do not see you using "fs" anywhere like I would expect.