Can VBA enumerate a COM object's methods or fields? - vba

I have a COM object (built with C#.NET) I'm using in VBA (Excel) and it would be really nice enumerate the COM object's fields and reference them automatically. In .NET, this could be done with reflection. Is there any way to do this in VBA?
So, instead of
Dim x As MyCOMObject
Set x = New MyCOMObject
x.f1 = 1
x.f2 = 2
x.f3 = 3
Something more like:
Dim x As MyCOMObject
Set x = New MyCOMObject
For i = 0 to COMFieldCount(x) - 1
SetCOMField(x, GetCOMFieldName(i), i+1)
Next i

You will probably need to refine this code a bit, but it does roughly what you are looking for.
First, you need to add reference to "Typelib information", TLBINF32.dll. I'm not sure if this is a part of Windows or came with some of the numerous SDKs I have installed on my machine, but it is in the System32 folder.
I am assuming that you are setting properties of a COM object, so you will be calling "property put" functions to set the values of your object. You may need to check for the datatypes of those properties, I haven't done any datatype conversion in my code.
The code looks like this:
'Define the variables
Dim tliApp As TLI.TLIApplication
Dim typeinfo As TLI.typeinfo
Dim interface As TLI.InterfaceInfo
Dim member As TLI.MemberInfo
'Initialize typelib reflector
Set tliApp = New TLI.TLIApplication
'Get the type information about myObject (the COM object you want to process)
Set typeinfo = tliApp.ClassInfoFromObject(myObject)
'Set all properties of all the object's interfaces
For Each interface In typeinfo.Interfaces
For Each member In interface.Members
'If this is a "property put" function
If member.InvokeKind = INVOKE_PROPERTYPUT Then
'Invoke the mebmer and set someValue to it.
'Note that you'll probably want to check what datatype to use and do some more error checking
CallByName myObject, member.Name, VbLet, someValue
End If
Next
Next

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.

Looping Through a dictionary with custom object items

I'm sure I'm missing the huge elephant in the room but I keep getting errors on this. I'm creating a public dictionary called Prompts and filling it with a custom class object in the sub below.
Public Sub SetPromptControls()
Dim PromptsRange As Range
Dim PromptRow As Range
Set PromptsRange = Range("LookUpTablePrompts")
Dim NewPrompt As clsPrompt
For Each PromptRow In PromptsRange.Rows
Set NewPrompt = New clsPrompt
NewPrompt.Name = PromptRow.Cells(1, 1)
NewPrompt.ControlType = PromptRow.Cells(1, 2)
NewPrompt.ComboboxValues = PromptRow.Cells(1, 3)
NewPrompt.HelpText = PromptRow.Cells(1, 4)
NewPrompt.TabIndex = PromptRow.Cells(1, 5)
NewPrompt.ColumnIndex = PromptRow.Cells(1, 6)
NewPrompt.TableIndex = PromptRow.Cells(1, 7)
NewPrompt.ControlName = PromptRow.Cells(1, 8)
Me.Prompts.Add NewPrompt.ControlName, NewPrompt
Next
End Sub
Now I'm trying to loop through the dictionary I just made in this next sub which is inside the same class. The problem is the for each loop keeps giving me object errors
Public Sub SetProductPromptMapping()
Dim ProductPromptMappingRange As Range
Dim SKURange As Range
Dim SKUPromptMapRow As Integer
Dim MapRow As Range
Dim Key As Variant
Dim Prompt As clsPrompt
Set ProductPromptMappingRange = Range("LookUpTablePromptMap")
Set SKURange = ProductPromptMappingRange.Find(PromptsForm.SKU, LookIn:=xlValues)
SKUPromptMapRow = SKURange.Row - 2
For Each Key In Prompts.Keys
Set Prompt = New clsPrompt
Prompt = Key
Me.ProductPromptMappingRow.Add Prompt.ControlName, ProductPromptMappingRange.Cells(SKUPromptMapRow, Prompt.TableIndex).Value
Next
End Sub
Ultimately I would like to loop through my Prompts dictionary and cast the current item back to my clsPrompt class object so that I can access its properties.
As Comintern correctly points out, you've been bit by a common mistake - trying to assign an object reference without the Set keyword. Here's the smallest example I can come up with that demonstrates the problem:
Option Explicit
Public Sub DoSomething()
Dim foo As MyClass
foo = New MyClass
End Sub
Here there's a local foo object variable that's assigned a reference (= New MyClass), but because the assignment is made without the Set keyword, running this would raise a runtime error 91:
Object variable or With block variable not set
Your code has that exact same issue:
Dim Prompt As clsPrompt
'...
'more code
'...
Prompt = Key
The code happily compiles, but will consistently raise that runtime error 91 when executed.
This mistake is common enough (just look at how many questions involve runtime error 91 right here on Stack Overflow), that I've decided to implement an inspection for it in the latest version of Rubberduck, an open-source COM add-in for the VBE that can help you clean up your code (I'm managing the project):
Object variable 'foo' is assigned without the 'Set' keyword
As far as Rubberduck can tell, this variable is an object variable, assigned without the 'Set' keyword. This causes run-time error 91 'Object or With block variable not set'.
Rubberduck would have caught that error =)
What it wouldn't have caught though, is that it doesn't make much sense to assign Prompt a new reference, just to re-assign it to some Variant right away. Again as Comintern correctly points out, you need to Set Prompt = Prompts(Key) here.

Return custom objects Excel 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.

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
.

How do I assign a value to a property where the property name is supplied at runtime in VBA?

I'm trying to create a worksheet that creates a list of values that will be used to initialize the values of an instantiated class.
For example, I might have the following in my initialization worksheet:
Property Name Value
StartingCol A
StartingRow 11
I'd then create a class that would parse this worksheet and provide me with an enumberable that I could use to initialize the properties of an instantiated object. However, I'm not sure how I might be able to specify the property value at runtime using a string rather than specifying it explicitly in code. You can get an idea of what I'm trying to accomplish in the code below:
Sub test_PropertyAssignment()
Dim sp As SheetParser
Dim strFieldName As String
Dim strFieldNameValue As String
Set sp = New SheetParser
'The property name is supplied explicitly'
sp.StartingCol = "B"
strFieldName = "StartingCol"
strFieldNameValue = "B"
sp.[how can I supply strFieldName to specify the property?] = strFieldNameValue 'Will not Work'
End Sub
Is there a way to use a string at runtime to specify the property name rather than specifying explicitly in code?
Look up the CallByName function in the VBA help.
You should be able to do something like:
Call CallByName(sp,strFieldName,vbLet,strFieldNameValue)