Object Module in VBA As A Container for Constant values? - vba

I am studing VBA object module and trying to use it as an easy way of retriave objects properties.
I write the code below and would really appreciate if you could tell me if there is a better way of doing it, or if I am in the right path for write some good code.
Consider that I intend to use this on a bigger project, so I need to know if it is well written.
Class Module Animals
Option Explicit
Private pCat As New Animal
Private pDog As New Animal
Private pFish As New Animal
Private Sub Class_Initialize()
pCat.Mammal = True
pCat.NumberOfPaws = 4
pDog.Mammal = True
pDog.NumberOfPaws = 4
pFish.Mammal = False
pFish.NumberOfPaws = 0
End Sub
Property Get Cat() As Animal
Set Cat = pCat
End Property
Property Get Dog() As Animal
Set Dog = pDog
End Property
Property Get Fish() As Animal
Set Fish = pFish
End Property
Class Module Animal
Option Explicit
Private nPaws As Integer
Private pMammal As String
Property Get NumberOfPaws() As Integer
NumberOfPaws = nPaws
End Property
Property Let NumberOfPaws(numPaws As Integer)
nPaws = numPaws
End Property
Property Get Mammal() As Boolean
Mammal = pMammal
End Property
Property Let Mammal(IsMammal As Boolean)
pMammal = IsMammal
End Property
Subroutine for testing the object modeules above
Option Explicit
Sub Test()
Dim objAnimal As New Animals
Debug.Print objAnimal.Cat.Mammal
Debug.Print objAnimal.Cat.NumberOfPaws
Debug.Print objAnimal.Dog.Mammal
Debug.Print objAnimal.Dog.NumberOfPaws
Debug.Print objAnimal.Fish.Mammal
Debug.Print objAnimal.Fish.NumberOfPaws
End Sub

