Create Instance of Class with String VBA [duplicate] - vba

Is there a way to set an object to the new instance of a class by using the text name of the class?
I will have a library of classes, and depending on some other variable, I want to get one of these classes at runtime.
E.g. I have "CTest1", "CTest2", "CTest3"
I would have function similar to the below
Function GetTestClass(lngClassNo as long) as Object
Dim strClassName as String
strClassName = "CTest" & CStr(lngClassNo)
Set GetTestClass = New instance of class(strClassName)
End Function

CallByName function can help you. Let's say there are some class modules in your project: clsSample0, clsSample1 and clsSample2. Add a new class module named clsSpawner, which lists all target classes as public variables having the same names, and declared with New keyword:
Public clsSample0 As New clsSample0
Public clsSample1 As New clsSample1
Public clsSample2 As New clsSample2
In a standard module add Function Spawn() code:
Function Spawn(sClassName) As Object
Set Spawn = CallByName(New clsSpawner, sClassName, VbGet)
End Function
Test it with some code like this:
Sub TestSpawn()
Dim objSample0a As Object
Dim objSample0b As Object
Dim objSample1 As Object
Dim objSample2 As Object
Set objSample0a = Spawn("clsSample0")
Set objSample0b = Spawn("clsSample0")
Set objSample1 = Spawn("clsSample1")
Set objSample2 = Spawn("clsSample2")
Debug.Print TypeName(objSample0a) ' clsSample0
Debug.Print TypeName(objSample0b) ' clsSample0
Debug.Print objSample0a Is objSample0b ' False
Debug.Print TypeName(objSample1) ' clsSample1
Debug.Print TypeName(objSample2) ' clsSample2
End Sub
How does it work? Spawn function instantiates clsSpawner and calls the clsSpawner instance to return requested property, and actually clsSpawner instance creates a new instance of the target class due to declaration with New keyword and returns the reference.

There's no reflection in VBA, so I don't think this is possible. You'd have to do something like the following I'm afraid:
Function GetTestClass(lngClassNo as long) as Object
Select Case lngClassNo
Case 1
Set GetTestClass = New CTest1
Case 2
Set GetTestClass = New CTest2
...
End Select
End Function
Unless that is your CTest classes are defined in a COM DLL, in which case you could use the CreateObject statement. You would need to use VB6 to create such a DLL though, you can't create DLLs in Excel, Access, etc.
Function GetTestClass(lngClassNo as long) as Object
Set GetTestClass = CreateObject("MyDll.CTest" & lngClassNo)
End Function

You can use metaprogramming to do this, although it does seem like quite a hack.
Here is an example that uses a couple of helper functions (omitted for brevity):
Public Function CreateInstance(typeName As String) As Object
Dim module As VBComponent
Set module = LazilyCreateMPCache()
If Not FunctionExists(typeName, module) Then
Call AddInstanceCreationHelper(typeName, module)
End If
Dim instanceCreationHelperName As String
instanceCreationHelperName = module.name & ".GetInstanceOf" & typeName
Set CreateInstance = Application.Run(instanceCreationHelperName)
End Function
Sub AddInstanceCreationHelper(typeName As String, module As VBComponent)
Dim strCode As String
strCode = _
"Public Function GetInstanceOf" & typeName & "() As " & typeName & vbCrLf & _
"Set GetInstanceOf" & typeName & " = New " & typeName & vbCrLf & _
"End Function"
Call AddFunction(strCode, module)
End Sub

VB class definitions are really defining COM interfaces behind the scenes, so one can define data types as an abstract interface definition with concrete implementations using the implements keyword.
To get any sort of polymorphism you have to do this, otherwise you will have problems with casting. It is somewhat fiddly but technically possible to do this with VB. If you want to dig into it find some of the advanced VB books by Dan Appleman or Matthew Kurland. I'm not sure if they're still in print but they're probably available through Amazon Marketplace.
This works with VB6 and I'm fairly sure it works with VBA.

You might be able to do it with a collection class or object array. All the objects are in one array.
In your class have a .Name property and when you create an instance of it do this:
Dim CTest() as New CTest
For n = 1 to 10
Redim Preserve CTest(n)
CTest(n).Name = "CTest" & CStr(n)
Next l
Quick and dirty. The above example would return 10 CTest objects in a single object array. You could also ditch the .Name and just use CTest(n).

