How to make class variables persist between modules in VBA - vba

I am trying to find a way for class variables to persist between modules.
I have a use class that stores typical user data: Name, domain, manager, etc.
I'd like to store this information throughout the life of the session (while the user is using the tool), but it looks like I can't do that. Below is an example and thank for your help/advice!
JP
Here is the class module:
Private cLoggedDomain As String
Private cLoggedRole As String
Private cDepartment As String
Private cEmployeeName As String
Private cManagerName As String
Private cEmp_ID As Long
Private cEmployeeInfo As Collection
Public Property Let SetUser(value As String)
'RECIEVES THE LOGGED DOMAIN AS STRING
'GETS THE DB ATTRIBUTES FROM SQL
Set cEmployeeInfo = GetInfoFromSearch("Employee, manager, department, ety_type, emp_ID", _
"domainID = '" & value & "'", _
"Employee", "v_roster_empViewALL")
cLoggedDomain = value
cEmployeeName = cEmployeeInfo(1)(1)
cManagerName = cEmployeeInfo(1)(2)
cDepartment = cEmployeeInfo(1)(3)
cLoggedRole = cEmployeeInfo(1)(4)
cEmp_ID = cEmployeeInfo(1)(5)
End Property
Public Property Get LoggedDomain() As String
LoggedDomain = cLoggedDomain
End Property
Public Property Let LoggedDomain(value As String)
cLoggedDomain = value
End Property
Public Property Get LoggedRole() As String
LoggedRole = cLoggedRole
End Property
Public Property Get LoggedDepartment() As String
LoggedDepartment = cDepartment
End Property
Public Property Get LoggedEmployeeName() As String
LoggedEmployeeName = cEmployeeName
End Property
Public Property Get LoggedManagerName() As String
LoggedManagerName = cManagerName
End Property
Public Property Get LoggedEmpId() As String
LoggedEmpId = cEmp_ID
End Property
And the module that uses it, which works fine:
Public Sub New_LoadMain()
Dim s As Worksheet
Dim loggedUser As New cRoles
'CHECK TO SEE IF USER IS LOGGED IN
If loggedUser.LoggedDomain = "" Then
'Set loggedUser = New cRoles
loggedUser.SetUser = Environ("username")
Else
End If
Call test
However, when I try to use the test module, I get a with block error?
Sub test()
Dim test As cRoles
Dim t As String
t = test.LoggedDepartment
End Sub

Class modules define the public interface for objects: they are blueprints that mean nothing until they are instantiated with the New keyword.
When you do this:
Dim test As cRoles
You allocate memory for an object pointer, and telling the compiler that this object implements the cRoles interface; that's how you can type test. and get a list of all the public members on that interface.
However that object pointer points to no object: it's Nothing (literally). You need to create a new instance of that class in order to access the object test is pointing to:
Set test = New cRoles
And now accessing test members will no longer throw error 91.
Now, each instance encapsulates its own state: think of each worksheet in your workbook as a Worksheet instance: each sheet has its own separate content, but all sheets can be manipulated through the same Worksheet interface, regardless of whether you're looking at Sheet1 or Sheet42.
The same is true for all instances of your cRoles class:
Dim test1 As cRoles
Set test1 = New cRoles
test1.SetUser = user1
Dim test2 As cRoles
Set test2 = New cRoles
test2.SetUser = user2
Debug.Print test1.LoggedEmpId, test2.LoggedEmpId
The two instances are completely distinct, and each hold their own internal state. If that's what you want, then in order to create an instance in one place and consume it in another place, you'll need to pass the object reference as a parameter:
Public Sub Test()
Dim thing As cRoles
Set thing = New cRoles
thing.SetUser = Environ("username")
DoSomething thing
End Sub
Private Sub DoSomething(ByVal auth As cRoles)
Debug.Print auth.LoggedEmpId
End Sub
Note:
You typically want to pass parameters ByVal
Avoid As New since that makes an auto-instantiated object, and that comes with behavior that may or may not be expected.
You could have a global-scope Public AuthInfo As cRoles variable declared in a standard module, then a procedure responsible for creating the object and setting this global-scope reference. Then you can access AuthInfo everywhere in your VBA project - the caveat being, that global variable can now be written to by any code in your VBA project. Prefer using local variables and parameters if possible.