If you plan to use a large number of Animal objects, it's wise not to create a new container for them as a new object, but to use a built-in one, like Collection, which has list and dictionary properties. Also you can use a Dictionary object.
It also makes sense to add the Name property to the Animal object to distinguish one object from another in the list. Additionally: you can make it convenient to create objects (like https://stackoverflow.com/a/15224115/15035314), as well as to display information about the object (see the procedure PrintMe()).
Animal class module
Option Explicit
Private mName As String
Private nPaws As Integer
Private pMammal As Boolean
Public Sub InitiateProperties(Name As String, Paws As Integer, Mammal As Boolean)
mName = Name
nPaws = Paws
pMammal = Mammal
End Sub
Public Sub PrintMe()
Debug.Print "The animal is " & mName & ", it has " & nPaws & " paws(s) and is" & IIf(pMammal, " ", " not ") & "a mammal"
End Sub
Property Get Name() As Integer
Name = mName
End Property
Property Let Name(theName As Integer)
mName = theName
End Property
Property Get NumberOfPaws() As Integer
NumberOfPaws = nPaws
End Property
Property Let NumberOfPaws(numPaws As Integer)
nPaws = numPaws
End Property
Property Get Mammal() As Boolean
Mammal = pMammal
End Property
Property Let Mammal(IsMammal As Boolean)
pMammal = IsMammal
End Property
Module1 common module
Option Explicit
' based on https://stackoverflow.com/a/15224115/15035314
Public Function CreateAnimal(Name As String, Paws As Integer, Mammal As Boolean) As Animal
Set CreateAnimal = New Animal
CreateAnimal.InitiateProperties Name, Paws, Mammal
End Function
Sub test()
Dim Animals As New Collection, A As Animal
Animals.Add CreateAnimal("Cat", 4, True), "Cat"
Animals.Add CreateAnimal("Dog", 4, True), "Dog"
Animals.Add CreateAnimal("Fish", 0, False), "Fish"
' and so on
For Each A In Animals
A.PrintMe
Next
Debug.Print "The cat has " & Animals("Cat").NumberOfPaws & " paws" 'get the animal from the collection by name
End Sub
Running test() prints:
The animal is Cat, it has 4 paws(s) and is a mammal
The animal is Dog, it has 4 paws(s) and is a mammal
The animal is Fish, it has 0 paws(s) and is not a mammal
The cat has 4 paws

Related

How to handle a function where the returned value type is not known at run-time (Object or Non-Object)

This question centers around the return value of a call to CallByName. I have a class called PropertyPtr which is meant to act as a generic pointer to an object property. It holds a reference to an Object, and the name of one of its properties. It exposes a Getter and Setter method.
PropertyPtr:
Option Explicit
Public Obj As Object
Public PropertyName As String
Public Sub Setter(Val As Variant)
If IsObject(Val) Then
CallByName Me.Obj, Me.PropertyName, VbSet, Val
Else
CallByName Me.Obj, Me.PropertyName, VbLet, Val
End If
End Sub
Public Function Getter() As Variant
If IsObject(CallByName(Me.Obj, Me.PropertyName, VbGet)) Then
Set Getter = CallByName(Me.Obj, Me.PropertyName, VbGet)
Else
Getter = CallByName(Me.Obj, Me.PropertyName, VbGet)
End If
End Function
In the Getter, my CallByName could return a object or not. But the only way I can see to test if the CallByName value will be an object is to end up running it twice - once to test inside an IsObject and then again to get a reference to the value. The only other way I could see doing this is trapping for an error. Then, you at least only SOMETIMES run the CallByName twice.
My question is: is there some other way to do this without running CallByName twice?
Okay, so if you really want to follow that route then I think you'll have to set an IsObj flag - probably at the point you set the property name.
However, I'd still maintain that using a Variant for either an Object or primitive type isn't a great idea, and the CallByName() function in this context comes with issues. My hesitations are that performance will be diminished and you'll have quite a task to keep the property strings aligned with the property names (should you update things in the future).
It is possible to implement a Mediator Pattern in VBA and I do feel you should consider this route. Below is a really basic example of how you could do it. I haven't bothered with an interface for the mediator, but I have created an interface for my participating classes (to cover the possibility that you're dealing with your own 'groups' of classes).
Mediator class (called cMediator):
Option Explicit
Private mSweets As Collection
Private Sub Class_Initialize()
Set mSweets = New Collection
End Sub
Public Sub RegisterSweet(sweet As ISweet)
Set sweet.Mediator = Me
mSweets.Add sweet
End Sub
Public Sub SendSugarLimit(limit As Long)
Dim sweet As ISweet
For Each sweet In mSweets
sweet.ReceiveSugarLimit limit
Next
End Sub
Public Sub ReceiveMeltingAlert(offender As String)
Dim sweet As ISweet
For Each sweet In mSweets
sweet.ReceiveEatNow offender
Next
End Sub
Participating classes Interface (called ISweet):
Option Explicit
Public Property Set Mediator(RHS As cMediator)
End Property
Public Sub ReceiveSugarLimit(g_perDay As Long)
End Sub
Public Sub ReceiveEatNow(offender As String)
End Sub
My two participating classes (cQtySweet and cWeightSweet):
Option Explicit
Implements ISweet
Public Name As String
Public SugarPerItem As Long
Public CanMelt As Boolean
Private pMediator As cMediator
Public Sub OhNoItsMelting()
pMediator.ReceiveMeltingAlert Name
End Sub
Private Property Set ISweet_Mediator(RHS As cMediator)
Set pMediator = RHS
End Property
Private Sub ISweet_ReceiveEatNow(offender As String)
If CanMelt Then Debug.Print offender & " is melting. Eat " & Name & "s now!"
End Sub
Private Sub ISweet_ReceiveSugarLimit(g_perDay As Long)
Dim max As Long
max = g_perDay / SugarPerItem
Debug.Print "Max " & Name & "s: " & max & "."
End Sub
Option Explicit
Implements ISweet
Public Name As String
Public SugarPer100g As Long
Public CanMelt As Boolean
Private pMediator As cMediator
Public Sub OhNoItsMelting()
pMediator.ReceiveMeltingAlert Name
End Sub
Private Property Set ISweet_Mediator(RHS As cMediator)
Set pMediator = RHS
End Property
Private Sub ISweet_ReceiveEatNow(offender As String)
If CanMelt Then Debug.Print offender & " is melting. Eat " & Name & " now!"
End Sub
Private Sub ISweet_ReceiveSugarLimit(g_perDay As Long)
Dim max As Long
max = g_perDay / (SugarPer100g / 100)
Debug.Print "Max " & Name & ": " & max & "g."
End Sub
Module Code:
Public Sub RunMe()
Dim m As cMediator
Dim qtySweet As cQtySweet
Dim weightSweet As cWeightSweet
Set m = New cMediator
Set qtySweet = New cQtySweet
With qtySweet
.Name = "Gobstopper"
.SugarPerItem = 5
.CanMelt = False
End With
m.RegisterSweet qtySweet
Set qtySweet = New cQtySweet
With qtySweet
.Name = "Wine Gum"
.SugarPerItem = 2
.CanMelt = True
End With
m.RegisterSweet qtySweet
Set weightSweet = New cWeightSweet
With weightSweet
.Name = "Sherbert"
.SugarPer100g = 80
.CanMelt = False
End With
m.RegisterSweet weightSweet
Set weightSweet = New cWeightSweet
With weightSweet
.Name = "Fudge"
.SugarPer100g = 50
.CanMelt = True
End With
m.RegisterSweet weightSweet
'Blasted government has reduced sugar allowance.
Debug.Print "New govt. limits..."
m.SendSugarLimit 200
'Phew what a scorcher - the fudge is melting in my pocket.
Debug.Print "Sweet alarm..."
weightSweet.OhNoItsMelting
End Sub
… and the output looks like this:
New govt. limits...
Max Gobstoppers: 40.
Max Wine Gums: 100.
Max Sherbert: 250g.
Max Fudge: 400g.
Sweet alarm...
Fudge is melting. Eat Wine Gums now!
Fudge is melting. Eat Fudge now!

