VB6/VBA Iterate through all predeclared class objects - excel

Can I dynamically iterate over all Predeclared objects?
This is a problem I've been dealing with for a while now. Ideally I'd iterate through all classes and check if they implement some interfaces. If they do then execute some code on them.
Currently I have to provide some array of classes to execute e.g:
ClassesToCheck = Array(Task_Class1,Task_Class2,Task_Class3,Task_Class4, ...)
Dim klass as object
For each klass in ClassesToCheck
if klass.implements(ITask) then
Call klass.execute()
end if
next
In an ideal world I would do something like this:
Dim klass as object
For each klass in GET_PREDECLARED_CLASS_OBJECTS_FROM_MEMORY()
if klass.implements(ITask) then
Call klass.execute()
end if
next
I don't expect there is any easy way to do this, but I have done a little research/exploration of the VBA runtime memory... I think it should be possible and have found some VB6 examples of this using the VBA6.DLL, however, unfortunately this DLL is not provided in Microsoft Office natively. However, the VBA6.DLL is likely 'compiled into' Microsoft Office itself. So the methods/fields should also be held in memory somewhere, you just need to know where they are using pointer math (is my theory)
I don't suppose anyone has had any experience with this?

A VB_PredeclaredId attribute makes your Class1 identifier automatically refer to a global-scope object by that name, e.g. UserForm1 is the name of a class module (one with a designer, but that part is irrelevant), and it's the name of a global, automagic object spawned by VBA at runtime, and the compiler knows Class1.DoStuff is legal because it knows Class1 has VB_PredeclaredId set to True.
Thanks to the work and contributions of Wayne Phillips (vbWatchDog) and other contributors, Rubberduck taps into this internal API.
As shown in the linked code (C#), you can get ahold of the ITypeLib for a VBA project from its References collection, by turning the pointer into a struct with this specific layout:
[StructLayout(LayoutKind.Sequential)]
internal struct VBEReferencesObj
{
IntPtr _vTable1; // _References vtable
IntPtr _vTable2;
IntPtr _vTable3;
IntPtr _object1;
IntPtr _object2;
public IntPtr _typeLib; // <--- here's the pointer you want
IntPtr _placeholder1;
IntPtr _placeholder2;
IntPtr _refCount;
}
In VBA that would be a user-defined Type that might look like this:
Public Type VBEReferencesObj
vTable1 As LongPtr
vTable2 As LongPtr
vTable3 As LongPtr
object1 As LongPtr
object2 As LongPtr
typelibPointer As LongPtr '<~ only this one matters
placeholder1 As LongPtr
placeholder2 As LongPtr
refCount As LongPtr
End Type
Once you have the pointer to the ITypeLib, you should be able to get the VBA project's type library.
From there, you'll want to iterate the types, and from there determine whether the type's TYPEFLAGS has TYPEFLAG_PREDECLID turned on (we do this here).
Obviously this is a lot of extremely crash-prone trial-and-error coding, and I wouldn't recommend doing any of this, but in any case, it's possible, if not adviseable.
Feel free to study Rubberduck.VBEEditor.ComManagement.TypeLibs namespaces.

I don't know how to leverage the tlbinf32.dll, to get class info directly from memory, however, If you stuck with you current approach, but used a collection of objects instead of an array of objects, you could do the following:
Private ConditionalExecution()
Dim PredeclaredClasses As Collection
Set PredeclaredClasses = New Collection
PredeclaredClasses.Add Task_Class1
PredeclaredClasses.Add Task_Class2
PredeclaredClasses.Add Task_Class3
PredeclaredClasses.Add Task_Class4
ExecuteIfImplementsInterface PredeclaredClasses
End Sub
Private Sub ExecuteIfImplementsInterface(ByVal Classes As Collection)
Dim klass as object
For each klass in classes
If TypeOf klass Is ITask Then klass.execute()
next
End Sub
Ofcourse this would be contigent on the fact that these classes would have to be instantiated via "dimming" them as the interface they are implementing like so:
Dim bar As ITask
Set bar = New Task_Class1
SideNote: If you were wondering, Yes I suck at coming up with method names.

Related

Using .NET HashTable Return Type in VBA

I have created a .NET library in VB.NET and there is a function that returns an object of HashTable.
I have been searching for how to access the elements in the HashTable object in Excel VBA but can't find a solution. I am new to VBA so pardon me. I have searched but can't find a way out.
For instance, after something like this, I don't know how to access the data.
Dim hashData As Object
Set hashData = obj.getHashData
Please help
Dim hashData As Object
Set hashData = obj.getHashData
If getHashData is returning a HashTable, then hashData is a late-bound HashTable, and you can invoke its members, including its Item property:
Dim value As Variant
value = hashData.Item("key")
You're not getting compile-time validation on the late-bound member calls against Object, so you need to be particularly careful for typos, because Option Explicit cannot save you when late binding is involved. Refer to the HashTable documentation linked above for what members you can invoke.
Adding a reference to mscorlib.tlb (you'll find it under C:\Windows\Microsoft.NET\Framework\v4.0.30319, or reference the equivalent from \Framework64 if your Excel is 64-bit - bitness of the library needs to match the bitness of the host application) would normally allow for early binding, but while this library is COM-visible, it's intended to be used from managed (.net) code, so you're accessing these objects from interfaces - the concrete types don't expose any members directly:
Knowing that Hashtable implements the IDictionary interface, we can use early binding and get compile-time validation and IntelliSense if we declare hashData As IDictionary:
Dim hashData As mscorlib.IDictionary
Set hashData = New mscorlib.Hashtable
hashData.Add "foo", 42
Debug.Print hashData.Item("foo") 'prints 42
Note that the Item property is exposed as the default member:
This means you can have the Item member call implicit, exactly as you could do with any standard VBA collection object:
Dim hashData As mscorlib.IDictionary
Set hashData = New mscorlib.Hashtable
hashData.Add "foo", 42
Debug.Print hashData("foo") 'prints 42
Early-bound code is much easier to write, especially when you're not familiar with the types involved. However if the project is referencing the 64-bit framework and your macros need to run on 32-bit Excel, you'll want to stick to late binding to avoid binding issues.
Also note, iterating the Hashtable object with a For Each loop isn't going to work, because of how enumerators work in VBA vs how they work in .NET; the Keys and Values collections are objects implementing the ICollection interface, so iterating them will be non-trivial as well: a For Each loop won't work, and while you can set up a For i = 0 To hashData.Keys.Count - 1, you can't get the item at index i from an ICollection.
But we know that ICollection inherits IEnumerable, and IEnumerable does work with For Each, so we can cast the Keys collection to IEnumerable, and iterate all keys and values like so:
Dim hashData As mscorlib.IDictionary
Set hashData = obj.getHashData
Dim hashKeys As mscorlib.IEnumerable
Set hashKeys = hashData.Keys
Dim k As Variant
For Each k In hashKeys
Debug.Print k, hashData(k) 'outputs the key and its associated value
Next
The problem is that you can't cast to IEnumerable with late-bound code or without a reference to mscorlib.tlb, and late binding somehow won't see the GetEnumerator member, so this raises error 438:
Dim hashKeys As Object
Set hashKeys = hashData.Keys
Dim k As Variant
For Each k In hashKeys ' error 438, hashKeys isn't exposing the enumerator
Debug.Print k, hashData(k)
Next
Conclusion: if you need the VBA code to run on both 32 and 64 bit hosts, you'll have to jump through hoops to get late-bound code to work. I would recommend working early-bound with the 64-bit framework if you're on a 64-bit host, and distributing a separate copy of the macro that references the 32-bit framework for 32-bit hosts. A bit of a pain to distribute, but less painful than getting late bound code to work.

Do I need the Me keyword in class modules?

These two subs do the same thing when inside a class.
Sub DemoMe( )
Me.AboutMe ' Calls AboutMe procedure.
End Sub
Sub DemoMe( )
AboutMe ' Does the same thing.
End Sub
What is the point? Does the Me keyword do anything? What is the preferred way of an object accessing its own members?
tldr; No, although there are situations where it can be useful.
From the VBA language specification (5.3.1.5):
Each procedure that is a method has an implicit ByVal parameter called
the current object that corresponds to the target object of an
invocation of the method. The current object acts as an anonymous
local variable with procedure extent and whose declared type is the
class name of the class module containing the method declaration. For
the duration of an activation of the method the data value of the
current object variable is target object of the procedure invocation
that created that activation. The current object is accessed using the
Me keyword within the <procedure-body> of the method but cannot be
assigned to or otherwise modified.
That's all it is, just a "free" local variable that refers to the specific instance that the method is being called on. This also happens to be the default context for the procedures during their invocation, so it can be omitted if the code is intended to operate on the current instance. Although as #HansPassant points out in the comment above, it also allows the editor to bind to the interface and provide IntelliSense.
That said, there are a couple instances where you would either want to or have to use it (this is by no means an exhaustive list):
Naming collisions:
If your class has a member that "hides" a built-in VBA function, it can be used to make the scope explicit:
Public Property Get Left() As Long
'...
End Property
Public Property Get Right() As Long
'...
End Property
Public Property Get Width() As Long
Width = Me.Right - Me.Left
End Property
Equity Checks:
Public Function Equals(other As Object) As Boolean
If other Is Me Then
Equals = True
Exit Function
End If
'...
End Function
Fluent Functions:
This can be a useful pattern for compositing objects - you perform an action, then return the instance of the class so they can be "chained". Excel's Range interface does this in a lot of cases:
Public Function Add(Value As Long) As Class1
'Do whatever.
Set Add = Me
End Function
Public Sub Foo()
Dim bar As New Class1
bar.Add(1).Add(1).Add 1
End Sub
Not any more than there are reasons to use this in Java, C#, or any other language: it's a reserved identifier that represents the current instance of the class - what you do with that is up to your imagination.
What is the preferred way of an object accessing its own members?
Indeed, an object doesn't need the Me keyword to access it own public interface. Same as this in other languages, I'd even call it redundant. However it can sometimes be a good idea to explicitly qualify member calls with Me, especially when the class has a VB_PredeclaredId attribute (e.g. any UserForm): referring to UserForm1 in the code-behind of UserForm1 yields a reference to the default instance of the class, whereas qualifying member calls with Me yields a reference to the current instance of that class.
Accessing Inherited Members
VBA user code can't do class inheritance, but a lot of VBA classes do have a base class. The members of UserForm when you're in the code-behind of UserForm1, and those of Worksheet when you're in the code-behind of Sheet1, aren't necessarily easy to find. But since the inherited members show up in IntelliSense/auto-complete, you can type Me. and browse a list of members inherited from the base class, members that you would otherwise need to know about in order to invoke.
A class creating an instance of itself inside itself? That I've never seen.
You're missing out! I do this all the time, to enable referring to the object instance held by a With block, inside a Factory Method - like this GridCoord class.
Public Function Create(ByVal xPosition As Long, ByVal yPosition As Long) As IGridCoord
With New GridCoord
.X = xPosition
.Y = yPosition
Set Create = .Self
End With
End Function
Public Property Get Self() As IGridCoord
Set Self = Me
End Property
Note that while the GridCoord class exposes a getter and a setter for both X and Y properties, the IGridCoord interface only exposes the getters. As a result, code written against the IGridCoord interface is effectively working with read-only properties.
Another use is to get the name of the class module, without needing to hard-code it. This is particularly useful when raising custom errors: just use TypeName(Me) for the Source of the error.
The Builder Pattern notoriously returns Me, which enables a "fluent API" design that makes it possible to write code that incrementally builds complex objects through chained member calls, where each member returns Me (except the final Build call, which returns the type of the class being built):
Dim thing As Something
Set builder = New ThingBuilder
Set thing = builder _
.WithFoo(42) _
.WithBar("test") _
.WithSomething _
.WithSomethingElse
.Build
#PBeezy : In addition to my comment :
Me, refers to the object it's coming from so AboutMe resides in the class. If you had another instance, say this is Class1, you'd have dim c as Class1, as soon as you create an instance of Class1 in Class1, you need to tell the compiler which class you are using, the holding class or the instance created in, where, me.class1.aboutme would be logically valid. You can also create, a class for each cell in a workbook, then you could refer to A1's class from B1's class. Also, if there is a public function/sub called AboutMe, this also helps.
Class (clsPerson)
Public c1 As clsPerson
Public strPersonName As String
Public Function NAME_THIS_PERSON(strName As String)
strPersonName = strName
End Function
Public Function ADD_NEW_CHILD(strChildName As String)
Set c1 = New clsPerson
c1.strPersonName = strChildName
End Function
Normal module
Sub test()
Dim c As New clsPerson
c.NAME_THIS_PERSON "Mother"
c.ADD_NEW_CHILD "Nathan"
Debug.Print c.strPersonName
Debug.Print c.c1.strPersonName
End Sub
Gives these results
Mother
Nathan

Late Binding a UDT in a form module to pass as a parameter

I have an access database and I'm attempting to write some VBA to increase automation.
I have a module I've entitled Global Variables which I've successfully used to define global constants (file paths etc) and a module ReportCode which has two main subrouties, one to run a query with ADODB (scraping form params where needed - returning a recordset), and a second which takes this record set and writes the data out to an excel template.
Given I may want to have multiple queries write to multiple tabs I thought the best way was to define a ExportDocument object to contain common parameters and a OrgReport object, containing query and tab specific parameters - then gather multiple OrgReport objects in a collection.
I'd hope to then pass just these two parameters into the main subroutine. This turns out to be a pain in VBA (or at least compared to ruby!).
Here you can see how I've defined by custom objects
Option Private Module
' Define Custom Doc Object
Public Type ExportDocument
TeamName As String
TemplatePath As String
SaveName As String
SavePath As String
End Type
' Define Custom Report Object
Public Type OrgReport
Query As String
Fields As Variant
Sheet As String
StartCol As Integer
StartRow As Integer
Headers As Boolean
End Type
And here is the code in my form which then called an additional module which does the heavy lifting - I know that part works because it did before I tried to go all OOP on this...
Private Sub my_report_from_form_Click()
' Prep Query Inputs
Dim TeamX_Report As OrgReport
TeamX_Report.Query = "qry_TeamReporting Query"
TeamX_Report.Sheet = "RawData"
TeamX_Report.StartCol = 1
TeamX_Report.StartRow = 2
TeamX_Report.Headers = True
TeamX_Report.Fields = Nothing
' Prep Document Inputs
Dim Teamx_Doc As ExportDocument
Teamx_Doc.TeamName = "MyTeam"
Teamx_Doc.TemplatePath = strReportTemplatePath & "MyTeam.xltm"
Teamx_Doc.SaveName = ""
Teamx_Doc.SavePath = strReportSavePath & Teamx_Doc.TeamName
' Init and set collection for CHAIN reports
Dim TeamReports As New Collection
TeamReports .Add Item:=TeamX_Report, Key:=TeamX_Report.Query
Call export_data_dump(Teamx_Doc, TeamReports)
End Sub
This gives me the issue of:
Only public user defined types defined in public object modules can be
used as parameters or return types for public procedures of class
modules or as fields of public user defined types
Following advice here I changed
Dim Teamx_Doc As ExportDocument
to
Teamx_Doc = CreateObject("ExportDocument")
But alas now i get
Run-time error '429': ActiveX component can't create object VBA
All references to this problem seem to be related to calling code from the Word., Excel. or Outlook. codebases, so perhaps I'm just missing a prefix for my own module stored within my database?
Best lead I've found is this one, which seems to suggest there's deeper issues with what i'm trying to do, or that i may get around parts by calling Friend, though I'm lost to where and how.
Is there a way I can late bind my UDT Objects within my form code, stash one in a collection then pass both to a subroutine that will be able to grab params from the first 'Doc' object and then iterate through the second 'report' object?
VBA >_<
There's no reason I can see why this doesn't work:
Dim Teamx_Doc As ExportDocument
Especially if you're not getting an error on line
Dim TeamX_Report As OrgReport
I've used custom Public Types before - no need for CreateObject
Though the docs seem to say it's just fine, can you try removing the
Option Private Module
The error message is kinda misleading. You simply can't put variables with an User-defined Type into a collection.
Option 1: Use an array instead. This actually sounds like it would work well for what you want to do.
Option 2: Create a class module instead of an UDT for OrgReport. Then instantiate objects of that class, those you can add to a collection.
See
Excel VBA Collections and Custom Data Types
and
http://www.mrexcel.com/forum/excel-questions/16849-adding-user-defined-types-collection.html

Worksheets vs. Worksheets(1), can't I do this from .net interop?

Our object model contains a class called Unit and a collection of these called Units (which is stored in a Dictionary). These objects have unique Names and Keys (they originally came from a SQL db that enforced this) so I have added:
Public Units(N as String) As Unit ...
Public Units(K as Integer) As Unit...
which return a Unit object from the Units collection.
In Excel VBA, one can refer to most objects using similar methods; Worksheets(1) returns the first sheet, while Worksheets("Bob") returns the named sheet. But they have one additional method, Worksheets, which returns the entire collection. It's as if they have this method...
Public Worksheets() As List(Of Worksheet)
But you can't use List in interop (right?) so it's more like...
Public Worksheets() As ArrayList
So how would I do the same basic API in .net with interop? That is, have three methods...
Public Units(N as String) As Unit ...
Public Units(K as Integer) As Unit...
Public Units() As ArrayList...
As I understand it only the first method of a given name is exported (is this correct?). So how does Excel do it, and can I fake that in .net?
VBA's Worksheets is not a method. It is a class, Worksheets, that has a default property Item that accepts a parameter of type Variant. There is no overloading (COM does not support it), it's just that Variant can hold both a number or a string.
If you want a similar structure in VB.NET, you can have a collection class that implements a default property as VB.NET understands it, and this time you can overload it.
Public Class UnitsCollection
Default Public ReadOnly Property Item(ByVal i As Integer) As Unit
Get
Return ...
End Get
End Property
Default Public ReadOnly Property Item(ByVal i As String) As Unit
Get
Return ...
End Get
End Property
End Class

Use ObjPtr(Me) to return the Name of a Custom Class Instance?

I understand that ObjPtr will return the address of an object in memory and that it points to a structure called IUNKNOWN and that there is some kind of Interface definition encoded in that to expose the Object structure, but I couldn't figure out how to determine the interfaces for a VBA Custom Class Object and how to use that to return the Name property of an Object.
It's more "nice to have" than essential, but I just want to know the name of an object instance at run time so that I can include it in my trace messages.
Can anyone explain how to do this or, better yet direct me to a reference so I can figure it out?
EDIT
To re-state my aim:
To make a custom class objects that is able to figure out the name of its particular instance.
For example
Dim oObject1 as Class1, oObject2 as Class1
Set oObject1 = New Class1
Set oObject2 = New Class1
Debug.Print oObject1.instanceName & " " & oObject2.instanceName
In the immediate window:
oObject1 oObject2
Is this possible in VBA?
If VBA runtime has a Symbol Table - since it is interpretive I think maybe it does - and I had a way of exposing it, then I could make a Property Get procedure to access the symbol Table and search on the Address - ObjPtr(Me) - to return the semantic name of the instance of the class.
I'm pretty sure this is a dumb question but, hopefully, the process of realising its a dumb question is helpful to my understanding.
Example of a Symbol Table
Address Type Name
00000020 a T_BIT
00000040 a F_BIT
00000080 a I_BIT
20000004 t irqvec
20000008 t fiqvec
2000000c t InitReset
20000018 T _main
20000024 t End
Take NO for an answer. It's not possible to return an instance name as a String literal in VBA.
I still don't understand the reason you may want to do that... Anyway
The easiest way to know each instance code name would be to create a property for a class that stores the actual name. This would only expose the name as a String property and not an actual reference to the object - it already has a reference - itself!
So create a class module
Class1
Option Explicit
Public MyName as String
and in Module1 all it takes is
Option Explicit
Sub Main()
Dim c As Class1
Set c = New Class1
c.MyName = "c"
Debug.Print c.MyName
End Sub
And there you go :)
Another way would be to create a Dictionary to store both KEY/VALUE pairs.
Sub Main()
Dim c As Class1
Set c = New Class1
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.Add "c", c
Debug.Print dict.Exists("c")
End Sub
Now, it's possible to actually do what you want but it would be a really ugly way. Here's how as I am not going to demonstrate.
You would create an instance of a custom class. Using ObjPtr you can get it's reference in memory. Then you would need a mechanism that scans your module code line by line and finds names of all variables you've dimensioned. Once you retrieve a list of all variables you would need a mechanism which tries to create an instance of the same type (class). Once you get past that point you could try to myNewObj = c ("c" would be the obj instance) programmatically. If that succeed then you would do ObjPt for both and match their addresses in memory - you get a match you know the variable name. Grree please do not do it that way :P
TypeName(obj) will return the Type of any variable in VBA:
Dim c as Class1
set c = new Class1
Debug.print TypeName(c) '==> "Class1"
FYI, I've also historically wanted to access the symbol table also. The idea was to get local variables from the previous scope by name. In that way you could make string interpolation:
a = "World"
Debug.Print StringInterp("Hello ${a}")
https://github.com/sancarn/VBA-STD-Library/blob/master/docs/VBAMemoryAnalysis.txt
https://github.com/sancarn/VBA-STD-Library/blob/master/docs/VBAMemoryAnalysis2.txt
No luck making a general function yet.

Resources