Add Collection as an item within another collection - Class - Excel VBA - vba

I'm currently trying to build a collection of items wherein a collection might contain an another collection as an Item within.
I've set two collections and created a class module for each:
col1 - (linked to Class1); and col2 - (linked to Class2)
Below are my Class Modules:
Class1:
Option Explicit
Private pTestC1A As String
Private pTestC1B As Collection
Public Property Let TestC1A(Value As String)
pTestC1A = Value
End Property
Public Property Get TestC1A() As String
TestC1A = pTestC1A
End Property
Property Set TestC1B(col2 As Collection)
Set pTestC1B = col2
End Property
Property Get TestC1BElements(v As Integer) As String
TestC1B = pTestC1B(v)
End Property
Class2:
Option Explicit
Private pTestC2A As String
Public Property Let TestC2A(Value As String)
pTestC2A = Value
End Property
Public Property Get TestC2A() As String
TestC2A = pTestC2A
End Property
Below is my Module code
Sub Test()
Set col1 = New Collection
Set col2 = New Collection
Set cV = New Class1
cV.TestC1A = "First Collection"
Set aV = New Class2
aV.TestC2A = "Second Collection"
sKey1 = CStr(aV.TestC2A)
col2.Add aV, sKey1
Set cV.TestC1B = col2
sKey2 = CStr(cV.TestC1A)
col1.Add cV, sKey2
If Err.Number = 457 Then
MsgBox "Error Occured"
ElseIf Err.Number <> 0 Then Stop
End If
Err.Clear
Msgbox col1(1).TestC1A ' works fine
Msgbox col2(1).TestC2A ' works file
MsgBox col1(1).TestC1B(1).TestC2A ' does not work - 450 run-time error
End Sub
As per the above code, I'm successfully able to get the values of the items if I reference each collection respectively, however I'm getting a "Wrong number of arguments or invalid property assignment" run-time error if I try to get the item value in a nested fashion.
It would be appreciated if someone can help point out where I'm going wrong, and perhaps shed some light on the way the class module handles the Property Set & Get parameters for a collection.

You are missing a Get TestC1B property in your Class1 class module:
Property Get TestC1B() As Collection
Set TestC1B = pTestC1B
End Property
Once that is present you will be able to make calls to col1(1).TestC1B(1) and access it's .TestC2A property
Background
You did the right thing by using private variables in your classes and using properties to give read/write access to your private variables. You'll get a lot more control this way.
Property Get gives read access to that property (and broadly speaking, the underlying private variable). For example you can use Range.Address to return (read) the address of a range object.
Property Let and Set give write access to the property. Use Set for objects. For example Range.Value = 1 will write the new value to the range.
Consider, Range.Address = $A$1. Since there is no Property Set for the address of a range, this will not change the address of the range. It will consider the Range.Address part a Get call and evaluate something like $A$1 = $A$1 returning TRUE in this example

Related

Create a class modules with parent and child classes in VBA