Putting Excel Tables into a Custom Class

I'm new to using VBA classes, and I think what I want to do -- using them with ListObjects -- is more of an "intermediate" than "beginner" technique.
Let's say I have two tables related to cars.
tblCarDesc
ID MAKE MODEL DOORS ENGINE
1 Chevrolet Corvette 2 V8
2 Ford Escort 4 V6
3 Rolls-Royce SilverCloud 4 V8
tblCarProd
ID COUNTRY TYPE
1 US Sport
2 US Economy
3 UK Luxury
(The same cars are in both tables, and shown by the ID numbers.)
I want to have a class called objCars that includes the fields (columns) from both tables. That way, when referring to Car #3, objCars.Make would be "Rolls-Royce" and objCars.Type would be "Luxury".
1) Is there a way to import both tables into objCars?
Perhaps I would create an array big enough to hold all the columns, then load both tables into it. The tutorial I've been reading says that I would then create a Collection, loop through each row of the array, make a new instance of objCars, and assign objCars.Make, objCars.Model, etc., for each row. Then each item of the Collection would contain a car. (Or something like that. I don't really know much about Collections either.) If that's right, is it the best way?
2) How exactly does one refer to a specific car? The examples I've read like to loop through Collections and work on each item therein, but what if I want to extract a particular item? I know the Ford is Car #2; how do I get the objCars.Make and objCars.Model for that particular ID number?
I would have two classes. A class clsCar for one car and a class clsCars for a collection of cars.
Each of this classes may have setter and getter methods and also may have custom methods if needed. Especially the clsCars should have a set of getBy...-methods to get a car or a collection of cars from the collection by criterion.
Example:
clsCar:
Private pID As Long
Private pMAKE As String
Private pMODEL As String
Private pDOORS As Integer
Private pENGINE As String
Private pCOUNTRY As String
Private pTYPE As String
Public Property Get ID() As Long
ID = pID
End Property
Public Property Let ID(Value As Long)
pID = Value
End Property
Public Property Get MAKE() As String
MAKE = pMAKE
End Property
Public Property Let MAKE(Value As String)
pMAKE = Value
End Property
Public Property Get MODEL() As String
MODEL = pMODEL
End Property
Public Property Let MODEL(Value As String)
pMODEL = Value
End Property
Public Property Get DOORS() As Integer
DOORS = pDOORS
End Property
Public Property Let DOORS(Value As Integer)
pDOORS = Value
End Property
Public Property Get ENGINE() As String
ENGINE = pENGINE
End Property
Public Property Let ENGINE(Value As String)
pENGINE = Value
End Property
Public Property Get COUNTRY() As String
COUNTRY = pCOUNTRY
End Property
Public Property Let COUNTRY(Value As String)
pCOUNTRY = Value
End Property
Public Property Get CarTYPE() As String
CarTYPE = pTYPE
End Property
Public Property Let CarTYPE(Value As String)
pTYPE = Value
End Property
Public Function toString() As String
toString = pID & "; " & _
pMAKE & "; " & _
pMODEL & "; " & _
pDOORS & "; " & _
pENGINE & "; " & _
pCOUNTRY & "; " & _
pTYPE
End Function
clsCars:
Private pCars As collection
Private Sub Class_Initialize()
Set pCars = New collection
End Sub
Public Sub add(oCar As clsCar)
pCars.add oCar
End Sub
Public Function getByIndex(lIndex As Long) As clsCar
Set getByIndex = pCars.Item(lIndex)
End Function
Public Function getByID(lID As Long) As clsCar
Dim oCar As clsCar
For Each oCar In pCars
If oCar.ID = lID Then
Set getByID = oCar
End If
Next
End Function
Public Function getByEngine(sEngine As String) As collection
Dim oCar As clsCar
Set getByEngine = New collection
For Each oCar In pCars
If oCar.ENGINE = sEngine Then
getByEngine.add oCar
End If
Next
End Function
default Module:
Public oCars As clsCars
Sub initialize()
Dim oCar As clsCar
Dim oListObject As ListObject
Dim oListRow As ListRow
Dim oCells As Range
Set oCars = New clsCars
Set oListObject = Worksheets("Sheet1").ListObjects("tblCarDesc")
For Each oListRow In oListObject.ListRows
Set oCells = oListRow.Range.Cells
Set oCar = New clsCar
oCar.ID = oCells(, 1).Value
oCar.MAKE = oCells(, 2).Value
oCar.MODEL = oCells(, 3).Value
oCar.DOORS = oCells(, 4).Value
oCar.ENGINE = oCells(, 5).Value
oCars.add oCar
Next
Set oListObject = Worksheets("Sheet1").ListObjects("tblCarProd")
Dim lID As Long
For Each oListRow In oListObject.ListRows
Set oCells = oListRow.Range.Cells
lID = oCells(, 1).Value
Set oCar = oCars.getByID(lID)
If Not oCar Is Nothing Then
oCar.COUNTRY = oCells(, 2).Value
oCar.CarTYPE = oCells(, 3).Value
End If
Next
MsgBox oCars.getByIndex(2).toString
For Each oCar In oCars.getByEngine("V8")
MsgBox oCar.toString
Next
End Sub
I would use a class for each, and an array of each also, so arrCars holds clsCars, and arrProd holds clsProduction. I would then use the index of each for each array when populating, so arrCars(1)=Corvette and arrProd(1)=US Sport then from each you can refer to the others, so if x=3, cars(x) and prod(x) will be correct. Or use a vlookup in excel first, and make one larger table, with the need for only 1 ID then, if that is the way they are related, but can a Bentley also be 3. Its not quite clear what you mean by the 2nd table, is there an entry for each car, or is it like catgeorising the car further, just using a certain selection. Another idea would be to have an extra property in the car class, of ProductionID and then use a static production "table" of classes to refer to.

