Managing Collections inside a Class - excel

i'm trying to work with classes in vba and i haven't found any documentation related to managing objects inside objects such as arrays or collections inside a class.
Let's say i have a class person and a class address and i want to manage addresses of a person.
Address
Private pStreet as String
Private pZip as Int
Public Property Let Street(val As String)
pStreet = val
End Property
Public Property Get Street() As String
Street = pStreet
End Property
Public Property Let Zip(val As String)
pZip = val
End Property
Public Property Get Zip() As String
Zip = pZip
End Property
Person
Private pName As String
Private pSurname As String
Private pAddresses As Collection
Public Property Let Name(val As String)
pName = val
End Property
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Surname(val As String)
pSurname = val
End Property
Public Property Get Surname() As String
Surame = pSurname
End Property
Private Sub Class_Initialize()
Set pAddresses = New Collection
End Sub
Private Sub Class_Terminate()
Set pAddresses = Nothing
End Sub
Public Sub addAddress(ByVal val As Address)
pAddresses.Add val
End Sub
Public Property Get Addresses() As Collection
Set Addresses = pAddresses
End Property
Public Property Get Address(ByVal Index As Long) As Address
Set Address = pAddresses(Index)
End Property
Module1
Sub test()
Dim x As Person
Set x = New Person
Dim a1 As Address
Set a1 = New Address
Dim a2 As Address
Set a2 = New Address
x.Name = "Mark"
x.Surname = "Doe"
a1.Street = "first avenue 213"
a1.Zip = "41242"
a2.Street = "second avenue 213"
a2.Zip = "55242"
x.addAddress a1
x.addAddress a2
Debug.Print x.Address(0)
End Sub
how should i deal with the address collection inside the person class?
for instance, how could i retrieve all the addresses or the second address of the collection? x.addresses(1) doesn't work.

The Person class should hold a collection of addresses, to avoid having to expand and preserve an array. The collection should be initialized/terminated as soon as the class is initialized/terminated.
See an example:
Private pAddresses as Collection
Private Sub Class_Initialize()
Set pAddresses = New Collection
End Sub
Private Sub Class_Terminate()
Set pAddresses = Nothing
End Sub
The class can expose the whole address collection via a property for looping, or a single address accessed by index.
Public Property Get Addresses() As Collection
Set Addresses = pAddresses
End property
Public Property Get Address(ByVal Index As Long) As Address
Set Address = pAddresses(Index)
End property
Then you can loop:
Dim p As Person, a As Address
For Each a In p.Addresses
a.Zip = ...
Next
Or get a single address by index:
Set a = p.Addresses(1)
Lastly, a simple Add method, to add an address to the person:
Public Sub AddAddress(ByVal param As Address)
pAddresses.Add param
End Sub
You could also add (or replace) an address using a property by supplying both the address object and the index, but I dont know how useful it is in your case. Of course you need to make sure Index is valid.
Public Property Let Address(ByVal Index As Long, ByVal param As Address)
pAddresses.Add param, Index
End property
Then to call it:
p.Addresses(Index) = a
To enforce the Set keyword, since we're dealing with objects, change Let to Set. Then, you need to set-it:
Set p.Addresses(Index) = a

Related

Excel vba - class constructor method not working, debug error