Related

Compile error: Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions

I'm struggling with a little bit of VBa and Excel. I need to create a structure in VBa, which is a Type. The problem I have is, I get an error message when I try to execute the code! I feel I need to explain how I have arrived where I am in case I've made an error.
I have read that to create a type, it needs to be made public. As such I created a new Class (under Class Modules). In Class1, I wrote
Public Type SpiderKeyPair
IsComplete As Boolean
Key As String
End Type
And within ThisWorkbook I have the following
Public Sub Test()
Dim skp As SpiderKeyPair
skp.IsComplete = True
skp.Key = "abc"
End Sub
There is no other code. The issue I have is I get the error message
Cannot define a public user-defined type within an object module
If I make the type private I don't get that error, but of course I can't access any of the type's properties (to use .NET terminology).
If I move the code from Class1 into Module1 it works, but, I need to store this into a collection and this is where it's gone wrong and where I am stuck.
I've updated my Test to
Private m_spiderKeys As Collection
Public Sub Test()
Dim sKey As SpiderKeyPair
sKey.IsComplete = False
sKey.Key = "abc"
m_spiderKeys.Add (sKey) 'FAILS HERE
End Sub
Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions
I have looked into this but I don't understand what it is I need to do... How do I add the SpiderKeyPair to my collection?
Had the exact same problem and wasted a lot of time because the error information is misleading. I miss having List<>.
In Visual Basic you can't really treat everything as an object. You have Structures and Classes which have a difference at memory allocation: https://learn.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/data-types/structures-and-classes
A Type is a structure (so are Arrays), so you if you want a "List" of them you better use an Array and all that comes with it.
If you want to use a Collection to store a "List", you need to create a Class for the object to be handled.
Not amazing... but it is what the language has available.
You seem to be missing basics of OOP or mistaking VBA and VB.NET. Or I do not understand what are you trying to do. Anyhow, try the following:
In a module write this:
Option Explicit
Public Sub Test()
Dim skpObj As SpiderKeyPair
Dim m_spiderKeys As New Collection
Dim lngCounter As Long
For lngCounter = 1 To 4
Set skpObj = New SpiderKeyPair
skpObj.Key = "test" & lngCounter
skpObj.IsComplete = CBool(lngCounter Mod 2 = 0)
m_spiderKeys.Add skpObj
Next lngCounter
For Each skpObj In m_spiderKeys
Debug.Print "-----------------"
Debug.Print skpObj.IsComplete
Debug.Print skpObj.Key
Debug.Print "-----------------"
Next skpObj
End Sub
In a class, named SpiderKeyPair write this:
Option Explicit
Private m_bIsComplete As Boolean
Private m_sKey As String
Public Property Get IsComplete() As Boolean
IsComplete = m_bIsComplete
End Property
Public Property Get Key() As String
Key = m_sKey
End Property
Public Property Let Key(ByVal sNewValue As String)
m_sKey = sNewValue
End Property
Public Property Let IsComplete(ByVal bNewValue As Boolean)
m_bIsComplete = bNewValue
End Property
When you run the Test Sub in the module you get this:
Falsch
test1
-----------------
-----------------
Wahr
test2
Pay attention to how you initialize new objects. It happens with the word New. Collections are objects and should be initialized as well with New.

dim Pull data from lower variable to higher

