Auto-restart and then continue the Sub in VB.NET - vb.net

Well I have some subs like:
Private Sub somesub()
'Processses that reach 900 mb in 1 hour and an half
End Sub
I want to restart the app, dispose memory and then return to where I was.
Exactly, I have an app that adds contacts and well It reach 900mb when 2000 contacts are added... I want to stop every 200 contacts, and do that I have said, and the code that I have no tested:
Imports SKYPE4COMLib
Public Class frmMain
Dim pUser As SKYPE4COMLib.User
Dim contactos As Integer
If contactos < 200 Then
For Each oUser In ListBox1.Items
pUser = oSkype.User(oUser)
pUser.BuddyStatus = SKYPE4COMLib.TBuddyStatus.budPendingAuthorization
oSkype.Friends.Add(pUser)
contactos += 1
Next
Else
'System.Windows.Forms.Application.Restart()
'I need a code that continues where I was, here...
End If
End Sub
End Class
What can I do? Thanks!

I have written some code below that may solve your problem. It certainly should save your position to a file and then when the file runs again, it would reload that position.
A couple of points.
I moved your declaration of pUser, and set it to nothing when I was done. This way the object is marked for disposal immediately.. you might get more than 200 revolutions out of this with that structure change.. but it might be a tad slower.
You will need some sort of reload for your listbox. I assume that you did not include it as part of your sample for brevity.
I changed your foreach loop to a for loop structure. This allows you to track your position within the list .. as a result I had to create a new oUser since you were iterating that in the foreach and did not list its type, you will need to fix that part of the code.
Obviously I have not compiled the below code, but it should give you a decent start on what your trying to do.
Be careful of the Process.Start, as you can set the current process to start another process and wait for that process to exit before exiting the current one and that would be very very very very bad and really cause an OutOfMemoryException quickly. You need to let the current process start the next instance and then without checking to see if it was successful at starting it .. exit. Or if you have used the restart command in your comments use that. The process spawning method might do what your wanting more effectively because your starting a new process on the computer and letting the old one be garbage collected (thus releasing the resources it was using.
Imports SKYPE4COMLib
Public Class frmMain
'Put code here to load position from the file
Dim startingPosition as Integer = 0
If IO.File.Exists("c:\filename.txt")
Using sr as New IO.StreamReader("c:\filename.txt")
sr.Read
StartingPosition = Convert.ToInteger(sr.ReadToEnd)
sr.Close
End Using
End If
'Probably needs some code somewhere to reload your listbox
Dim contactos As Integer
Dim CurrentPosition as Integer = 0
If contactos < 200 and StartingPosition < ListBox1.Items.Count Then
For x as integer = StartingPosition to ListBox1.Items.Count - 1
Dim oUser as <YOURTYPEHERE> = Ctype(ListBox1.Items(x), <YOURTYPEHERE>)
Dim pUser As SKYPE4COMLib.User
pUser = oSkype.User(oUser)
pUser.BuddyStatus = SKYPE4COMLib.TBuddyStatus.budPendingAuthorization
oSkype.Friends.Add(pUser)
contactos += 1
pUser = Nothing 'set the garbage collection to collect this.
CurrentPosition = x
Next
Else
'Save Your place to an external File, all your doing here is opening a file
'and saving the index of where you are in the listbox.
Using sw as New IO.StreamWriter("c:\filename.txt")
sw.Write(CurrentPosition)
sw.Close
End Using
'use the process namespace to have this app start the next copy of your app
'be careful not to use WaitForExit or you will have two copies in memory...
Process.Start("exename")
'or if the command below exists.. use that.. I never have.
'System.Windows.Forms.Application.Restart()
'I need a code that continues where I was, here...
End If
End Sub
End Class

Related

What is causing the delay between recordset.update and the form/report getting the information?

Short version
I'm entering information in a database and fetching it shortly after, but for some reason, when I enter the information, it isn't immediately entered, so that when I try to fetch it, I get old results. Why does this happen? I thought the operations were synchronous.
Long version
I have a split Access database. At the moment the backend is on my own hard drive to speed up testing, eventually this backend will land on a server. Back when it was a combined frontend/backend database and before I had done a major code refactor (tbh, it was quite the clusterfornication before that), and now this is happening in a number of different scenarios, but pretty much every time I enter information and try to fetch it right after that. Why this happens is a mystery to me, since everything I was reading told me there is no multi-threading in VBA and that everything is synchronous if not specified otherwise, and I haven't enabled any asynchronous options.
Two Examples:
I add a record to the database then refresh the form that contains those new records. I'm not going to post the full code (unless it is deemed necessary), since I've modularized the code a lot. But essentially it boils down to this: the user clicks a button which executes this:
Private Sub Anhang_hinzufügen_Click()
If IsNull(Me.Parent.ID) Then
MsgBox "Bitte erst Felder ausfüllen, und anschließend Anhänge hinzufügen", vbInformation
Else
AnhängeAuswählen Me.Parent.Name, Me.Parent.ID
Me.Form.Requery
End If
End Sub
As part of the AnhängeAuswählen method, the method AddRecord is called:
Function AddRecord(TableName As String, fields() As String, values) As Long
Dim Table As DAO.Recordset
Set Table = LUKSVDB.OpenRecordset(TableName)
Table.AddNew
For i = LBound(fields) To UBound(fields)
If TypeName(Table.fields(fields(i)).Value) = "Recordset2" Then
Dim rs2 As DAO.Recordset2
Set rs2 = Table.fields(fields(i)).Value
If IsArray(values(i)) Then
For j = LBound(values(i)) To UBound(values(i))
rs2.AddNew
rs2!Value = values(i)(j)
rs2.Update
Next j
Else
rs2.AddNew
rs2!Value = values(i)
rs2.Update
End If
Else
Table.fields(fields(i)) = values(i)
End If
Next i
AddRecord = Table!ID
Table.Update
Table.Close
End Function
The record is created, that's not the problem. But when it executes Me.Form.Requery, the new record doesn't appear in the form. Only when I execute Me.Form.Requery a fraction of a second later does the record appear.
I add a record to the database using a form, update some information in the recordset with VBA, then requery the subreport with the records. The record appears immediately, but the details I added programmatically only appear when I execute Me.Parent.Requery a couple of seconds later.
The first form is a data entry form, so that as soon as the data is saved, it's blank in order to create a new record. The previous should then appear in the form. The button to create the new record looks like this:
Private Sub Anmerkung_Hinzufügen_Click()
currentID = Me.ID
mSaved = True
If Me.Dirty Then Me.Dirty = False
UpdateRecord "Anmerkungen", currentID, StringArray("Person", "Datum"), Array(User, Now)
Me.Parent.Requery
End Sub
The UpdateRecord is similar to the AddRecord method:
Function UpdateRecord(TableName As String, ByVal ID As Integer, fields() As String, values)
Dim Table As DAO.Recordset
Set Table = SeekPK(TableName, ID, True)
Table.Edit
For i = LBound(fields) To UBound(fields)
If TypeName(Table.fields(fields(i)).Value) = "Recordset2" Then
Dim subtable As DAO.Recordset2
Set subtable = Table.fields(fields(i)).Value
If IsArray(values(i)) Then
On Error Resume Next
Dim t
t = LBound(values(i))
If Developer Then On Error GoTo -1 Else On Error GoTo Fehler
If Err.Number = 0 Then
For j = LBound(values(i)) To UBound(values(i))
subtable.AddNew
subtable!Value = values(i)(j)
subtable.Update
Next j
End If
Else
subtable.AddNew
subtable!Value = values(i)
subtable.Update
End If
Else
Table.fields(fields(i)) = values(i)
End If
Next i
Table.Update
Table.Close
End Function
Does anyone know why this happens, and how I can prevent it? I could do a bit of a workaround with timers on the forms, so that it refreshes the form a couple of seconds later, but that seems like a kludgy workaround to me, especially considering I don't know how long it specifically takes, and the times could change drastically once the backend is on the server.
Additional information, in case it's necessary:
In the code I've posted I've removed some additional code for error handling and performance logging, but it doesn't have any impact on what's happening otherwise.
When the database is opened, a global variable LUKSVDB As DAO.Database is initialized:
Function ConnectDatabase(Backend As Integer)
Select Case Backend
Case 0: DatenOrt = 'redacted, folder in which the production/beta database is located on the server
Case 1: DatenOrt = 'redacted, folder in which I have a personal testing database on the server
Case 2: DatenOrt = 'redacted, folder in which I have the testing database on my own computer
End Select
Set LUKSVDB = OpenDatabase(DatenOrt & "\LUKS-Verwaltung_be.accdb", False, False, ";pwd=PASSWORD")
End Function
For testing purposes, ConnectDatabase is launched with a value of 2. However, if it's a problem on my own SSD, where latency is just about 0, then I can only assume it will be a problem on the server as well, where the latency is definitely not 0.

How do Settings/Options save their selves?

Not sure if my title makes much sense, so I will try to explain my question here. So basically I am expanding my program by allowing things to be customized within it.
Say for example I do this: I click on File -> Options, and a new form is opened with tabs. I have different settings that you can toggle via dropdown box and checkboxes. Now once a user sets the settings they want, or don't want, they click on a button that says either "OK" or "Cancel".
What is the method to saving these settings, or reverting back to the original settings? Do you save via txt file, or is this a default function within a certain line of code?
UPDATE:
So I fixed my previous issue. Now I am having another with the saves. The saves are working good, but I want to use them in selecting my CheckListBox Collection range and also have that range load on start as well. so these are the 2 things that I have been using to do so, that results in adding to the previous, set, collection.
Working for RNG:
Dim rand As New Random()
Dim winners = Enumerable.Range(1, My.Settings.numberSetting).OrderBy(Function(r) rand.Next()).Take(5).ToArray()
Not working for Onload CheckListBox:
Me.LotteryNumbers.Items.Add(1, My.Settings.numberSetting)
If I remove the 1 from Me.LotteryNumbers.Items.Add, the result is this:
This ought not compile:
LotteryNumbers.Items.Add(1, My.Settings.numberSetting)
The overload which takes a second argument expect a Boolean to set the item added to Checked or not. One way is to add items in a loop:
Dim maxNums = My.Settings.numberSetting
' make sure it is empty
clb.Items.Clear()
For n As Int32 = 1 To maxNums
clb.Items.Add(n.ToString)
Next
I don't like using items in Settings as variables, so it grabs the current value to use. Another way uses AddRange:
clb.Items.AddRange(Enumerable.Range(1, maxNums).Select(Function(s) s.ToString()).ToArray())
Items is an collection of Object, so the Select converts to string to add them.
NEVER ORDER BY RANDOM.NEXT()
Mostly you get lucky, but it's not guaranteed. It's only a matter of time before that code blows up on you at run time. The longer the sequence to be sorted, the more likely you are to get an exception.
What you should do instead is implement a Fisher-Yates sort method:
Private rand As New Random()
Public Function Shuffle(Of T)(ByVal items As IList(Of T)) As IList(Of T)
For i As Integer = items.Count - 1 To 1 Step -1
Dim j As Integer = rand.Next(i + 1)
Dim temp As T= items(i)
items(i) = items(j)
items(j) = temp
Next
Return items
End Function
Solution for the working code to update and save checklistbox box count.
Private Sub OptionOkButton_Click(sender As Object, e As EventArgs) Handles OptionOkButton.Click
Main.LotteryNumbers.Items.Clear()
My.Settings.numberSetting = CInt(NumberCombo.Text)
Dim maxNum = My.Settings.numberSetting
Main.LotteryNumbers.Items.AddRange(Enumerable.Range(1, maxNum).Select(Function(s) s.ToString()).ToArray())
My.Settings.Save()
Me.Close()
End Sub

Saving user's DataGridViewColumn widths for multiple forms/controls

Typically, when designing a DataGridView, I try to size the columns so that nothing will need to be resized by the user. This practice works most of the time, but I recently had a user change her Windows settings so that text would display larger than usual.
That single act broke all of the tedious sizing that I worked so hard on. I have looked into saving column width per user and allowing them to be saved to the registry. The issue I run into is having to create a field in the application settings for each and every value that I want to save to the registry.
When saving settings for a single form, that is not a problem, and I do use the application settings for this purpose to save the main window size/location so that users can determine the optimal view of the application.
My question is:
Is there a way to save an array to the registry, or perhaps otherwise save dynamic values into the registry, without adding these values to the application settings in advance? Ideally, I will just have a "ColumnWidths" application setting or something along those lines, and dynamically add column name/width for any column that is resized by the user.
I have the following code, which is fine if I add a new setting for each individual form that I want to save sizing for, but I'm hoping to achieve this with a single setting, that will save column sizing for multiple DataGridView/Forms.
Private Sub SaveColumnSettings()
If My.Settings.CustomColumnWidths Is Nothing Then
My.Settings.CustomColumnWidths = New StringCollection
End If
For Each dc As DataGridViewColumn In grdBackOrderedItems.Columns
My.Settings.CustomColumnWidths.Add(dc.Width.ToString())
Next
End Sub
Private Sub LoadColumnSettings()
For i As Integer = 0 To My.Settings.CustomColumnWidths.Count - 1
grdBackOrderedItems.Columns(i).Width = CInt(My.Settings.CustomColumnWidths(i))
Next
End Sub
Settings allows for a Collection type, however it is a string collection (initial post seemed not to know this).
The current edit is dicey since it assumes one collection will fit all DGVs on all forms (that is, the same number of columns for all of them) and you are only adding to the Collection every time you save - making it larger. Since you want to save the users optimal view you should have one collection per DGV since they might well make col4 on DGVFoo wider than elsewhere.
Rather than adding a collection Setting for each DGV (ie "DGVFooColumns", "DGVBarColumns" etc), which requires each one be hard coded to map to the correct entry, I would use a custom class and serialize it.
Most everything regarding them can be internalized to the class. This is not a finished class, but a rough out of making one class service any and all DGVs (it should be very close):
' for the collection
Imports System.Collections.ObjectModel
Imports System.Runtime.Serialization.Formatters.Binary
<Serializable>
Public Class GridLayouts
<Serializable>
Friend Class GridLayout
Public Property GridName As String
Public Widths As New List(Of Integer)
' Some Serializers will require a simple ctor
Public Sub New()
Widths = New List(Of Integer)
End Sub
Public Sub SaveLayout(name As String, dgv As DataGridView)
' something the code uses to map them
' other than "DataGridView1" of which there may be several
GridName = name
' ToDo: loop thru DGV save widths to Widths
End Sub
End Class
Private mCol As Collection(Of GridLayout)
Private myFile As String
Public Sub New(filename As String)
myFile = filename
mCol = New Collection(Of GridLayout)
End Sub
Private Function IndexOf(grdName As String) As Integer
For n As Integer = 0 To mCol.Count - 1
If grdName = mCol(n).GridName Then
Return n
End If
Next
Return -1 ' not found
' alternatively find by name:
'Dim item = mCol.FirstOrDefault(Function(i) i.GridName = grdName)
'Return item
End Function
Public Sub StoreLayout(name As String, dgv As DataGridView)
Dim ndx As Integer = IndexOf(name)
If ndx <> -1 Then
mCol.RemoveAt(ndx) ' throw away old one
End If
Dim grd As New GridLayout
grd.SaveLayout(name, dgv)
mCol.Add(grd)
End Sub
Public Sub RestoreLayout(name As String, dgv As DataGridView)
Dim ndx As Integer = IndexOf(name)
If ndx = -1 Then
Exit Sub
End If
For n As Integer = 0 To mCol(ndx).Widths.Count - 1
' loop thru grid and set the columns
' maybe check that the col sizes are equal
dgv.Columns(n).Width = mCol(n).Widths(n)
Next n
End Sub
Public Sub Save()
' ToDo: Make a backup (?)
' Add a Try/Catch and convert to function
Dim bf As New BinaryFormatter
Using fs As New FileStream(myFile, FileMode.OpenOrCreate)
bf.Serialize(fs, mCol)
End Using
End Sub
Public Sub Load()
Dim bf As New BinaryFormatter
Using fs As New FileStream(myFile, FileMode.Open)
mCol = CType(bf.Deserialize(fs), Collection(Of GridLayout))
End Using
End Sub
End Class
Notes:
1) The Example has SaveLayout at the Class-Item level and RestoreLayout at the Collection Level. I would pick one way or the other not split it. This is was for illustrative purposes. I would generally favor the ClassItem Level for both.
2) Rather than the BinaryFormatter, you can use the XML Serializer. I personally loathe it.
3) Note that the collection class saves/loads its own data.
4) System.Collections.ObjectModel is required for Collection(of T) and prevent VB from wanting the use the vile VisualBasic Collection (which stores only Object).
5) Code elsewhere could be optimized so that you only store a layout if they actually change the widths and click something (or have an AutoSaveChanges option).
6) A fair amount is riding on the names being unique so that the layouts can be found.
Usage
Dim SaveFile As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
CompName,
ProgramName,
filename)
' e.g C:\Users\Ziggy\MyCompany\ThisProd\GridSettngs.bin
' application-wide collection of grid layouts for this user
Private myGrids As New GridLayouts(SaveFile)
...
myGrids.StoreLayout("Customers", datagridView12)
myGrids.RestoreLayout("Orders", datagridView34)

