VB Attributes: What are they and why should we use them?

Did you know that there’s code in your VBA projects that you can’t see in your editor? Want to see it? (Of course you do.)

So, just right click on any module and click Export.

Export File

Save the file some where convenient and open it up in any text editor, like Notepad.

VERSION 1.0 CLASS
BEGIN
    MultiUse = -1 'True
END
Attribute VB_Name = "Class1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

'@TestClass

'TestMethod
Public Sub TestMethod1()
    'hello there
End Sub

And here we see the hidden header that is in all of your VBA classes and modules. However, only three of these do anything in VBA, to the best of my knowledge.

  1. VB_Name
  2. VB_PredeclaredId
  3. VB_Exposed

VB_Name is pretty self explanatory, it stores the name you gave it in the properties window. VB_PredeclaredId is much more interesting. If you change the value of VB_PredeclaredId to true in a class file, that class will be given a global default instance when your code starts running. Now, globals are bad, we all know that, so use this with care, but this feature allows us to simulate static class methods in our VBA code. What does that all really mean? It means that we can access class methods without first creating a new instance of the class. We can access the methods via our global default instance.

So, instead of

    Dim math As New clsMath
    math.Add 1, 2

We can do this

    Math.Add 1, 2

It’s a poor example, I know. We could accomplish the piece of code above with a regular module, but it really does allow us to do some pretty cool things.

I won’t touch much on the remaining attribute, VB_Exposed, other than to say that it holds the value of the class instancing property. 1 == false. 2 == true. None of the other values are available in VBA. They weren’t brought over from VB6.

But wait, there’s more…

That’s the just the header, the module level attributes. There are also a number of attributes that can be applied to module variables (fields), properties, and procedures. There’s a slightly different syntax for variables, so let’s start there.

  1. VB_VarUserMemId: Determines the order of the variables in the Object Broswer. A value of 0 (zero) declares the variable to be the default member of the class.
  2. VB_VarDescription: The value of this attribute will be displayed in the Object Broswer.

So, placing this code into our module, and re-importing it back into our project, produces this in the Object Browser.

Public FooCount As Integer
Attribute FooCount.VB_VarDescription = "Specifies the number of Foos"

Object Browser Variable Description

 

This allows us to put our documentation into a highly visible location and  the same works for Properties and Procedures, only we drop the “Var” from the attributes.  So, change our public field into a read-only default property.

private pFooCount As Integer

Property Get FooCount() As Integer
Attribute FooCount.VB_Description = "Returns the number of Foos"
Attribute FooCount.VB_UserMemId = 0

FooCount = pFooCount

End Property

Object Browser Default Property

Now, when we have a Class1 object, it will return the FooCount by default.

Dim cls As New Class1
Dim i As Integer

i = cls

' is equivalent to

i = cls.FooCount

When might you want to do this? Well, for one, when you want to create a strong typed collection. Which brings us to one last detail. There is one more special value for VB_UserMemId and that value is -4. Negative 4 always indicates that the function being marked should return a [_NewEnum] enumerator.  This sample is from the stack overflow link above (corrected to make Item the default member of the class).

Public Property Get Item(index As Long) As cShape
    Attribute Item.VB_UserMemId = 0
    Set Item = myCustomCollection.Item(index)
End Property

Public Property Get NewEnum() As IUnknown
    Attribute NewEnum.VB_UserMemId = -4
    Attribute NewEnum.VB_MemberFlags = "40"
    Set NewEnum = myCustomCollection.[_NewEnum]
End Property

Below is a summary of the syntax and values.

 

' Header
Attribute VB_Name = "ClassOrModuleName"
Attribute VB_GlobalNameSpace = False ' ignored
Attribute VB_Creatable = False ' ignored
Attribute VB_PredeclaredId = False ' a Value of True creates a default global instance
Attribute VB_Exposed = True ' Controls how the class can be instanced.

'Module Scoped Variables
Attribute variableName.VB_VarUserMemId = 0 ' Zero indicates that this is the default member of the class.
Attribute variableName.VB_VarDescription = "some string" ' Adds the text to the Object Browser information for this variable.

'Procedures 
Attribute procName.VB_Description = "some string" ' Adds the text to the Object Browser information for the procedure.
Attribute procName.VB_UserMemId = someInteger
    '  0: Makes the function the default member of the class.
    ' -4: Specifies that the function returns an Enumerator.

Note that there can be only one default member in any class. You can not have both a default member variable and a default procedure.

, , , ,

  1. #1 by Johny Why on July 12, 2016 - 9:14 pm

    hi, great site! i noticed that If you change the value of VB_PredeclaredId to true in a class, you do NOT need to even declare a variable for the class. You can reference the class by name directly.

    Like

  2. #2 by Christopher J. McClellan on July 13, 2016 - 2:20 am

    Thanks! That’s correct. Setting PredeclaredId to true creates a default global instance. It’s the same trick UserForms use. Cheers.

    Like

  3. #3 by Johny Why on July 16, 2016 - 12:43 am

    new question: can VB_PredeclaredId be used with class arrays? This is used to share event-code between multiple controls:

    Example:
    in a class called cControlEvents, in declarations, i can say:

    Public WithEvents lstBox As MSForms.ComboBox

    Private Sub lstSearch_Change()
    Debug.Print lstBox
    End Sub

    then, in a module, i can say:

    Dim oControlEvents() As New cControlEvents

    Sub WireUpControls
    ReDim Preserve oControlEvents(1)
    Set oControlEvents(0).lstBox = MyListBox
    Set oControlEvents(1).lstBox = MyOtherListBox
    End Sub

    ——
    I’m concerned that we are creating a new class as soon as the Workbook is opened (due to New keyword). You cannot exclude the New keyword in the module declaration. You cannot do this in WireUpControls:

    Set oControlEvents = New cControlEvents

    I’m wondering if VB_PredeclaredId can improve this code.

    thx!

    Like

  4. #4 by Rubberduck VBA on July 19, 2017 - 2:45 pm

    The latest build of Rubberduck now allows you to modify attributes using magic comments / “annotations”:

    Module level:
    ‘@PredeclaredId
    ‘@Exposed

    Member level:
    ‘@Description(“this will be the attribute’s value”)
    ‘@DefaultMember
    ‘@Enumerator

    An inspection compares annotations with the actual attributes, and a quickfix symchronizes them for you!

    https://github.com/rubberduck-vba/Rubberduck/releases

    Like

Leave a comment