The purpose of this article is to introduce various capabilities of a class property. It is not meant to serve as an introduction to classes and objects. See the references section for introductory pages.
The typical use of a Class Property is to assign a value or to query its value as in the example below that defines the radius of a circle. This code would go in a class module named clsCircle.
Dim LclRadius As Double
Property Get Radius() As Double
Radius = LclRadius
End Property
Property Let Radius(uRadius As Double)
LclRadius = uRadius
End Property
Here are three things to consider. First, there are two different procedures, one to query the value and another to assign it. Second, there’s no requirement that there be only one statement in the Get or the Let procedure. In fact, there could be just about any number of statements in either procedure. Third, there’s no rule that both the Get and the Let procedures be present. These three together mean there’s a lot more one can do beyond the basic use of a property. In this document we will explore some of the possibilities. There is one critical weakness in how properties work and we will explore a workaround towards the end of this document.
Validating a property value
The first improvement is to validate the value of a property. In the case of a circle, the radius cannot be negative. So, we can enhance the code to ensure that it is not.
If uRadius >= 0 Then LclRadius = uRadius _
Else MsgBox “Radius cannot be assigned a negative value”
End Property
Creating a Write-Once-Read-Many attribute
We can modify the above code to allow the Radius property to be assigned a value only once.
Property Get Radius() As Double
Radius = LclRadius
End Property
Property Let Radius(uRadius As Double)
If RadiusAssigned Then
MsgBox “Radius, a ‘write-once’ attribute, already has a value”
Exit Property
End If
If uRadius >= 0 Then
LclRadius = uRadius
RadiusAssigned = True
Else
MsgBox “Radius cannot be assigned a negative value”
End If
End Property
Verify that a Property has been initialized
We can use the same concept as above to verify that a property has been initialized. Thus, the property value is returned only after it has been initialized.
If RadiusAssigned Then Radius = LclRadius _
Else MsgBox “Radius property is uninitialized”
End Property
A “Virtual” Property
A property doesn’t have to have a variable associated with it. The Radius property has a private LclRadius variable that holds its value. But that is not strictly necessary. Consider a circle’s Diameter property. Since it is simply twice the radius there is no need for a separate variable to contain the diameter.
Diameter = Radius * 2
End Property
Property Let Diameter(uDiameter As Double)
Radius = uDiameter / 2
End Property
Using Properties inside the Class Module
The above example also illustrates another useful point. Even though the variable LclRadius is available to all the procedures of the class one can always use the actual property itself. In fact, unless there is a compelling reason not to, one should always use the property since this has the benefit that any additional code for the property (such as the validation of the radius) will be correctly executed.
A Public vs. a Private Property
There may a valid reason for creating a property such that it is available to other members of the class but not to a client. This is accomplished by declaring the corresponding Get or Let procedure private. Consider the Diameter property from above. Clearly, it makes good sense for the client to know the diameter of the circle. So, the Get procedure should be public (the default). Also suppose we decide that the client cannot change the diameter (all changes should be through the Radius property). At the same time, we decide that procedures inside the class itself should be allowed to change the Diameter property. We accomplish this by making the Let procedure private as in:
Diameter = Radius * 2
End Property
Private Property Let Diameter(uDiameter As Double)
Radius = uDiameter / 2
End Property
Now, code within the class can set the Diameter value with something like:
However, a client would be unable to do something like the below since it would result in a compile time error “Method or Data Member not found”
Referring to an instance of the class (the Me object)
The Me keyword is the way code inside the class module can refer to the instance of the object created from the class. One could call it a “self reference,” I suppose. Me is the equivalent of a client referring to a variable of the class. VBE’s Intellisense capability will show the same properties and methods that a client would be able to use.
The use of Me is relevant in the context of a private property since a procedure in the class module can refer to a private property such as Diameter above. However, assigning a value to the Diameter of the Me object would fail since Diameter is publicly read-only.
Read-only Property
Since the Get and Let property procedures are separate entities, one can always exclude one (or the other). To implement a read-only property, simply exclude the Let procedure. In the case of the circle class, once the client specifies the radius, other properties such as the area or the perimeter are easy to calculate. However, if we assume that the client cannot specify the area (or perimeter) directly, we can create read-only properties with
Area = Application.WorksheetFunction.Pi() * Radius ^ 2
End Property
Property Get Perimeter() As Double
Perimeter = 2 * Application.WorksheetFunction.Pi() * Radius
End Property
Similarly, one can implement a write-only property by creating the Let procedure but excluding the corresponding Get procedure.
Property with an argument
Just like a subroutine or a function can have one or more arguments passed to it, so can a property. Suppose we want to provide a property that returns the length of the arc corresponding to a specified angle. The length of the arc is calculated as the perimeter / (2*Pi) * angle of the arc, which is also the same as radius * angle of the arc. So, we would get the property
ArcLen = Perimeter * ArcAngleInRadians _
/ (2 * Application.WorksheetFunction.Pi())
‘The above illustrates how one property can use another property _
to return a calculated value. Of course, the length of an arc _
is also the simpler _
ArcLen = Radius * ArcAngleInRadians
End Property
Similarly, a Let procedure can also have an argument list. In the case of a property where the Get procedure has zero arguments, the corresponding Let procedure already has 1 argument, the value of the Let assignment. Similarly, when the Get procedure has an argument list, the corresponding Let procedure has 1 more argument than the Get procedure. The value of the Let statement is the last argument. So, if we were to allow the client to specify the radius of a circle through the ArcLen property keeping in mind that while it helps demonstrate this capability it is not really a good idea for a ‘production’ system we might have something like:
Radius = uArcLen / ArcAngleInRadians
End Property
Raising an Error
Just as we can raise an error in any procedure in our code modules, one can also raise an error in a class module. Suppose we decide to replace our Radius property’s Get procedure so that it raises an error if Radius is uninitialized.
If RadiusAssigned Then
Radius = LclRadius
Else
Err.Raise vbObjectError + 513, “clsCircle.Radius”, _
“clsCircle.Radius: Radius property is uninitialized”
End If
End Property
Now, if we were to query the value of the Radius property before assigning a value to it, we would get a runtime error.
Sample Use of the circle’s properties
In a standard module, enter the code below and then execute it. It creates a circle of radius 1 and then displays its diameter, area, perimeter, and the length of the arc corresponding to 1/4th the circle.
Sub testCircle()
Dim aCircle As clsCircle
Set aCircle = New clsCircle
With aCircle
.Radius = 1
MsgBox “Diameter=” & .Diameter & “, Area=” & .Area _
& “, Perimeter=” & .Perimeter _
& “, ArcLen(Pi()/2)=” _
& aCircle.ArcLen(Application.WorksheetFunction.Pi() / 2)
End With
End Sub
Difference between Set and Let property procedures
Suppose we have another class, clsPoint, that contains 2 properties, the X and Y coordinates of the point.
Dim LclX As Double, LclY As Double
Property Get X() As Double: X = LclX: End Property
Property Let X(uX As Double): LclX = uX: End Property
Property Get Y() As Double: Y = LclY: End Property
Property Let Y(uY As Double): LclY = uY: End Property
Now, in our clsCircle class, we could specify the center of our circle as:
Property Get Center() As clsPoint
Set Center = LclCenter
End Property
Property Set Center(uCenter As clsPoint)
Set LclCenter = uCenter
End Property
Note that the Get procedure can Set the property. However, if we used a Let procedure and tried to Set the module variable, it would not work. Try it. Instead, one must use a Set procedure as in the above example.
We can now extend the testCircle subroutine (it’s in the standard module).
Sub testCircle()
Dim aCircle As clsCircle
Set aCircle = New clsCircle
With aCircle
.Radius = 1
MsgBox “Diameter=” & .Diameter & “, Area=” & .Area _
& “, Perimeter=” & .Perimeter _
& “, ArcLen(Pi()/2)=” _
& aCircle.ArcLen(Application.WorksheetFunction.Pi() / 2)
End With
Dim myCenter As clsPoint
Set myCenter = New clsPoint
With myCenter
.X = 1
.Y = 2
End With
With aCircle
Set .Center = myCenter
MsgBox .Center.X & “, “ & .Center.Y
End With
End Sub
Creating private “property variables”
One of the biggest weaknesses in the current implementation of a class is that any variable associated with a property must be declared at the module level. This makes the variable visible to and, worse modifiable by, any code anywhere in the module. Essentially, the variable is global to the entire module.
One generic way to make a variable persistent but not global is to declare it as static inside a procedure. That, of course, does not work with a Property since typically there are two procedures associated with a property (a Get and a Let or a Get and a Set). But, what if our property procedures called a common private procedure? Then, we could declare our local variables in this common procedure.
Create a function that declares the variable(s) associated with a property as static within its own scope. Now, the only way to access the variable is through the function and the function can contain all the code required to assign or query a property value.
Optional uCenter As clsPoint) As clsPoint
Static LclCenter As clsPoint
If GetVal Then Set myCenter = LclCenter _
Else Set LclCenter = uCenter
End Function
Property Get Center() As clsPoint
Set Center = myCenter(True)
End Property
Property Set Center(uCenter As clsPoint)
myCenter False, uCenter
End Property
The variable LclCenter above is private to myCenter. No procedure in the module can directly access LclCenter. All access has to be through myCenter; we have cut off unrestricted access to the variable.
One can verify the above works by simply running the testCircle code (without making any changes to it). You will get the same result.
I had hoped that with the .Net declaration of a property one would be able to declare variables local to it but unfortunately it remains impossible. The result of the below is a syntax error on the dim X… statement indicating the declaration is not allowed in the Property.
Public Property aProp()
dim X as boolean
Get
End Get
Set(ByVal value)
End Set
End Property
End Class
Summary
There’s a lot one can do with a class property beyond just associating it with a variable. The list includes, but is not limited to, introducing data validation as well as implement write-once or read-only (or write-only) properties. One can also restrict the scope of variables associated with a property.
This document shared some ideas on the subject. For those wondering, yes, I can think of some possibilities that were not discussed here. Of course, I am sure there are even more possibilities that I haven’t thought of.
References
There is much information on the subject of classes and objects. Just search Google. Two introductory topics I found — and I don’t know how the compare with other information on the subject — are Dick Kusleika’s blog post at http://www.dailydoseofexcel.com/archives/2004/09/28/classes-creating-custom-objects/ and Chip Pearson’s introduction to the subject at http://www.cpearson.com/excel/Classes.aspx Chip addresses a couple of the issues addressed above as well as topics I opted to exclude from this article.
create article, I mainly use classes to add objects and then some maths / procedures specific to those objects.
for example on you circle, class I would an optional height and then one surface and one volume function (with the same error and data validation of course).
The thing I love about classes: its reducing the clutter in your code a lot and makes things a lot easier to understand for people reading your code (assuming that you document well your class)
oops wanted to say Great article not create… !
Amazing article! I’ve been reading the site for awhile and this post has really clarified a lot that I’ve been trying to figure out through trial and error – thanks!!!
Yes, Tushar, thank you! An awesome lunch-time read!
One question I have is how significant of a problem is the module-level variable? Is it a matter of memory or a matter of diligently coding so as not to reuse the variable outside the Let/Get/Set properties?
Good article Tushar, quick question. Is your code indenting style considered the norm? Specifically, you indent the “End Sub”, “Next” and “End If” lines. I’ve seen it done both ways, which one is correct?
Tushar, I have nothing intelligent to add, but have to echo the thanks for such a well-written and concise summary.
Charles: There is really no such thing as incorrect or correct indenting, it is a matter of style. As long as you make your code easier to read and understand.
I don’t indent the end sub’s, just because I like it that way.
as a rule I always get Bullen to sort out my indentation (and anyone elses for that matter).
OA LTD Smart indenter. Don’t leave home without it.
Nice article
The indentation bugs me, but I figure that’s intentional.
In the case of indentation, it is better to conform, than “improve”, else future development will introduce bugs.
In some cases, instead of using Err.Raise, you can make the property a Variant and return a CVErr(####) Object, which is less of a showstopper.
That lets you test the property value from outside the class using If Typename(thatProperty) like “Error*”.
Or, you can test the property using: If IsError(thatProperty) Then …
Thanks for the article, helped me a lot!