Global variable resets when adding a command button with code

I have this short piece of code
Public n As Integer
Public Sub Foo()
For i = 0 To 4
MyModule.n = MyModule.n + 1
Next i
End Sub
which is defined in a Module named MyModule. This code is working as expected: After executing for the first time 'MyModule.n' has the value 5. After executing the second time it has the value 10 and so on.
When I extend the code, to add a CommandButton and place it onto the working sheet, the global variable MyModule.n loses it's value on every new call of Foo:
Public n As Integer
Public Sub Foo()
Dim btn As OLEObject
For i = 0 To 4
Set btn = Worksheets("Aufträge").OLEObjects.Add(ClassType:="Forms.CommandButton.1", _
Left:=122, Top:=321, Width:=30, Height:=30)
MyModule.n = MyModule.n + 1
Next i
End Sub
The code seems to work because the command button is created and placed correctly. Why does the global variable reset if executing the second code fragment?
Furthermore I can't place a break point after or inside the For-Loop in the second code fragment. I get the message Can't enter break mode at this time.
Based on the search I did & my conclusion is that you can't add controls dynamically to the worksheet and retain the state of variables.
Here is why: Adding a button will force the sheet to goto design mode & hence reset of variables.
Supporting links
http://www.pcreview.co.uk/forums/dynamically-adding-activex-controls-via-vba-kills-global-vba-heap-t3763287p2.html
https://web.archive.org/web/20101215134333/http://support.microsoft.com/kb/231089 (originally //support.microsoft.com/kb/231089)
You don't need to use a for next loop for the first example. Why don't you do that :
Public n As Integer
Public Sub Foo()
MyModule.n = 5
End Sub
This will do the thing in one operation.
In the second example you don't add one button, you add 5 of them. Check the help fo "For Next".

