by Jim Karabatsos - GUI Computing
I was idly playing around with some data validation code the other day and stumbled upon something that really pulled me up short, so I thought I'd share it with you. In a nutshell, ordinary code modules (ie BAS files) can contain properties, defined using the Property Get/Let/Set procedures you generally use in a class or form module.
I don't really know why I thought this, but I was certain that properties only applied to objects. Properties are a natural by-product of implementing COM smarts into VB. The COM specification allows only methods to be exposed, not data members. In order to allow access to "public variables" exposed by a COM object, the compiler converts references to such a variable into calls to appropriate getXXX and setXXX access methods and hides all this behind the scenes. If you're going that far anyway, it's a pretty small step to allow the programmers to define their own access methods and pretty soon you have a property implementation just like the one in VB.
I guess I just never made the next step forward: if you have this code to convert a source-language variable reference into a procedure invocation, it's probably more work to specifically disable it for code modules than to enable it everywhere.
I did a quick search through the on-line help and I did not find anywhere that specifically says properties only apply to objects, so I think this is a legitimate feature of VB5. I have no idea if this worked in VB4; I've long since removed it from my PC and the idea of re-installing it makes be break out in a cold sweat…
OK, what can we do with this feature (other than gape in amazement)?
Well, let me relate what I was doing when I discovered this feature. Prompted by a discussion with Mark on date entry validation, I was toying with a couple of routines that implemented on-the-fly, subtle validation. Basically, the effect I wanted to achieve was to allow the user to enter anything into a text box, but to provide real-time feedback as to whether the current entry was a valid date. What I came up with was a couple of procedures that could be called from within the Change() and LostFocus() events of the text box.
I envisaged calling CheckTextbox from within the Change event, and FormatTextbox from the LostFocus event. Both of these procedures take a textbox control parameter. The CheckTextbox procedure checks whether the contents can be interpreted as a date and, if so, set the text colour of the control to black. If the contents cannot be interpreted as a date, then the text is red. FormatTextbox will do all that and also format the date in a standard, unambiguous format.
I just used IsDate() to do some really basic validation but there is no limit to what you could allow; one popular suggestion is "y" and "-1" for yesterday, "0" for today, "+1" for tomorrow, "+1w" for next week, "+Mon" for next Monday and so on. All of this is possible and even quite easy, once you have the basic framework set up. At this stage, I was thinking about that framework.
I really like to make this sort of code really easy to use. An easy way to do this would be to wrap it up in an ActiveX control but, to be honest, I find the way VB does control creation rather irksome. If you've written a control in Delphi by using inheritance, you will know what I mean; if you haven't then you just can't appreciate the difference so I'll move on. Regardless, I probably would have gone the custom control route eventually (in fact, I probably will anyhow). It's just that I was exploring alternatives when I stumbled across properties in BAS files.
I wanted to be able to just add the module to the project and just add these two calls to the event procedures of any text box used for entering date values. However, I also wanted to allow for some level of customisation. I wanted to allow the developer to set the colours used for displaying the valid and invalid entries (two colours for each to allow customisation of both foreground and background colours) and I also wanted to allow the standardised date format to be set by the developer. This is all CS101 stuff: just provide some global variables to hold these and then set them during application start-up, either in Sub Main or in the main form's Load event.
The problem with this approach is that it requires the developer to do this initialisation. This is because you cannot define initialised variables in VB and you don't have an Initialize event for BAS modules.
Remember, I was playing around at this stage; had I been in more of a hurry I would have just gone the standard route of defining an "Init" method that the developer must call before using the library, and then moved on to the next project. As it was, however, I was waiting on four different clients to get back to me so I was in a DoEvents loop <g>.
I thought to myself: what if I made the global variables variants? I could then check whether they were empty (using IsEmpty) whenever I wanted to use them and, if so, initialise them to a default value. So I tried something like this:
Attribute VB_Name = "DateCheck" Option Explicit Public ForeColorOK As Variant Public BackColorOK As Variant Public ForeColorBAD As Variant Public BackColorBAD As Variant Public DefaultFormat As Variant
Then later in the code:
If Result Then If IsEmpty(ForeColorOK) Then ForeColorOK = vbWindowText End If If IsEmpty(BackColorOK) Then BackColorOK = vbWindowBackground End If NewFore = ForeColorOK NewBack = BackColorOK Else If IsEmpty(ForeColorBAD) Then ForeColorBAD = vbRed End If If IsEmpty(BackColorBAD) Then BackColorBAD = vbWindowBackground End If NewFore = ForeColorBAD NewBack = BackColorBAD End If
OK, I'm not that stupid. I quickly realised that I should just bunch all the IsEmpty tests into the aforementioned Init procedure and just unconditionally call that at the start of every other procedure:
Public Sub Init() If IsEmpty(ForeColorBAD) Then ForeColorBAD = vbRed End If If IsEmpty(BackColorBAD) Then BackColorBAD = vbWindowBackground End If If IsEmpty(ForeColorOK) Then ForeColorOK = vbWindowText End If If IsEmpty(BackColorOK) Then BackColorOK = vbWindowBackground End If If IsEmpty(DefaultFormat) Then DefaultFormat = "d mmm yyyy" End If End Sub
There were a couple of problems with this approach, however. One was that there was no validation of what the programmer set into these variables. If a programmer did this:
DateCheck.DefaultFormat = "\M\O\R\O\N"
then the system would insult the user every time the date entry field lost focus. Well, that's OK. You don't need to protect your code from malicious abuse. The real problem was that I had to remember to protect every access to the five global variables with a call to Init. This was difficult enough to do in my own code; it would be impossible to enforce in the user's code.
What I really wanted was a property. Click, click, click. The gears started turning. I brought up the Insert Procedure dialog and saw that the Property option button was not disabled. What the heck… I tried it and - blow me down with a feather - it worked!
In the end, I just made the previously global variables private to the module and wrote property Get/Let procedures for them. The Get procedures include the IsEmpty test and will quietly initialise their matching variable when it is first used. All other code in the module (as well as all non-module code) simply uses the properties. Much cleaner and much more maintainable.
The DateCheck module has been put back on hold (because those clients did indeed get back to me - all four at once <sigh>) so I haven't done all the fancy validation and shortcuts I want to do, but if you're interested the source code is available.
Thinking back on this exercise, what VB really needs is the ability to define initialised variables. This is what I really want to do:
Public ForeColorOK As Long = vbWindowText Public BackColorOK As Long = vbWindowBackground Public ForeColorBAD As Long = vbRed Public BackColorBAD As Long = vbWindowBackground Public DefaultFormat As String = "d mmm yyyy"
Oh, and while I'm at it, I also want to do this:
Public Const sTab = Chr$(9)
but I've already complained about this one and no-one seems to be listening.
As for what else you might do with properties in BAS modules, the use that immediately springs to mind is to create one-and-only-one, global pseudo-objects. Consider an enhanced Printer or Screen modules. How about a FileSystem module that exposed files list as collections and so on? Quite frankly, I haven't thought about this enough to know how I might use it, but it sure seems this stuff must be good for something.
To wrap up, I thought I'd share a few other "didjanoes" with you that I've found people are generally not aware of.
Did you know that...
In VB5, you can call a Function as if it were a Sub if you just want to ignore the result. This means you can just send a message like this:
SendMessage txt.hWnd, EM_SETREADONLY, ByVal 1, ByVal 0instead of the more usual and much uglier:
Junk& = SendMessage(txt.hWnd, EM_SETREADONLY, ByVal 1, ByVal 0)
In VB4 and 5, labels are local in scope to their procedure. This means that EVERY procedure can have a label called "Error_Handler" or "Exit_Point" which makes it much easier to writer boilerplate error handling code.
In VB5, you can omit the parentheses after a function name if you are not passing any parameters. For example, one function I have is called AppPath (note the lack of a period). This simply returns the App.Path property with a single trailing back-slash. Now, I can just use the function name in a statement as if it was a variable, like this:
IniFile = AppPath & App.EXEName & ".INI"
In Word 97 (Australian dictionaries etc.), typing "boiler-plate" causes the grammar checker to suggest that you use "boilerplate". Accepting this offends the spelling checker, which has no suggestions to make at all. I like it when MS is silly too…