Related

How to make class variables persist between modules in 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.

How to reference a global variable in 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

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.

How to harness auto-completion of strings?

I'm writing an application in which I have to pass strings as parameters. Like these:
GetValue("InternetGatewayDevice.DeviceInfo.Description")
GetValue("InternetGatewayDevice.DeviceInfo.HardwareVersion")
CheckValue("InternetGatewayDevice.DeviceInfo.Manufacturer")
ScrambleValue("InternetGatewayDevice.DeviceInfo.ModelName")
DeleteValue("InternetGatewayDevice.DeviceInfo.ProcessStatus.Process.1")
The full list is about 10500 entries, and i tought that i'd be really lost in searching if i misspell something.
So I am trying to declare a namespace for every string segment (separated by ".") and declare the last as a simple class that widens to a String of its FullName (except the base app namespace):
Class xconv
Public Shared Widening Operator CType(ByVal d As xconv) As String
Dim a As String = d.GetType.FullName
Dim b As New List(Of String)(Strings.Split(a, "."))
Dim c As String = Strings.Join(b.Skip(1).ToArray, ".")
Return c
End Operator
End Class
So I'd have these declarations:
Namespace InternetGatewayDevice
Namespace DeviceInfo
Class Description
Inherits xconv
End Class
End Namespace
End Namespace
This way IntelliSense is more than happy to autocomplete that string for me.
Now I'd have to do this for every possible string, so I opted (in order to retain my sanity) to make a method that does that:
Sub Create_Autocomlete_List()
Dim pathlist As New List(Of String)(IO.File.ReadAllLines("D:\list.txt"))
Dim def_list As New List(Of String)
Dim thedoc As String = ""
For Each kl As String In pathlist
Dim locdoc As String = ""
Dim el() As String = Strings.Split(kl, ".")
Dim elc As Integer = el.Length - 1
Dim elz As Integer = -1
Dim cdoc As String
For Each ol As String In el
elz += 1
If elz = elc Then
locdoc += "Class " + ol + vbCrLf + _
"Inherits xconv" + vbCrLf + _
"End Class"
Else
locdoc += "Namespace " + ol + vbCrLf
cdoc += vbCrLf + "End Namespace"
End If
Next
locdoc += cdoc
thedoc += locdoc + vbCrLf + vbCrLf
Next
IO.File.WriteAllText("D:\start_list_dot_net.txt", thedoc)
End Sub
The real problem is that this is HORRIBLY SLOW and memory-intense (now i dot a OutOfMemory Exception), and I have no idea on how Intellisense would perform with the (not available in the near future) output of the Create_Autocomlete_List() sub.
I believe that it would be very slow.
So the real questions are: Am I doing this right? Is there any better way to map a list of strings to auto-completable strings? Is there any "standard" way to do this?
What would you do in this case?
I don't know how Visual Studio is going to perform with thousands of classes, but your Create_Autocomlete_List method can be optimized to minimize memory usage by not storing everything in memory as you build the source code. This should also speed things up considerably.
It can also be simplified, since nested namespaces can be declared on one line, e.g. Namespace First.Second.Third.
Sub Create_Autocomlete_List()
Using output As StreamWriter = IO.File.CreateText("D:\start_list_dot_net.txt")
For Each line As String In IO.File.ReadLines("D:\list.txt")
Dim lastDotPos As Integer = line.LastIndexOf("."c)
Dim nsName As String = line.Substring(0, lastDotPos)
Dim clsName As String = line.Substring(lastDotPos + 1)
output.Write("Namespace ")
output.WriteLine(nsName)
output.Write(" Class ")
output.WriteLine(clsName)
output.WriteLine(" Inherits xconv")
output.WriteLine(" End Class")
output.WriteLine("End Namespace")
output.WriteLine()
Next
End Using
End Sub
Note the use of File.ReadLines instead of File.ReadAllLines, which returns an IEnumerable instead of an array. Also note that the output is written directly to the file, instead of being built in memory.
Note Based on your sample data, you may run into issues where the last node is not a valid class name. e.g. InternetGatewayDevice.DeviceInfo.ProcessStatus.Process.1 - 1 is not a valid class name in VB.NET. You will need to devise some mechanism to deal with this - maybe some unique prefix that you could strip in your widening operator.
I'm also not sure how usable the resulting classes will be, since presumably you would need to pass an instance to the methods:
GetValue(New InternetGatewayDevice.DeviceInfo.Description())
It seems like it would be nicer to have Shared strings on a class:
Namespace InternetGatewayDevice
Class DeviceInfo
Public Shared Description As String = "Description"
Public Shared HardwareVersion As String = "HardwareVersion"
' etc.
End Class
End Namespace
So you could just reference those strings:
GetValue(InternetGatewayDevice.DeviceInfo.Description)
However, I think that would be a lot harder to generate without creating name clashes due to the various levels of nesting.

