If you check the project references for any project, the first three items in the list you will see are Visual Basic For Applications, Visual Basic Runtime Objects And Procedures, and Visual Basic Objects And Procedures. Because these references contain the definitions for all the intrinsic Visual Basic subroutines, functions, and objects, you will see that, following the rules of the previous paragraph, it is theoretically possible to define local versions of any intrinsic Visual Basic subroutine, function, or object within the program. Unfortunately, this is not the case, as there are some functions that cannot be subclassed for various reasons that I will discuss later in the chapter.
One useful application of subclassing that we have implemented is in replacing the standard Visual Basic MsgBox function. The code for our MsgBox function is shown here:
Public Function MsgBox(ByVal isText As String, _ Optional ByVal ivButtons As Variant, _ Optional ByVal ivTitle As Variant, _ Optional ByVal ivLogText As Variant, _ Optional ByVal ivBut1Text As Variant, _ Optional ByVal ivBut2Text As Variant, _ Optional ByVal ivBut3Text As Variant, _ Optional ByVal ivBut4Text As Variant) As Integer '================================================================== ' ' Module: Message_Box. Function: MsgBox. ' ' Object: General ' ' Author - TMS Programmer. TMS Ltd. ' Template fitted : Date - 01/07/95 Time - 14:27 ' ' Function's Purpose/Description In Brief ' ' Internal, replacement MsgBox function and statement used in ' TMS Tools. Allows four buttons, configurable button caption text, ' and logging of error text to a LOG file. See nInternalMsgBox ' for more information. ' ' This function provides a simple wrapper around frmIMsgBox's ' nMb2InternalMsgBox method, allowing that method to be called ' without bothering with the class prefix. As such, its name is ' not nMb1MsgBox but MsgBox so as to keep its name short and such ' that it overrides VBA's function/statement of the same name. ' ' Revision History: ' ' BY WHY & WHEN AFFECTED ' TMS Programmer. TMS Ltd. - Original Code 01/07/95, 14:27 ' ' INPUTS - See nMb2InternalMsgBox for a description. ' ' ' OUTPUTS - Possibly nothing, possibly an integer; i.e., as ' it's a VB function, its return value may be ignored. ' See nMb2InternalMsgBox for a full description. ' '================================================================== ' Set up general error handler. On Error GoTo Error_In_General_MsgBox: Const ERROR_ID = "Message_Box - General_MsgBox" Call TrTMS2TraceIn(ERROR_ID) ' ========== Body Code Starts.========== Dim fp As Form Dim sTitle As String ' Create the message box form. Set fp = New frmIMsgBox Load fp If IsMissing(ivTitle) Then sTitle = App.MsgBoxTitle & App.Title Else sTitle = App.MsgBoxTitle & CStr(ivTitle) End If ' Create and show the message box. MsgBox = fp.nMb2InternalMsgBox(isText, ivButtons, sTitle, _ ivLogText, ivBut1Text, _ ivBut2Text, ivBut3Text, ivBut4Text) ' Destroy the message box form. Unload fp Set fp = Nothing ' ========== Body Code Ends. ========== Call TrTMS2TraceOut(ERROR_ID) Exit Function ' Error handler Error_In_General_MsgBox: ' Store error details on stack. Pass procedure name. Err.Push ERROR_ID ' Add rollback code here... Err.Pop ' Call TMS error handler. Select Case frmErrorHandler.nEhTMSErrorHandler Case ERROR_ABORT: Resume Exit_General_MsgBox Case ERROR_RETRY: Resume Case ERROR_IGNORE: Resume Next Case ERROR_END: Call GuDoEndApp Case Else: End End Select Exit_General_MsgBox: End Function
This is an example of a routine where we have completely replaced the original Visual Basic functionality. However, it is also possible to use the existing functionality and extend it, as in the following example:
Public Property Get EXEName() As String EXEName = UCase$(VB.App.EXEName & _ IIf(App.InDesign = True, ".VBP", ".EXE")) End Property
Here we are simply taking the EXEName property of the App object and reformatting it to our own standard. Note that to access the original Visual Basic property we must fully qualify the reference. When this is done, Visual Basic ignores the rules above for resolving the location of the routine and instead resolves it directly using the explicit reference.
The potential for subclassing Visual Basic intrinsic functionality should not be underestimated. In the MsgBox example alone, the scope you have for customization is enormous. For example, the TMS MsgBox function allows you to log any message displayed with the vbCritical flag. It contains auto-reply functionality in which you can have the message box automatically choose a reply after a given period of time. You can also configure up to four buttons with custom captions. All this functionality from one Visual Basic method!
Functions that have multiple definitions but return a different result type (for example, Format and Format$) are also not possible to subclass fully. The problem here is that two functions are needed to subclass the function properly, but Visual Basic sees Public Function Format(…) As Variant and Public Function Format$(…) As String as being the same function and raises a compile-time error of a duplicate function definition. Here, the only option is to subclass the more generic function (in this example, the more generic function is Format, as it returns a Variant) and issue instructions to developers not to use Format$.
Other functions break Visual Basic's own rules on parameters. An example of this is InStr, which takes four arguments, of which the first and fourth arguments are optional. As Visual Basic's rules state that optional arguments must be at the end of a list of arguments, clearly we cannot subclass this function to match exactly the internal definition of it. To get around this, we either have four arguments, with the last two optional, or simply use a ParamArray of Variants as a single argument. Both of these solutions require more work within the new version of the function in order to work out which of the four arguments have been supplied.
One "nice to have" feature would be to wrap the subclassed functions in an ActiveX DLL such that the source code was "hidden" from the developers. Unfortunately, this currently isn't possible in Visual Basic. In the beginning of this section, I described the method Visual Basic uses to determine where to find the definition of a function. Remember the three references I listed that always appear at the top of the project references list? Unfortunately, there is no way to prevent those three references from being at the top of the list. This means that the reference to your ActiveX server with the subclassed functions is always below them in the list and, as a result, your subclassed functions are never called. It's a pity that this approach isn't possible, as it would not only hide the details of what extra functionality the subclassed functions contain, but it would also remove the clutter of the extra source code from the application.
Because you are creating new objects of your own, it is essential to include all the properties and methods of the original object and simply pass the calls through to the original object where there is no extra functionality required. An example of this is the hInstance property of the App object, shown below.
Public Property Get hInstance() As Long hInstance = VB.App.hInstance End Property
NOTE
Note that hInstance is a read-only property of the VB.App object so you would not code a Property Let for it.
One note of caution with the Err object relates to the Raise method. Although you should probably add a Raise method to your subclassed Err object for completeness, you should add code to it to ensure it is never called. The reason for this becomes obvious when you stop to think about it. Your Err.Raise method would deal with the error by passing the required details to VBA.Err.Raise. This would then generate an error, with the source of the error being your Err object every time! When subclassing the Err object, you must ensure that VBA.Err.Raise statements generate all errors raised in the program.