i have this class and i am trying to make a class constructor or factory method (not sure how's the right name in VBA). When i try to run it i get a dialog with written debug error, and ig highlights the set row of the test module. What's wrong? What is the right way to instantiate the collection in the constructor? is it better to use the keyword this when using let/get ?
Class Address
Private pStreet As String
Private pZip As Integer
Public Property Let Street(val As String)
pStreet = val
End Property
Public Property Get Street() As String
Street = pStreet
End Property
Public Property Let Zip(val As Integer)
pZip = val
End Property
Public Property Get Zip() As Integer
Zip = pZip
End Property
Class Person
Private pName As String
Private pSurname As String
Private pAddresses As New Collection
Public Property Let Name(val As String)
pName = val
End Property
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Surname(val As String)
pSurname = val
End Property
Public Property Get Surname() As String
Surame = pSurname
End Property
Private Sub Class_Initialize()
Set pAddresses = New Collection
End Sub
Private Sub Class_Terminate()
Set pAddresses = Nothing
End Sub
Public Sub addAddress(ByVal val As Address)
pAddresses.Add val
End Sub
Public Property Get Addresses() As Collection
Set Addresses = pAddresses
End Property
Public Property Get Address(ByVal Index As Long) As Address
Set Address = pAddresses(Index)
End Property
Public Function CreatePerson(ByVal Name As String, ByVal Surname As String) As Person
With New Person
.pName = Name
.pSurname = Surname
Set CreatePerson = .Self
instance
End With
End Function
test Module
sub test()
Dim x as Person
Set x = Person.CreatePerson("Mike","Jordan")
end sub
Another option for creating a factory method is to use another class:
PersonFactory Class
Option Explicit
Public Function Create(ByVal Name As String, ByVal Surname As String, ByVal Street As String, ByVal Zip As Integer) As Person
Dim a As Address
Set Create = New Person
Create.Name = Name
Create.Surname = Surname
Set a = New Address
a.Street = Street
a.Zip = Zip
Create.Addresses.Add a
End Function
Test Module
Private Sub Test()
Dim pf As PersonFactory
Dim p As Person
Set pf = New PersonFactory
Set p = pf.Create("Mike", "Jordan", "my street", 11111)
End Sub
You have several errors
Your anonymous new is for GiantCorp and Not Person
You have no self method to return the Me Instance created by the With New Person
3 No idea what 'instance' is doing.
Your address class does not manage a collection of addresses, nor does you person class
Here is updated code for your person class. Don't feel too bad, Factory classes in VBA are actuall a tricky subject when you first encounter them.
Option Explicit
'#PredecalredId
'#exposed
Private Type Properties
Name As String
Surname As String
Address As Address
End Type
Private p As Properties
Public Property Let Name(ipName As String)
p.Name = ipName
End Property
Public Property Get Name() As String
Name = p.Name
End Property
Public Property Let Surname(ipSurname As String)
p.Surname = ipSurname
End Property
Public Property Get Surname() As String
Surame = p.Surname
End Property
' This property will fail as the Address class is not a collection
Public Sub addAddress(ipAddress As Address)
Set p.Address = ipAddress
End Sub
Public Function CreatePerson(ByVal ipName As String, ByVal ipSurname As String) As Person
With New Person 'GiantComp no idea what this GiantComp' class is doing here
' Private fields cannot be accessed here, you need to forward them to the self function
'.pName = ipName
'.pSurname = ipSurname
Set CreatePerson = .Self(ipName, ipSurname)
End With
End Function
Public Function Self(ByVal ipName As String, ByVal ipSurname As String) As Person
' You are now inside the anonymous Person class you created with 'With nEw Person' so you can now access private fields
p.Name = ipName
p.Surname = ipSurname
Set Self = Me
End Function
You will also need to set the PredeclaredId attribute. This involves either exporting you class, editing the relevant attribute and reimporting, or, much more conveniently, using the attribute annotation '#PredecaredId provided by the free and fantastic Rubberduck add in for VBA.
Good luck in creating an addresses collection class to manage you addresses. Lots of examples are available of how to wrap a collection to produce a collection class.

Excel VBA declaring an arraylist inside of a class

I'm trying to create a Staff class module, with strings for surname etc, and an arraylist to be used for storing / calling between 0-10 strings dependant on how many are added when used.
The class module is called StaffClass and contains:
Private m_surname As String
Private m_districts as ArrayList
' Surname Prop
Property Get surname() As String
surname = m_surname
End Property
Property Let surname(surname As String)
m_surname = Name
End Property
' District Prop
' This is where i'm getting confused
Private Sub Class_ArrList()
Set m_districts = New ArrayList
End Sub
Property Get districts() As ArrayList
districts = m_districts
End Property
Property Let districts(districts as ArrayList)
m_districts = districts
End Property
The Main Module contains:
Dim newStaff As StaffClass
Set newStaff = New StaffClass
newStaff.surname = "Smith"
' This is where I want to add to the arraylist
newStaff.districts(0) = "50"
I'm aware I'm missing loads, but struggling to find much relating to collections inside classes for VBA.
Hoping you can help!
You can put the arraylist initialization routine in a Class_Initialize, and add a methods to the class to add/insert/etc each item. (Or you could add a method to add the arraylist as a single object).
Also, since ArrayList is an object, you'll need to use the Set keyword when retrieving it.
eg:
Class module
Option Explicit
Private m_surname As String
Private m_districts As ArrayList
' Surname Prop
Property Get surname() As String
surname = m_surname
End Property
Property Let surname(surname As String)
m_surname = surname
End Property
Property Get districts() As ArrayList
Set districts = m_districts
End Property
Function addDistrict(Value As String)
m_districts.Add Value
End Function
Private Sub Class_Initialize()
Set m_districts = New ArrayList
End Sub
Regular Module
Option Explicit
Sub par()
Dim newStaff As StaffClass
Dim V As ArrayList
Set newStaff = New StaffClass
With newStaff
.surname = "Smith"
.addDistrict 50
.addDistrict "xyz"
End With
Set V = newStaff.districts
Stop
End Sub

Creating Class properties with sub levels

I've been reading this topic on how to use class modules.
My goal is to improve my code performance and readability so I think I'm in the right path.
But I have some questions about the limitations.
In my head i want to do this:
Is it possible to achieve such a structure?
The topic I've read has very few examples and this is not handled. I'm assuming this would be possible with collections of collections, but I not sure how to look for this.
My data comes from 2 tables, one has all the items but the department and the other one has the ID's alongisde the departments. Both tables have the dates of the current month as headers and their Schedule/Department depending on the table.
I'd know how to achieve this for one day, but not for a whole month.
This is how I wrote the basics for my class:
Option Explicit
Private DirNeg As String
Private Agrup As String
Private DNI As String
Private Centro As String
Private Servicio As String
Private Nombre As String
Property Get Business() As String
Business = DirNeg
End Property
Property Let Business(ByVal sBusiness As String)
DirNeg = sBusiness
End Property
Property Get Group() As String
Group = Agrup
End Property
Property Let Group(ByVal sGroup As String)
Agrup = sGroup
End Property
Property Get ID() As String
ID = DNI
End Property
Property Let ID(ByVal sID As String)
DNI = sID
End Property
Property Get Location() As String
Location = Centro
End Property
Property Let Location(ByVal sLocation As String)
Centro = sLocation
End Property
Property Get Service() As String
Service = Servicio
End Property
Property Let Service(ByVal sService As String)
Servicio = sService
End Property
Property Get Name() As String
Name = Nombre
End Property
Property Let Name(ByVal sName As String)
Nombre = sName
End Property
On the other hand, is it correct to fill the whole class on the Class_Initializeevent? My data will always be the same so I don't need to loop in a normal module to fill the class, it could be done everytime the class is created.
EDIT/UPDATE:
This is how my data looks like:
Schedules alongside Agent's info
Departments alongside Agent's ID
clAgent Class Module:
Option Explicit
Private DirNeg As String
Private Agrup As String
Private DNI As String
Private Centro As String
Private Servicio As String
Private Nombre As String
Private Fechas As Object
Property Get Business() As String
Business = DirNeg
End Property
Property Let Business(ByVal sBusiness As String)
DirNeg = sBusiness
End Property
Property Get Group() As String
Group = Agrup
End Property
Property Let Group(ByVal sGroup As String)
Agrup = sGroup
End Property
Property Get ID() As String
ID = DNI
End Property
Property Let ID(ByVal sID As String)
DNI = sID
End Property
Property Get Location() As String
Location = Centro
End Property
Property Let Location(ByVal sLocation As String)
Centro = sLocation
End Property
Property Get Service() As String
Service = Servicio
End Property
Property Let Service(ByVal sService As String)
Servicio = sService
End Property
Property Get Name() As String
Name = Nombre
End Property
Property Let Name(ByVal sName As String)
Nombre = sName
End Property
Property Get clFechas(ByVal StringKey As String) As clFechas
With Fechas
If Not .Exists(StringKey) Then
Dim objFechas As New clFechas
.Add StringKey, objFechas
End If
End With
End Property
Private Sub Class_Initialize()
Set Fechas = CreateObject("Scripting.Dictionary")
End Sub
clFechas Class Module:
Option Explicit
Private Modos As Object
Private Horarios As Object
'Aqiço creamos la propiedad Modo para la clase Fecha
Public Property Get Modo(ByVal StringKey As String) As String
Modo = Modos(StringKey)
End Property
Public Property Let Modo(ByVal StringKey As String, ByVal StringValue As String)
Modos(StringKey) = StringValue
End Property
Public Property Get Keys() As Variant
Keys = Modos.Keys
End Property
'Aquí creamos la propiedad Horario para la clase Fecha
Public Property Get Horario(ByVal StringKey As String) As String
Modo = Horarios(StringKey)
End Property
Public Property Let Horario(ByVal StringKey As String, ByVal StringValue As String)
Horarios(StringKey) = StringValue
End Property
Public Property Get Keys() As Variant
Keys = Horarios.Keys
End Property
'Iniciamos la clase
Private Sub Class_Initialize()
Set Modos = CreateObject("Scripting.Dictionary")
Set Horarios = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Class_Terminate()
Set Modos = Nothing
Set Horarios = Nothing
End Sub
You don’t seem to have any issues with regular properties so let’s focus on the complex ones; Schedule and Department. Both are the same, so same rules apply to both.
The property is basically list, the date is the index and the item is an object. I personally prefer to work with dictionaries as I can look if a key exist etc.
So, your Agent class could look something like this:
Option Explicit
Private m_schedules As Object
Public Property Get Schedule(ByVal Key As Date) As Schedules
With m_schedules
If Not .Exists(Key) Then .Add Key, New Schedules
End With
Set Schedule = m_schedules(Key)
End Property
'For testing purposes - can be ommited.
Public Property Get Keys() As Variant
Keys = m_schedules.Keys
End Property
'For testing purposes - can be ommited.
Public Property Get Count() As Long
Count = m_schedules.Count
End Property
Private Sub Class_Initialize()
Set m_schedules = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Class_Terminate()
Set m_schedules = Nothing
End Sub
The Schedules class:
Option Explicit
Private m_schedule As String
Public Property Get Schedule() As String
Schedule = m_schedule
End Property
Public Property Let Schedule(ByVal param As String)
m_schedule = param
End Property
Now, let's test it:
Sub Test()
Dim obj As Agent
Set obj = New Agent
obj.Schedule(#1/9/2019#).Schedule = "Schedule 1"
obj.Schedule(#2/9/2019#).Schedule = "Schedule 2"
obj.Schedule(#3/9/2019#).Schedule = "Schedule 3"
PrintToDebug obj
'Lets make a change
obj.Schedule(#2/9/2019#).Schedule = "Schedule 2222"
PrintToDebug obj
End Sub
Private Sub PrintToDebug(ByVal obj As Agent)
Debug.Print ""
Dim m As Variant
With obj
For Each m In .Keys
Debug.Print "Key: " & m & String(3, " ") & "Value: " & .Schedule(m).Schedule
Next m
End With
Debug.Print "Total Items: " & obj.Count
End Sub
Output:
'Key: 09/01/2019 Value: Schedule 1
'Key: 09/02/2019 Value: Schedule 2
'Key: 09/03/2019 Value: Schedule 3
'Total Items: 3
'Key: 09/01/2019 Value: Schedule 1
'Key: 09/02/2019 Value: Schedule 2222
'Key: 09/03/2019 Value: Schedule 3
'Total Items: 3
Additional information regarding the Dictionary object can be found here: Dictionary object
Also keep this in mind. It's quite important:
If key is not found when changing an item, a new key is created with
the specified newitem. If key is not found when attempting to return
an existing item, a new key is created and its corresponding item is
left empty.
If the dictionary item is not a simple string, let me know to update the answer. Sorry, I couldnt read the data in the screenshots. :)

Acessing a parent's get property gives "Object variable or with block..." error

A have a series of objects, a complex data structure, a parent, and a child. The child contains an instance of the parent, has Let and Get Properties, and one method. When the method requests one of the ComplexData objects from the parent, I'm given the old RunTime Error 91 - Object Variable or With Variable not set message. The child is packaging up all the ComplexData objects from iteself and its parent. The error is generated when the Parent Property Get TitleField() is called by the child.
These are the classes (in bold):
ComplexData
Private sName As String
Private vValue As Variant
Public Property Let Name(sInput As String)
sName = sInput
End Property
Public Property Get Name() As String
Name = sName
End Property
Public Property Let Value(vInput)
vValue = vInput
End Property
Public Property Get Value()
Value = vValue
End Property
ParentClass
Private oTitle As ComplexData
Private Sub Class_Initialize()
Set oTitle = New ComplexData
oTitle.Name = "title"
End Sub
Public Property Let Title(vInput)
oTitle.Value = "Lorum"
End Property
Public Property Get Title()
Value = oTitle.Value
End Property
Public Property Set TitleField(oInput As ComplexData)
Set oTitle = oInput
End Property
Public Property Get TitleField() As ComplexData
TitleField = oTitle 'GENERATES ERROR
End Property
ChildClass
Private oParent As ParentClass
Private oContentData As ComplexData
Private Sub Class_Initialize()
Set oParent = New ParentClass
Set oContentData = New ComplexData
oContentData.Name = "content"
End Sub
Public Property Let Content(sInput As String)
oContentData.Value = sInput
End Property
Public Property Get Content() As String
Content = oContentData.Value
End Property
Public Function getFields()
getFields = Array(oContentData, oParent.TitleField)
End Function
I can get around this by setting oTitle to Public in the parent class, and requesting the object directly instead of using the Property.
I'm calling this from a spreadsheet using the following:
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
Set MyChild = New ChildClass
Fields = MyChild.getFields
field0 = Fields(0).Name
field1 = Fields(1).Name
MsgBox field0 & field1
End Sub
As #Vityata mentioned, adding a “Set” within the Get property fixes the issue.

VBA Class() object as property of another class

I'm trying to create a class to hold a variable number of items (which are themselves another class object).
So, I have Class 2:
' Class 2 contain each individual quote elements (OTC and MRC)
Private pOTC As String
Private pMRC As String
Public Property Get OTC() As String
OTC = pOTC
End Property
Public Property Let OTC(Value As String)
pOTC = Value
End Property
Public Property Get MRC() As String
MRC = pMRC
End Property
Public Property Let MRC(Value As String)
pMRC = Value
End Property
Then Class 1 contains an array of Class 2:
Private pCurr As String
Private pQuote(20) As Class2
Public Property Get Curr() As String
Curr = pCurr
End Property
Public Property Let Curr(Value As String)
pCurr = Value
End Property
Public Property Set Quote(Index As Integer, cQuote As Class2)
Set pQuote(Index) = cQuote
End Property
Public Property Get Quote(Index As Integer) As Class2
Quote = pQuote(Index)
End Property
And what I would like to do is something like:
Dim myQuotes As Class1
Set myQuotes = New Class1
myQuotes.Curr = "GBP"
myQuotes.Quote(3).OTC = "1200"
The first line setting myQuotes.Curr is no problem, however when I try to set a value inside the array the next line errors with Run-time 91 Object variable or With block variable not set
Any pointers as to what I'm doing wrong and how can I set the values for the elements within the class array?
Thanks in advance!
When you myQuotes.Quote(3) you call Property Get Quote which has an issue.
Your internal array of Class2 is not instantiated so pQuote(Index) refers to an array element of Nothing, when you then myQuotes.Quote(3).OTC = you try to assign to Nothing which fails.
You need to make sure pQuote(Index) is instanced; you can do this on demand:
Public Property Get Quote(Index As Integer) As Class2
If (pQuote(Index) Is Nothing) Then Set pQuote(Index) = New Class2
Set Quote = pQuote(Index)
End Property
(Note the required Set)
Or by adding an intitialisation routine to Class1:
Private Sub Class_Initialize()
Dim Index As Long
For Index = 0 To UBound(pQuote)
Set pQuote(Index) = New Class2
Next
End Sub
You need to set them as New Class2 in Class1:
For intI = LBOUND(pQuote) to UBOUND(pQuote)
Set pQuote(intI) = New Class2
Next IntI
Just as you do with Class1 in your final script.
Maybe it should be
Public Property Let Quote(Index As Integer, cQuote As Class2)
Set pQuote(Index) = cQuote
End Property

Resources