How to create a compound object in VBA?

I cannot make my way through the Microsoft help, which is great provided you know what the answer is already, so I'm stuck.
Is it possible for me to create my own compound object (I assume that this is the term) such that, for example, the object could be a person and would have the following sub-classes:
Firstname - String
Surname - String
Date of birth - Datetime
Gender - String (M/F accepted)
Height - Real number
Sorry if it seems like a very basic question (no pun intended) but I haven't used Visual Basic for a long time, and Microsoft Visual Basic was never my forté.
You should consider using class modules instead of types. Types are fine, but they're limited in what they can do. I usually end up converting my types to classes as soon as I need some more function than a type can provide.
You could create a CPerson class with the properties you want. Now if you want to return a FullName property, you can write a Property Get to return it - something you can't do with a type.
Private mlPersonID As Long
Private msFirstName As String
Private msSurname As String
Private mdtDOB As Date
Private msGender As String
Private mdHeight As Double
Private mlParentPtr As Long
Public Property Let PersonID(ByVal lPersonID As Long): mlPersonID = lPersonID: End Property
Public Property Get PersonID() As Long: PersonID = mlPersonID: End Property
Public Property Let FirstName(ByVal sFirstName As String): msFirstName = sFirstName: End Property
Public Property Get FirstName() As String: FirstName = msFirstName: End Property
Public Property Let Surname(ByVal sSurname As String): msSurname = sSurname: End Property
Public Property Get Surname() As String: Surname = msSurname: End Property
Public Property Let DOB(ByVal dtDOB As Date): mdtDOB = dtDOB: End Property
Public Property Get DOB() As Date: DOB = mdtDOB: End Property
Public Property Let Gender(ByVal sGender As String): msGender = sGender: End Property
Public Property Get Gender() As String: Gender = msGender: End Property
Public Property Let Height(ByVal dHeight As Double): mdHeight = dHeight: End Property
Public Property Get Height() As Double: Height = mdHeight: End Property
Public Property Get FullName() As String
FullName = Me.FirstName & Space(1) & Me.Surname
End Property
Then you can create a CPeople class to hold all of your CPerson instances.
Private mcolPeople As Collection
Private Sub Class_Initialize()
Set mcolPeople = New Collection
End Sub
Private Sub Class_Terminate()
Set mcolPeople = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Set NewEnum = mcolPeople.[_NewEnum]
End Property
Public Sub Add(clsPerson As CPerson)
If clsPerson.PersonID = 0 Then
clsPerson.PersonID = Me.Count + 1
End If
mcolPeople.Add clsPerson, CStr(clsPerson.PersonID)
End Sub
Public Property Get Person(vItem As Variant) As CPerson
Set Person = mcolPeople.Item(vItem)
End Property
Public Property Get Count() As Long
Count = mcolPeople.Count
End Property
Public Property Get FilterByGender(ByVal sGender As String) As CPeople
Dim clsReturn As CPeople
Dim clsPerson As CPerson
Set clsReturn = New CPeople
For Each clsPerson In Me
If clsPerson.Gender = sGender Then
clsReturn.Add clsPerson
End If
Next clsPerson
Set FilterByGender = clsReturn
End Property
With this class, you can For Each through all the instances (google custom class and NewEnum to see how to do that). You can also use a Property Get to return a subset of the CPerson instances (females in this case).
Now in a standard module, you can create a couple of CPerson instances, add them to your CPeople instance, filter them, and loop through them.
Public Sub FillPeople()
Dim clsPerson As CPerson
Dim clsPeople As CPeople
Dim clsFemales As CPeople
Set clsPeople = New CPeople
Set clsPerson = New CPerson
With clsPerson
.FirstName = "Joe"
.Surname = "Blow"
.Gender = "M"
.Height = 72
.DOB = #1/1/1980#
End With
clsPeople.Add clsPerson
Set clsPerson = New CPerson
With clsPerson
.FirstName = "Jane"
.Surname = "Doe"
.Gender = "F"
.Height = 62
.DOB = #1/1/1979#
End With
clsPeople.Add clsPerson
Set clsFemales = clsPeople.FilterByGender("F")
For Each clsPerson In clsFemales
Debug.Print clsPerson.FullName
Next clsPerson
End Sub
There's defintely more learning curve to creating classes, but it's worth it in my opinion.
I think you need to use TYPE syntax, like this:
TYPE person
Firstname As String
Surname As String
Date_of_birth As Date ' instead of Datetime
Gender As String '(M/F accepted)
Height As Single 'instead of Real number
END TYPE
Sub Test()
Dim aTest As person
End Sub