I have
Dim m_LedgerList As New Ejm.Financial.Entities.LedgerList
m_LedgerList = My.StaticData.LedgerList
m_LedgerList.Filter = "LedgerfunctionID = 2"
but the filter i put on m_LedgerList.Filter pull trough to My.StaticData.LedgerList
Any idees how to stop the filter to go up?
This line
m_LedgerList = My.StaticData.LedgerList
makes your variable m_LedgerList reference the same data referenced by the variable My.StaticData.LedgerList. This is not a copy, this is merely two variables that look at the same data in memory. Thus any action that you perform on the m_LedgerList variable acts on the same data seen by the My.StaticData.LedgerList.
If you want to have a different set of data then you need to duplicate the original data in a new memory location. This could be done inside the LedgerList class with something like this
Public Class LedgerList
Public Function Duplicate() As LedgerList
' This create a new memory area for a new LedgerList
Dim result = new LedgerList()
' Code to duplicate and add the elements in this
' class to the new list
return result
End Function
End Class
Now you can go with
m_LedgerList = My.StaticData.LedgerList.Duplicate()
m_LedgerList.Filter = "LedgerfunctionID = 2"
You can implement ICloneable as follows:
Class LedgerList
Implements ICloneable
Property Property1 As String
Property Filter As String
Public Function Clone() As Object Implements ICloneable.Clone
Dim myclone As New LedgerList
myclone.Property1 = Me.Property1
myclone.Filter = Me.Filter
Return myclone
End Function
End Class
Then to make a copy of your object:
Dim m_LedgerList As Ejm.Financial.Entities.LedgerList = DirectCast(My.StaticData.LedgerList.Clone, Ejm.Financial.Entities.LedgerList)
m_LedgerList.Filter = "LedgerfunctionID = 2"

Populate a class and then use it later

I have a class like
Public Class Location
Public Name As String
Public Column As Integer
Public Row As Integer
Public Occupant As String
End Class
In my code I have a subroutine to fill the class
Sub Populate Location(ByValue coordinate As String)
Dim Here As New Location
Here.Location = coordinate.SubString(0,3)
Here.Column = SomeFunction(coordinate, Gere.location)
Here.Row = AnotherFunction(coordinate, Here.Column)
Here.Occupant = ArrayOfOccupant(column, row)
End Sub
All of that is filled in one press of a button.
Later I want to click another button and use the Here class to do other things.
What are my options or what do I search for?
As is, your class object only exists inside that procedure.
Dim Here As New Location ' variable declaration, instancing
' Scope is this module or form
Sub PopulateLocation(ByValue coordinate As String)
' assuming this might get reused, create a new instance
Here = New Location
Here.Location = coordinate.SubString(0,3)
Here.Column = SomeFunction(coordinate, Here.location)
Here.Row = AnotherFunction(coordinate, Here.Column)
Here.Occupant = ArrayOfOccupant(column, row)
End Sub
Other subs in that module will have access to Here because it now has class/module level Scope

How can I allow all subroutines within my program to access a certain variable?

In the code below, I want to be able to access the enteredusername and enteredpassword variables from any sub routine. How would I accomplish this?
Using rdr As New FileIO.TextFieldParser("f:\Computing\Spelling Bee\stdnt&staffdtls.csv")
rdr.TextFieldType = FieldType.Delimited
rdr.Delimiters = New String() {","c}
item = rdr.ReadFields()
End Using
Console.Write("Username: ")
enteredusername = Console.ReadLine
Console.Write("Password: ")
Dim info As ConsoleKeyInfo
Do
info = Console.ReadKey(True)
If info.Key = ConsoleKey.Enter Then
Exit Do
End If
If info.Key = ConsoleKey.Backspace AndAlso enteredpassword.Length > 0 Then
enteredpassword = enteredpassword.Substring(0, enteredpassword.Length - 1)
Console.Write(vbBack & " ")
Console.CursorLeft -= 1
Else
enteredpassword &= info.KeyChar
Console.Write("*"c)
End If
Loop
Dim foundItem() As String = Nothing
For Each line As String In File.ReadAllLines("f:\Computing\Spelling Bee\stdnt&staffdtls.csv")
Dim item() As String = line.Split(","c)
If (enteredusername = item(0)) And (enteredpassword = item(1)) Then
foundItem = item
Exit For
End If
Next
To allow ALL classes within your program access the variable, you need to make it class-level and define it with Public and Shared.
Demonstration:
Public Class MainClass
Public Shared enteredusername As String
Public Shared enteredpassword As String
Private Sub SomeSub()
' Some Code ...
' You can access it here:
enteredusername = "something"
enteredpassword = "something else"
' ... More Code ...
End Sub
End Class
Public Class AnotherClass
'Also, please note, that this class can also be in another file.
Private Sub AnotherSub()
' Some Code ...
' You can also access the variable here, but you need to specify what class it is from, like so:
Console.WriteLine(MainClass.enteredusername)
Console.WriteLine(MainClass.enteredpassword)
' ... More Code ...
End Sub
End Class
Also, on a separate note, the Public and Shared modifiers can also be used on methods. If you make a method Private or don't specify anything, the method will only be accessible from methods in the same class. If you use only Public, other classes can access the method, but they will need to create a instance of the class, like so:
Dim AC As New AnotherClass
AC.AnotherSub()
If you use both the Public and the Shared modifiers, other classes will be able to access the method directly, without creating a new instance. But, you must note, that Shared methods cannot access non-Shared methods or variables. Other classes can access Public Shared methods like so:
AnotherClass.AnotherSub()
It depends on the scope. If you want all of the subroutines in the current class to be able to access them then make them a field of the class
Class TheClassName
Dim enteredusername As String
Dim enteredpassword As String
...
End Class
If you want all subroutines in all classes and modules to be able to access them then make them a module level field
Module TheModuleName
Dim enteredusername As String
Dim enteredpassword As String
...
End Module
I recommend against this approach though. Sure it's easier in the short term because it requires less ceremony and thought on the uses of the values. But long term it serves to reduce the maintainability of your code base

