dim Pull data from lower variable to higher - vb.net

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"

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.

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

Returning Class Object from Inherited class

I'm trying to teach myself reflection and have been googling but I can't wrap my head around it entirely. I created a class called DataClass which contains a method called GetClassFromDB as you can see below, which will be inherited from multiple classes.
What I am attempting to do is have my dataclass read the TableName property that is defined within objResults. Once I pull in the tablename from objResults I would query the SQL database for a dataset. Once I have the dataset I would create a new object of the same TYPE inheriting this class (Which will be different types) and populate it from the dataset. Once I have the newly populated class I will return it for use.
I believe I have gotten most of the way there properly (Please correct me if there is a better way), but my real question is this. How can I create a new class of the type thats deriving that class from that string name that I getting in my code, or the type. I would want to have all the accessible properties from objResults available.
Namespace MyApp
Public Class DataClass
Private _TableName As String
Private _Name As String
Overridable ReadOnly Property TableName As String
Get
Return _TableName
End Get
End Property
Public Overloads Function GetClassFromDB() As Object
Try
Dim BaseObject As New Object
'Get the object name
Dim objName As String = MyBase.GetType().Name
'Gets the type thats calling this method
Dim objDerived As Type = MyBase.GetType()
'Get the property info to request the tablename from the derived class
Dim TableName As PropertyInfo = objDerived.GetProperty("TableName")
Dim TableNameString As String = TableName.GetValue(Me, Nothing).ToString
'Once I get the table name from objResults I can perform the SQL
Dim QueryResults as DataSet = SQLiteCLass.Query("Select * FROM TableNameString")
'Once I get data from the SQL I want to create a new object of the type deriving this method.
'In this example is objResults
Dim NewObject as objDerived
'Now I can fill my new object with the results and return it as an object
'THIS IS MY QUESTION - How can I create a new object of the TYPE that I receive from Reflection
Return False
Catch ex As Exception
Return False
End Try
End Function
End Class
End Namespace
and this is a sample class that would inherit my dataclass.
Public Class objResults
Inherits MyApp.DataClass
Private _GameID As Guid
Public Property GameID As Guid
Get
Return _GameID
End Get
Set(ByVal value As Guid)
_GameID = value
End Set
End Property
Public Overrides ReadOnly Property TableName As String
Get
Return "This is my tablename"
End Get
End Property
End Class
and this is how I would use this in code.
Dim objResult as New objResults
Dim TodaysResult as objResultsCollection
TodaysResult = objResult.GetClassFromDB()

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.)

Pass arguments to Constructor in VBA

