Return custom objects Excel VBA - vba

I have dificulties figuring how a function can return an object in Excel VBA.
For example, in Java, I am used to write it like this:
Private ArrayList<> getARandomArrayList() {
//... My code
return anArrayList;
}
This method should return an arrayList that I can use.
If I do this in Excel, I believe it is supposed to look like this:
Function getARandomArrayList() As System.Collections.ArrayList
'... My code
getARandomArrayList = anArrayList
End Function
When I try to use this kind of function, I get a "Compile error: User-defined type not defined" error window. If I use variables type like Double or String, I have no problem. It is only with objects that I get errors.

A VBA.Collection may work for you, depending on your needs. (Edit: And as Vincent points out in the comments, no additional references are required.) Here's what it'd look like to use one:
Function getSomeCollection() As VBA.Collection
'Declare a collection
Dim newCollection As VBA.Collection
'Initialize the collection
Set newCollection = New VBA.Collection
'Add a string.
'Other methods are Item (access by index), Count, and Remove (by index)
newCollection.Add "hello"
'Reference the collection (note it's 1-based)
MsgBox newCollection(1)
'Set the return value
Set getSomeCollection = newCollection
End Function

As Rory said:
You have to set a reference to the relevant object library in order to be able to declare a variable as a type contained in it
I went in reference and activated System librairies.

Related

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.

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

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
.

Alternatives to using a Collection class

I have been looking through old code to get familiar with the system I use and found a piece of code that I feel can be used better.
What goes on here is some data gets added to the collection(around 150 string variables, some with two variables(variableName/VariableValue), most with only one(VariableName)). It will try to set a module level string variable to the item of the collection passing it the index(variableName) then if there's a value setting the VariableVAlue to the module level variable.
What I feel needs work is that if the collection is passed a variable and the variable doesn't have a value it will return a "" which would cause a runtime error hence there's a On Error GoTo Handler code to manually add a "" to the collection. I feel there's a better way to do this rather than knowing there will be a runtime issue then solving it after catching it. Would there be a way to have a return "" not throw an exception or would the use of an Array also work here since it's a "collection" as well?
Here's an example to try to help visualize:
Public Function GetCollectionVariable(ByVal varName as string) as String
If collection1 Is Nothing Then
m_collection1 = New Collection
End If
On Error GoTo Handler
GetCollectionVariable = collection1.Item(VarName)
exit function
Handler:
collection1.add("", VarName)
GetCollectionVariable = ""
End FUnction
Thanks for your time!!
If Collection1 is a dictionary, you can use TryGetValue.

Restrict type in a Collection inside a class module