VBA - Using .NET class library

We have a custom class library that has been built from the ground up that performs a variety of functions that are required for the business model in place. We also use VBA to automate some data insertion from standard Microsoft packages and from SolidWorks.
To date we have basically re-written the code in the VBA application macro's, but now are moving to include the class library into the VBA references. We've registered the class library for COM interop, and made sure that it is COM visible. The file is referencable, we have added the <ClassInterface(ClassInterfaceType.AutoDual)> _ tag above each of the Public Classes, so that intellisense 'works'.
With that said, the problem now arises - when we reference the class library, for this instance let's call it Test_Object, it is picked up and seems to work just fine. So we go ahead and try a small sample to make sure it's using the public functions and returning expected values:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim test As New Test_Object.Formatting
Dim t As String
t = test.extractNumber("abc12g3y45")
Target.Value = t
End Sub
This works as expected, returning 12345 in the selected cell/s.
However, when I try a different class, following the exact same procedure, I get an error (Object variable or With block variable not set). Code is as follows:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim test As New Test_Object.SQLCalls
Dim t As String
t = test.SQLNumber("SELECT TOP 1 ID from testdb.dbo.TESTTABLE") 'where the string literal in the parentheses is a parameter that is passed.
Target.Value = t
End Sub
This fails on the t = test.SQLNumber line. It also fails on another function within that SQLCalls class, a function that returns the date in SQL format (so it is not anything to do with the connection to the database).
Can anyone assist in what could be causing this error? I've googled for hours to no avail, and am willing to try whatever it takes to get this working.
Cheers.
EDIT: (added in the .SQLNumber() method)
Function SQLNumber(query As String) As Double
Dim tno As Double
Try
Using SQLConnection As SqlConnection = New SqlConnection(Connection_String_Current)
SQLConnection.Open()
SQLCommand = New SqlCommand(query, SQLConnection)
tno = SQLCommand.ExecuteScalar
End Using
Catch ex As System.Exception
MsgBox(ex.Message)
End Try
Return tno
End Function
For comparison, the extractNumber() method:
Function extractNumber(extstr As String) As Double
Dim i As Integer = 1
Dim tempstr As String
Dim extno As String = ""
Do Until i > Len(extstr)
tempstr = Mid(extstr, i, 1)
If tempstr = "0" Or tempstr = "1" Or tempstr = "2" Or tempstr = "3" Or tempstr = "4" Or tempstr = "5" Or tempstr = "6" Or tempstr = "7" Or tempstr = "8" Or tempstr = "9" Or tempstr = "." Then
extno = extno & tempstr
End If
i = i + 1
Loop
If IsNumeric(extno) Then
Return CDbl(extno)
Else
Return 0
End If
End Function
With the help of vba4all, we managed to delve down right to the issue.
When I tried to create a new instance of an object using Dim x as new Test_Object.SQLCalls, I was completely oblivious to the fact that I had not re-entered this crucial line:
<ClassInterface(ClassInterfaceType.None)> _.
Prior to doing this, I had this in my object explorer which has both the ISQLCalls and SQLCalls in the Classes section
But wait, ISQLCalls isn't a class, it's an interface!
By entering the <ClassInterface(ClassInterfaceType.None)> _ back in the SQLCalls class, the object explorer looked a bit better:
And low and behold, I could now create a new instance of the class, and the methods were exposed.
tldr:
I needed to explicitly declare the interface and use <InterfaceType(ComInterfaceType.InterfaceIsDual)> on the interface and <ClassInterface(ClassInterfaceType.None)> on the class.
Many thanks to vba4all, who selflessly devoted their time to assist in this issue.