I am a novice user of class module.
I don't understand the concept of a class module well.
I want to configure a class module similar to the basic objects of Excel like Worksheet or Cells or ETC..
So I want to control it by creating a parent object and creating its child objects.
Child Class - Defect
Option Explicit
Private pDefectSymptom As String
Private pDefectLevel As Integer
Property Get DefectSymptom() As String
DefectSymptom = pDefectSymptom
End Property
Property Let DefectSymptom(ByVal vDefectSymptom As String)
pDefectSymptom = vDefectSymptom
End Property
Property Get DefectLevel() As Integer
DefectLevel = pDefectLevel
End Property
Property Let DefectLevel(ByVal vDefectLevel As Integer)
pDefectLevel = vDefectLevel
End Property
Function Delete()
'???
End Function
Property Get Parent() As Object
'???
End Property
Parent Class - Defects
Private Defects As New Collection
Function Add(DefectSymptom As String, Optional DefectLevel As Integer) As Defect
Dim NewDefect As Defect
Set NewDefect = New Defect
NewDefect.DefectSymptom = DefectSymptom
NewDefect.DefectLevel = DefectLevel
Defects.Add NewDefect
'Add = NewDefect 'Error! Like the open command of workbook, I want to return an object or just command
End Function
Property Get Count() As Long
Count = Defects.Count
End Property
Property Get Item(Index As Long) As Defect
Item = Defects(Index) 'Error! I don't know what raise Error.
End Property
My question are.
How to add Command like Open Command of workbook. Return or just command.
Why Raise Error Item Property? how to fix that?
Hiding Private variable. Because office office objects seem to be hidden.
enter image description here
If you have time, please help with Delete command and Parent command.
How to add Command like Open Command of workbook. Return or just
command.
You just need to return the newly created object in the function. Keep in mind since we're dealing with objects, we need to Set the object's reference.
Public Function Add(DefectSymptom As String, Optional DefectLevel As Integer) As Defect
Dim NewDefect As Defect
Set NewDefect = New Defect
NewDefect.DefectSymptom = DefectSymptom
NewDefect.DefectLevel = DefectLevel
Defects.Add NewDefect
Set Add = NewDefect '<- here
End Function
Why Raise Error Item Property? how to fix that?
Same as the above, you need to Set the object's reference.
Property Get Item(Index As Long) As Defect
Set Item = Defects(Index)
End Property
To delete, simply supply the index to the function. However, this method must reside where the collection is (parent) since a Defect object cannot delete itself.
Function Delete(ByVal Index As Long)
Defects.Remove Index
End Function
Lastly, to hold a reference to the parent, each child must hold a reference to it in a private variable. Then you need to set the parent when creating a new item using the keyword Me.
So in the Defect class, create a private field.
Private mParent As Defects
Property Set Parent(ByVal objDefects As Defects)
Set mParent = objDefects
End Property
Property Get Parent() As Defects
Set Parent = mParent
End Property
With this done, amend the Add() method to store the reference.
Public Function Add(DefectSymptom As String, Optional DefectLevel As Integer) As Defect
Dim NewDefect As Defect
Set NewDefect = New Defect
NewDefect.DefectSymptom = DefectSymptom
NewDefect.DefectLevel = DefectLevel
Set NewDefect.Parent = Me '<- here
Defects.Add NewDefect
Set Add = NewDefect '<- here
End Function
Not sure this is a good idea though. I tend to avoid circular references altogether, since a child can hold the parent in memory by holding a reference to it. You will need to make sure to clear the reference to the Parent when deleting the item.
Lastly, you should avoid creating the Defects collection like this. Instead, you should make use of the class constructor and destructor.
This method is called automatically when a new class is created:
Private Sub Class_Initialize()
Set Defects = New VBA.Collection
End Sub
This method is called just before the class is destroyed from memory.
Private Sub Class_Terminate()
Set Defects = Nothing
End Sub

Is it possible to change the appearance of a custom class's object in the VBA editor's locals and watch windows? [duplicate]

