How to set current object (Me) to a new object stored in an array in Visual Basic for Excel - vba

I have an array of objects in an excel vba project. I have created another instance of the same class and set 1 of its properties. I am then trying to search through the array of objects to find the object in the array that matches the current one on the same property. I would like to set the current object to the one in the array inside one of the current object's methods using the self reference Me.
I tried:
Set Me = objectArray(index)
This does not work. It says that this is an improper use of the Me keyword. Is there a way to set the current object to another object of the same type? Thanks!
Edit:
I have an object that has child objects:
Me.friShift.shiftType.loadFromArray
Here, shiftType is the object of type CVocabulary, which is my self defined class. It has a sub called loadFromArray that looks like this:
Public Sub loadFromArray()
Dim index As Integer
index = searchVocabArray(Me.typed)
If (index = -1) Then
Exit Sub
End If
Set Me = vocabArray(index)
End Sub
vocabArray() is a global array containing CVocabulary objects.
If it is not possible to Set an object from within itself, I can try something else. This is just the easiest and most direct way of doing this. I'm sure I can just set each parameter from the current object to the value of the parameter from the object in the array, but if it was possible to do something like the above, that would have been my preferred method.

You can do it by giving itself to the function as a parameter. I'll show it in VBScript because the classes are more clear, but the concept is the same as in VBA:
public myObject
set myObject = new x
myObject.ChangeMe MyObject
msgbox typename(myObject) ' <- outputs 'y'
class x
public sub changeMe(byref object)
set object = new y
end sub
end class
class y
' just an empty class
end class
But this is not a good programming pattern and could cause messy code (maintenance and debugging would be an issue) and even memory leaks. You should create an (Abstract) Factory, Builder or Provider that returns an object as you ask for it.
Factory: creates a new predefined object
Builder: creates a new object that is configured in the builder
Provider: returns an existing object that is predefined earlier

I don't beleive you can use Me in this context - you are trying to use Me as it was used in VB6 (which was equivalent to 'this' in C#). This is not appropriate in VBA.
Without some code snippets its hard to see what you are doing. Can you perform the search in a module and create instances of this class there? You can then do:
Set class2 = objectArrayofClass1(index)

As you've already seen that Me cannot be changed. You can handle memorized objects through
a function in a public Module like basExternal:
Public Function loadFromArrayByIndex(ByVal lIndex)
dim xobj as Object
Set xobj = vocabArray(lIndex)
'
' do modifications and handling on this object:
' ...
'
End Function
.

Related

How do I use class modules to set up parent and child variables?