VBA Class properties returning empty strings

I am trying to create a class in VBA for the first time. I have looked up some solutions and I don't see anything wrong with my class, but when I run the test code, the integer returns correctly but the strings return empty:
Class
Property Let Name(strName As String)
pName = strName
End Property
Property Get Name() As String
Name = pName
End Property
Property Let Class(strClass As String)
pClass = strClass
End Property
Property Get Class() As String
Class = pClass
End Property
Property Let Aggro(intAggro As Integer)
pAggro = intAggro
End Property
Property Get Aggro() As Integer
Aggro = pAggro
End Property
Test Procedure
Sub ClassTest()
Dim Dog1 As New Critter
Dog1.Name = "Labrador"
Dog1.Class = "Canine"
Dog1.Aggro = 0
Debug.Print Dog1.Name 'returns ""
Debug.Print Dog1.Class 'returns ""
Debug.Print Dog1.Aggro 'returns 0
End Sub
The only thing you have wrong is you haven't define private variables to hold your property values. It appears the integer is working because Integer initializes to 0, and you are 'setting' the value to 0. Just add this to the top of your class and try again:
Private pName as String
Private pClass as String
Private pAggro as Integer
:D

Get properties of VB element

How can I see what properties an element has in a VB script? Example:
Dim list : Set list = CreateObject( "Scripting.Dictionary" )
' ... Fill List ...
WriteListElements list
...
Sub WriteListElements ( list )
Dim e, le
For Each e In list
Set le = list(e) ' what properties does le have?
le.name_of_user_defined_attribut ' I want to access a property but dont know the exact name
Next
End Sub
I use a Tool with a VBScript API. In that API I can read (user defined) attributes from that Tool. But while running the script I get an error telling me that it does not know the name of that user defined attribut. But I use it in the tool. Now I would like to know which attributes are availble in the array above to see if the user defined attributes are named specificly.
Not really possible. Only very basic type information is available in the VBScript runtime. Ideally you could create an adapter that translates your tool's objects into standard Dictionary objects and iterate the Keys. If that's not possible, the best you can do is check the type name for each object before invoking its members. Example:
<html>
<body>
<script type="text/vbscript">
Class Human
Private m_name
Public Property Get Name
Name = m_name
End Property
Public Property Let Name(newName)
m_name = newName
End Property
End Class
Dim joe
Set joe = new Human
joe.Name = "Joe Coder"
Dim list
Set list = CreateObject( "Scripting.Dictionary" )
list.Add "a", 5
list.Add "b", joe
list.Add "c", "apples"
WriteListElements list
Sub WriteListElements ( list )
Dim e
For Each e In list
If (TypeName(list.Item(e)) = "Human") Then
document.write("We have found a Human: " &_
"<b>" & list.Item(e).Name & "</b>")
End If
Next
End Sub
</script>
</body>
</html>
Will you be able to get the list of properties from a script like this?
http://www.vbsedit.com/scripts/misc/wmi/scr_1332.asp
And, you can then use eval() or execute to get the values of these properties.
Dim list : Set list = CreateObject( "Scripting.Dictionary" )
' ... Fill List ...
WriteListElements list
...
Sub WriteListElements ( list )
Dim e, le
For Each e In list
Set le = e.Items
Response.Write le(name_of_user_defined_attribut)
Next
End Sub
It is easy - use a pseudo reflection:
class Developer
Public reflection
'=============================
'Private properties
private mId
private mFirstName
private mLastName
private sub Class_Initialize()
reflection = Array("Id","FirstName","LastName")
end sub
private sub Class_Terminate()
end sub
'=============================
'public properties
public property get Id()
Id = mId
end property
public property let Id(val)
mId = val
end property
public property get FirstName()
FirstName = mFirstName
end property
public property let FirstName(val)
mFirstName = val
end property
public property get LastName()
LastName = mLastName
end property
public property let LastName(val)
mLastName = val
end property
end class
For each property in obj.reflection
document.write(property)
document.write( Eval ("obj." & property) )
Next