Although an experienced VBA programmer it is the first time that I make my own classes (objects). I am surprised to see that all properties are 'duplicated' in the Locals Window. A small example (break at 'End Sub'):
' Class module:
Private pName As String
Public Property Let Name(inValue As String)
pName = inValue
End Property
Public Property Get Name() As String
Name = pName
End Property
' Normal module:
Sub Test()
Dim objTest As cTest
Set objTest = New cTest
objTest.Name = "John Doe"
End Sub
Why are both Name and pName shown in the Locals Window? Can I in some way get rid of pName?
As comments & answers already said, that's just the VBE being helpful.
However if you find it noisy to have the private fields and public members listed in the locals toolwindow, there's a way to nicely clean it up - here I put the Test procedure inside ThisWorkbook, and left the class named Class1:
So what's going on here? What's this?
Here's Class1:
Option Explicit
Private Type TClass1
Name As String
'...other members...
End Type
Private this As TClass1
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
The class only has 1 private field, a user-defined type value named this, which holds all the encapsulated data members.
As a result, the properties' underlying fields are effectively hidden, or rather, they're all regrouped under this, so you won't see the underlying field values unless you want to see them:
And as an additional benefit, you don't need any pseudo-Hungarian prefixes anymore, the properties' implementations are crystal-clear, and best of all the properties have the exact same identifier name as their backing field.
All the Inspection windows not only show the public interface of the objects to you, but also their private members. AFAIK there is nothing you can do about it.
Consider it a nice feature to get even more insights while debugging.
In my experience this is less of an issue in real world objects as they tend to have more fields and properties. Assuming a consistent naming (as your example shows), fields and properties are nicely grouped together.
If you really dont want to see even Mathieu's This you could wrap it into a function. This is a bit more involved, and can be achieved using
a second class that stores the data in public variables. This will be marginally slower then Mattieu's implementation
a collection object that accesses the data using keys. This does not require additional clutter in the project exporer's 'class module' list but will be a little slower if you call the This repeatedly in fast sucession
An example for each is given below. If you break in the Class's Initialisation function, you can add me to the watch window and only the Name property will be listed
Using 2 Objects example
insert a classmodule and name it: InvisibleObjData
Option Explicit
Public Name As String
Public plop
Private Sub Class_Initialize()
Name = "new"
plop = 0
End Sub
insert a classmodule and name it: InvisibleObj
Option Explicit
Private Function This() As InvisibleObjData
Static p As New InvisibleObjData 'static ensures the data object persists at successive calls
Set This = p
End Function
Private Sub Class_Initialize()
This.Name = "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
This.plop = 111: Debug.Print This.plop
End Sub
Property Let Name(aname As String): This.Name = aname: End Property
Property Get Name() As String: Name = This.Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleObj
If you dont like splitting the class over two objects, a similar behaviour can be generated using a 'wrapped' collection object:
insert a classmodule and name it: InvisibleCol
Option Explicit
Private Function This() As Collection
Static p As New Collection
'static ensures the collection object persists at successive calls
'different instances will have different collections
'note a better dictionary object may help
Set This = p
End Function
Private Function add2this(s, v)
'a better dictionary object instead of the collection would help...
On Error Resume Next
This.Remove s
This.Add v, s
End Function
Private Sub Class_Initialize()
add2this "name", "invisible man": Debug.Print Name
Me.Name = "test": Debug.Print Name
add2this "plop", 111
Debug.Print This("plop") ' use the key to access your data
Debug.Print This!plop * 2 ' use of the BANG operator to reduce the number of dbl quotes
' Note: This!plop is the same as This("plop")
End Sub
Property Let Name(aname As String): add2this "name", aname: End Property
Property Get Name() As String: Name = This!Name: End Property
'_______________________________________________________________________________________
' in the immediate window type
'
' set x=new invisibleCol

Getting a collection property of a class take a property of another class of another type?

