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.

Double Clicking Through a List

I have a cell with data validation. It's set as an in-cell dropdown list and contains two items: Yes and No. I want to make it so that I can double click on that cell to toggle between yes and no. But wait, that's not good enough. What about longer lists? Yes, I want something that will iterate through all the items on a data validation list by double clicking. Dare I dream.

Here's my first go at it. I figure it's going to need some work, like what happens when the user double clicks on something that's not a range, but it's a start. I've basically handled two types of lists: the kind where you hard code values separated by commas (international issue here?), and the the range reference. Oh, and I need to test named ranges, but I think they'll work.

Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
   
    Dim dv As Validation
    Dim sDv1 As String
    Dim vaList As Variant
    Dim i As Long
    Dim vOldValue As Variant
   
    On Error Resume Next
        Set dv = Target.Validation
        sDv1 = dv.Formula1
    On Error GoTo 0
   
    If Len(sDv1)> 0 Then 'only if the cell has dv
        If dv.Type = xlValidateList Then
            Cancel = True 'don't do the default action
            vOldValue = Target.Value
            vaList = GetValidList(dv.Formula1) 'return single dim array
            For i = LBound(vaList) To UBound(vaList)
                If vaList(i) = Target.Value Then
                    If i = UBound(vaList) Then
                        Target.Value = vaList(LBound(vaList))
                    Else
                        Target.Value = vaList(i + 1)
                    End If
                    Exit For
                End If
            Next i
            If Target.Value = vOldValue Then 'if cell was blank
                Target.Value = vaList(LBound(vaList)) 'go to first item
            End If
        End If
    End If
   
End Sub
 
Private Function GetValidList(sForm As String) As Variant
   
    Dim vArr As Variant
    Dim vaReturn As Variant
    Dim i As Long
    Dim bIsRange As Boolean
   
    On Error Resume Next
        vArr = Evaluate(sForm) 'for range reference
    On Error GoTo 0
   
    If IsError(vArr) Then 'for csv list
        vArr = Split(sForm, ",")
        bIsRange = False
    Else
        bIsRange = True
    End If
 
    If bIsRange Then 'conver to single dim array
        ReDim vaReturn(0 To UBound(vArr, 1) - 1)
        For i = LBound(vArr, 1) To UBound(vArr, 1)
            vaReturn(i - 1) = vArr(i, 1)
        Next i
    Else
        vaReturn = vArr
    End If
   
    GetValidList = vaReturn
   
End Function

This code is in the sheet's class module (Sheet1 in my case). Test it out if you like. Let me know if you see any errors or better ways.

P.S. Why is Target.Validation always something (that is, Not Nothing) even if the cell doesn't have validation?

Efficient Looping

In a previous post, I demonstrated how to use constants to improve your code. Then everyone started beating up my loop. The code wasn't solving a real life problem, so I just threw any old loop together. It wasn't relevant because that's not what the post was about. To fight back, I created a highly improbably backstory in the comments to make my loop look at least as efficient as everyone else's.

But no matter how crazy my story was, I couldn't subvert Peltier's comment about reading the range into an array. So I tried to see what kind of time differences we're talking about. I wrote this code:

Sub DoBoth()
   
    Dim lStart As Long
    Dim i As Long
   
    lStart = Timer
        For i = 1 To 10
            FindTotals2
        Next i
    Debug.Print Timer - lStart
    lStart = Timer
        For i = 1 To 10
            UseArray
        Next i
    Debug.Print Timer - lStart
   
End Sub
 
Sub FindTotals2()
   
    Dim rCell As Range
   
    Const sFIND As String = "Total"
   
    For Each rCell In Sheet1.Columns(1).Cells
        If Left$(rCell, Len(sFIND)) = sFIND Then
            'Do something
        End If
    Next rCell
   
End Sub
 
Sub UseArray()
   
    Dim vArr As Variant
    Dim i As Long
   
    Const sFIND As String = "Total"
   
    vArr = Sheet1.Columns(1).Value
   
    For i = LBound(vArr) To UBound(vArr)
        If Left$(vArr(i, 1), Len(sFIND)) = sFIND Then
            'Do something
        End If
    Next i
   
End Sub

And got these results:

The array is quite a bit faster. I don't think Timer is hyper-accurate, but relatively the differences are pretty clear.

Testing Strings Using Left

The "wrong" way:

Sub FindTotals()
   
    Dim rCell As Range
   
    For Each rCell In Sheet1.Columns(1).Cells
        If Left$(rCell, 5) = "Total" Then
            'Do something
        End If
    Next rCell
   
