Storing custom class in a public VBA collection - vba

I'm making a quiz generator application in Word with VBA. The user generates the questions one at a time, so I'm trying to store all questions in a public collection. The sub to add the question object to the collection is as follows:
Option Explicit
Public questionCollection As VBA.Collection
'Initialize question collection
Sub addToQuestionCollection(cQuestion As Object)
Dim key As Long
'Set question collection if it has not yet been initialized
If questionCollection Is Nothing Then
Set questionCollection = New VBA.Collection
End If
'Generate key for question
key = Bas04CRC32Hash.CRC32(cQuestion.question)
'Assign key to question
cQuestion.Id = key
'Adds question object to question collection
questionCollection.add cQuestion, CStr(key)
End Sub
The subroutine executes perfectly fine, but as soon as it's over the collection loses the value and become empty. I'm positive that questionCollection is never dimmed in my project (I must have checked 20 times by now) so the scope declaration can't be the issue. How can I ensure that the collection retains all of the question objects while the document remains open?
Thank you

After some more digging I found the answer:
My quiz generator uses OLEObjects to represent the questions. Apparently, creating an OLEObject dynamically resets global variables, which really sucks. If anyone knows a way around this I'd appreciate it greatly.

Related

SolidWorks VBA - Translating API Help into useable code

I'd like to do what feels like a fairly simple task, and I've found the specific API Help pages which should make it clear, but, I can't actually make things work.
The Key steps that I would like to achieve are:
Rename the active document
Update References to this document to accommodate new name
Save active document.
This help page shows the Usage for renaming the doc, and under the "Remarks" heading, includes links to the next two steps, mentioning them off hand as if implementing them would be easy.
https://help.solidworks.com/2020/English/api/sldworksapi/SolidWorks.Interop.sldworks~SolidWorks.Interop.sldworks.IModelDocExtension~RenameDocument.html?verRedirect=1
The trouble is, I'm a bit of a VBA beginner - usually I get by with the 'record' function, and then tidying things up from there - but undertaking the steps above manually doesn't result in anything being recorded at all for one reason or another.
Assuming I am able to pass in the item to be renamed (I'll define a variable at the start of the Sub for this e.g. swModel = swApp.ActiveDoc), and the new name (NewName = "NEW NAME HERE"), How would I translate the Help API into a Sub that I can actually run?
Two of them suggest declaring as a Function, and one as a Public Interface - I've never used these before - do these just run in a standard Module? Do I need to write a 'master Sub' to call the different functions sequentially, or could these be included directly in the sub, if they're only to be used once?
[Feeling a little lost - it's demoralizing when the help files aren't all that helpful]
Let me know if there's any more information missing that I can add to improve my question - as I said, I'm fairly new to this coding thing...
The "record" function is sometimes a good point to start but there are a lot of functions it can't recognize while you execute them manually.
The API Help is then useful to find out how to use a specific function.
In almost every example the use of a specific method (e.g. RenameDocument) is only shown abstract. There is always a instance variable which shows you the object-type needed to call this method. So you can use these in every sub you want, but beforehand need access to the specific instance objects.
For your example the RenameDocument method is called with an object of the type IModelDocExtension. First thing for you to do is to get this object and then you can call the method as described in the help article.
Under Remarks in the article you find additional information for what you maybe have to do before or after calling a method.
For your example it is mentioned that the renaming takes permanently place after saving the document.
And finally here is what you want to do with some VBA code:
Dim swApp As SldWorks.SldWorks
Dim swModel As ModelDoc2
Sub main()
' get the solidworks application object
Set swApp = Application.SldWorks
'get the current opened document object
Set swModel = swApp.ActiveDoc
' get the modeldocextension object
Dim swModelExtension As ModelDocExtension
Set swModelExtension = swModel.Extension
Dim lRet As Long
lRet = swModelExtension.RenameDocument("NEW NAME")
If lRet = swRenameDocumentError_e.swRenameDocumentError_None Then
MsgBox "success renaming"
Else
MsgBox "failed with error: " & lRet
End If
End Sub
Afterwars you have to process the return value to check for errors described in this article: https://help.solidworks.com/2020/English/api/swconst/SolidWorks.Interop.swconst~SolidWorks.Interop.swconst.swRenameDocumentError_e.html

Access an Collection within a form via VBA out of a Class-module

I just like to reorder the VBA of a bunch of Forms in Access, doing all nearly the same. So I created a class clsPopup and I just want to pass some collections out of all this Forms into the class, so I can access their entries over there or in there or how ever.
I could pass the Form-element of the Popup to the class as I created a sub named Load in clsPopup like
Dim m_frm As Form
sub Load(frm As Form)
Set m_frm = frm
debug.print m_frm.colSp("Name")
end sub
In the Form I tried
Dim m_clsPopup As clsPopup
Dim colSp As Collection
sub Form_Load()
Set m_clsPopup = New clsPopup
Set colSp = New Collection
colSp.Add "SomeString", "Name"
m_clsPopup.Load Me.Form
end sub
At this point I got the 2465 Runtime error in the line of m_clsPopup.Load Me.Form.
My main idea is, to just collect all the needed data within the Forms as collections and than I easily could work with them in the class.
Of course I thought of arrays, but collections seems so much more handy and I could avoid some terrible indexing.
Ahh, and it needs to work in Access 2010. Might that be the problem?
I feel like just a tiny pice of code is missing. Could anyone help to create nice code out of a masterpiece of redundancy?

VB6 map string to integer for headers

I'm trying to parse a CSV File into a VB6 application in order to update multiple records on a table on SQL with existing single record updating code already in the form. The CSV Files will have a header row whixh can be used to validate the information going into the correct place in the ADODB recordset. In C++ you can use a map to say like
map<String s, int x> column
column<"First Name", -1>
column<"Last Name",-1>
Then create a counter across the comma delimited values where if the third value is Last Name then the code could be written to change
column<"Last Name",-1> to column<"Last Name",3> and if x != -1 in any of the maps the file is valid for use, I would then loop through the remaining records and parse into a container using something similar to
strLastName = Array<column[3]>
to assign the record values to the correct variables. I am still very new to VB6, how can I accomplish something similar in VB6 and what containers should be used? So far I have
Public Sub GetImportValues()
On Error GoTo GetImportValues_Error:
Dim intFileNum As Integer
Open Path For Input As #intFileNum
Do Until EOF(intFileNum)
Line Input #intFileNum, vbCrLf
FunctionThatSavesInformationToSQL
Loop
Close #intFileNum
GetImportValues_Exit:
Exit Sub
GetImportValues_Error:
Err.Source = "frmMemberAdd.GetImportValues" & " | " & Err.Source
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
with a dialog box returning the path as a string using App.path in a separate Function
*****************************************************Slight change to answer
The collection was on track for what I had asked but I did have to change it to dictionary because you cannot return items on a collection which kept me from comparing the items and changing the keys but dictionary can. Make sure if you use dictionary you switch the item and key.
If I understand your question correctly, you're trying to create a map (Dictionary<string, int> in C#). In VB6, you can use Collection for this purpose - it's roughly equivalent to C#'s Dictionary<string, object>. It uses String keys and stores all values as Variant. For example:
Dim oColl As Collection
Set oColl = New Collection
oColl.Add -1, "ColumnName"
Dim nColumnIndex As Long
'Get column index for column name.
nColumnIndex = oColl.Item("ColumnName")
If nColumnIndex = -1 Then
nColumnIndex = ...
'When you want to update a column index in the collection, you
'first have to remove the item and then add it back with the right
'index.
oColl.Remove "ColumnName"
oColl.Add nColumnIndex, "ColumnName"
End If
Edit 1:
One word of warning regarding VB6: you'll see many samples doing this:
Dim oObj As New SomeClass
It's ok to do this in VB.Net but don't ever do this in VB6. Declare and instantiate the object on separate statements because the single-statement form generates code where oObj is checked for Nothing and set to an instance before each use. This slows down your code (unnecessary checks) and creates hard-to-find bugs if you're using an instance that's supposed to be gone.
Always do this instead:
Dim oObj As SomeClass
Set oObj = New SomeClass
...
'Clean up the object when you're done with it. Remember, there's
'no garbage collection in COM / VB6, you have to manage object
'lifetimes.
Set oObj = Nothing
Also, use Long instead of Integer as much as you can - Long is a 32-bit integer, while Integer is only 16-bits. VB6 type names can be misleading frequently. Here's an old answer of mine with a bit more detail (not strictly related to your question but useful).
Alternatively, you can create a simplified wrapper around the .NET Dictionary class and expose it as a COM object: this would allow you to call it from VB6. This would likely be (somewhat) slower than Collection and it'd require the .NET Framework for your VB6 project to run.
Edit 2:
As #CMaster commented, Dictionary is available from the Microsoft Scripting Runtime library - you need to add a reference to it to use it (this is why I prefer Collection - it has no dependency). This answer has details about how to use it.

Public variables are not REALLY public in VBA in Forms

Below is a question that I will answer myself, however it caused a GREAT deal of frustration for me and I had a lot of trouble searching for it on the web, so I am posting here in hopes of saving some time & effort for others, and maybe for myself if I forget this in the future:
For VBA (in my case, MS Excel), the Public declaration is supposed to make the variable (or function) globally accessible by other functions or subroutines in that module, as well as in any other module.
Turns out this is not true, in the case of Forms, and I suspect also in Sheets, but I haven't verified the latter.
In short, the following will NOT create a public, accessible variable when created in a Form, and will therefore crash, saying that the bYesNo and dRate variables are undefined in mModule1:
(inside fMyForm)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
However, if you make the slight alteration below, it all will work fine:
(inside fMyForm)
Private Sub SetVals()
bYesNo = Me.cbShouldIHaveADrink.value
dRate = CDec(Me.tbHowManyPerHour.value)
End Sub
(Presume the textbox & checkbox are defined in the form)
(inside mModule1)
Public bYesNo As Boolean`
Public dRate As Double
Private Sub PrintVals()
Debug.Print CStr(bYesNo)
Debug.Print CStr(dRate)
End Sub
mModule1 will work perfectly fine and, assuming that the fMyForm is always called first, then by the time the PrintVals routine is run, the values from the textbox and checkbox in the form will properly be captured.
I honestly cannot possibly fathom what MS was thinking with this change, but the lack of consistency is a huge suck on efficiency, learning idiosyncracies like these, which are so poorly documented that a Google search in 2013 for something that has likely been around for a decade or more is so challenging to search.
First comment:
Userform and Sheet modules are Object modules: they don't behave the same way as a regular module. You can however refer to a variable in a userform in a similar way to how you'd refer to a class property. In your example referring to fMyForm.bYesNo would work fine. If you'd not declared bYesNo as Public it wouldn't be visible to code outside of the form, so when you make it Public it really is different from non-Public. – Tim Williams Apr 11 '13 at 21:39
is actually a correct answer...
As a quick add-on answer to the community answer, just for a heads-up:
When you instantiate your forms, you can use the form object itself, or you can create a new instance of the form object by using New and putting it in a variable. The latter method is cleaner IMO, since this makes the usage less singleton-ish.
However, when in your userform you Call Unload(Me), all public members will be wiped clean. So, if your code goes like this:
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Show(vbModal)
If Not oForm.bCancelled Then ' <- poof - bCancelled is wiped clean at this point
The solution I use to prevent this, and it is a nice alternative solution for the OP as well, is to capture all IO with the form (i.e. all public members) into a separate class, and use an instance of that class to communicate with the form. So, e.g.
Dim oFormResult As CWhateverResult
Set oFormResult = New CWhateverResult
Dim oForm as frmWhatever
Set oForm = New frmWhatever
Call oForm.Initialize(oFormResult)
Call oForm.Show(vbModal)
If Not oFormResult.bCancelled Then ' <- safe
There are other limitations to Public within Excel VBA.
MSoft documentation in learn.microsoft.com states that public variables are global to the VBA project - it's not true.
Public variables are only global to the workbook within which they are declared, and then only across standard modules. Public variables declared within workbook code are not visible in standard modules, even though standard module sub's are - which are defined to be public.
Public variables declared in one workbook's standard modules are certainly not accessible from other workbooks in the same VBA project, contrary to the MSoft documentation.

How to prevent VBA variables from being shared across Word documents?

I have a VBA template project that runs automatically when a Word document is opened. However, if I open multiple documents, they all share the variables values. How can declare these variables to be only associated with the active window or active document?
I tried declaring them in a Class Module, but that did not help. Switching between opened document I can see that these variables are shared.
Any input is appreciated...
This what I have in my Module:
Option Private Module
Dim CurrentCommand As String
Public Function SetCurrentCommand(command)
CurrentCommand = command
End Function
Public Function GetCurrentCommand()
GetCurrentCommand = CurrentCommand
End Function
More Info: The code/Macro start at AutoExec like this:
Public Sub Main()
Set oAppClass.oApp = Word.Application
If PollingRate <> "" Then Application.OnTime Now + TimeValue(PollingRate), "CaptureUserViewState"
End Sub
And the CaptureUserViewState is a Sub that resides in a different Module and does all teh checks (comparing new values to last recorded ones) and here how this Sub does the check:
If WL_GetterAndSetter.GetLastPageVerticalPercentage <> pageVerticalPercentScrolled Then
'Update the last value variable
WL_GetterAndSetter.SetLastPageVerticalPercentage (pageVerticalPercentScrolled)
'log change
End If
You don't give us much information, but I assume you declared public variables at module level like this:
Public myString As String
Public myDouble As Double
From VBA documentation:
Variables declared using the Public statement are available to all procedures in all modules in all applications unless Option Private Module is in effect; in which case, the variables are public only within the project in which they reside.
The answer is to use Option Private Module.
When used in host applications that allow references across multiple projects, Option Private Module prevents a module’s contents from being referenced outside its project.
[...] If used, the Option Private statement must appear at module level, before any procedures.
EDIT You have now clarified that you declare your variables using Dim at module level. In this case, Option Private Module is irrelevant.
Variables declared with Dim at the module level are available to all procedures within the module.
i.e. regardless of whether you're using Option Private Module or not.
If you're finding that the values are retained between runs, then that must be because you are running a procedure from the same module from the same workbook. You may think you're doing something else, but in reality this is what you're doing.
EDIT
In your class module, instead of Dim CurrentCommand As String try Private CurrentCommand As String. Without more information it's hard to debug your program. I'm just taking random potshots here.
What you need to do is store multiple versions of the variables, one set per document.
So I would suggest that you create a simple class to hold the different values.
You then store them in a collection mapping the data-set with the document name or similar as the key.
In classmodule (MyData), marked as public:
Public data1 as String
Public data2 as Integer
In module with the event-handlers:
Dim c as new Collection 'module global declaration
Sub AddData()
Dim d as new MyData 'Your data set
d.data1 = "Some value"
d.data2 = 42
c.add Value:=d, Key:=ActiveDocument.name
End Sub
Then when you enter the event-handler you retrieve the data and use the specific set for the currently active document.
Sub EventHandler()
Dim d as MyData
set d = c.item(ActiveDocument.name)
'use data
'd.data1...
End Sub
Please not that this code is just on conceptual level. It is not working, You have to apply it to your problem but it should give you some idea on what you need to do. You will need to add alot of error handling, checking if the item is already in the collection and so on, but I hope you understand the concept to continue trying on your own.
The reason for this is because, as I understand the situation from your question, you only have one version of your script running, but multiple documents. Hence the script have to know about all the different documents.
On the other hand, If each document would have their own code/eventhandlers, hence having multiple versions of the script running, then you don't need the solution provided above. Instead you need to be careful what document instance you reference in your script. By always using "ThisDocument" instead of "ActiveDocument" you could achieve isolation if the code is placed in each open document.
However, as I understood it, you only have one version of the script running, separate from the open documents, hence the first solution applies.
Best of luck!
You might want to store the Document Specific details using
The Document.CustomDocumentProperties Property
http://msdn.microsoft.com/en-us/library/office/aa212718(v=office.11).aspx
This returns a
DocumentProperties Collection
Which you can add new Properties to Using
Document.CustomDocumentProperties.Add(PropertyName, LinkToContent, Value, Type)
And then Read From using
Document.CustomDocumentProperties.Item(PropertyName)
A downside, or bonus, here is that the properties will remain stored in the document unless you delete them.
This may be a good thing or a bad thing