Nested VBA collection of classes returning "Argument not optional" error - vba

I am trying to represent the idea of nested classes with collections of child classes with the natural example of Grandmother - Married Daughters - Little kids case, so I have 3 classes as following:
' Class GrandMother:
Private pMarriedDaughter As Collection
Public Property Get MarriedDaughter() As Collection
MarriedDaughter = pMarriedDaughter
End Property
Public Property Set MarriedDaughter(C As Collection)
Set pMarriedDaughter = C
End Property
' Class MarriedMom:
Private pChildren As Collection
Public Property Get Children() As Collection ' ERROR HERE!
Children = pChildren
End Property
Public Property Set Children(C As Collection)
Set pChildren = C
End Property
' Child Class:
Private pName As String
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(s As String)
Let pName = s
End Property
And the Main Routine that tries to populate the classes:
Sub TestGrandMother()
' Create 3 Childs
Dim Child_1a As New Child: Child_1a.Name = "Bill"
Dim Child_1b As New Child: Child_1b.Name = "Sam"
Dim Child_2a As New Child: Child_2a.Name = "Sahar"
' Create 2 Married Daughters:
Dim Mamy1 As New MarriedMom
Dim Mamy2 As New MarriedMom
' Add the the children to the married daughters
Set Mamy1.Children = New Collection
Mamy1.Children.Add Child_1a
Mamy1.Children.Add Child_1b
Set Mamy2.Children = New Collection
Mamy2.Children.Add Child_2a
' Create Grandmother
Dim GrandMa As GrandMother: Set GrandMa = New GrandMother
Set GrandMa.MarriedDaughter = New Collection
GrandMa.MarriedDaughter.Add Mamy1
GrandMa.MarriedDaughter.Add Mamy2
' Now cycle childs Name and debug:
Dim aChild As New Child
For Each aChild In GrandMa.MarriedDaughter.Children
Debug.Print GrandMa.MarriedDaughter.Children.Name
Next aChild
End Sub

In both cases where that error occurs you need to use the Set keyword as working with an object. That is just for the error type you comment on.
e.g.
Set Children = pChildren
Set MarriedDaughter = pMarriedDaughter
The following GrandMa.MarriedDaughter does not expose a .Children btw.
Perhaps
Dim aChild As MarriedMom, nextChild As Child
For Each aChild In GrandMa.MarriedDaughter
For Each nextChild In aChild.Children
Debug.Print nextChild.Name
Next
Next aChild

Related

How to find blank value from all member of class in vb.net

I have third party object which contain so many member with integer, string and Boolean. I want to update that record whose value is not null or blank
You can use reflection to achieve what you want:
Sub Main()
Dim obj As Test = new Test()
Dim type As Type = GetType(Test)
Dim info As PropertyInfo() = type.GetProperties()
For Each propertyInfo As PropertyInfo In info
Dim value As String = propertyInfo.GetValue(obj)
If propertyInfo.PropertyType = GetType(String) And String.IsNullOrEmpty(value)
' empty value for this string property
End If
Next
End Sub
public Class Test
Public Property Test As String
End Class

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.

Get value of a property with propertyinfo object

