Programs execute in an uncertain world. Even a completely bug-free program can encounter problems during execution. Any number of interactions between the user, the hardware (host computer and network), and other active programs in a multitasking
environment can result in error conditions that stop a Visual Basic program cold.
When Visual Basic programs encounter these error conditions, they terminate immediately and unceremoniously. A user can easily lose important data. Even when the error condition is entirely caused by the user, the user's dissatisfaction and anger is
directed toward the software and its author(s).
High quality software must be written with a means for intercepting errors when they occur. At best, software should provide a mechanism for remedying the error condition; at least, software should allow a graceful exit with no data loss. This process
is commonly referred to as error handling.
The folks at Microsoft define runtime errors as "errors that occur while your code is running and that result from attempts to complete an invalid operation." These errors are reported to the program by the Visual Basic runtime engine as
numerical codes. In Visual Basic 4.0, these codes are long integers. The codes (as you are probably aware) fall in the range of 2,147,483,648 to 2,147,483,647that's approximately 4.3 billion unique errors that the system can report. There is
some logic (although not a lot) to the error codes. Table 5.1 summarizes the ranges of error codes.
Ranges |
Description |
Comments |
|
3 to 94 |
System errors |
Miscellaneous errors concerning interactions with memory, disks, and various programming errors like Return without GoSub. |
|
260 to 428 |
DDE and form-related messages |
These errors include some important interactions between programs and with Windows, such as No timer available. |
|
429 to 451 |
OLE automation messages |
These return information about the external condition of OLE automation servers, whether it can be located, if it doesnt respond in a certain amount of time, and so on. Internal conditions in servers are passed to the client program by adding an internal error code to the constant, vbObjectError and returning that as the error number. These are important errors to attend to if you are interested in the OLE automation client and server functions of Visual Basic 4.0. |
|
452 to 521 |
Miscellaneous |
These messages cover interactions with resource messages(RES) files, the Clipboard, printers, and other truly miscellaneous messages. |
|
2055 to 3751 |
Data access |
A large range of database access errors that object errors return error conditions from the MS Access/Jet database engine. |
|
20476 to |
Common dialog |
These messages are spread out over a wide range 28671, 31001, control messages of numerical values; not a sign of careful logic 32751 to 32766 in assigning error numbers. |
|
30000 to 30017 |
Grid messages |
Error messages from the Visual Basic grid control. |
|
31003 to 31039 |
OLE control |
Messages from the OLE control for Visual Basic 4.0. |
|
|
|
|
There are really three activities that make up error handling:
A common example of an error condition is addressing a floppy disk drive with no disk in it. In this case, trapping the error means intercepting the error before Visual Basic terminates the program. In this situation, the error is Error 71, Disk not
ready. Handling the error simply means instructing the user to place a disk in the drive or to close the drive door and then instructing the program to resume processing. If the error is unexpected or unrecoverable, that is, something the error handler
cannot process, then the next step is reporting the error. This means returning the error condition to the default error-handling routines of the compiler (which cause, in this case, an unceremonious termination of the program).
There is a set of language constructs for error processing that have been present in various dialects of Basic for a number of years. Visual Basic 4.0 supports these legacy tools with which you may already be familiar. Error trapping is enabled by using
the On Error construct, as follows:
On Error Goto Label1 ... Error prone actions On Error Goto 0
On Error ... is the statement that turns error handling on and off. The On Error statement has three forms:
Most error-handling routines are variations on the following code skeleton:
Label1:
Select Case Err
...Case X ' an anticipated error
'Fix the error
Resume
Case Else 'Unanticipated Errors
Msg$ = Error$(Err)
MsgBox Msg$
Error Err
End Select
Exit Sub 'or Function
After the label, error handling code often uses some branching structure (If...Then...Else...End If or Select Case) to process the anticipated errors; it also uses a default clause (Else or Case Else). Information about the nature of the error is
retrieved from the system Err object that contains a long integer.
The Error[$]() function returns what Microsoft calls a descriptive string (these strings are still rather cryptic and may not give the programmer or the user much helpful information if the programmer relies on them solely for the information returned
to the user). The Error statement returns the error condition to the compiler's default error processing. The syntax of the Error statement can take one of two forms:
Error Err Error number
This construct is useful for the handling of unanticipated errors in an error handler. Although Visual Basic 4.0 supports both the Error() function and the Error statement, new features in Visual Basic 4.0 provider richer information and finer control
of the default processing of errors. These new features are described in the following section.
Visual Basic 4.0 introduces three new tools for managing errors and retaining the legacy error management functions. The new tools include the Err object (for Visual Basic errors), the Errors collection and Error object (for data access object errors),
and options that customize the development environment's error handling behavior.
The new Err object, like any OLE object, exposes properties and methods for reporting and responding to error conditions. Table 5.2 lists the properties of the Err object; Table 5.3 lists the methods of the Err object.
Property |
Description |
|
Number |
The number identifying the error. This the default property of the Err object. |
|
Description |
The descriptive name associated with an error. |
|
Source |
The name of the object or application in which the error originated. |
|
HelpFile |
A fully qualified path to a Windows Help file. |
|
HelpContext |
A Context ID referring to a specific topic in a help file. |
|
|
|
Method |
Description |
|
Raise |
Generates a runtime error. |
|
|
|
The syntax for the Raise method is shown here:
object.Raise(Number[, Source[, Description[, HelpFile[, HelpContext]]]])
Note that only the first parameter, Number, is required. The syntax also supports named arguments. Because the default property of the Err object is the Number property, code using the Error() function and the Error statement still executes correctly.
The Error object reports data access object errors and is very similar to the Err object in its structure. The differences are that the Error object has no LastDLLError property and no Raise or Clear methods. Table 5.4 describes the properties of the
Error object.
Property |
Description |
|
Number |
The number identifying the error. This the default property of the Error object. |
|
Description |
The descriptive name associated with an error. |
|
Source |
The name of the object or application in which the error originated. |
|
HelpFile |
A fully qualified path to a Windows Help file. |
|
|
|
The Errors collection belongs to the DBEngine object. When a data access action occurs, the collection is populated with all the occurring errors. When another data access action occurs, the collection is emptied and repopulated with any new errors.
Like all collections, the Errors collection has only one property: Count. By examining the Count property of the Errors collection, you can determine whether the data access action executed without error:
If DBEngine.Errors.Count <> 0 then ... process errors.... End If
Environment options that customize error handling are located in the Advanced tab of the Options dialog box (see Figure 5.1). To access the Options dialog box, choose Tools | Options. The error-handling options are listed here:
Figure 5.1. The Advanced tab of the Options dialog box.
The Break on All Errors option causes the development environment to switch from Run to Break mode whenever any error occurswhether or not an error handler is active. This setup allows the programmer to identify the occurrence of errors even when
the error handler is active. Break on All Errors can be especially useful for developing and debugging the error-handling routines.
The Break on Unhandled Errors option is most useful in later debugging and testing of an application, when the programmer is more confident of the error-handling routines and is more interested in unhandled errors.
The Break in OLE Server option is most useful when debugging OLE servers created in Visual Basic. The object application here is another instance of Visual Basic, in which the OLE server is running in the development environment's Run mode while a test
client is being run in the first instance of Visual Basic. If an error condition in the client application occurs, the server application breaks at its current position.
Figure 5.2. Windows 95 with two instances of Visual Basic running.
The tools and mechanics of error handling are, unfortunately, just the tip of the iceberg. There are basically three types of error handling: Inline, Local, and Centralized. The following sections explore the techniques of these different types of error
handling and some of the design issues that make one type more appropriate than another in particular situations.
Inline error handling disables the default error handling of Visual Basic and checks for errors after each line of error-prone code. This emulates the behavior of languages that do not raise exceptions like the C language. C was designed to give the
programmer ultimate freedom (and consequently ultimate responsibility) for the code he or she writes. Therefore, C programmers have the ability to totally ignore most if not all of the error conditions that occur in the course of execution (at his or her
own risk, I hasten to add). In C you must deliberately check for errors when you expect them to occur, which is to say that you handle them inline. In Visual Basic, inline error handling always begins with the On Error Resume Next construct, which causes
Visual Basic to continue with the next line of code after an error occurs. There are a variety of strategies for processing errors in this way. Microsoft mentions three in the documentation for Visual Basic to which we add a fourth:
Each of these strategies is described in the following sections.
In this strategy, errors occurring when processing information in functions are returned as long integers or as variants from the function itself. The initiating procedure turns off error checking. The following code fragments illustrate this approach:
Sub DoSomethingImportant(strParameter as String)
Dim vResult as variant
On Error Resume Next
vResult = MessAroundWithStrings(strParameter)
If vResult <> 0 then 'An error occurred in the function
...Handle Errors...
Else
...Continue Processing
End If
'You might include this line for completeness but it isn't required
On Error Goto 0
End Sub
Function MessAroundWithStrings(strAString as String) as Variant
...Do processing...
If Err <> 0 then
MessAroundWithStrings = Err
Else
MessAroundWithStrings = 0
End If
End Function
Figure 5.3. The Visual Basic code window with a block of error-handling code.
In this strategy, the root procedure sets error handling to a local or centralized error handler; called subs and functions use the Raise method of the Err object (preferred) or the Error statement to return an error state to the root procedure for
handling. In the following code, note that the function MessAroundWithStrings uses the Raise method of the Err object to return error conditions to the calling sub (DoSomethingImportant). The same action could be accomplished by using the Error statement
(a legacy tool) with the Err objects default property (Number) substituting the line
Err.Raise Err.Number
with the line
Error Err.
Sub DoSomethingImportant(strParameter as String)
Dim vResult as String
On Error Resume Goto ErrorHandler
vResult = MessAroundWithStrings(strParameter)
Continue processing
'You might include this line for completeness but it isn't required
On Error Goto 0
Exit Sub
ErrorHandler:
...Handle Error
'If successful
Resume 'This will resume in the sub where the error occurred
'If not
...Exit gracefully...
End Sub
Function MessAroundWithStrings(strAString as String) as String
...Do processing on strAString...
If Err <> 0 then
Err.Raise Err.Number
Else
MessAroundWithStrings = strAString
End If
End Function
This approach may be the preferred one when a programmer uses programmer-defined errors as a means of communication between procedures or processes in the application. In this way, both system and programmer-defined errors can be handled with the same
code.
In this approach to error handling, the root procedure passes a variant as a parameter to the called functions and procedures. If an error occurs, the type of the variant is set to type Error and the value is set to Err.Number. In the following code, we
pass a separate variant parameter (vResult) the function. MessAroundWithStrings function stores the error number (0 if none) in this parameter. The calling function checks the variant type of vResult. If it is of type vbError, check the Err object for the
current error information and handle the error.
Sub DoSomethingImportant(strParameter as String)
Dim vResult as Variant
Dim strResult as String
On Error Resume Next
strResult = MessAroundWithStrings(strParameter, vResult)
If VarType(vResult) = vbError then 'An error occurred in the function
...Handle Errors...
Else
...Continue Processing
End If
'You might include this line for completeness but it isn't required
On Error Goto 0
End Sub
Function MessAroundWithStrings(strAString as String, varAResult as Variant) as _String
Dim strBuffer as String
strBuffer = strAString
...Do processing on strBuffer...
If Err <> 0 then
varAResult = CVErr(Err.Number)
Else
MessAroundWithStrings = strBuffer
End If
End Function
The variant parameter is another method that is useful for user-defined errors; you can set the value of the vResult parameter to a user-defined error number and set its type to vbError. Using variants of type vbError can be combined with the first
strategy (returning the error value in the return parameter). A combined approach might look like the following code. Note that the function returns a variant instead of a string. In checking the return value, check first to see if it is of type vbError.
Then handle the error or deal with the string information contained in the variant .
Sub DoSomethingImportant(strParameter as String)
Dim vResult as variant
On Error Resume Next
vResult = MessAroundWithStrings(strParameter)
If VarType(vResult) = vbError then 'An error occurred in the function
...Handle Errors...
Else 'You can treat the variant as a string
...Continue Processing
End If
'You might include this line for completeness but it isn't required
On Error Goto 0
End Sub
Function MessAroundWithStrings(strAString as String) as Variant
Dim strBuffer as String
StrBuffer = strAString
...Do processing on strBuffer...
If Err <> 0 then
MessAroundWithStrings = CVError(Err.Number)
Else
MessAroundWithStrings = CVar(strBuffer)
End If
End Function
This is one of the uses of variants in which the overhead in memory and the loss of speed caused by using variants is probably worth the resulting simplicity and convenience of the code.
Of course, one approach to error handling (and probably the wisest approach) is to program in such a way as to identify or eliminate errors before they occur. One approach is to use this Assert() function. It is based on ASSERT macros from the C
language. Here is the code for the Assert() function that I have developed and (like the Twilight Zone) offer for your consideration:
Function Assert (Condition As Boolean, Message As String,
Optional HelpFile as Variant, Optional Context as _Variant) as Boolean
If Condition = False Then
#If DebugMode Then
Dim Style, Title
Style = vbStop + vbOK
Title = "ERROR MESSAGE"
If IsMissing(HelpFile) or IsMissing(Context) then
MsgBox Msg, Style, Title
Else
MsgBox Msg, Style, Title, HelpFile, Context
Stop
#Else
Dim fileName As String, fileNum As Integer
Dim buf As String
buf = Date$ & Chr$(9) & Time$ & Chr$(9) & Message
fileName = App.Path & "\error.log"
fileNum = FreeFile
Open fileName For Append As #fileNum
Print #fileNum, Date$, Time$, Message
Close #fileNum
End If
#End If
Assert = Condition
End Sub
The Assert() function requires a condition parameter and a string parameter for a custom message; it optionally accepts help file and context references. The function returns a Boolean based on the condition parameter. The function is set up so that it
goes into Break mode in the Visual Basic IDE after showing an informative message box. In the production executable, the information is written to an error log (so that, if your program bombs on a client's computer, you have the information about errors
written to disk so that you can fix the problem). The following code snippet shows how the Assert() function can anticipate errors:
If Not Assert(FileName <> "", "FileObject.FileRename: FileName not initialized!") _Then
'Get a valid FileName or exit gracefully
End If
If any attempt to open the file follows, the runtime error 76 occurs, Path not found. By checking for this condition, the error is avoided and the program can correct the condition.
Local error handling occurs in the procedure itself. The stubs of code discussed in the first part of this chapter on the basics of error handling are examples of localized error handling. Each procedure with error-prone activities has its own
error-handling code. Each procedure can raise unanticipated errors, returning them step by step up the call chain for handling by calling procedures. Notice in the following example code how some error-handling code is local to the MessAroundWithStrings
function. In the previous examples, the error-handling code was located completely in the calling function DoSomethingImportant.
Sub DoSomethingImportant(strParameter as String)
Dim vResult as variant
On Error Goto MyHandler
vResult = MessAroundWithStrings(strParameter)
...Continue Processing
'You might include this line for completeness but it isn't required
On Error Goto 0
Exit Sub
MyHandler:...Handle Errors from this Sub and from the function it calls
End Sub
Function MessAroundWithStrings(strAString as String) as String
Dim strBuffer as String
On Error Goto SecondHandler
StrBuffer = strAString
...Do processing on strBuffer...
MessAroundWithStrings = strBuffer
Exit Function
SecondHandler:
Select Case Err.Number
Case X
...Handle some errors but pass unhandled errors back to calling procedure:
Case Else
Err.Raise Number:=Err.Number
End If
End Function
Figure 5.4. The Calls dialog box.
Localized error handling can invoke multiple error handlers in the same procedure, as shown in the following example:
Sub DoSomethingImportant(strParameter as String)
On Error Goto StringHandler
strParameter = MessAroundWithStrings(strParameter)
...Continue Processing
On Error Goto FileHandler
Open "Myfile.txt"for Output as #1
...Process File
Close #1
'You might include this line for completeness but it isn't required
On Error Goto 0
Exit Sub
StringHandler:
...Handle string Errors
Exit Sub
FileHandler:
...Handle file errors
End Sub
The final type of error handling is centralized error handling. Centralized error handling processes all errors through a single error-handling routine. Centralized error handling is difficult to implement in Visual Basic. Theoretically, if you invoke
error handling from a Sub Main, you can then redirect all error handling to a centralized routine. However, this makes for a complicated and error-prone error-handling routine.
A more effective way of doing quasi-centralized error handling is to modularize code and have central error-handling code in code (.BAS), form (.FRM), or class (.CLS) modules based on the function and data of those modules. The new class modules of
Visual Basic 4.0 present a great opportunity for object-oriented programming in Visual Basic.
One of the aspects of object-oriented programming is encapsulation. Encapsulation means isolating the data and processing in one module from all othersthat is, creating a black box with clearly defined inputs and outputs. This approach allows you
to develop and, more importantly, reuse error-handling code for specific types of processing.
One useful strategy is to establish programmer-defined errors and then use the Visual Basic Err object and error-handling routines to trap and correct these errors. The documentation for Visual Basic 4.0 suggests that programmers define errors based on
the vbObjectError offset. At press time, the specific offset of the constant has not been determined. If it is important for you to know the numerical value, you can find it by typing ? vbObjectError in the Debug window. You should use the constant
rather than a hard-coded number to maintain forward compatibility in your code.
The documentation also requires user-defined errors to be in the range of 1 to 65,535. Because this range also includes errors reported from OLE controls you may be using in your application, check the error code ranges of these controls. Nonetheless,
you should have a large range of open error codes.
Suppose that an application depends on a specific data file (such as an initialization file). When the application determines that the initialization file has been deleted, the code can define and raise an error. When the error handler encounters this
error, it can write a default initialization file to disk or prompt the user with a Preferences dialog box to obtain initialization information.
One thing I have come to hate is adding error-handling code to individual modules that perform file I/O. I also find it difficult to keep the syntax of file I/O forever straight in my head. When Visual Basic 4.0 offered the possibility of developing
classes, I thought it was one area that would be very useful to me: developing class wrappers for frequently used code that can be plugged in to any project and accessed with the familiar metaphor of properties and methods. Because file and disk I/O is an
especially error-prone activity, it helps to localize error handling for this activity in a single class module.
Ordinary and class modules have their own properties in Visual Basic 4.0. Figure 5.5 shows the Properties window for the FileClass class.
Figure 5.5. The Properties window for the FileClass module.
In this case, the FileClass object has properties of Instancing = 2 (Creatable Multiuse),Public = False, and Name = FileClass. This setup means that the objects of this class can be created and accessed only from within the project using the module.
These objects can be created using Dim or GetObject, and there can be multiple instances of this object in use at the same time. Table 5.5 lists the properties and Table 5.6 lists the methods of the FileClass object.
Property (Data Type) |
Description |
|
Name (String) |
Read/Write. This is the fully qualified path to a particular file. |
|
Path (String) |
Read only. This is the path segment of the filename. |
|
Title (String) |
Read only. This is the filename segment of the name. |
|
Mode (Integer) |
Read/Write. This is the mode for which the file is opened. The class supports Input, Output, Append, and Binary modes of file access. |
|
Access (Integer) |
Read/Write. This is the access restriction that you can optionally include in the Open syntax. The options include Read, Write, and ReadWrite. |
|
Length (Long) |
The number of bytes in the file. |
|
DateTime (Variant) |
Returns the date and time of the last revision of the file. |
|
|
|
Method (Returns) |
Description |
|
FileOpen() |
Returns an integer that is the valid file number of the open file. |
|
FileClose |
Closes the file wrapped in the FileClass object. |
|
FileMove strNewPath |
Moves the object's file and resets the object to point at the new location for the file. |
|
FileRename strNewName |
Renames the object's file and resets the object to point at the new location. |
|
FileDelete |
Deletes the object's file from disk and sets a flag to remind the programmer (if necessary) that the file has been deleted. |
|
FileCreate strNewFile |
Creates a new null file, gives an overwrite warning, and resets the file object to point to the new file. |
|
FileCopy strNewName[, varRegisterNew] |
Copies the object's file to another location; optionally points the file object to the new instance of the file if the varRegisterNew parameter equals TRUE. |
|
|
|
The class module begins with the declarations shown in Listing 5.1.
Option Explicit
#If Win32 Then
Private Declare Function GetWindowsDirectory Lib "kernel32"
Alias "GetWindowsDirectoryA" (ByVal lpBuffer As String, _
ByVal nSize As Long) As Long
#Else
Private Declare Function GetWindowsDirectory Lib "Kernel" _
(ByVal lpBuffer As String, ByVal nSize As Integer) As Integer
#End If
'Mode constants
Private Const MODE_APPEND = 0
Private Const MODE_BINARY = 1
Private Const MODE_INPUT = 2
Private Const MODE_OUTPUT = 3
'Access constants
Private Const ACCESS_READ = 0
Private Const ACCESS_WRITE = 1
Private Const ACCESS_READWRITE = 2
'File Object MyError Constants
Private Const FOBJ_ERROR_RESUME = 0
Private Const FOBJ_ERROR_RESUMENEXT = 1
Private Const FOBJ_ERROR_FILENAME = 2
Private Const FOBJ_ERROR_UNRECOVERABLE = 3
Private Const FOBJ_ERROR_UNRECOGNIZABLE = 4
'Constants for trappable file I/O errors
Private Const ErrOutOfMemory = 7
Private Const ErrBadFileNameOrNumber = 52
Private Const ErrFileNotFound = 53
Private Const ErrFileAlreadyOpen = 55
Private Const ErrDeviceIO = 57
Private Const ErrFileAlreadyExists = 58
Private Const ErrDiskFull = 61
Private Const ErrBadFileName = 64
Private Const ErrTooManyFiles = 67
Private Const ErrPermissionDenied = 68
Private Const ErrDiskNotReady = 71
Private Const ErrCantRename = 74
Private Const ErrPathFileAccessError = 75
Private Const ErrPathNotFound = 76
'Internal Property Variables
Private FilePath As String
Private FileTitle As String
Private FileName As String
Private FileMode As Integer
Private FileAccess As Integer
Private FileNumber As Integer
Private LastError As Long
'Flag variables
Private AmOpen As Boolean
Private AmDeleted As Boolean
This is pretty standard stuff: constants are defined to increase the readability and maintainability of the code. Two Visual Basic 4.0-specific features are used here: The first is the conditional compilation option; declarations are included for both
the 16-bit and the 32-bit APIs. This way the module can be added to any project for either platform and compile correctly.
The second feature is the use of Public (new to Visual Basic 4.0 from VBA) instead of Global, and the deliberate use of Private variables accessed through Property methods.
At the heart (not at the head) of the module are private utility functions used by the public methods (see Listing 5.2).
'**********************************************************
'
' Private Utility Functions
'
'
'
'**********************************************************
Private Sub ProcessPathTitleAndName(newName As String)
Dim BackSlash As Integer
If InStr(newName, "\") Then
BackSlash = RInstr(0, newName, "\")
FilePath = Left$(newName, BackSlash - 1)
FileTitle = Mid$(newName, BackSlash + 1)
ElseIf InStr(newName, ":") Then
Dim CurDrive As String
Dim TargetDrive As String
TargetDrive = Left$(newName, 1)
CurDrive = CurDir$
If Left$(CurDrive, 1) <> TargetDrive Then
ChDrive TargetDrive
FilePath = CurDir$
ChDrive CurDrive
Else
FilePath = CurDir$
End If
FileTitle = Mid$(newName, InStr(newName, ":") + 1)
Else
FilePath = CurDir$
FileTitle = newName
End If
FileName = FilePath & "\" & FileTitle
End Sub
'=======================================================
Private Sub DoFileCopy(Source As String, Target As String, _
Optional Overwrite As Variant)
Dim ErrorMsg As String, SourceNum As Integer, TargetNum As Integer
Dim buffer As String, TheLength As Long
ErrorMsg = "FileObject.DoFileCopy: Attempting "
ErrorMsg = ErrorMsg & "copy/move operation on non-existent file!"
If Assert(FileExists(Source), ErrorMsg) Then
SourceNum = FreeFile: TargetNum = FreeFile
On Error GoTo DoFileCopyError
Open Source For Binary Access Read As SourceNum
TheLength = LOF(SourceNum)
Open Source For Binary Access Read As SourceNum
Open Target For Binary Access Write As TargetNum
On Error GoTo 0
If TheLength < 60000 Then
'Take the file in bits
Do Until TheLength < 60000
buffer = String$(0, 60000)
Get SourceNum, , buffer
Put TargetNum, , buffer
TheLength = TheLength - Len(buffer)
Loop
buffer = String$(0, TheLength)
Get SourceNum, , buffer
Put TargetNum, , buffer
Else
buffer = String$(0, TheLength)
Get #SourceNum, , buffer
Put TargetNum, , buffer
End If
Close #SourceNum
Close #TargetNum
End If
Exit Sub
DoFileCopyError:
Dim action As Integer, ErrNumber As Integer
action = Errors()
Select Case action
Case 0
Resume
Case 1
Resume Next
Case 2, 3
Exit Sub
Case Else
ErrNumber = Err.Number
Err.Raise ErrNumber
End Select
End Sub
'============================================================
Sub DeletedMsg()
Dim msg, style
msg = "You have deleted the file """ & FileName & "."""
msg = msg & " You must reinitialize the FileObject with a "
msg = msg & "new valid file name before proceeding!"
style = vbCritical + vbOKOnly
MsgBox msg, style, App.Title
End Sub
'========================================================
Private Function OverwriteWarning(FileName As String) As Integer
Dim msg As String, style As Integer
msg = "The file, " & FileName & ", already exists in the current "
msg = msg & "directory. Overwrite it?"
style = vbQuestion Or vbYesNo
OverwriteWarning = MsgBox(msg, style, App.Title)
End Function
'=========================================================
Private Function RInstr(Start As Integer, Source As String, _
Goal As String) As Integer
Dim Index As Integer, N As Integer
If Start <> 0 Then Index = Start Else Index = Len(Source)
For N = Index To 1 Step -1
If Mid$(Source, N, 1) = Goal Then
RInstr = N
Exit Function
End If
Next
RInstr = 0
End Function
The module uses the centralized error-handling function shown in Listing 5.3.
Private Function Errors() As Integer
Dim MsgType As Integer, msg As String, response As Integer
Dim NewFileNameNeeded As Boolean
Dim DoResume As Boolean
Dim DoResumeNext As Boolean
'Return Value Meaning Return Value Meaning
' 0 Resume 2 Filename Error
' 1 Resume Next 3 Unrecoverable Error
' 4 Unrecognized Error
MsgType = vbExclamation
Select Case Err.Number
Case ErrOutOfMemory '7
msg = "The operating system reports that there is not "
msg = msg & "enough memory to complete this operation. "
msg = msg & "You can try closing some other applications and then "
msg = msg & "click Retry to try again or you can click Cancel to exit."
MsgType = vbExclamation + vbRetryCancel
DoResume = True
'Resume or Exit
Case ErrBadFileNameOrNumber, ErrBadFileName
msg = "That file name is illegal!"
NewFileNameNeeded = True
DoResume = True
'Resume
Case ErrFileNotFound
msg = "That file does not exist. Create it?"
MsgType = vbExclamation + vbOKCancel
DoResumeNext = True
'Resume Next
Case ErrFileAlreadyOpen
msg = "That file is already in use."
MsgType = vbExclamation + vbRetryCancel
NewFileNameNeeded = True
'New Name
Case ErrDeviceIO
msg = "Internal disk error."
MsgType = vbExclamation + vbRetryCancel
DoResume = True
'Resume
Case ErrFileAlreadyExists
msg = "A file with that name already exists. "
msg = msg & "Replace it?"
MsgType = vbExclamation + vbOKCancel
NewFileNameNeeded = True
'New Name
Case ErrDiskFull
msg = "This disk is full. Continue?"
MsgType = vbExclamation + vbOKCancel
DoResume = True
'Resume
Case ErrTooManyFiles
msg = "The operating system reports that too "
msg = msg & "many files are currently open. You "
msg = msg & "can try closing some other applications "
msg = msg & "and then try again."
MsgType = vbExclamation + vbRetryCancel
DoResume = True
'Resume
Case ErrPermissionDenied
msg = "You have tried to write to a file that is in "
msg = msg & "use or is designated as read-only."
NewFileNameNeeded = True
'New Name
Case ErrDiskNotReady
msg = "Insert a disk in the drive and close the door."
MsgType = vbExclamation + vbOKCancel
DoResume = True
'Resume
Case ErrPathFileAccessError, ErrPathNotFound
msg = "The operating system cannot locate this file on "
msg = msg & "this path. Check to make sure that the file "
msg = msg & "name and path have been entered correctly "
msg = msg & "and then try again."
NewFileNameNeeded = True
Case Else
Errors = 4
Exit Function
End Select
response = MsgBox(msg, MsgType, "File Error")
Select Case response
Case vbRetry, vbOK
If NewFileNameNeeded Then
LastError = FOBJ_ERROR_FILENAME
ElseIf DoResume Then
LastError = FOBJ_ERROR_RESUME
ElseIf DoResumeNext Then
LastError = FOBJ_ERROR_RESUMENEXT
Else
LastError = FOBJ_ERROR_UNRECOVERABLE
End If
Case Else
LastError = FOBJ_ERROR_UNRECOGNIZABLE
End Select
Errors = LastError
End Function
The Errors() function is adapted from the example code for centralized error handling in the Visual Basic 4.0 Programmer's Guide. It is changed in a few regards: it relies on constants and is a little more readable. The return values are revised so that
the function can instruct various calling procedures to try for a new filename if that is an easy solution for the error condition. The function also stores the handler's assessment to a variable called LastError.
The properties of the FileClass object are accessed with the Property methods shown in Listing 5.4.
'*****************************************************
'
' Property Procedures
' Path, Name, Mode, Access,
' Length, DateTime,
'
'
'*****************************************************
Public Property Get Path() As String
If AmDeleted Then
DeletedMsg
Exit Property
End If
Path = FilePath
End Property
'========================================================
Public Property Let Name(newName As String)
If Not FileExists(FileName) Then
Dim msg, style, answer
msg = "The file, """ & newName & """ does not exist. "
msg = msg & "Create it?"
style = vbQuestion Or vbYesNo
answer = MsgBox(msg, style, App.Title)
If answer = vbYes Then
FileCreate newName
Else
Exit Property
End If
Else
ProcessPathTitleAndName newName 'Checks for Drive, Directory, etc.
End If
End Property
'==========================================================
Public Property Get Name() As String
If AmDeleted Then
DeletedMsg
Exit Property
End If
Name = FileName
End Property
'==========================================================
Public Property Get Title() As String
If AmDeleted Then
DeletedMsg
Exit Property
End If
Title = FileTitle
End Property
'===========================================================
Public Property Let Mode(NewMode As Integer)
If AmDeleted Then
DeletedMsg
Exit Property
End If
If NewMode <> FileMode Then
FileMode = NewMode
If AmOpen Then
Close #FileNumber
FileOpen
End If
End If
End Property
'============================================================
Public Property Get Mode() As Integer
If AmDeleted Then
DeletedMsg
Exit Property
End If
Mode = FileMode
End Property
'==============================================================
Public Property Let Access(NewAccess As Integer)
If AmDeleted Then
DeletedMsg
Exit Property
End If
If NewAccess <> FileAccess Then
FileAccess = NewAccess
If AmOpen Then
Close #FileNumber
FileOpen
End If
End If
End Property
'===============================================================
Public Property Get Access() As Integer
If AmDeleted Then
DeletedMsg
Exit Property
End If
Access = FileAccess
End Property
'================================================================
Public Property Get FileError() As Integer
FileError = LastError
End Property
'================================================================
Public Property Get Length() As Long
If AmDeleted Then
DeletedMsg
Exit Property
End If
Dim FileNum As Integer
If Assert(FileName <> "", _
"FileObject.Length: FileName not initialized!") Then
FileNum = FreeFile
Open FileName For Binary Access Read As #FileNum
Length = LOF(FileNum)
Close FileNum
End If
End Property
'===============================================================
Public Property Get DateTime() As Variant
If AmDeleted Then
DeletedMsg
Exit Property
End If
If Not Assert(FileName <> "", _
"FileObject.FileOpen: FileName not initialized!") Then
Exit Property
End If
If Assert(FileExists(FileName), _
"FileObject.DateTime: FileName not initialized!") Then
DateTime = FileDateTime(FileName)
End If
End Property
Finally, there are the method subs and functions. Note that subs and functions become methods in a class module simply by being declared as Public (see Listing 5.5).
'**********************************************************
'
' Methods
'
' FileOpen, FileClose, FileMove, FileRename,
' FileDelete and FileError
'
'**********************************************************
Public Function FileOpen() As Integer
If AmDeleted Then
DeletedMsg
Exit Function
End If
If Not Assert(FileName <> "", _
"FileObject.FileOpen: FileName not initialized!") Then
Exit Function
End If
If AmOpen Then Close #FileNumber
Dim dummy As Variant
FileNumber = FreeFile
Select Case FileMode
Case MODE_APPEND
Select Case FileAccess
Case ACCESS_READ
dummy = Assert(False, _
"FileObject.FileOpen: " & _
"ReadOnly Access specified for Append action!")
AmOpen = False
Case ACCESS_WRITE
On Error GoTo FileOpenError
Open FileName For Append Access Write As #FileNumber
AmOpen = True
FileOpen = FileNumber
On Error GoTo 0
Exit Function
Case ACCESS_READWRITE
dummy = Assert(False, _
"FileObject.FileOpen: " & _
"ReadWrite Access specified for Append action!")
AmOpen = False
Case Else
End Select
Case MODE_BINARY
Select Case FileAccess
Case ACCESS_READ
On Error GoTo FileOpenError
Open FileName For Binary Access Write As #FileNumber
AmOpen = True
FileOpen = FileNumber
On Error GoTo 0
Exit Function
Case ACCESS_WRITE
On Error GoTo FileOpenError
Open FileName For Binary Access Write As #FileNumber
AmOpen = True
FileOpen = FileNumber
On Error GoTo 0
Exit Function
Case ACCESS_READWRITE
On Error GoTo FileOpenError
Open FileName For Binary Access Read Write As #FileNumber
AmOpen = True
FileOpen = FileNumber
On Error GoTo 0
Exit Function
Case Else
End Select
Case MODE_INPUT
Select Case FileAccess
Case ACCESS_READ
On Error GoTo FileOpenError
Open FileName For Input Access Read As #FileNumber
AmOpen = True
FileOpen = FileNumber
On Error GoTo 0
Exit Function
Case ACCESS_WRITE
dummy = Assert(False, _
"FileObject.FileOpen: " & _
"Attempting Access Write with Input mode!")
Exit Function
Case ACCESS_READWRITE
dummy = Assert(False, _
"FileObject.FileOpen: " & _
"Attempting Access Read Write with Input mode!")
Exit Function
Case Else
End Select
Case MODE_OUTPUT
Select Case FileAccess
Case ACCESS_READ
dummy = Assert(False, _
"FileObject.FileOpen: " & _
"Attempting Access Read with Output mode!")
Exit Function
Case ACCESS_WRITE
On Error GoTo FileOpenError
Open FileName For Output Access Write As #FileNumber
AmOpen = True
FileOpen = FileNumber
On Error GoTo 0
Exit Function
Case ACCESS_READWRITE
dummy = Assert(False, _
"FileObject.FileOpen: " & _
"Attempting Access Read Write with Output mode!")
Exit Function
Case Else
End Select
Case Else
dummy = Assert(False, _
"FileObject.FileOpen: " & _
"Incorrect File Mode parameter set!")
Exit Function
End Select
FileOpenError:
Dim action As Integer, ErrNumber As Integer
action = Errors()
Select Case action
Case 0
Resume
Case 1
Resume Next
Case 2, 3
Exit Function
Case Else
ErrNumber = Err.Number
Err.Raise ErrNumber
Err.Clear
End Select
End Function
'=========================================================
Public Sub FileClose()
If AmDeleted Then
DeletedMsg
Exit Sub
End If
If Not Assert(FileName <> "", _
"FileObject.FileOpen: FileName not initialized!") Then
Exit Sub
End If
If AmOpen Then
Close #FileNumber
FileNumber = 0
AmOpen = False
End If
End Sub
'========================================================
Public Sub FileMove(NewPath As String)
If Not Assert(FileName <> "", _
"FileObject.FileMove: FileName not initialized!") Then
Exit Sub
End If
'Check Drive Spec
Dim newName As String, SourceNum As Integer, TargetNum As Integer
If Right$(NewPath, 1) = "\" Then 'Get the path in shape
newName = NewPath & FileTitle
Else
newName = NewPath & "\" & FileTitle
End If
If InStr(NewPath, ":") Then 'There is a drive spec included
If Left$(newName, 1) <> Left$(FileName, 1) Then
'Different drive, Name command won't work
DoFileCopy FileName, newName
Kill FileName
ProcessPathTitleAndName newName
End If
Else
On Error GoTo FileMoveError
Name FileName As newName
On Error GoTo 0
ProcessPathTitleAndName newName
End If
Exit Sub
FileMoveError:
Dim action As Integer, ErrNumber As Integer
action = Errors()
Select Case action
Case 0
Resume
Case 1
Resume Next
Case 2, 3
Exit Sub
Case Else
ErrNumber = Err.Number
Err.Raise ErrNumber
Err.Clear
End Select
End Sub
'==============================================================
Public Sub FileRename(newName As String)
If Not Assert(FileName <> "", _
"FileObject.FileRename: FileName not initialized!") Then
Exit Sub
End If
On Error GoTo FileRenameError
If InStr(newName, ":") Then 'there is a drive spec
If Left$(newName, 1) <> Left$(FileName, 1) Then
DoFileCopy FileName, newName
Kill FileName
Else
Name FileName As newName
End If
Else
Name FileName As newName
End If
On Error GoTo 0
ProcessPathTitleAndName newName
FileRenameError:
Dim action As Integer, ErrNumber As Integer
action = Errors()
Select Case action
Case 0
Resume
Case 1
Resume Next
Case 2, 3
Exit Sub
Case Else
ErrNumber = Err.Number
Err.Raise ErrNumber
Err.Clear
End Select
End Sub
'===============================================================
Public Sub FileDelete()
If Not Assert(FileName <> "", _
"FileObject.FileOpen: FileName not initialized!") Then
Exit Sub
End If
If AmOpen Then Close #FileNumber
If AmDeleted Then
DeletedMsg
Exit Sub
End If
Kill FileName
AmDeleted = True
FileNumber = 0
End Sub
'===================================================
Public Sub FileCreate(newName As String)
Dim FileNum As Integer
Dim choice As Integer
If FileExists(newName) Then
choice = OverwriteWarning(newName)
If choice = vbNo Then Exit Sub
End If
FileNum = FreeFile
Open newName For Output As #FileNum
Close FileNum
ProcessPathTitleAndName newName
End Sub
'===================================================
Public Sub FileCopy(newName As String, Optional RegisterNew As Variant)
If Not Assert(FileName <> "", _
"FileObject.FileOpen: FileName not initialized!") Then
Exit Sub
End If
DoFileCopy FileName, newName
If Not IsMissing(RegisterNew) And RegisterNew = True Then
ProcessPathTitleAndName newName
End If
End Sub
The functions shown in Listing 5.6 are called when an instance of type FileClass is created or destroyed.
'**********************************************************
'
' Class Initialization and Destruction
'
'
'
'**********************************************************
Private Sub Class_Initialize()
Dim nResult As Integer
Dim buffer As String
'Initializes the object to an ubiquitous file
'This works in tandem with the inifile object
'by setting things to point to WIN.INI
FileTitle = "WIN.INI"
buffer = String$(200, 0)
nResult = GetWindowsDirectory(buffer, Len(buffer))
FilePath = Left$(buffer, nResult)
FileName = FilePath & "\" & FileTitle
FileMode = MODE_BINARY
FileAccess = ACCESS_READWRITE
End Sub
Private Sub Class_Terminate()
If FileNumber <> 0 Then
Close #FileNumber
End If
End Sub
When a file object is first initialized, the default filename, mode, and access values are set. When the object is terminated (that is, when the object variable is Set equal to Nothing or when the object variable goes out of scope), if a file is open,
that file is closed.
To use a FileClass object in your programs, you follow these basic steps:
After these steps are completed, you can address the FileClass object in your code with a sequence like the following, which opens an initialization file and populates an array with all the section names in the file:
With myFile
.Access = ACCESS_READ
.Mode = MODE_INPUT
End With
FileNum = myFile.FileOpen()
'Clear and refill the Sections array
Erase mySections
counter = 0
Do
Line Input #FileNum, buf
If Len(buf) <> 0 Then
If Left$(buf, 1) = "[" Then ' it's a section name
ReDim Preserve mySections(counter + 1)
Index = InStr(2, buf, "]") - 2
mySections(counter) = Mid$(buf, 2, Index)
counter = counter + 1
End If
End If
Loop Until EOF(FileNum)
myFile.FileClose
To take full advantage of the module, you must include the following declarations in a form or module of your program:
'Mode constants Public Const MODE_APPEND = 0 Public Const MODE_BINARY = 1 Public Const MODE_INPUT = 2 Public Const MODE_OUTPUT = 3 'Access constants Public Const ACCESS_READ = 0 Public Const ACCESS_WRITE = 1 Public Const ACCESS_READWRITE = 2 'File Object MyError Constants Public Const FOBJ_ERROR_RESUME = 0 Public Const FOBJ_ERROR_RESUMENEXT = 1 Public Const FOBJ_ERROR_FILENAME = 2 Public Const FOBJ_ERROR_UNRECOVERABLE = 3 Public Const FOBJ_ERROR_UNRECOGNIZABLE = 4
This inconvenience is necessary because class modules in Visual Basic 4.0 cannot contain public constant declarations; to use constants, you have to declare them elsewhere in your application.
Error handling is an important aspect of professional-quality programming. It protects the users of your software from the mystifying disappearance of the program and from the frustrating loss of data. The basic mechanisms of error handling in Visual
Basic 4.0 are similar to earlier versions and other flavors of the Basic language. Visual Basic 4.0 gives richer and finer control of error reporting through the Err object, the Error object, and Errors collection for data access and with new programming
environment options. Finally, design issues like the types of errors anticipated and the modular design of the application determine which pattern of error handling (Inline, Local, or Centralized) is best for a particular situation.