I have a collection inside a class module. I'd like to restrict the object type that is "addable" to this collection, i.e. collection should only ever accept objects of one given type and nothing else.
Is there any way to enforce the type of objects added to a collection?
From what I can tell, there is no built-in way to do this. Is the solution then to make this collection private, and build wrapper functions for the methods usually accessible for Collections, i.e. Add, Remove, Item, and Count?
I kinda hate having to write 3 wrapper functions that add no functionality, just to be able to add some type enforcement to the Add method. But if that's the only way, then that's the only way.
There is no way to avoid wrapper functions. That's just inherent in the "specialization through containment/delegation" model that VBA uses.
You can build a "custom collection class", though. You can even make it iterable with For...Each, but that requires leaving the VBA IDE and editing source files directly.
First, see the "Creating Your Own Collection Classes" section of the old Visual Basic 6.0 Programmer's Guide:
http://msdn.microsoft.com/en-us/library/aa262340(v=VS.60).aspx
There is also an answer here on stackoverflow that describes the same thing:
vb6 equivalent to list<someclass>
However, those are written for VB6, not VBA. In VBA you can't do the "procedure attributes" part in the IDE. You have to export the class module as text and add it in with a text editor. Dick Kusleika's website Daily Dose of Excel (Dick is a regular stackoverflow contributer as you probably know) has a post from Rob van Gelder showing how to do this:
http://www.dailydoseofexcel.com/archives/2010/07/04/custom-collection-class/
In your case, going to all that trouble - each "custom collection" class needs its own module - might not be worth it. (If you only have one use for this and it is buried in another class, you might find that you don't want to expose all of the functionality of Collection anyway.)
This is what I did. I liked Rob van Gelder's example, as pointed to by #jtolle, but why should I be content with making a "custom collection class" that will only accept one specific object type (e.g. People), forever? As #jtolle points out, this is super annoying.
Instead, I generalized the idea and made a new class called UniformCollection that can contain any data type -- as long as all items are of the same type in any given instance of UniformCollection.
I added a private Variant that is a placeholder for the data type that a given instance of UniformCollection can contain.
Private mvarPrototype As Variant
After making an instance of UniformCollection and before using it, it must be initialized by specifying which data type it will contain.
Public Sub Initialize(Prototype As Variant)
If VarType(Prototype) = vbEmpty Or VarType(Prototype) = vbNull Then
Err.Raise Number:=ERR__CANT_INITIALIZE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__CANT_INITIALIZE) & _
TypeName(Prototype)
End If
' Clear anything already in collection.
Set mUniformCollection = New Collection
If VarType(Prototype) = vbObject Or VarType(Prototype) = vbDataObject Then
' It's an object. Need Set.
Set mvarPrototype = Prototype
Else
' It's not an object.
mvarPrototype = Prototype
End If
' Collection will now accept only items of same type as Prototype.
End Sub
The Add method will then only accept new items that are of the same type as Prototype (be it an object or a primitive variable... haven't tested with UDTs yet).
Public Sub Add(NewItem As Variant)
If VarType(mvarPrototype) = vbEmpty Then
Err.Raise Number:=ERR__NOT_INITIALIZED, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__NOT_INITIALIZED)
ElseIf Not TypeName(NewItem) = TypeName(mvarPrototype) Then
Err.Raise Number:=ERR__INVALID_TYPE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__INVALID_TYPE) & _
TypeName(mvarPrototype) & "."
Else
' Object is of correct type. Accept it.
' Do nothing.
End If
mUniformCollection.Add NewItem
End Sub
The rest is pretty much the same as in the example (plus some error handling). Too bad RvG didn't go the whole way! Even more too bad that Microsoft didn't include this kind of thing as a built-in feature...
I did almost the same code of Jean-François Corbett, but I adapted because for some reason wasn't working.
Option Explicit
Public pParametro As String
Private pColecao As New Collection
Public Sub Inicializar(ByVal parametro As String)
pParametro = parametro
End Sub
Public Sub Add(NewItem As Object)
If TypeName(NewItem) <> pParametro Then
MsgBox "Classe do objeto não é compatível à coleção"
Else
pColecao.Add NewItem
End If
End Sub
Public Property Get Count() As Long
Count = pColecao.Count
End Property
Public Property Get Item(NameOrNumber As Variant) As Variant
Set Item = pColecao(NameOrNumber)
End Property
Sub Remove(NameOrNumber As Variant)
pColecao.Remove NameOrNumber
End Sub
Then, when i want to create an instance from CCollection I make like the code bellow:
Set pFornecedores = New CCollection
pFornecedores.Inicializar ("CEmpresa")
Where CEmpresa is the class type from the object I want
Yes. The solution is to make your collection private and then make public wrapper functions to add, remove, getitem and count etc.
It may seem like hassle to write the additional code but it is a more robust solution to encapsulate the collection like this.

How can I evaluate a string into an object in VBA?

In my previous question, How do I assign a value to a property where the property name is supplied at runtime in VBA?, I learned to use CallByName to set a property in a class at run time.
This time, however, I'm trying to figure out how to get an object at run time from a string.
For example, let's say I have a string with the following data: Worksheets("RAW DATA").Range("A1").QueryTable.
Here's what I might try to do where the data above is the input for strParam below:
Function GetObject(strParam As String) As Object
GetObject = SomeFunction(strParam)
End Function
In this case, GetObject should return a QueryTable when evaluated against Worksheets("RAW DATA").Range("A1").QueryTable. Is there anything in VBA that could take the place of SomeFunction from the example above?
Active Scripting Engine can help you. Instantiate ScriptControl ActiveX, use .AddObject() method to add reference to Excel's Application object to the script control's execution environment, set the third parameter to True to make all Application's members accessible too. Then just use .Eval() method to evaluate any property or method, which is the Application's member. The example below shows evaluation of Worksheets() property:
Sub TestQueryTable()
Dim objQueryTable As QueryTable
Dim strEvalContent As String
strEvalContent = "Worksheets(""RAW DATA"").Range(""A1"").QueryTable"
Set objQueryTable = EvalObject(strEvalContent)
objQueryTable.Refresh
MsgBox objQueryTable.Connection
End Sub
Function EvalObject(strEvalContent As String) As Object
With CreateObject("ScriptControl")
.Language = "VBScript"
.AddObject "app", Application, True
Set EvalObject = .Eval(strEvalContent)
End With
End Function
If you are on 64-bit Office, this answer may help you to get ScriptControl to work.
This time you're out of luck. There is no VBA equivalent of eval (not in Excel anyway...there is in Access VBA).
(Application.Evaluate() evaluates strings as Excel expressions, not as VBA code.)
There's the "Evaluate" method (or [ ] brackets). I don't think it will do exactly what you expect - as in run VBA code found in a string. You can look it up in the VBA help menu.