First of all, excuse my explanation of my problem if I use incorrect terminology, I'm not experienced and self-taught.
I am learning to use class modules to set up "Objects" to easier reference variables and to run common functions. The issue I'm having is that I can't find information on how to set up a class module which can act as a collection to make use of the add function inherent in a collection.
For Example, I have my parent class module named clsSchool
in this class module, I have defined an object so that I can set a "child" class, clsTeacher
In my class module for clsTeacher, I have set a string variable Name. This is how my 2 class modules look.
clsSchool
Public Student As Object
clsTeacher
Public Name as String
In my module I have
modSchool
Set mySchool = New clsSchool
Set mySchool.Teacher = New clsTeacher
mySchool.Teacher.Name = "Jim"
At this point, my code is exactly what I want it is very easy to use the variable mySchool.Teacher.Name to recall "Jim", I can also use a while loop with mySchool.Teacher to recall various variables I have defined in my clsTeacher
I problem is that I want to add multiple teachers without having to set multiple classes at the top of my code as the number of teachers can vary. I.e. the following does work but has limitations.
modSchool
Set mySchool = New clsSchool
Set mySchool.Teacher1 = New clsTeacher
Set mySchool.Teacher2 = New clsTeacher
Set mySchool.Teacher3 = New clsTeacher
mySchool.Teacher1.Name = "Jim"
mySchool.Teacher2.Name = "Jack"
mySchool.Teacher3.Name = "John"
What I would like is something similar to how collections work. I.e. I want to find the total number of teachers (which varies) and create a for loop to create a new clsTeacher for each unique teacher. What I want is something like the following, but I'm stuck and I can't find any resources which help explain how to set up the following.
modSchool
Set mySchool.Teacher = New clsTeacher
n_teachers = 6
for i=1 to n_teachers
mySchool.Teacher(i).Name = Range("A1").Offset(i,0)
Next i
This way I can easily recall the name of teacher 1 or teacher 2 and use while/for loops to do so.
You can create a class (clsTeachers) which hides a collection of clsTeachers. Below is a sample of some of the things that can be done - effectively adding to what a Collection can do.
Private pTeachers as New Collection ' Debate: New in the declaration, or New in Class_Initialise?
Property Get Count() as Long
Count = pTeachers
End Property
Sub AddTeacher(NewTeacher as clsTeacher) ' enforces type.
pTeachers.Add(NewTeacher)
End Sub
Function SortTeachers1() as clsTeachers
Dim tNewTColl as clsTeachers
'some sort routine using the existing collection and adding in order to a new class
Set SortTeachers = tNewColl
End Function
Sub SortTeachers2()
Dim tNewColl as New Collection
'some sort routine using the existing collection and the new one
Set pTeachers = tNewColl
End Sub
Sub PrintTeachers()
Dim tTchr as clsTeacher
For each tTchr in pTeachers
'valid print command or add to string for later printing
Next 'tTchr
End Sub
The methods are only limited by your imagination. The one drawback I have found is no easy way to use this in a 'For Each' loop. The standard For I = 1 to MyTeachers.Count is a useful fallback - just not as neat. In the class you can have:
Property Get Teacher(Index as Long) as clsTeacher
'error check here to ensure Index exists
if tIndexValid then
Teacher = pTeachers(Index)
else
Teacher = Nothing 'or however you want to handle this
end if
End Property
I have found this way of doing collections useful because you can do validation and tailored outputs hidden from the main code view. As you can see by the signatures, you can also enforce types (i.e. your chosen collection cannot have anything but Teachers in it).
** some references for doing a for-each with custom collections:
https://www.mrexcel.com/forum/excel-questions/466141-use-custom-class-each-loop.html
How to implement custom iterable class in VBA
which includes other links.

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.

VBA With obj - how to reference the obj inside the With without naming it?

If I am inside a With block that is referencing the object I want to reference for a function call for example , must I reference the object by name or is there a "this" ,"me" ref that I can use?
I Have done a search online but not finding much about it in VBA.
Dim shExport As Worksheet
With shExport
.......
.......
'works as expected
GetData(shExport)
'but how can this be achieved without naming it
GetData(this)
GetData(me)
GetData()
........
End With
Function GetData(sh As Worksheet) As Integer
.....
.....
End Function
You could specify a property of the object that supports the .Parent property to get a reference back to the object. For example:
With shExport
GetData(.Cells.Parent)
End With
There's no way to reference the object other than directly call it (also look: How to access the object itself in With ... End With).
What you can do is to generically call the worksheets collection like this:
ActiveWorkbook.Worksheets("Sheet1") or ActiveWorkbook.Sheets(1) etc.
Regarding the with block - You can use a dot . inside it to reference the object's child elements/fields, but you can't send the dot . as a parameter.

How do I know if an object is already referenced?

In some VBA attached to an Excel 2003 spreadsheet I need to make use of some objects that take a while to instantiate - so I only want to do the 'set' thing once...
It's easier the show the code than to write an explanation!
' Declare the expensive object as global to this sheet
Dim myObj As SomeBigExpensiveObject
Private Sub CommandButtonDoIt_Click()
' Make sure we've got a ref to the object
If IsEmpty(myObj) Then ' this doesn't work!
Set myObj = New SomeBigExpensiveObject
End If
' ... etc
End Sub
How can I check if myObj has already been set?
I've tried IsNull(myObj) and IsEmpty(myObj) - both skip the 'set', regardless of the state of myObj. I can't do
if myObj = Nil then
or
if myObj = Empty then
or
if myObj = Nothing then
Any ideas?
SAL
This should work:
If myObj IS Nothing Then
(note the "IS") If that does not work, then there must be asynchronous intialization implemented specifically by that Class because COM init calls are synchronous by default. So, you need to check the doc for, or talk to the developer about, the Big class for some property or synch method for you to wait on.