I wanted to first thank you all for the help you've given me implicitly over the last few months! I've gone from not knowing how to access the VBA IDE in Excel to writing fully integrated analysis programs for work. I couldn't have done it without the community here.
I'm currently trying to overhaul the first iteration of a data analysis program I wrote while learning how to code in VBA. While purpose driven and only really legible to myself, the code worked; but was a mess. From folks on this site I picked up Martin's Clean Code and gave it a read on how to try and be a better programmer.
From Martin's Clean Code, it was impressed on me to prioritize abstraction and decoupling of my code to allow for higher degrees of maintenance and modularization. I found this out the hard way since very minor changes requested above my pay grade would require massive and confusing rewrites! I'm trying to eliminate that problem going forward.
I am attempting to rewrite my code in terms of single responsibility classes (at least, where it is possible) and I am a bit confused. I apologize if my question isn't clear or if I'm using the wrong terminology. I want to be able to generate a collection of specific strings (the names of our detectors to be specific) with no duplicates from raw instrument data files from my lab. The purpose of this function is to assemble a bunch of metadata in a class and use it to standardize our file system and prevent clerical errors from newbies and old hands when they use the analysis program.
The testing initialization sub is below. It pops open a userform asking for the user to select the filepaths of the three files in the rawdatafiles class; then it kills the userform to free memory. The metadata object is currently for testing and will be rewritten properly when I get the output I want:
Sub setup()
GrabFiles.Show
Set rawdatafiles = New cRawDataFiles
rawdatafiles.labjobFile = GrabFiles.tboxLabJobFile.value
rawdatafiles.rawdatafirstcount = GrabFiles.tboxOriginal.value
rawdatafiles.rawdatasecondcount = GrabFiles.tboxRecount.value
Set GrabFiles = Nothing
Dim temp As cMetaData
Set temp = New cMetaData
temp.labjobName = rawdatafiles.labjobFile
'this works fine!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
End Sub
The cMetadata class I have currently is as follows:
Private pLabjobName As String
Private pDetectorsOriginal As Collection
Private pDetectorsRecheck As Collection
Private Sub class_initialize()
Set pDetectorsOriginal = New Collection
Set pDetectorsRecheck = New Collection
End Sub
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
When I step through the code it starts reading the "public property get rawdatafirstcount() as string" and throws the error after "End Property" and points back to the "temp.detectorsOriginal = rawdatafiles.rawdatafirstcount" line in the initialization sub.
I think I'm at least close because the temp.labjobName = rawdatafiles.labjobFile code executes properly. I've tried playing around with the data types since this is a collection being assigned by a string but I unsurprisingly get data type errors and can't seem to figure out how to proceed.
If everything worked the way I want it to, the following function would take the filepath string from the rawdatafiles.rawdatafirstcount property and return for me a collection containing detector names as strings with no duplicates (I don't know if this function works exactly the way I want since I haven't been able to get the filepath I want to parse properly in the initial sub; but I can deal that later!):
Function getDetectors(filePath As String) As Collection
Dim i As Integer
Dim detectorsCollection As Collection
Dim OriginalRawData As Workbook
Set OriginalRawData = Workbooks.Open(fileName:=filePath, ReadOnly:=True)
Set detectorsCollection = New Collection
For i = 1 To OriginalRawData.Worksheets(1).Range("D" & Rows.Count).End(xlUp).Row
detectorsCollection.Add OriginalRawData.Worksheets(1).Cells(i, 4).value, CStr(OriginalRawData.Worksheets(1).Cells(i, 4).value)
On Error GoTo 0
Next i
getDetectors = detectorsCollection
Set detectorsCollection = Nothing
Set OriginalRawData = Nothing
End Function
Thanks again for reading and any help you can offer!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
It throws an error because, as others have already stated, the Set keyword is missing.
Now with that out of the way, a Set keyword is NOT what you want here. In fact, sticking a Set keyword in front of that assignment will only buy you another error.
Let's look at this property you're invoking:
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
You're trying to assign detectorsOriginal with what appears to be some String value that lives in some TextBox control on that form you're showing - but the property's type is Collection, which is an object type - and that's not a String!
Now look at the property that does work:
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
This one is a String property, with a Property Let mutator that uses the fileName parameter it's given.
The broken one:
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Is a Set mutator, takes a Collection parameter, and doesn't use the originalFilepath parameter it's given at all!
And this is where I'm confused about your intention: you're passing what has all the looks of a String except for its type (Collection) - the calling code wants to give it a String.
In other words the calling code is expecting this:
Public Property Let detectorsOriginal(ByVal originalFilepath As String)
See, I don't know what you meant to be doing here; it appears you're missing some pOriginalFilepath As String private field, and then detectorsOriginal would be some get-only property that returns some collection:
Private pOriginalFilePath As String
Public Property Get OriginalFilePath() As String
OriginalFilePath = pOriginalFilePath
End Property
Public Property Let OriginalFilePath(ByVal value As String)
pOriginalFilePath = value
End Property
I don't know what you're trying to achieve, but I can tell you this:
Don't make a Property Set member that ignores its parameter, it's terribly confusing code.
Don't make a Property (Get/Let/Set) member that does anything non-trivial. If it's not trivially simple and has a greater-than-zero chance of throwing an error, it probably shouldn't be a property. Make it a method (Sub, or Function if it needs to return a value) instead.
A word about this:
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
Whenever you Dim something As New, VBA will automatically instantiate the object whenever it's referred to. In other words, this wouldn't throw any errors:
Dim FSO As New FileSystemObject
Set FSO = Nothing
pLabjobName = FSO.GetBaseName(fileName)
Avoid As New if you can. In this case you don't even need a local variable - use a With block instead:
With New FileSystemObject
pLabjobName = .GetBaseName(fileName)
End With
May not be your issue but you're missing Set in your detectorsOriginal Set/Get methods:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
So the error is one I've made a time or two (or more). Whenever you assign an object to another object, you have to use the Set reserved word to assign the reference to the Object.
In your code do the following:
In Sub setup()
Set temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
And in the cMetadata class change the Public Property Set detectorsOriginal(originalFilepath As Collection) property to the following:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Also in your function Function getDetectors(filePath as String) as Collection change the statement afterNext i` to
Set getDetectors = detectorsCollection
Also, I'm very glad to hear that you've learned how to use VBA.
When you're ready to create your own Custom Collections, check out this post. Your own custom Collections.
I also book marked Paul Kelly's Excel Macro Mastery VBA Class Modules – The Ultimate Guide as well as his Excel VBA Dictionary – A Complete Guide.
If you haven't been to Chip Pearson's site you should do so. He has a ton of useful code that will help your delivery your projects more quickly.
Happy Coding.

VBA Class with Collection of itself

I'm trying to create a class with a Collection in it that will hold other CASN (kind of like a linked list), I'm not sure if my instantiation of the class is correct. But every time I try to run my code below, I get the error
Object variable or With block not set
CODE BEING RUN:
If (Numbers.count > 0) Then
Dim num As CASN
For Each num In Numbers
If (num.DuplicateOf.count > 0) Then 'ERROR HERE
Debug.Print "Added " & num.REF_PO & " to list"
ListBox1.AddItem num.REF_PO
End If
Next num
End If
CLASS - CASN:
Private pWeek As String
Private pVendorName As String
Private pVendorID As String
Private pError_NUM As String
Private pREF_PO As Variant
Private pASN_INV_NUM As Variant
Private pDOC_TYPE As String
Private pERROR_TEXT As String
Private pAddressxl As Range
Private pDuplicateOf As Collection
'''''''''''''''' Instantiation of String, Long, Range etc.
'''''''''''''''' Which I know is working fine
''''''''''''''''''''''
' DuplicateOf Property
''''''''''''''''''''''
Public Property Get DuplicateOf() As Collection
Set DuplicateOf = pDuplicateOf
End Property
Public Property Let DuplicateOf(value As Collection)
Set pDuplicateOf = value
End Property
''''' What I believe may be the cause
Basically what I've done is created two Collections of class CASN and I'm trying to compare the two and see if there are any matching values related to the variable .REF_PO and if there is a match I want to add it to the cthisWeek's collection of class CASN in the DuplicateOf collection of that class.
Hopefully this make sense... I know all my code is working great up to this point of comparing the two CASN Collection's. I've thoroughly tested everything and tried a few different approaches and can't seem to find the solution
EDIT:
I found the error to my first issue but now a new issue has appeared...
This would be a relatively simple fix to your Get method:
Public Property Get DuplicateOf() As Collection
If pDuplicateOf Is Nothing Then Set pDuplicateOf = New Collection
Set DuplicateOf = pDuplicateOf
End Property
EDIT: To address your question - "So when creating a class, do I want to initialize all values to either Nothing or Null? Should I have a Class_Terminate as well?"
The answer would be "it depends" - typically there's no need to set all your class properties to some specific value: most of the non-object ones will already have the default value for their specific variable type. You just have to be aware of the impact of having unset variables - mostly when these are object-types.
Whether you need a Class_Terminate would depend on whether your class instances need to perform any "cleanup" (eg. close any open file handles or DB connections) before they get destroyed.

Class property error VBA

I am getting runtine error 91: Object vriable or With block variable not set whne I run a simple class manipulation code.
Here are my classes
cTask:
Private pMile As cMile
Public Property Get Mile() As cMile
Set Mile = pMile
End Property
Public Property Set Mile(Value As cMile)
Set pMile = Value
End Property
Private Sub Class_Initializer()
Set Me.Mile = New cMile
End Sub
cMile:
Private pstatus As String
Public Property Get status() As String
status = ppstatus
End Property
Public Property Let status(Value As String)
pstatus = Value
End Property
And the sub:
Sub testt()
Dim ct As New cTasks
ct.Mile.status = "compl"
Debug.Print ct.Mile, ct.Mile.status
End Sub
The code goes from the sub to the get property in cTask. When about to execute the "End Propety" line the error pops up.
I guess something must be wrong with my classes but I don not know what. I have just recently started using classes. Any ideas?
Thank you
You have a typo:
Class_Initializer()
should be
Class_Initialize()
This prevents ct's Mile from being created so accessing it raises the error you see.
ppstatus is also spelt incorrectly.