Using a Class Property to do more than just Assign or Query a Value

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.

Option Explicit
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.

Property Let Radius(uRadius As Double)
    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.

Dim LclRadius As Double, RadiusAssigned As Boolean
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.

Property Get Radius() As Double
    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.

Property Get Diameter() As Double
    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:

Property Get Diameter() As Double
    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:

    Diameter = 2

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”

    aCircle.Diameter = 2

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

Property Get Area() As Double
    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

Property Get ArcLen(ArcAngleInRadians As Double) As Double
    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:

Property Let ArcLen(ArcAngleInRadians As Double, uArcLen As Double)
    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.

Property Get Radius() As Double
    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.

Option Explicit
 
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.

Option Explicit
 
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:

Dim LclCenter As clsPoint
 
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).

Option Explicit
 
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.

Private Function myCenter(GetVal As Boolean, _
        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.

Private Class Class1
    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.

Posted in Uncategorized

12 thoughts on “Using a Class Property to do more than just Assign or Query a Value

  1. 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)

  2. 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!!!

  3. 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?

  4. 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?

  5. Tushar, I have nothing intelligent to add, but have to echo the thanks for such a well-written and concise summary.

  6. 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.

  7. 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.

  8. 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.

  9. 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*”.


Posting code? Use <pre> tags for VBA and <code> tags for inline.

Leave a Reply

Your email address will not be published.