Is there a way to get value of a object properties with a propertyinfo object?
psudo code:
propertyinfoObject = Text
myobject.toCommand(propertyinfoObject)
The psudo code above should do the same as
myobject.Text
My goal is to create a simpel Properties form that will work on any object (Later I will use keywords to filter out what options I want the use to see).
My real code
Public Class PropertiesForm
Dim propertyInfoVar() As PropertyInfo
Dim Properties As New Form2
Dim listItem As New ListViewItem
Dim stringarray() As String
Public Sub New(ByRef sender As Object)
propertyInfoVar = sender.GetType().GetProperties()
For Each p In propertyInfoVar
stringarray = {p.Name.ToString, #INSERT VALUE SOMEHOW HERE#}
listItem = New ListViewItem(stringarray)
Properties.ListView1.Items.Add(listItem)
Next
Properties.Visible = True
End Sub
EDIT
Just use propertyGrid as suggested below!
The standard PropertyGrid already does all that for you. Filtering properties is not so obvious, here's how:
The control includes a BrowsableAttributes property which allows you to specify that only properties with the specified attribute value should be shown. You can use existing attributes, or custom ones. This is specifically for tagging visible props:
<AttributeUsage(AttributeTargets.Property)>
Public Class PropertyGridBrowsableAttribute
Inherits Attribute
Public Property Browsable As Boolean
Public Sub New(b As Boolean)
Browsable = b
End Sub
End Class
Apply it to an Employee class to hide pay rates or anything else:
Public Class Employee
<PropertyGridBrowsable(True)>
Public Property FirstName As String
...
<PropertyGridBrowsable(False)>
Public Property PayRate As Decimal
<PropertyGridBrowsable(False)>
Public Property NationalInsuranceNumber As String
Test code:
Dim emp As New Employee With {.Dept = EmpDept.Manager,
.FirstName = "Ziggy",
.PayRate = 568.98D,
...
.NationalInsuranceNumber = "1234567"
}
propGrid.BrowsableAttributes = New AttributeCollection(New PropertyGridBrowsableAttribute(True))
propGrid.SelectedObject = emp
BrowsableAttributes is a collection, so you can add several.

Adding a custom class collection to another custom class collection

Ok to start off, I read through this.
It is close although it doesn't answer my specific question. This talks about taking smaller collections and adding items to a larger main collection. Then destroying the smaller collection.
I have two definitions under Class Modules.
TimeDet
Option Explicit
Public recDate As String
Public recQty As String
Public recDieNo As String
Public recCatID As String
Public recCatName As String
Public recGroupID As String
Public recGroupName As String
TimeRec
Option Explicit
Private objTimeRec As Collection
Private Sub Class_Initialize()
Set objTimeRec = New Collection
End Sub
Private Sub Class_Terminate()
Set objTimeRec = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Set NewEnum = objTimeRec.[_NewEnum]
End Property
Public Sub Add(obj As TimeDet)
objTimeRec.Add obj
End Sub
Public Sub Remove(Index As Variant)
objTimeRec.Remove Index
End Sub
Public Property Get Item(Index As Variant) As TimeDet
Set Item = objTimeRec.Item(Index)
End Property
Property Get Count() As Long
Count = objTimeRec.Count
End Property
Public Sub Clear()
Set objTimeRec = New Collection
End Sub
Public Sub FillFromArray(Arr As Variant)
Dim i As Long, obj As TimeDet
For i = 1 To UBound(Arr)
Set obj = New TimeDet
obj.recDate = Arr(i, 1)
obj.recQty = Arr(i, 2)
obj.recDieNo = Arr(i, 3)
obj.recCatID = Arr(i, 4)
obj.recCatName = Arr(i, 5)
obj.recGroupID = Arr(i, 6)
obj.recGroupName = Arr(i, 7)
Me.Add obj
Next
End Sub
Then in the code I am using it this way:
Sub Test()
Dim RecSet1 As TimeRec, Record As TimeDet
Dim fSet1 As TimeRec, fRecord As TimeDet
Dim repArray() As Variant
Dim startDT As Date, endDT As Date, dieNo As String
repArray() = Sheet4.Range("A2:G" & Sheet4.Range("A2").End(xlDown).Row)
Set RecSet1 = New TimeRec
Set fSet1 = New TimeRec
RecSet1.FillFromArray (repArray())
startDT = "1-1-2015"
endDT = "1-1-2016"
dieNo = "16185"
For Each Record In RecSet1
If Record.recDate <= endDT And Record.recDate >= startDT And Record.recDieNo = dieNo Then
fSet1.Add (Record)
End If
Next
End Sub
I am getting an error when I try to add the Record object to the fSet1 object.
"Object doesn't support this method or property"
The Record object is Type TimeDet which as you can see up in the class module my Add method is expecting type TimeDet.
Either I am missing something very simple and have blinders on, or this is a bigger issue.
The array has 200,000 records roughly. I am attempting to create a smaller subset of filtered data. Maybe I am approaching this from the wrong way.
Your error is not at Add but at For Each
Most likely you copied your TimeRec Class. In VBA, you can't create enumerable classes inside the VBE (VBA IDE). There's a different way of creating Enumerable classes.
Open a notepad, copy all your class code and then add this attribute to NewEnum property Attribute NewEnum.VB_UserMemId = -4
Then import the class.
This is always hidden in VBA code, but can be seen in text editors.
Also add this attribute to Item property, it will make it default and allows syntax like ClassName(1)
Attribute Item.VB_UserMemId = 0
So , your code in text editor/ notepad should be:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Class1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Private objTimeRec As Collection
Private Sub Class_Initialize()
Set objTimeRec = New Collection
End Sub
Private Sub Class_Terminate()
Set objTimeRec = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = objTimeRec.[_NewEnum]
End Property
Public Sub Add(obj As Class2)
objTimeRec.Add obj
End Sub
Public Sub Remove(Index As Variant)
objTimeRec.Remove Index
End Sub
Public Property Get Item(Index As Variant) As Class2
Attribute Item.VB_UserMemId = 0
Set Item = objTimeRec.Item(Index)
End Property
Property Get Count() As Long
Count = objTimeRec.Count
End Property
Public Sub Clear()
Set objTimeRec = New Collection
End Sub
The answer to this particular problem was to remove the parenthesis form my Add method. That being said, the attribute info being hidden was really good info and would have probably contributed to the problem after I figured out that removing the parenthesis fixed it.

vba deep copy/clone issue with class object dictionary

I have a dictionary in my Main Sub (KEY = string; VALUE = Class Object). The Class Object consists of two dictionaries. As I collect data and check the values stored in the Dictionary Values (Class Object - dictionaries) I noticed that only the last values are getting stored. What I mean is that all the Values in my dictionary in my Main Sub are pointing to the same dictionary reference, hence, all the instances of my Class Objects contain the same data. This means that I need to make a clone of my Class Objects (deep copy?). I have successfully done this before with Class Objects that only stored simple values, but not with dictionaries. I need help cloning my Class Object that contains dictionaries.
MAIN SUB
Dim dGroup As New Scripting.Dictionary ' Main Dictionary
'
' loop thru a listbox
For i = 0 To UserForm1.ListBox1.ListCount - 1
Gname = UserForm1.ListBox1.List(i) ' get listbox names
' populate temp dictionary
Set dic = FNC.GET_SESSION_FILE_ELEMENTS(mySesFile, Gname)
'
' instantiate new Class Object
Dim NewCol As New cVM_Col
Call NewCol.INIT(dic) ' pass the dictionary to a 'constructor'
dGroup.Add Gname, NewCol.CLONE ' add to the MAIN SUB dictionary
'
Set dic = Nothing ' clear the temp dictionary
Next i
CLASS OBJECT
Private dElms As Scripting.Dictionary
Private dDat As Scripting.Dictionary
'
Private Sub Class_Initialize()
Set dElms = New Scripting.Dictionary
Set dDat = New Scripting.Dictionary
End Sub
'
Public Sub INIT(inp As Scripting.Dictionary)
Set dElms = inp
End Sub
'
Public Function CLONE()
Set CLONE = New cVM_Col
Set CLONE.dElms = dElms ' <-- THIS IS WHERE IT CRASHES
Set CLONE.dDat = dDat
End Function
Normally my CLONE function works when I am only cloning simple data types like String or Long or Double. I've never had to do this with a Dictionary.
To CLONE the dictionary objects in my CLASS Objects I had to make the following changes:
CLASS OBJECT
(Modified CLONE function)
Public Function CLONE()
Set CLONE = New cVM_Col
CLONE.Elms = dElms
CLONE.Dat = dDat
End Function
(Added Properties)
Public Property Get Elms() As Scripting.Dictionary
Set Elms = dElms
End Property
Public Property Let Elms(p As Scripting.Dictionary)
Set dElms = p
End Property
'
Public Property Get Dat() As Scripting.Dictionary
Set Dat = dDat
End Property
Public Property Let Dat(p As Scripting.Dictionary)
Set dDat = p
End Property