Validation Functions
by Jon Peltier
Peltier Technical Services, Inc., Copyright © 2009.
Licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.
Today in Daily Dose of Excel, Dick wrote about Code Construction. One of his topics was to pull pieces of a routine out into a separate procedure, particularly if that piece of a routine included fiddling with On Error Resume Next and On Error Goto 0.
Dick’s example was to replace this section of code:
On Error Resume Next mcolMyCollection.Add oMyObject, CStr(oMyObject.ID) On Error GoTo ErrorHandler
with this single line
AddToColl mcolMyCollection, oMyObject, CStr(oMyObject.ID)
that calls a small subroutine:
Public Sub AddToColl(col As Collection, vToAdd As Variant, sKey As String)
On Error Resume Next
col.Add vToAdd, sKey
On Error GoTo 0
End Sub
This takes the interrupted error handling out of the main code, preventing a distracted programmer from turning it off altogether.
My suggestion was to turn the AddToColl subroutine into a function, which returns information to the calling sub. It might be a Boolean to indicate whether the action within the procedure was successful, or a Long (a User Defined Type) to indicate what error may have occurred, or as AlexJ suggested, a string containing an error message.
I often take a compound approach, where the function returns True or False to signify success, and an argument passes back to the calling procedure with an applicable message.
The following function is an example. It accepts the address of a range and returns a message as arguments, and returns True if the range meets the criteria.
Function NumericRangeValidator(sAddress As String, sMessage As String) As Boolean
Dim rTest As Range
' false until it passes all tests
NumericRangeValidator = False
On Error Resume Next
Set rTest = Range(sAddress)
On Error GoTo 0
' is address a valid range
If rTest Is Nothing Then
sMessage = "Invalid address."
GoTo ExitFunction
End If
' is there anything in the range
If WorksheetFunction.CountA(rTest) = 0 Then
sMessage = "Empty range."
GoTo ExitFunction
End If
' is the range completely populated with numbers
If WorksheetFunction.Count(rTest) < rTest.Rows.Count * rTest.Columns.Count Then
sMessage = "Range not fully populated with numbers."
GoTo ExitFunction
End If
' if execution got this far, it's a valid range
NumericRangeValidator = True
ExitFunction:
End Function
The message describes the problem with the range (invalid address, empty range, etc.), and only if all tests are passed is the value of the function True.
The following example shows how to use such a function. An If statement tests the value of the function. If it is true, the range is processed further by the calling code (in this case it performs a simple sum). If the function is false, the error message passed back by the function is displayed.
Sub TestNumericRangeValidator()
Dim strAddress As String, strMessage As String
strAddress = Selection.Address
If NumericRangeValidator(strAddress, strMessage) Then
' process the successful range
strMessage = "The sum of values in the range is " & WorksheetFunction.Sum(Range(strAddress))
MsgBox strMessage, vbExclamation
Else
' show the message
MsgBox strMessage, vbCritical
End If
End Sub
This approach is very flexible. The routine can be called from multiple places in a larger application. Each calling location can perform a different action based on the success of the function. The code above added the values, another routine could send it to a chart. If the range is not validated, the code above showed a message box, but it could have simply skipped the message box and died quietly. Or it could have shown the message in a label on a user form to explain to the user why the range they selected on the form cannot be used.
The benefit of passing arguments as well as an actual value for the function is I don’t have to test whether a string matches some stored string to determine whether the range was validated.
Possibly Related Posts:
- Get Open or Save-As Filename
- File Name Functions
- UDF to Calculate an Arbitrary Formula
- Extend Range to Add New Series (VBA)
- Label Each Series in a Chart
- VBA to Split Data Range into Multiple Chart Series
- Quick VBA Routine: XY Chart with Axis Titles
- Count Bold Cells in a Range
- VB Editor Settings
- Connect Two XY Series with Arrows – 2007 Error
Posted: Thursday, March 12th, 2009 under VBA.
Comments: 12
Comments
Comment from TV
Time: Thursday, March 12, 2009, 11:58 am
Dang, I need to brush up on VBA. I didn’t realize reassigning the input variable inside the subfunction also reassigns the input variable in the calling function. That’s a handy way to pass multiple outputs. I figured it would need to be a global variable for that to work.
Comment from Jon Peltier
Time: Thursday, March 12, 2009, 12:29 pm
I didn’t mention it, and I should have, that by default a value is passed into a procedure ByRef, which means a reference to the memory containing the value is passed. This memory is changed, so when execution returns to the main procedure, the referenced memory contains the updated value.
You can pass value variables ByVal, which means just a copy of the value is passed, and it is lost when execution reverts to the calling procedure.
Someone correct me if I’m wrong, but I think object variables are always passed ByRef.
Comment from JP
Time: Thursday, March 12, 2009, 1:53 pm
I believe you’re correct Jon, but I can’t find the page in PED that mentions it. I only found the bit on page 63 that talks about ByRef and ByVal. But I do believe it’s mentioned later in the text.
I like the boolean approach best, especially for the straightforward routines that either should work or completely fail. Then just test for that in your calling procedure.
Comment from Jon Peltier
Time: Thursday, March 12, 2009, 5:11 pm
There is a difference in how objects behaved when passed ByVal or ByRef, and it’s explained in Objects and ByVal or ByRef | O’Reilly Media. But it took a complex example to show this difference. For the most part, in simple cases, there is no measurable difference.
Comment from Bob Phillips
Time: Thursday, March 12, 2009, 6:24 pm
I think that all good experienced developers, of which we all know that you are one, should always explicitly declare whether a parameter is being passed ByVal or ByRef. Helping to spread good practices is part of the role IMO .
As I mentioned in DDoE, I have gotten into the habit of rarely passing a function result back as a function result, I rather pass all results back (I often require more than one, even if only a non-critical return code in addition to the functional value) as ByRef parameters. My function then passes back whether it was a critical failure or not. For example
Public Function MyFunction( _ ByVal input As String, _ ByRef output1 As Long, _ ByRef output2 As String) As Boolean MyFunction = True On Error Goto MyFunction_err 'some code that does stuff and sets up output1 and output2 MyFunction_exit: Exit Function MyFunction_err: MyFunction = False Resume MyFunction_exit End Function
and call it like so
If Not MyFunction("Bob", mpAge, mpSex) then
'handle this error
Exit Function
End IF
'now process the mpAge and mpSex variables returned
Comment from Jon Peltier
Time: Thursday, March 12, 2009, 8:36 pm
Bob -
You’re right, I should start showing ByVal and ByRef. In fact, I should probably start using them in my own code. I’ll start tomorrow.
Comment from Anonymous
Time: Friday, March 13, 2009, 5:45 am
Thank you for tidying up my code, presumably there are tags to do that, what are they [code]...[/code]?
You didn’t spot that I missed the Function keyword in the Function signature though
Comment from Jon Peltier
Time: Friday, March 13, 2009, 5:52 am
Public Function MyFunction would have sounded redundant even if I had noticed (O Captain, my Captain), but it’s fixed now.
The code uses html tags (<pre> </pre>), none of the square bracket tags, because I really don’t like how any of those plugins work. Until I get four hundred comments a day, it’s not too burdensome to fix it myself. Those of us with OCD have lots of energy for that sort of thing.
Comment from Doug Glancy
Time: Friday, March 13, 2009, 7:21 pm
I notice that Bob sets the function to true at the beginning and changes the value to false in the error handler, whereas Jon explicitly sets it to false and uses Goto to break out of the function before it turns true if there’s an error. I’m used to doing it Jon’s way (although I’ve stopped explicitly setting the function to false). What are the advantages of your way, Bob?
Comment from Jon Peltier
Time: Friday, March 13, 2009, 10:54 pm
Doug -
I’l wait for Bob’s take on this, but I think it’s a kind of half-empty, half-full situation. I don’t think there’s any real reason to do it one way or the other. It just makes sense in my mind not to claim success until you’ve ruled out failure.
Comment from Doug Glancy
Time: Saturday, March 14, 2009, 12:15 pm
I thought about this some more, and it seems like starting at False makes it easier to return the reasons that the function fails, either runtime or validation errors.
Comment from Jon Peltier
Time: Saturday, March 14, 2009, 12:31 pm
With every test you have an opportunity to define a piece of information to pass back to the calling routine. I don’t think where you start makes much difference.
















Write a comment
I welcome comments from my readers. If you have an opinion on this post, if you have a question or if there is anything to add, I want to hear from you. Whether you agree or disagree, please join the discussion.
If you want to include an image in your comment, post it on your own site or on one of the many free image sharing sites, and include a link in your comment. I'll download your image and insert the necessary html to display the image inline.
Read the PTS Blog Comment Policy.