How to reference a global variable in VBA - vba

In Access VBA I know you can reference an object by using a string: Me("string"). I want to reference a global variable in a function using a string, thus allowing me to use the function for different states.
e.g. I have invoiceBtn and infoBtn buttons, I want to call the same function but using different variables. There are global variables infoBool and invoiceBool
Private Sub infoBtn_Click()
functionName("info")
End Sub
Private Sub invoiceBtn_Click()
functionName("invoice")
End Sub
in the function:
public infoBool As Boolean
public invoiceBool As Boolean
public Function functionName(typeString As String)
Me(typeString & "Bool") = false
Me(typeString & "Btn").visible = false
End Function
The first Me() doesn't compile, the second Me() does, is there a way to obtain a reference to a variable using a string?
edited: I hope it is clear enough now. This is just example code, not used in my program.

In some cases you can try to use Eval function for evaluating functions, but it doesn't work with variables.
You can use collection for storing values or references to objects, so in this case your code will look like
Public col As Collection
Public Function functionName(typeString As String)
'for scalar data types element should be replaced, cannot change value
col.Remove typeString & "Bool"
col.Add False, typeString & "Bool"
'object references can be used directly
col(typeString & "Btn").Visible = False
End Function
Before using this you should add all desired values or object references
Set col = New Collection
col.Add True, "infoBool"
col.Add Me.InfoBtn, Me.InfoBtn.Name

Related

Type mismatch trying to set data in an object in a collection

