Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a loopable container class in VBA

Tags:

excel

vba

I have been trying to clean up my code a bit and make it more similar to the Excel object model, and I was wondering if it is possible to create a "loopable" container class in VBA, e.g. similar to how you can do:

Dim Sheet As Worksheet
For Each Sheet In ThisWorkbook.Worksheets
   ' ...
Next Sheet

I want this functionality for my own container.

Say I create my own class called Container which contains items of some class ItemType (this can simply be an empty class for this example):

' Class Container
' The container contains items of a class I will call ItemType

Private Type MContainer
  Items As Collection ' Could also be implemented in terms of an array
End Type

Private This As MContainer

Public Property Get Item(ByVal Index As Long) As ItemType
Attribute Item.VB_UserMemId = 0 'Makes it so I can access elements like a Collection
  Set Item = This.Items(Index)
End Property

Public Function Add() As ItemType
  This.Items.Add
  Set Add = This.Items(This.Items.Count)
End Function

Private Sub Class_Initialize()
  Set This.Items = New Collection
End Sub

I then want to loop through the items in my container with the For Each..., but this doesn't work. See the following example for how I ideally want it to work:

Public Sub MyMethod()

  Dim Stuff As New Container
  Stuff.Add

  Dim Element As ItemType
  For Each Element In Stuff ' <- This will not work
    ' Do something
  Next Element

End Sub

The final For loop is what I am looking at making work. Is this possible? Basically the issue is that I can't call For Each on my Container class similar to how you can with e.g. the Excel.Sheets class. Is this possible to achieve in VBA?

like image 595
Jonas Glesaaen Avatar asked Mar 04 '23 06:03

Jonas Glesaaen


1 Answers

For Each iteration requires a special member attribute value to work, and a NewEnum property or function returning an IUnknown.

Every collection class that can be iterated with a For Each loop has a hidden [_NewEnum] member (the square brackets are required for accessing the hidden member, since the underscore prefix is illegal for an identifier in VBA.

Tweaking module and member attributes isn't possible to do directly in the VBE, so you need to remove/export the module, modify it in e.g. Notepad++, save the changes, then re-import it into your project.

Or, have Rubberduck (disclaimer: I contribute to this open-source project) do it for you, using annotations (aka "magic comments"):

'@Enumerator
'@Description("Gets an enumerator that iterates through the internal object collection.")
Public Property Get NewEnum() As IUnknown
    Set NewEnum = this.Items.[_NewEnum]
End Function

'@DefaultMember
'@Description("Gets/sets the element at the specified index.")
Public Property Get Item(ByVal index As Long) As ItemType
    Set Item = this.Items(index)
End Property

Then parse the project (Ctrl+`) and bring up the Inspection Results toolwindow (Ctrl+Shift+i) - there should be a number of "Missing Attribute" results under "Rubberduck Opportunities":

inspection results

Click "Fix all occurrences in module" in the bottom pane, to synchronize the hidden attributes with the annotation comments.

If you have "Missing Annotation" results, Rubberduck has determined that a module/member has a non-default value for a given attribute, and is able to similarly add an annotation comment that surfaces/documents it with a comment.

The Code Explorer (Ctrl+R), the Rubberduck toolbar, and the VBE's own Object Browser (F2) will display the contents of the VB_Description attribute, so @Description annotations are particularly useful to have on any public procedure.

Object Browser:

Object Browser showing member description

Code Explorer:

Code Explorer showing member description

Rubberduck toolbar:

RD toolbar showing member description

like image 124
Mathieu Guindon Avatar answered Mar 13 '23 02:03

Mathieu Guindon