What is a class?
The term is known to everyone that read anything about programming in VBA. It defines objects, yes. But what else do we know about them? What don’t we know about them?
VBA being built on top of COM has a number of implications, and explains a number of language features and the mechanics around classes and objects. Let’s dive into what makes a class module, and what being written in a class module means for your code.
In order to define a class, a class module needs to contain metadata, information that VBA will use when it needs to create an object. This metadata is thoroughly specified in MS-VBAL section 4.2.
If we add a new class module to a VBA project, name it
Thing, then export the code file, it should look something like this (minus
Option Explicit if you don’t have the “require variable declaration” VBE setting enabled for some reason):
VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "Thing" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = False Attribute VB_Exposed = True Option Explicit
VERSION 1.0 CLASS is the header VBA is looking for – if you export a standard module, it will not have that header. It won’t have the
BEGIN...END block that follows either.
MultiUse = -1 is controlled by the
Instancing property, which you can find in the Properties toolwindow:
VBA supports two values for this property (VB6 has more, depending on the project type):
- Private (default)
By default, a VBA class is private, thus only visible / only exists in the project in which it’s defined. In an add-in project, this is useful for a private/internal API. When an add-in project needs to be referenced from other VBA projects, it needs a public API – provided by PublicNotCreateable class modules. These classes can be used in referencing projects, but you can only
New them up in the project they’re defined in.
We could imagine that, if a class module’s property grid had more rows than that, the other values would all be listed between the
END keywords. So where’s the
(Name) coming from?
Attribute VB_Name = "Thing"
Attribute token, as per specifications, must always appear at the beginning of a line. Attribute names are a bit cryptic, and they’re not all specified or well-documented. In fact, different VBA host applications may support different attributes… but all VBA implementations support
VB_Name and the attributes that control instantiation:
- VB_GlobalNameSpace (false)
- VB_Createable (false if public)
- VB_PredeclaredId (false by default)
- VB_Exposed (true if public)
3 of these 4 attributes are controlled by the Instancing property.
VB_PredeclaredId is normally
False, but special classes such as a
UserForm have it set to
True; the VBE provides no tooling to manipulate this attribute, but VBA will honor its value if you modify it manually (or if Rubberduck does it for you).
The instancing property of the class module determines whether & how the class is able to be instantiated; an instance of a class is an object that is defined by that class. In VBA you can create a new object with the
New keyword or the
CreateObject function – but
CreateObject will not work with VBA user code, because the instancing modes supported in VBA don’t allow it.
Being an object, an instance of a class can only ever exist at run-time. Objects only ever exist at run-time.
There are several ways objects can come into existence in VBA.
- Issued to the left-hand side of a
Setassignment, using a
Newkeyword or a
CreateObjectfunction call on the right-hand side;
- Issued to a
Withblock from a
- On the first encounter (or first since last destruction) of the name of a class module with the
VB_PredeclaredIdattribute value set to
- On the first encounter (or first since last destruction) of an object reference declared with
Similar to how an Excel project’s
ThisWorkbook module “inherits” the
Workbook members, VBA classes “inherit” a standard “VB user class” that provides two hooks on two particularly important stages in the object’s lifetime:
Private Sub Class_Initialize() End Sub Private Sub Class_Terminate() End Sub
Given this code:
Private Sub Test() Dim foo As Class1 Set foo = New Class1 With New Class1 End With End Sub
Initialize handler would run twice – once at
New Class1 as the right-hand-side of the Set assignment is being evaluated, so before the
foo reference is assigned. Then once more at
New Class1 in the
With block, as the block is entered but before the object reference is captured by the
Terminate handler would first run at
End With for the
With instance, and the
foo instance would terminate at
End Sub, because that’s where it’s going out of scope (and it’s not
Nothing before that, and nothing else is keeping alive a reference to the object).
Keep in mind that declarations aren’t executable statements, so this code runs both handlers exactly 0 times:
Private Sub Test() Dim foo As New Class1 End Sub
As New object is only actually created where
foo is first referenced. If
Class1 exposed a
Public Sub DoSomething() method, and the above procedure had
foo.DoSomething under the declaration,
Initialize would run at the dereferencing operator (
.), just before the member call to the
DoSomething method; the
Terminate handler would run at
Auto-instantiated objects (declared with
As New), like the default instance of class modules with a
PredeclaredId attribute set to
True, are re-created whenever they are no longer in scope but suddenly need to exist again:
Private Sub Test() Dim foo As New Class1 foo.DoSomething Set foo = Nothing Debug.Print foo Is Nothing End Sub
As New, the
foo.DoSomething member call would be made against
Nothing and this code would raise run-time error 91. Because of
As New, the assignment to
Nothing does terminate the object instance, but a new one is spawned again when
foo is dereferenced again – so even though the object was just destroyed, the
Debug.Print statement correctly (but confusingly) outputs
False, since merely referencing the object
foo has caused VBA to re-create it behind your back.
This behavior is marginally useful, but definitely not as obvious at it should be: that is why Rubberduck warns about
As New declarations.
The members of a class are the fields (module variables), properties, and methods of the class – whether private or public. Fields should be
Private and encapsulate the instance state.
Property Get members can expose private fields in a read-only manner, and
Sub procedures are dubbed methods.
Avoid write-only properties; if external code can write to a property, it should be able to read that value back.
A class module can define a default member by specifying a
VB_UserMemId member attribute with a value of
0 (or with Rubberduck, by simply adding a
'@DefaultMember annotation/comment). For collection classes, convention is to have an
Item property as the default member, such that
things(i) is equivalent to
things.Item(i). The hidden attribute, like all attributes, is only visible when the code file is exported to be viewed in a text editor that isn’t the VBE:
'@DefaultMember Public Property Get Item(ByVal index As Variant) As Variant Attribute Item.VB_UserMemId = 0 '... End Property
In any other case, default members should be avoided: a class with a default member can be let-coerced into a value, and this easily makes things confusing – especially when considering that a default member might be returning an object that itself has a default member, that returns an object, that itself has a default member, that returns an object, …the compiler needs to work some recursive logic just to resolve what types/classes are involved: needless to say, us mere puny humans reading the code, understandably have no idea.
Let-coercion happens when you try to evaluate an object as a value expression, for example:
We know that this instruction prints “Microsoft Excel” just like it would if we fully spelled out
Debug.Print Application.Name, but we can’t know how. We can’t, because if we inspect the
Application class in the Object Browser, we find that its default property is a hidden
String property, and we can only best-guess that the implementation of that property is yielding the
Name property. The result is an opaque API that does things implicitly, and nobody knows what these things are. This is bad, because at face value,
Debug.Print Application makes no sense as a statement if we know that
Application is an object.
If we try to let-coerce an object that doesn’t have a default member, we get run-time error 438 “object doesn’t support this property or method”, which we normally see when a late-bound member call fails. An with a hidden default member, we ensure that the inner workings of our APIs remain an arcane black box for everyone consuming our APIs. Don’t do this to your future self.
Without this language feature, the
Set keyword wouldn’t be needed – assigning an object reference would never be ambiguous!
Dim rng As Variant rng = Sheet1.Range("A1:A10") 'ambiguous: is rng the Range or its default value? Set rng = Sheet1.Range("A1:A10") 'unambiguous: definitely the Range reference
Every class defines a default interface – the
Public members determine what’s on that interface. When a variable is declared
As Class1, the members available to IntelliSense are the members of the default interface of
Class1. Interfaces cannot expose fields and/or events, so if a class defines public fields, they are exposed as read/write properties; events are simply ignored.
An interface defines how an object can interact with the outside world – or rather, how the outside world can interact with an object: it’s like a boundary, an interface between the object and its client. The object encapsulates state, its interface allows the outside world to manipulate it.
Having a default interface implies that objects can have non-default interfaces, too: an object can implement as many interfaces as needed. In VBA that’s done with the
Implements keyword. And because every class defines a public interface, any class can implement any other.
Say we had a
Thing class, we would declare an object variable of that type like this:
Dim instance As Thing Set instance = New Thing
This makes the compiler understand that when we access the
instance object, we are seeing it through the lens of its default
Thing interface, so the member we get in the drop-down list when we type the
. dereferencing operator in
instance., are the public members defined in the
Thing class module.
Thing implemented an
ISomething interface, and we declared it like this:
Dim instance As ISomething Set instance = New Thing
We would still be looking at a
Thing, however we would now be seeing it through the lens of its
ISomething interface, and the members we would get in the drop-down list when we typed
instance., would be the public members defined in the
ISomething class module.
The benefits of this might seem very much theoretical and far-fetched if we’re coming from purely procedural programming, but from a COM standpoint, this is very much Tuesday – meaning, declaring explicit interfaces that formally define how we intend the outside world to be able to interact with our objects, is standard practice. .NET code intended to be consumed by COM, makes a cleaner API when it defines explicit interfaces.
When a class defines a public interface, but no implementation for these members, we give that class a generic name that we traditionally prefix with a very much Hungarian
I. This makes it easy to identify purely abstract classes (interfaces) in a sea of modules. With Rubberduck’s Code Explorer, classes that are implemented by other classes, as well as classes annotated with an
'@Interface annotation/comment, get a special dedicated “interface” icon that can help easily identify interface modules in a code base, prefixed or not.
This prefixing tradition was carried into modern practices, and you will find this
I prefix everywhere in the .NET framework, just as you will find it in a lot of (often hidden) classes in standard VBA libraries – so in order to blend in and deliver a clear consistent message to the reader/maintainer, interface class module names should be prefixed with an
If you dig into the hidden classes of the Excel type library, you will find hidden interfaces that expose members that should look familiar:
IOLEObjectEvents, and many others; these hidden interfaces are your biggest clue about how interfaces and events are related, and indirectly, about how events work under COM: this mechanism explains why a VBA interface cannot expose events directly. So what are events then?
Easily the very first aspect of writing VBA code we are exposed to, event handler procedures are defined by a special implicit interface that we specify in VBA code using the
Event keyword in a class module. When we type the space following the
RaiseEvent keyword, the editor assists by listing the members of that interface:
Raising an event can be seen as a special kind of call statement that invokes a callback – a procedure that’s written in some other class module: an event handler procedure, that may or may not execute code that we know nothing about. The handler procedure runs synchronously, so the next statement to run after a
RaiseEvent statement, will run after the handler procedure has returned (this is particularly useful for events that define a
ByRef cancel As Boolean parameter)… unless the handler bombed:
Option Explicit Public Event Something(ByVal foo As String, ByVal bar As Long) Public Sub DoSomething() On Error Resume Next RaiseEvent Something("foo", 42) Debug.Print "done" End Sub
This is where the notion of callback matters: the above code will never print “done” if a handler runs into an unhandled error – execution will simply stop (and VBA will pop that familiar “unhandled error” box). If the
Something handler were a “normal” procedure call, “done” would get printed whether or not an error was unhandled in the procedure.
We can handle
Workbook events in the
ThisWorkbook document class module, because the code of
ThisWorkbook already includes every member of a
Workbook class, even before we write a single line of code in it:
Debug.Print TypeOf ThisWorkbook Is Excel.Workbook 'True
This type of class inheritance unfortunately isn’t possible with VBA user code, but we can also express the relationship with composition, if we visualize the
ThisWorkbook module like this:
Option Explicit Private WithEvents Workbook As Workbook Private Sub Class_Initialize() Set Workbook = Me End Sub
The net effect being that in the
ThisWorkbook module we have a
Workbook event provider we can pick from the left-hand side code pane drop-down, listing the available events in the right-hand side drop-down – exactly as if there was an implicit
WithEvents Workbook private field in any other class module:
We use the
WithEvents keyword to declare an object variable for which we would like to handle events. The keyword is only legal in class modules (this includes document class modules and userform modules), and the
As clause may only specify a class that exposes public events. Moreover, an auto-instantiated object cannot be declared
As New is also illegal.
Events/callbacks are particularly useful to make application components that don’t “see” each other, still be able to talk to each other and interact: the class that’s raising an event doesn’t know if there’s someone answering the call on the other side, even less who that someone is and what they do for a living. They are a tool that can help decoupling components… to an extent: the problem is that in VBA classes, events are essentially considered implementation details – they are not exposed on the class’ default interface, even though they’re
Public: the only class that is allowed to access an event, is the class that defines it – no other class can raise an event, unless we expose a method specifically for that:
Public Event Something() Public Sub OnSomething() RaiseEvent Something End Sub Public Sub DoSomething() OnSomething() '... End Sub
WithEvents keywords provide language-level assistance with the otherwise relatively complex wiring-up of callbacks. In order to use them with interfaces, we can use an adapter pattern with formalized
SomethingEvents interfaces – like how the Battleship project demonstrates with the