How can you construct objects passing arguments directly to your own classes?
Something like this:
Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)
Not being able to do this is very annoying, and you end up with dirty solutions to work this around.
Here's a little trick I'm using lately and brings good results. I would like to share with those who have to fight often with VBA.
1.- Implement a public initiation subroutine in each of your custom classes. I call it InitiateProperties throughout all my classes. This method has to accept the arguments you would like to send to the constructor.
2.- Create a module called factory, and create a public function with the word "Create" plus the same name as the class, and the same incoming arguments as the constructor needs. This function has to instantiate your class, and call the initiation subroutine explained in point (1), passing the received arguments. Finally returned the instantiated and initiated method.
Example:
Let's say we have the custom class Employee. As the previous example, is has to be instantiated with name and age.
This is the InitiateProperties method. m_name and m_age are our private properties to be set.
Public Sub InitiateProperties(name as String, age as Integer)
m_name = name
m_age = age
End Sub
And now in the factory module:
Public Function CreateEmployee(name as String, age as Integer) as Employee
Dim employee_obj As Employee
Set employee_obj = new Employee
employee_obj.InitiateProperties name:=name, age:=age
set CreateEmployee = employee_obj
End Function
And finally when you want to instantiate an employee
Dim this_employee as Employee
Set this_employee = factory.CreateEmployee(name:="Johnny", age:=89)
Especially useful when you have several classes. Just place a function for each in the module factory and instantiate just by calling factory.CreateClassA(arguments), factory.CreateClassB(other_arguments), etc.
EDIT
As stenci pointed out, you can do the same thing with a terser syntax by avoiding to create a local variable in the constructor functions. For instance the CreateEmployee function could be written like this:
Public Function CreateEmployee(name as String, age as Integer) as Employee
Set CreateEmployee = new Employee
CreateEmployee.InitiateProperties name:=name, age:=age
End Function
Which is nicer.
I use one Factory module that contains one (or more) constructor per class which calls the Init member of each class.
For example a Point class:
Class Point
Private X, Y
Sub Init(X, Y)
Me.X = X
Me.Y = Y
End Sub
A Line class
Class Line
Private P1, P2
Sub Init(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
If P1 Is Nothing Then
Set Me.P1 = NewPoint(X1, Y1)
Set Me.P2 = NewPoint(X2, Y2)
Else
Set Me.P1 = P1
Set Me.P2 = P2
End If
End Sub
And a Factory module:
Module Factory
Function NewPoint(X, Y)
Set NewPoint = New Point
NewPoint.Init X, Y
End Function
Function NewLine(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
Set NewLine = New Line
NewLine.Init P1, P2, X1, Y1, X2, Y2
End Function
Function NewLinePt(P1, P2)
Set NewLinePt = New Line
NewLinePt.Init P1:=P1, P2:=P2
End Function
Function NewLineXY(X1, Y1, X2, Y2)
Set NewLineXY = New Line
NewLineXY.Init X1:=X1, Y1:=Y1, X2:=X2, Y2:=Y2
End Function
One nice aspect of this approach is that makes it easy to use the factory functions inside expressions. For example it is possible to do something like:
D = Distance(NewPoint(10, 10), NewPoint(20, 20)
or:
D = NewPoint(10, 10).Distance(NewPoint(20, 20))
It's clean: the factory does very little and it does it consistently across all objects, just the creation and one Init call on each creator.
And it's fairly object oriented: the Init functions are defined inside the objects.
EDIT
I forgot to add that this allows me to create static methods. For example I can do something like (after making the parameters optional):
NewLine.DeleteAllLinesShorterThan 10
Unfortunately a new instance of the object is created every time, so any static variable will be lost after the execution. The collection of lines and any other static variable used in this pseudo-static method must be defined in a module.
When you export a class module and open the file in Notepad, you'll notice, near the top, a bunch of hidden attributes (the VBE doesn't display them, and doesn't expose functionality to tweak most of them either). One of them is VB_PredeclaredId:
Attribute VB_PredeclaredId = False
Set it to True, save, and re-import the module into your VBA project.
Classes with a PredeclaredId have a "global instance" that you get for free - exactly like UserForm modules (export a user form, you'll see its predeclaredId attribute is set to true).
A lot of people just happily use the predeclared instance to store state. That's wrong - it's like storing instance state in a static class!
Instead, you leverage that default instance to implement your factory method:
[Employee class]
'#PredeclaredId
Option Explicit
Private Type TEmployee
Name As String
Age As Integer
End Type
Private this As TEmployee
Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As Employee
With New Employee
.Name = emplName
.Age = emplAge
Set Create = .Self 'returns the newly created instance
End With
End Function
Public Property Get Self() As Employee
Set Self = Me
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Age() As String
Age = this.Age
End Property
Public Property Let Age(ByVal value As String)
this.Age = value
End Property
With that, you can do this:
Dim empl As Employee
Set empl = Employee.Create("Johnny", 69)
Employee.Create is working off the default instance, i.e. it's considered a member of the type, and invoked from the default instance only.
Problem is, this is also perfectly legal:
Dim emplFactory As New Employee
Dim empl As Employee
Set empl = emplFactory.Create("Johnny", 69)
And that sucks, because now you have a confusing API. You could use '#Description annotations / VB_Description attributes to document usage, but without Rubberduck there's nothing in the editor that shows you that information at the call sites.
Besides, the Property Let members are accessible, so your Employee instance is mutable:
empl.Name = "Jane" ' Johnny no more!
The trick is to make your class implement an interface that only exposes what needs to be exposed:
[IEmployee class]
Option Explicit
Public Property Get Name() As String : End Property
Public Property Get Age() As Integer : End Property
And now you make Employee implement IEmployee - the final class might look like this:
[Employee class]
'#PredeclaredId
Option Explicit
Implements IEmployee
Private Type TEmployee
Name As String
Age As Integer
End Type
Private this As TEmployee
Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As IEmployee
With New Employee
.Name = emplName
.Age = emplAge
Set Create = .Self 'returns the newly created instance
End With
End Function
Public Property Get Self() As IEmployee
Set Self = Me
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Age() As String
Age = this.Age
End Property
Public Property Let Age(ByVal value As String)
this.Age = value
End Property
Private Property Get IEmployee_Name() As String
IEmployee_Name = Name
End Property
Private Property Get IEmployee_Age() As Integer
IEmployee_Age = Age
End Property
Notice the Create method now returns the interface, and the interface doesn't expose the Property Let members? Now calling code can look like this:
Dim empl As IEmployee
Set empl = Employee.Create("Immutable", 42)
And since the client code is written against the interface, the only members empl exposes are the members defined by the IEmployee interface, which means it doesn't see the Create method, nor the Self getter, nor any of the Property Let mutators: so instead of working with the "concrete" Employee class, the rest of the code can work with the "abstract" IEmployee interface, and enjoy an immutable, polymorphic object.
Using the trick
Attribute VB_PredeclaredId = True
I found another more compact way:
Option Explicit
Option Base 0
Option Compare Binary
Private v_cBox As ComboBox
'
' Class creaor
Public Function New_(ByRef cBox As ComboBox) As ComboBoxExt_c
If Me Is ComboBoxExt_c Then
Set New_ = New ComboBoxExt_c
Call New_.New_(cBox)
Else
Set v_cBox = cBox
End If
End Function
As you can see the New_ constructor is called to both create and set the private members of the class (like init) only problem is, if called on the non-static instance it will re-initialize the private member. but that can be avoided by setting a flag.
First, here is a very quick summary/comparison of the baseline approach and the top three answers.
The baseline approach: These are the basic ways to construct new instances of a class:
Dim newEmployee as Employee
Dim newLunch as Lunch
'==Very basic==
Set newEmployee = new Employee
newEmployee.Name = "Cam"
newEmployee.Age = 42
'==Use a method==
Set newLunch = new Lunch
newLunch.Construct employeeName:= "Cam" food:="Salad", drink:="Tea"
Above, Construct would be a sub in the Lunch class that assigns the parameter values to an object.
The issue is that even with a method, it took two lines, first to set the new object, and second to fill the parameters. It would be nice to do both in one line.
1) The Factory class (bgusach): Make a separate class ("Factory"), with methods to create instances of any other desired classes including set-up parameters.
Possible use:
Dim f as Factory 'a general Factory object
Dim newEmployee as Employee
Dim newLunch as Lunch
Set f = new Factory
Set newEmployee = f.CreateEmployee("Bob", 25)
Set newLunch = f.CreateLunch("Bob", "Sandwich", "Soda")
When you type "f." in the code window, after you Dim f as Factory, you see a menu of what it can create via Intellisense.
2) The Factory module (stenci): Same, but instead of a class, Factory can be a standard module.
Possible use:
Dim newEmployee as Employee
Dim newLunch as Lunch
Set newEmployee = CreateEmployee("Jan", 31) 'a function
Set newLunch = CreateLunch("Jan", "Pizza", "JuiceBox")
In other words, we just make a function outside the class to create new objects with parameters. This way, you don't have to create or refer to a Factory object. You als don't get the as-you-type intellisense from the general factory class.
3) The Global Instance (Mathieu Guindon): Here we return to using objects to create classes, but sticking to the class-to-be-made. If you modify the class module in an external text editor you can call class methods before creating an object.
Possible use:
Dim newEmployee as Employee
Dim newLunch as Lunch
Set newEmployee = newEmployee.MakeNew("Ace" 50)
Set newLunch = newLunch.MakeNew("Ace", "Burrito", "Water")
Here MakeNew is a function like CreateEmployee or CreateLunch in the general factory class, except that here it is in the class-to-be-made, and so we don't have to specify what class it will make.
This third approach has a fascinating "created from itself" appearance to it, permitted by the global instance.
Other ideas: Auto-instancing, a Clone method, or a Parent Collection Class
With auto instancing (Dim NewEmployee as new Employee, note the word "new"), you can achieve something similar to the global instance without the setup process:
Dim NewEmployee as new Employee
NewEmployee.Construct("Sam", 21)
With "new" in the Dim statement, the NewEmployee object is created as an implied pre-step to calling its method. Construct is a Sub in the Employee class, just like in the baseline approach.[1]
There are some issues with auto instancing; some hate it, a few defend it.2 To restrict auto-instancing to one proto-object, you could add a MakeNew function to the class as I used with the Global Instance approach, or revise it slightly as Clone:
Dim protoEmployee as new Employee 'with "new", if you like
'Add some new employees to a collection
Dim someNames() as Variant, someAges() as Variant
Dim someEmployees as Collection
someNames = array("Cam", "Bob", "Jan", "Ace")
someAges = array(23, 45, 30, 38)
set someEmployees = new Collection
for i = 0 to 3
someEmployees.Add protoEmployee.Clone(someNames(i), someAges(i))
next
Here, the Clone method could be set up with optional parameters Function Clone(optional employeeName, optional employeeAge)and use the properties of the calling object if none are supplied.
Even without auto-instancing, a MakeNew or Clone method within the class itself can create new objects in one line, once you create the proto-object. You could use auto-instancing for a general factory object in the same way, to save a line, or not.
Finally, you might want a parent class. A parent class could have methods to create new children with parameters (e.g., with Employees as a custom collection, set newEmployee = Employees.AddNew(Tom, 38)). For a lot of objects in Excel this is standard: you can't create a Worksheet or a Workbook except from its parent collection.
[1]One other adjustment relates to whether the Construct method is a Sub or a Function. If Construct is called from an object to fill in its own properties, it can be a Sub with no return value. However, if Construct returns Me after filling in the parameters, then the Factory methods/functions in the top 2 answers could leave the parameters to Construct. For example, using a factory class with this adjustment could go: Set Sue = Factory.NewEmployee.Construct("Sue", "50"), where NewEmployee is a method of Factory that returns a blank new Employee, but Construct is a method of Employee that assigns the parameters internally and returns Me.
Another approach
Say you create a class clsBitcoinPublicKey
In the class module create an ADDITIONAL subroutine, that acts as you would want the real constructor to behave. Below I have named it ConstructorAdjunct.
Public Sub ConstructorAdjunct(ByVal ...)
...
End Sub
From the calling module, you use an additional statement
Dim loPublicKey AS clsBitcoinPublicKey
Set loPublicKey = New clsBitcoinPublicKey
Call loPublicKey.ConstructorAdjunct(...)
The only penalty is the extra call, but the advantage is that you can keep everything in the class module, and debugging becomes easier.
Why not this way:
In a class module »myClass« use Public Sub Init(myArguments) instead of Private Sub Class_Initialize()
Instancing:
Dim myInstance As New myClass: myInstance.Init myArguments