Accessing Items in a Collection from a Class

-EDIT Fixed
I was missing one thing and doing one thing wrong. First I was missing a function to access the collection by index. And I should of been using a for Loop instead of a for each loop in my module code
I forgot to add this to the collection class
Public Function GetPayRecords(ByVal index As Variant) As PayRecords
Set GetPayRecords = pObjCol.item(index)
End Function
and replaced
For Each vItem In .GetPayRecords
....code to do stuff
Next vItem
with this in the module
Dim x As Integer
For x = 1 To .Count
Debug.Print .GetPayRecords(x).PY_PayRecord.CEOCompanyID
Debug.Print .GetPayRecords(x).PY_PayRecord.OrigBankID
Next x
I'm writing a program that has 8 Classes. Each class represents a specific record type.
I have an overall Class that contains those 8 classes which is for simplicity when coding in the Module. I only have to declare one class which gives me access to all 8 classes. I have a collection which contains all the records types. Once all the logic of loading the individual records is complete they get added to the collection. This all works perfectly and I can see all the records in the collection. The final step, which happens to be where i'm having the problem, I need to extract each item within the collection by record type and write it to a csv. The problem I encounter is trying to iterate through each record.
Here's how the structure looks
Classes
clsAllRecordTypes
clsRecordType1
clsRecordType2
...
clsRecordType8
Collection
clsColRecords
The problem is in the retrieval
Module
Dim PayRecord As PayRecords 'Class of Classes
Dim PayRecordList As bankCollection
...code to load all the payrecords
With payrecordlist
Foreach vItem in .pObjCol
debug.print .pObjCol.Item(?) ' not sure why i can't see all 8
next vItem
End With
When I add vItem to the watch I can see each and every record type filled up with information but yet i Can not access it. Below is the Class of classes and collection
Class of Classes
Option Explicit
'This class is a representation of all the record types that apply to our Payment Manager
'It aggregates all the record types (classes) into one class. That one class is used in the main processing module for simplicty
'
Private pPayRecord As New PayRecord
Private pPNAR_OP As New PNAR_OP
Private pPNAR_RP As New PNAR_RP
Private pSuppACHREC As New SuppACHRec
Private pSuppCCRRec As New SuppCCRRec
Private pSuppCHKRec As New SuppCHKRec
Private pDocumentDelieveryRec As New DocumentDeliveryRecord
Private pInvoiceRecords As New InvoiceRecords
Public Property Get PY_PayRecord() As PayRecord
Set PY_PayRecord = pPayRecord
End Property
Public Property Let PY_PayRecord(ByVal newPayRecord As PayRecord)
Set pPayRecord = newPayRecord
End Property
Public Property Get PA_PNAR_OP() As PNAR_OP
Set PA_PNAR_OP = pPNAR_OP
End Property
Public Property Let PA_PNAR_OP(ByVal newPNAR_OP_Record As PNAR_OP)
Set pPNAR_OP = newPNAR_OP_Record
End Property
Public Property Get PA_PNAR_RP() As PNAR_RP
Set PA_PNAR_RP = pPNAR_RP
End Property
Public Property Let PA_PNAR_RP(ByVal newPNAR_RP_Record As PNAR_RP)
Set pPNAR_RP = newPNAR_RP_Record
End Property
Public Property Get AC_SuppACH() As SuppACHRec
Set AC_SuppACH = pSuppACHREC
End Property
Public Property Let AC_SuppACH(ByVal newSuppACH_Record As SuppACHRec)
Set pSuppACHREC = newSuppACH_Record
End Property
Public Property Get AC_SuppCCR() As SuppCCRRec
Set AC_SuppCCR = pSuppCCRRec
End Property
Public Property Let AC_SuppCCR(ByVal newSuppCCR_Record As SuppCCRRec)
Set pSuppCCRRec = newSuppCCR_Record
End Property
Public Property Get AC_SuppCHK() As SuppCHKRec
Set AC_SuppCHK = pSuppCHKRec
End Property
Public Property Let AC_SuppCHK(ByVal newSuppCHK_Record As SuppCHKRec)
Set pSuppCHKRec = newSuppCHK_Record
End Property
Public Property Get DocumentDeliveryRecord() As DocumentDeliveryRecord
Set DocumentDeliveryRecord = pDocumentDelieveryRec
End Property
Public Property Let DocumentDeliveryRecord(ByVal newDocumentDeliveryRecord As DocumentDeliveryRecord)
Set pDocumentDelieveryRec = newDocumentDeliveryRecord
End Property
Public Property Get InvoiceRecords() As InvoiceRecords
Set InvoiceRecords = pInvoiceRecords
End Property
Public Property Let InvoiceRecords(ByVal newInvoiceRecord As InvoiceRecords)
Set pInvoiceRecords = newInvoiceRecord
End Property
Collection Class
Option Explicit
Private pHeaderRec As New HeaderRec
Private pNewPayRecords As New PayRecords
Public pObjCol As Collection
Private pTrailerRec As New TrailerRec
Private Sub Class_Initialize()
Set pObjCol = New Collection
End Sub
Private Sub Class_Terminate()
Set pObjCol = Nothing
End Sub
Public Property Get HD_HeaderRecord() As HeaderRec
Set HD_HeaderRecord = pHeaderRec
End Property
Public Property Let HD_HeaderRecord(ByVal newHeaderRecord As HeaderRec)
Set pHeaderRec = newHeaderRecord
End Property
Sub Add(ByVal newPayRecs As PayRecords)
pObjCol.Add newPayRecs
End Sub
Property Get Count() As Long
Count = pObjCol.Count
End Property
Public Property Get TR_TrailerRecord() As TrailerRec
Set TR_TrailerRecord = pTrailerRec
End Property
Public Property Let TR_TrailerRecord(ByVal newTrailer_Record As TrailerRec)
Set pTrailerRec = newTrailer_Record
End Property
I'm sorry if this doesn't help, because your explanation is hard to follow. But, I'll assume that you are saying that you have an object of type Payrecords, which contains references to seven other objects of types PNAR_OP, PNAR_RP, etc. Each of these latter objects contain "20-30 fields" that you want to get at. You ask how to loop through all of these.
A simple way to do that is to use an array. Yes, you can foreach through Collections or (better yet) Dictionaries, but arrays work, they're easy to understand, and they were iterating through objects when Collections were running around in diapers.
Let your Payrecords have a property of type Object(6). When you initialize it, instantiate one of each of the seven objects and add it to the array (for example, "Set myPayrecordsObjects(3) = New SubCCRRec" and so on). To loop through, just use a for next loop to loop through the 7 objects.
Since you provide no information about how you structure your "fields" within these objects, I'll recommend that you iterate through the Fields collection of the ADO object to loop through those. (If you're not using the ADO Fields collection, well, your attention to detail gets mine in return.)