While loop causes the app to go slow? Any idea why?

I have a simple code that looks up a text file, reads the line of text, splits the string by semi-colons and then posts the results.
After it has done this, I have created a really simple while loop to waste 10 seconds before going for it again.... here is the code:
Private Sub checkTemps()
While Abort = False
Try
fileReader = New StreamReader(directory.Text & "currentTemp.dat")
rawData = fileReader.ReadLine()
fileReader.Close()
Dim dataArray() As String
dataArray = rawData.Split(";")
updateOutput("1", dataArray(0), dataArray(1))
updateOutput("2", dataArray(2), dataArray(3))
updateOutput("3", dataArray(4), dataArray(5))
updateOutput("4", dataArray(6), dataArray(7))
stpWatch.Start()
While stpWatch.Elapsed.Seconds < 10 And Abort = False
pollInterval(stpWatch.ElapsedMilliseconds)
End While
stpWatch.Stop()
stpWatch.Reset()
Catch ex As Exception
msgbox("oops!")
End Try
End While
closeOnAbort()
End Sub
But when it gets to the "time-wasting" loop - it seems to slow the whole application down? And I can't work out why!
So a couple of questions... is there a better way to do all this? and second - can anyone spot a problem?
All the other commands seem to run fine - there isn't much else to this app. I have another program that updates the dat file with the values, this is simply a client side app to output the temperatures.
Any help would be appreciated.
Andrew
More info:
I should explain what the pollInterval sub does!
Private Delegate Sub pollIntervalDelegate(ByVal value As Integer)
Private Sub pollInterval(ByVal value As Integer)
If Me.InvokeRequired Then
Dim upbd As New pollIntervalDelegate(AddressOf pollInterval)
Me.Invoke(upbd, New Object() {value})
Else
ProgressBar1.Value = value
End If
End Sub
Your loop is a very tight loop continually calling pollInterval. This will tie up the application until the loop condition is met.
You should use the Sleep method to pause this thread for the required amount of time.
If you want to show the progress (as per your update) you could put the Sleep into the loop and sleep for 1 second (or half a second?) at a time:
While stpWatch.Elapsed.Seconds < 10 And Abort = False
Sleep(1000) <-- NOT 100% sure of the syntax here,
but the time is specified in milliseconds
pollInterval(stpWatch.ElapsedMilliseconds)
End While
You should go with
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(10).TotalMilliseconds);