I am getting Runtime Error 13 when trying to update an object stored in a collection. Here is a minimal example.
The class (Class2) of the objects to be stored in the collection.
Option Explicit
Private pHasA As Boolean
Private pHasB As Boolean
Private pSomeRandomID As String
Property Get HasA() As Boolean
HasA = pHasA
End Property
Property Get HasB() As Boolean
HasB = pHasB
End Property
Property Let HasA(propValue As Boolean)
pHasA = propValue
End Property
Property Let HasB(propValue As Boolean)
pHasB = propValue
End Property
Property Let RandomID(propValue As String)
pSomeRandomID = propValue
End Property
Sub SetHasValues(key As String)
Select Case key
Case "A"
pHasA = True
Case "B"
pHasB = True
End Select
End Sub
Minimal code that reproduces the error:
Option Explicit
Private Sub TestCollectionError()
Dim classArray As Variant
Dim classCollection As Collection
Dim singleClass2Item As Class2
Dim iterator As Long
classArray = Array("A", "B", "C")
Set classCollection = New Collection
For iterator = LBound(classArray) To UBound(classArray)
Set singleClass2Item = New Class2
singleClass2Item.RandomID = classArray(iterator)
classCollection.Add singleClass2Item, classArray(iterator)
Next iterator
Debug.Print "Count: " & classCollection.Count
singleClass2Item.SetHasValues "A" ' <-- This code works fine.
Debug.Print "New Truth values: " & singleClass2Item.HasA, singleClass2Item.HasB
For iterator = LBound(classArray) To UBound(classArray)
classCollection(classArray(iterator)).RandomID = classArray(iterator)
classCollection(classArray(iterator)).SetHasValues classArray(iterator) '<-- Type mismatch on this line.
Next iterator
'***** outputs
'''Count: 3
'''New Truth values: True False
' Error dialog as noted in the comment above
End Sub
While the code above appears a little contrived, it is based on some real code that I am using to automate Excel.
I have searched for answers here (including the following posts), but they do not address the simple and non-ambiguous example that I have here. The answers that I have found have addressed true type mismatches, wrong use of indexing or similar clear answers.
Retrieve items in collection (Excel, VBA)
Can't access object from collection
Nested collections, access elements type mismatch
This is caused by the fact, that the parameter of your procedure SetHasValues is implicitely defined ByRef.
Defining it ByVal will fix your problem.
#ADJ That's annoying, but perhaps the example below will allow you to start making a case for allowing RubberDuck.
I've upgraded your code using ideas and concepts I've gained from the rubberduck blogs. The code now compiles cleanly and is (imho) is less cluttered due to fewer lookups.
Key points to note are
Not relying on implicit type conversions
Assigning objects retrieved from collections to a variable of the type you are retrieving to get access to intellisense for the object
VBA objects with true constructors (the Create and Self functions in class2)
Encapsulation of the backing variables for class properties to give consistent (and simple) naming coupled with intellisense.
The code below does contain Rubberduck Annotations (comments starting '#)
Updated Class 2
Option Explicit
'#Folder("StackOverflowExamples")
'#PredeclaredId
Private Type Properties
HasA As Boolean
HasB As Boolean
SomeRandomID As String
End Type
Private p As Properties
Property Get HasA() As Boolean
HasA = p.HasA
End Property
Property Get HasB() As Boolean
HasB = p.HasB
End Property
Property Let HasA(propValue As Boolean)
p.HasA = propValue
End Property
Property Let HasB(propValue As Boolean)
p.HasB = propValue
End Property
Property Let RandomID(propValue As String)
p.SomeRandomID = propValue
End Property
Sub SetHasValues(key As String)
Select Case key
Case "A"
p.HasA = True
Case "B"
p.HasB = True
End Select
End Sub
Public Function Create(ByVal arg As String) As Class2
With New Class2
Set Create = .Self(arg)
End With
End Function
Public Function Self(ByVal arg As String) As Class2
p.SomeRandomID = arg
Set Self = Me
End Function
Updated test code
Private Sub TestCollectionError()
Dim classArray As Variant
Dim classCollection As Collection
Dim singleClass2Item As Class2
Dim my_item As Variant
Dim my_retrieved_item As Class2
classArray = Array("A", "B", "C")
Set classCollection = New Collection
For Each my_item In classArray
classCollection.Add Item:=Class2.Create(my_item), key:=my_item
Next
Debug.Print "Count: " & classCollection.Count
Set singleClass2Item = classCollection.Item(classCollection.Count)
Debug.Print "Initial Truth values: " & singleClass2Item.HasA, singleClass2Item.HasB
singleClass2Item.SetHasValues "A" ' <-- This code works fine.
Debug.Print "New Truth values: " & singleClass2Item.HasA, singleClass2Item.HasB
For Each my_item In classArray
Set my_retrieved_item = classCollection.Item(my_item)
my_retrieved_item.RandomID = CStr(my_item)
my_retrieved_item.SetHasValues CStr(my_item)
Next
End Sub
The 'Private Type Properties' idea comes from a Rubberduck article encapsulating class variable in a 'This' type. My take on this idea is to use two type variable p and s (Properties and State) where p holds the backing variables to properties and s hold variables which represent the internal state of the class. Its not been necessary to use the 'Private Type State' definition in the code above.
VBA classes with constructors relies on the PredeclaredID attribute being set to True. You can do this manually by removing and saving the code, using a text editor to set the attributer to 'True' and then reimporting. The RUbberDuck attribute '#PredeclaredId' allows this to be done automatically by the RubberDuck addin. IN my own code the initialiser for class2 would detect report an error as New should not be used when Classes are their own factories.
BY assigning and intermediate variable when retrieving an object from a class (or even a variant) you give Option Explicit the best change for letting you n=know of any errors.
An finally the Rubberduck Code Inspection shows there are still some issues which need attention

Class property as ByRef argument not working

I am using a function to modify a series of strings, passing them ByRef as arguments to the modifying function. The caller's string variables are all modified as expected but the one argument which is a class property does not change, should this be possible?
The essentials of the class are:-
Private cRptRef As String
Public Property Get TestRefID() As String
TestRefID = cRptRef
End Property
Public Property Let TestRefID(Test_Ref As String)
cRptRef = Test_Ref
End Property
The function for modifying the strings has the following declaration
Public Function GetTestFileNames(ByRef hdrFile As String, _
ByRef calFile As String, _
ByRef dataFile As String, _
ByRef testRef As String _
) As Boolean
The call to GetTestFilenames is as follows:
If GetTestFileNames(HEADERpath, CALpath, RAWDATApath, _
ref) = False Then
All the string arguments are declared as global strings and are empty ("") before the call. After the call they typicaly have content like "d:{path to file{filename.csv}.
So all these statements in the function populate the target strings OK.
hdrFile = Replace(userFile, "##", PT_Rpt.Info.FindNode(TEST_REF_HDRsuffix).data, , , vbTextCompare)
dataFile = Replace(userFile, "##", PT_Rpt.Info.FindNode(TEST_REF_DATAsuffix).data, , , vbTextCompare)
calFile = Replace(userFile, "##", PT_Rpt.Info.FindNode(TEST_REF_CALsuffix).data, , , vbTextCompare)
But this statement fails to assign anything to its target string
testRef = Mid(userFile, InStrRev(userFile, Application.PathSeparator) + 1)
testRef = Left(testRef, InStrRev(testRef, "_") - 1)
Debug.Print "Class.TestRefID="; testRef
The Debug.Print statement prints the expected string, but the external reference is not affected. Is this something to do with it being a property?
The target string being Class.TestRefID in place of the testRef argument.
If I replace the class property in the argument list with a standard string variable, and then assign that to the class property then it I get the expected result, which seems unnecessary work.
Is there something I'm missing or is this not possible in VBA?
A member access expression is an expression that must first be evaluated by VBA before its result can be passed around.
If I have a Class1 module like this:
Option Explicit
Public Foo As String
And then a quick caller procedure:
Sub test()
With New Class1
bla .Foo
Debug.Print .Foo
End With
End Sub
Sub bla(ByRef bar As String)
bar = "huh"
End Sub
The test procedure will output an empty string.
The reason for this is because when you pass a member to a ByRef parameter of a procedure in VBA, you're not passing a reference to the member - you're passing a reference to the value held by that member.
So the member access expression is evaluated, evaluates to "", so "" is passed ByRef to the procedure, which assigns it to "huh", but the caller isn't holding a reference to the "" value, so it never gets to see the "huh" string that was assigned.
If I replace the class property in the argument list with a standard string variable, and then assign that to the class property then it I get the expected result, which seems unnecessary work
It's not unnecessary work, it's mandated, otherwise nothing is holding a reference to the result of the member expression.
Then again the real problem is a design issue, pointed out by Warcupine: the function doesn't want to byref-return 4 values, it wants to take a reference to this object, and assign its properties.

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.

Using a function to clean data in VBA

I am familiar with this post: How to Return a result from a VBA Function but changing my code does not seem to help.
I want to write a simple function in VBA that allows to lowercase an input sentence. I wrote this:
Private Function Converter(inputText As String) As String
Converter = LCase(inputText)
End Function
Sub test()
Dim new_output As String
new_output = Converter("Henk")
MsgBox (new_output)
End Sub
I tried following the advice I found at another stackoverflow post. I made me change this:
Private Function Converter(inputText As String)
Set outputText = LCase(inputText)
End Function
Sub test()
Dim new_output As String
Set new_output = Converter("Henk")
MsgBox (new_output)
End Sub
However, now I get an error that an object is required. Why does it require an object now? I dont get it...
Set outputText = LCase(inputText)
The Set keyword is reserved for Object data types. Unlike VB.NET, String in VBA is a basic data types.
So you dont Set a variable to a string. Drop the second version of your code altogether. It doesn't make sense. That "advice" was probably in another context.
To fix your first version
1- Assign the returned result to the name of the function Converter
2- It would be beneficial to specify explicitly the return type, as String. Currently it is a Variant that always embeds a String, so better make it explicit:
' vvvvvvvvv
Private Function Converter(inputText As String) As String
Converter = LCase(inputText) ' <------------ assign return to name of function
End Function

Array of variables declared as string

I have a query regarding creating an array of variables declared as string.
Below is my code. On debugging, the variables show no value.
Need help..
Module Module1
Public Status, PartStat, HomeStat, ClampStat, SldCylStat, PrsCylP1Stat,
PrsCylP2Stat, PrsCylP3Stat, PrsCylP4Stat, PunchStat, SysInProc, Home1,
Home2, Home3, CyclTim, TrqP1Stat, TrqP2Stat, TrqP3Stat, TrqP4Stat,
AngleP1Stat, AngleP2Stat, AngleP3Stat, AngleP4Stat As String
Function AutoReadStatus()
Dim StatArray = {HomeStat, ClampStat, SldCylStat, Home1, PrsCylP4Stat,
PrsCylP2Stat, Home2, PrsCylP3Stat, PrsCylP1Stat, Home3, PunchStat,
AngleP4Stat, AngleP2Stat, AngleP3Stat, AngleP1Stat, TrqP4Stat,
TrqP2Stat, TrqP3Stat, TrqP1Stat}
Status = ReadMultiReg(FormAuto.SP1, "03", "1258", "0013")
For i = 0 To ((Status.Length / 4) - 1)
StatArray(i) = CInt("&H" & Status.Substring(i * 4, 4))
Next
Return Nothing
End Function
End Module
It is not even showing the index of any variable from above array..
Label1.Text = Array.IndexOf(StatArray, SldCylStat)
When you assign a new value to the item inside the array, you assign a new value to the item inside the array (pun intended).
What that means is that item's array now reference the string (or rather the Integer implicitely converted to string as you don't have Option Strict On) you gave and the precedent reference (on your public field) is dropped.
Test this sample code and I think you will understand
Public item As String
Sub Test()
Dim array = {item}
Console.WriteLine(array(0) Is item) ' True
array(0) = "new value"
Console.WriteLine(array(0) Is item) ' False
End Sub
You can see now array(0) reference another object than the one referenced by the item field
As for how to solve it,yYou could pass all those string ByRef that way assignment inside the method would reflect outside of it but that would be tedious.
A "better" way IMO, would be to make a type (a Class) to hold all those string and pass an instance of that type to your method, that way you just mutate the same existing object.
Quick, contrived example :
Class SomeType
Property Item As String
End Class
Sub Test(instance As SomeType)
instance.Item = "new value"
End Sub
' Usage
Dim sample As New SomeType
' here sample.Item is Nothing
Test(sample)
' here sample.Item is "new value"