End Sub

The "right" way:

Sub FindTotals2()
   
    Dim rCell As Range
   
    Const sFIND As String = "Total"
   
    For Each rCell In Sheet1.Columns(1).Cells
        If Left$(rCell, Len(sFIND)) = sFIND Then
            'Do something
        End If
    Next rCell
   
End Sub

Both "right" and "wrong" are in quotes because, like many best practices, it can be a matter of taste and preference. In the second method, I can change the string in one place and I'm done. In the first, if I change the string, I also have to change the second argument of Left$().

Printing First Page of Email in Outlook

I print out my email. No, I'm not one of the Luddites that prints out every email (or has his secretary do it) because he doesn't understand how to work his email machine. My system for organization is loosely based on Getting Things Done and its derivative 43 Folders. Everything I have to do is represented by at least one piece of paper. So don't try to convince me to quit printing my email, because it's not going to happen.

If I get an email that requires some action in the future, I need one of these pieces of paper to represent that future action. Until recently, I would print the email to serve that purpose. As you already know, email can get quite long and that means I can end up with 10 sheets of paper where I only needed one. When you deal with accountants and lawyers, it's even worse because each of their messages in the thread has a page of disclaimers. Oddly, Outlook's print dialog doesn't let you specify a page range. There are some work arounds, but none of them are suitable for me. I read all my email in plain text and I reply in plain text (HTML is for web pages, not email). Since I'm using plain text, I use Outlook's built-in email editor rather than Word.

After I printed seven pages of an email today, I decided to finally write some code. I put a button on my new email commandbar and hooked it up to this procedure:

Sub PrintOnePage()
   
    Dim mi As MailItem
    Dim sBody As String
    Dim wdApp As Word.Application
   
    Const sORIG As String = "> -----Original Message-----"
   
    If TypeName(Application.ActiveInspector.CurrentItem) = "MailItem" Then 'only mail
        'create a forward to get the header
        Set mi = Application.ActiveInspector.CurrentItem.Forward
        sBody = mi.Body
        sBody = Mid(sBody, InStr(1, sBody, sORIG), 5000) 'Remove inserted signature
        Set wdApp = New Word.Application
       
        With wdApp.Documents.Add
            .Range.Text = sBody
            .PageSetup.LeftMargin = 18 '.25"
            .PageSetup.RightMargin = 18
            .PageSetup.TopMargin = 18
            .PrintOut False, False, wdPrintFromTo, "", "1", "1"
        End With
       
        wdApp.Quit False 'don't save changes
        Set wdApp = Nothing
        mi.Close olDiscard 'don't save changes
        Set mi = Nothing
    End If
   
End Sub

I didn't want to automate Word to do this, but I struggled with other options to limit it to one page. I know that printing out of Outlook puts 60 lines on the first page. However, when I tried to limit the text to the first 60 vbNewLine's, it didn't quite work out. I started to think that maybe Outlook doesn't put a vbNewLine after each line, but rather after each paragraph. I'm still not sure why that didn't work. So I resigned to automate Word and use it's page range feature to limit the print out.

I limit this to MailItems although it may work on other objects. I didn't want to test it. The MailItem I work with is a forwarded copy of the original. When I forward an email it puts the header information at the top of each email in the thread, so I get some needed information on my print out. Unfortunately, it also puts my signature in there, so I have to strip that part out by starting the string at the Original Message part. And I limit the string to 5,000 because that should be more than one page and there's no need to transfer more than that.

I kept getting a type mismatch error when using Word's PrintOut method. At first I thought it was because I was omitting optional arguments, but that really shouldn't be the case when I'm early binding. I seem to remember a problem with optional arguments using late binding - specifically that you have to include all optional arguments up until the one you want to include, then none after that. But I was still getting the error. Inexplicably, Word wants Strings for page numbers. You'll notice that my page numbers are in quotes.

Finally, I close Word without saving changes and discard the forwarded copy of the email.

VBE Bookmark add-in for Office 2000-2007

Hi all

New page on my site with a very useful add-in from Jim Rech.
http://www.rondebruin.nl/vbebookmarks.htm

VBEBookmarks.dll is a simple COM Add-in created by Jim Rech for the Microsoft Office 2000-2007 Visual
Basic Editor which lets you bookmark up to five locations in VBA projects for easy navigation among them.

Have fun

Ron de Bruin
http://www.rondebruin.nl/tips.htm