Here are the materials from Thursday night's presentation.
Last night I mentioned a newly-uncovered bug that exists in all versions of Access. To see the article, go to the Microsoft support site and search for KB article # Q191883.
The remainder of this page is the whitepaper in HTML format.
FMS Whitepaper
User-Proof Your Access Applications
By Dan Haught, Vice President of Product Development, FMS Inc.
www.fmsinc.com
Abstract
When you develop Access applications that are to be distributed to other users, you must take many factors into account. You must develop your application to run on varying hardware, operating system versions, directory structures and network servers. This session covers the key aspects of developing an Access Solution for distribution to other users. You'll learn about machine-independence, startup control, re-attaching linked tables, robust error-handling, long distance tech support, and developing for the least-common denominator machine. You will learn the techniques of developing and application that you can distribute with confidence. This session is essential information for developers and technical managers.
Introduction
When you develop Access applications that are to be distributed to other users, you must take many factors into account. You must develop your application to run on varying hardware, operating system version, directory structures, and network servers. This session covers the key aspects of developing Access solutions for distribution to other users. You'll learn about machine-independence, startup control, re-attaching linked tables, robust error handling, long distance tech support, and developing for the least common denominator machine. You will learn the techniques of developing an application that you can distribute with confidence.
First Things First: Split Your Application
Splitting an Access application into two separate databases is a common technique. When you are developing applications for distribution, this technique becomes required. First let's explain how this works. The first database, known as the Data database contains all tables and data. The other database, known as the Application database contains all other objects including queries, forms, reports, macros and modules. The two are joined together by linking (or "attaching" in Access 2 parlance) the tables from the Data database to the Application database.
The key benefit to this design is that you distribute new Application databases to your users without overwriting their data. Since the data is stored in separate database, you don't have to worry about importing or converting the user's data into your new version.
Access 95 and Access 97 offer an add-in that does this for you. For more information, search Access Online help for "database splitter".
Anticipating the User Environment
Have you ever spent a lot of time designing a form to look just right, only to have it appear incorrectly on another system? This is a common problem when developing visual applications for use under Windows. Since Windows offers the user so many configuration options, and there are so many ways that Windows can be installed, the application the looks and runs fine on your system may not work well on another system. This section covers some basic techniques to handle this problem.
Handle for Font Scaling
Windows 3.x offers both "Large Fonts" and "Small Fonts" options for displaying fonts. Windows 95 has these options, and adds arbitrary font scaling. These settings can drastically affect how your application works. It can be very difficult to design efficient forms that can handle for font scaling differences. It is probably best to require your users to run your application with the standard font scaling.
One thing you can do to alleviate this situation is to set any image or bitmap controls to "Zoom" or "Stretch". This allows Access to stretch or enlarge bitmaps to display correctly regardless of font scaling.
Don't Use Obscure Fonts
Standard Windows 3.x installations have a very limited set of fonts. Windows 95 expands the list of basic fonts, but it is still somewhat limited. Because of this, you should assume that your users will not have any fonts other than the standard ones. Do not use other fonts in your application because Access will try to approximate them on your user's system, sometimes with strange visual results.
Use System Colors
Because Windows allows you to specify your own system colors, and application designed on your system may appear strangely on another user's system. For example, if a user specifies a different system color for 3d objects, Access will use that color when it displays command buttons. If your application's command buttons are hardcode to be gray, they will appear strange in the user's color scheme.
To get around this, Access has a set of predefined color values that you can use to force Access to use system colors. The following table shows these values. For more information, search Access Online help for "color".
Screen element | Value |
Desktop | -2147483647 |
Application workspace | -2147483636 |
Window background | -2147483643 |
Window text | -2147483640 |
Menu bar | -2147483644 |
Menu text | -2147483641 |
Active title bar | -2147483646 |
Inactive title bar | -2147483645 |
Active title bar text | -2147483639 |
Inactive title bar text | -2147483629 |
Active border | -2147483638 |
Inactive border | -2147483637 |
Window frame | -2147483642 |
Scroll bars | -2147483648 |
Button face | -2147483633 |
Button shadow | -2147483632 |
Button text | -2147483630 |
Button highlight | -2147483628 |
Disabled text | -2147483631 |
Highlight | -2147483635 |
Highlighted text | -2147483634 |
Access 2.0 and SHARE.EXE
Access 2 requires that SHARE.EXE be loaded for correct operation. In Windows 3.1 and 3.11, this must be done in the Autoexec.Bat file. In Windows for Workgroups, this is handled by Windows automatically with the VSHARE driver. This is not an issue for Access 2.0 under Windows 95 or Windows NT, nor is it an issue for Access 95 or Access 97.
Assume There Will Be A Performance Difference
Despite the fact that your application works on your 32MB Pentium system with no apparent performance issues, you application running on a user's 33mhz 486 may be a different matter altogether. Be sure to test your application on the type of computers that will on you user's desks. At a minimum, be sure to put in hourglass cursors where an operation may take more than 1 second.
Attached Table Issues
If your application uses the two-database approach mentioned earlier, then you have to worry about attached table issues. When a user installs your application on their system, there is a good chance they will install it in a directory that is different from the one on your system where the database was created. This means that all links to attached tables will be broken on the user's system.
To get around this, your application should have a startup routine that fixes all link for attached table.
Note:
In Access 2.0 applications, the only way to invoke your startup routine is to through the AutoExec macro. Access 95 and 97 offer additional startup capabilities that are more robust, flexible and powerful. See the section later in this paper entitled "Application Startup Techniques" for more information.Step 1: Determining Where the Data Database Is
The first step is to determine where the data database resides. If it is always installed in the same directory as your application database, this is easy. Simply get the path of the current database using the following code technique:
Function GetCurrentDBPath() As String ' Comments : Returns the path of the current database Dim intCounter As Integer Dim strPath As String strPath = CurrentDb.Name For intCounter = Len(strPath) To 1 Step -1 If Mid$(strPath, intCounter, 1) = "\" Then Exit For End If Next intCounter GetCurrentDBPath = Left$(strPath, intCounter) strPath = CurrentDb.Name End Function
If your data database can placed in a directory other than the application database's directory, the above technique will obviously not work. In such a case, you need to prompt the user for the location of the data database.
Step 2: Fix the Attached Tables
Once you have the path where the data database exists, it is a simple matter to fix the attached tables. The following three procedures can be used to accomplish this:
Function ReAttachAllTables_TSB(strDatabase As String, strNewPath As String) As Integer ' Comments : Relinks all the attached tables to a new database ' Parameters: strDatabase - Path and name of database containing the attached table(s) or "" (blank string) for the current database ' strNewPath - full path of the database to attach to ' Returns : True if successful, False otherwise ' Dim dbsTemp As Database Dim intCounter As Integer Dim tdfTemp As TableDef Dim fOK As Integer On Error GoTo ReAttachAllTables_TSB_ERR If strDatabase = "" Then Set dbsTemp = CurrentDb() Else Set dbsTemp = DBEngine.Workspaces(0).OpenDatabase(strDatabase) End If fOK = True For intCounter = 0 To dbsTemp.TableDefs.Count - 1 Set tdfTemp = dbsTemp.TableDefs(intCounter) If GetAttachedType_TSB("", (tdfTemp.Name)) <> "" Then fOK = ReAttachTable_TSB((dbsTemp.Name), (tdfTemp.Name), strNewPath) If Not fOK Then Exit For End If End If Next ReAttachAllTables_TSB = (fOK = True) ReAttachAllTables_TSB_EXIT: dbsTemp.Close Exit Function ReAttachAllTables_TSB_ERR: ReAttachAllTables_TSB = False Resume ReAttachAllTables_TSB_EXIT End Function Function GetAttachedType_TSB(strDatabase As String, strTable As String) As String ' Comments : returns the type of the named attached table ' Parameters: strDatabase - name and path of database to look in or "" (blank string) for the current database ' strTable - name of table to check ' Returns : attachment type ' Dim dbsTemp As Database Dim tdfTemp As TableDef Dim strConnect As String Dim intPos As Integer Dim strTmp As String If strDatabase = "" Then Set dbsTemp = CurrentDb() Else Set dbsTemp = DBEngine.Workspaces(0).OpenDatabase(strDatabase) End If Set tdfTemp = dbsTemp.TableDefs(strTable) strConnect = tdfTemp.Connect If strConnect <> "" Then intPos = InStr(tdfTemp.Connect, ";") strTmp = Mid$(strConnect, 1, intPos - 1) If strTmp = "" Then strTmp = "Access" End If End If dbsTemp.Close GetAttachedType_TSB = strTmp End Function Function ReAttachTable_TSB(strDatabase As String, strTable As String, strPath As String) As Integer ' Comments : Re-attaches the named table to the named path ' Parameters: strDatabase - Path and name of database containing the attached table or "" (blank string) for the current database ' strTable - table name of the attached table ' strPath - path name of the database containing the real table ' Out : True if successful, False otherwise ' Dim dbsTemp As Database Dim tdfTemp As TableDef Dim strPrefix As String Dim strNewConnect As String On Error GoTo fAttachTable_TSB_ERR If strDatabase = "" Then Set dbsTemp = CurrentDb() Else Set dbsTemp = DBEngine.Workspaces(0).OpenDatabase(strDatabase) End If Set tdfTemp = dbsTemp.TableDefs(strTable) strPrefix = Left$(tdfTemp.Connect, InStr(tdfTemp.Connect, "=")) strNewConnect = strPrefix & strPath tdfTemp.Connect = strNewConnect tdfTemp.RefreshLink ReAttachTable_TSB = True fAttachTable_TSB_EXIT: dbsTemp.Close Exit Function fAttachTable_TSB_ERR: ReAttachTable_TSB = False Resume fAttachTable_TSB_EXIT End Function
Form and Report Issues
When creating forms and reports, be sure to consider the following issues.
Don't Make Your Forms Too Big
If you design your Access forms with a screen resolution of 640x480, this won't be an issue. But if, like most developers, you use a higher resolution, you may forget that form designed to fit on your 1024x768 screen will be too large for your users with the 640x480 setting. Always consider the lowest common denominator machine. The following table shows the maximum form size that will fit within a given resolution:
Resolution | Maximum Form Size |
640x480 | 6.6 x 3.6 inches |
800x600 | 8.2 x 5 inches |
1024x768 | 10.5 x 7 inches |
1280x768 | 13.5 x 9 inches |
Note that the above figures are baseline numbers. You must take into account other form sizing issues, such as the Windows 95 taskbar, and the use of scrollbars, navigation buttons and other form design elements.
Setting the Default Printer
Access saves printer settings with each form and report. This means that if you select a specific printer using a form or report's Print Setup dialog, that printer name is stored with the form or report. When you then distribute your database to another user, that form or report will not print unless the user has exactly the same printer connected to exactly the same port (or network path).
To avoid this error, open each form and report in design view and set the printer to "Default Printer".
Convert To Picture
Be sure to convert all bitmaps to pictures (available through the edit menu in Form design). This breaks the link between the bitmap and its associated application. In Access 95 and 97, use the new Image control to handle this automatically.
Macros
If you designed your application correctly, you are only using macros for Autoexec, AutoKeys and menu assignments. Macros for any other use have no place in professional application development, especially in applications that are to be distributed to other users.
Additionally, in Access 95 and 97, new startup properties make the Autoexec macro obsolete, and Access 97's inclusion of Command Bars make menu assignment macros obsolete.
Module Issues
Scoping and the Global Namespace
This section applies to Access 2.0 only.
In order for libraries to peacefully coexist with databases that are opened through the Access user-interface, Access defines rules that partition areas of memory for use by each database. These rules, known as scoping rules, tell Access how and where to look for objects and procedures that your application calls. If your application can potentially be installed on a machine that already has Access installed, and the user will be using their own MSACC20.INI initialization file, there is a chance that Access Basic names that you use in your application may conflict with library databases that the user may have installed. This section discusses this problem, and its resolution.
The scoping rules for libraries in Access 2 are somewhat confusing at first, and are inconsistent in places. The following rules are used for form, report and macro objects:
For Access Basic code, the scoping rules are more complex, yet more consistent than the rules for forms and reports. In order to understand the scoping rules for module code, you must understand the global namespace. When Access starts, it populates the global namespace by reading the [Libraries] section of the initialization file. Each library it finds there is opened behind the scenes. Access loads each standard module in each database and looks for all global code. This includes procedures that are defined without the Private keyword, global variables and constants, all Types, and all Declare functions. It then loads this global code into the global namespace. By the time it has finished loading, Access has a structure in memory identifying all objects in the global namespace, along with the name of the database where the objects are defined. It is through this mechanism that Access makes module code available to the current open database. Form and report modules are not opened during this process and are not added to the global namespace since, by definition, form and report modules cannot contain global code.
Populating the Global Namespace
It is important to note that this global namespace is also populated when you open a database through the Access user interface (for example with the File|Open Database menu command). All global elements in your database modules are added to the global namespace. The reason this is important is that you must take care to avoid global namespace conflicts. A conflict occurs when Access tries to load a procedure, constant, type or variable name into the global namespace, and an object of that name already exists. This is a common problem for first-time library developers. For example, assume your library contains a function called IsLoaded(). You start Access and it loads this function from your library into the global namespace. Then you try to open one of your databases that contains a module with a function that is also called IsLoaded(). Access generates an error message at this point and does not load that module from the current open database.
The key to avoiding global namespace conflicts is to ensure that each global object in your modules has a unique name that cannot conflict with other code names. This is typically handled by using prefixes or suffixes on your object names that are unique to each library. For example, if you have a library of code routines you use in many places, consider using a name such as "UTIL" as a unique identifier. Each of your non-private procedures, global variables and constants, types and Declares should use this name. So instead of defining the procedure named IsLoaded() in your library, you would call it IsLoaded_UTIL(). This suffix text reduces the chance of a conflict (also known as disambiguation) and allows your library and your current open database to coexist within the global namespace. Of course, to further reduce the potential of conflicts, you may have to come up with a more distinct disambiguator, such as your company name, or project name.
Option Explicit
While the use of Option Explicit in modules is not directly related to distributing applications, it is the single most important thing to do to reduce the number of bugs in your application. Every module in a distributed application should have Option Explicit set.
Calls to Library Databases
If your application uses library databases, you must ensure that the reference to the library database is established correctly on the user's machine. If you are using Access 2.0, this reference is made in your application's INI file in the [Libraries] section. The easiest way to handle library databases is to install them in the same directory as the Access runtime executable file. This ensures that Access will always be able to find the library.
VBA References
In Access 95 and Access 97, INI file references are no longer used to call code from library databases. The following section explains the concept of references, and how they can affect distributed applications.
Library Databases
A library database is a collection of procedures and database objects that you can call from any Microsoft Access application. You can use libraries to store routines that you use often, so you dont have to write the same routine for each application you create. You can also use libraries to distribute new features to your users.
When you create an application, the objects and Visual Basic code in the application database work only within that application. For example, you can call functions in the UtilityFunctions module in the Northwind sample database only from objects in that databasethey arent available from other databases. This structure works fine for objects and code that you use in only one application. However, you might find that you need functionality to be available from all your databases. This is achieved through the use of library databases.
A library database is structurally the same as any other Microsoft Access database. It can contain tables, queries, forms, reports and module code. The only real difference between a library database and regular database is that a library database is referenced by Microsoft Access directly. You do not open the library database, Microsoft Access does it for you.
References
All add-ins require access to one or more library databases. One of the more difficult to master aspects of library databases is referencing. Referencing means that Microsoft Access must know were to find the library database. By establishing a reference to a library database, you are letting Microsoft Access know where to look for the library database.
Under Microsoft Access 2.0, establishing a reference was easily accomplished by adding a key to the Microsoft Access Initialization File (typically called MSACC20.INI). For example, if you had a library database called C:\TOOLS\MYTOOLS.MDA, you could add the following line to the INI file:
[Libraries] C:\TOOLS\MYTOOLS.MDA=rw
From there on, any database could access objects in that library database. The setting made the libraries modules global.
Types of References
Because of the architecture of Microsoft Access 97 and VBA, there is no longer a concept of global modules. In order to establish a reference to a library database, you have to learn some new techniques. References can be established in several ways:
Creating a Library Reference
You can create a Library Reference by putting a reference to it in the [Libraries] section of the registry. This is essentially the same as putting it in the INI file under 2.0. However, doing this does not make the module global in the same way that Access 2.0. It only allows functions from the library database to be invoked as a menu add-in. If you need your database to be able to call functions from the library database, this method will not work.
Creating an Explicit Reference
Explicitly adding a reference that establishes a link between your database and the library. This method has the following restrictions:
When you create an explicit reference, Microsoft Access stores the explicit path to the referenced database. However, by default, Microsoft Access will look in several other locations for the referenced library database:
To add a path to registry for Microsoft Access to search for referenced databases, add a key called REFLIBPATHS in:
HKEY_LOCAL_MACHINE/SOFTWARE/MICROSOFT/ACCESS/7.0/
Under this key, you can add the following string and value:
String Value
mydatabase.mda x:\path
where mydatabase.mda is the name of your library database and x:\path is the full path to search in.
Creating a Runtime Reference
This technique establishes a reference at runtime using the Application.Run method. This method opens the explicitly named library database and executes the requested function. This method has the following restrictions:
The LoadOnStartup Key
You can add a key call LoadOnStartup to the following tree:
HKEY_LOCAL_MACHINE/SOFTWARE/MICROSOFT/OFFICE/80/ACCESS/WIZARDS
By adding this key and adding your library database to this section, you can control how your library database code is loaded. This, in effect, causes the type information from your library database to be loaded when Microsoft Access is started. By placing your add-ins in this section, you can save approximately 10% of loading time. This works because 10% of your wizards loading time will occur when Microsoft Access is started. This is only a time-shifting device and nothing more. It does not change the reference model described earlier.
Programmatically Creating References with Access 97
Access 97 exposes new Reference objects that allow you to programmatically create and delete references. The sample files for this session include an add-in called the FMS Reference Manager. With this add-in, you can view, create and delete references.
Listing References
The following code is used to retrieve the current references into a table:
Function fBuildRefsTable_RL(rstRefs As Recordset) As Boolean ' Comments : adds refs to the table ' Parameters: None ' Returns : True/False - success/failure ' Dim refTemp As Reference Dim varTemp As Variant For Each refTemp In Application.References With rstRefs .AddNew ' Reference objects don't have a properties collection ' (totally lame) so we have to check each on in code On Error Resume Next varTemp = refTemp.Name If Err = 0 Then !Name = varTemp Else !Name = "<error: " & Err.Number & ">" End If On Error GoTo 0 On Error Resume Next varTemp = refTemp.BuiltIn If Err = 0 Then !BuiltIn = varTemp Else !BuiltIn = False End If On Error GoTo 0 On Error Resume Next varTemp = refTemp.FullPath If Err = 0 Then !FullPath = varTemp Else !FullPath = "<error: " & Err.Number & ">" End If On Error GoTo 0 On Error Resume Next varTemp = refTemp.GUID If Err = 0 Then !GUID = varTemp Else !GUID = "<error: " & Err.Number & ">" End If On Error GoTo 0 On Error Resume Next varTemp = refTemp.IsBroken If Err = 0 Then !IsBroken = varTemp Else !IsBroken = False End If On Error GoTo 0 On Error Resume Next On Error GoTo 0 varTemp = refTemp.Kind If Err = 0 Then !Kind = IIf(varTemp = 0, "Typelib", "Project") Else !Kind = "<error: " & Err.Number & ">" End If On Error GoTo 0 varTemp = refTemp.Major If Err = 0 Then !Major = varTemp Else !Major = "<error: " & Err.Number & ">" End If On Error GoTo 0 varTemp = refTemp.Minor If Err = 0 Then !Minor = varTemp Else !Minor = "<error: " & Err.Number & ">" End If .Update End With Next refTemp End Function
Adding References
The following code show how to programmatically add a reference:
Function fAddRef() As Boolean ' Comments : adds a reference ' Parameters: none ' Returns : true/false - success/failure ' Dim refAdd As Reference Dim strName As String Dim fOK As Boolean Dim strPath As String Dim intSaveErr As Integer Dim strSaveErr As String Dim strMsg As String On Error GoTo PROC_ERR ' Assume failure fOK = False strPath = strGetOpenFile_RL("C:\WINDOWS\SYSTEM") If strPath <> "" Then If MsgBox("Add a reference to " & UCase(strPath) & "?", _ vbYesNo + vbQuestion) = vbYes Then DoCmd.Hourglass True On Error Resume Next Set refAdd = Application.References.CreateFromFile(strPath) intSaveErr = Err.Number strSaveErr = Err.Description On Error GoTo 0 If intSaveErr <> 0 Then fOK = False Beep strMsg = "Could not add a reference to " & vbCrLf & _ UCase(strPath) & vbCrLf & "Error was: " & _ strSaveErr MsgBox strMsg, vbExclamation, gstrAppName_RL Else fOK = True End If DoCmd.Hourglass False End If End If fAddRef = fOK PROC_EXIT: Exit Function PROC_ERR: fAddRef = False Resume PROC_EXIT End Function
Deleting References
The following code shows how to programmatically delete a reference:
Function fDeleteRef(strName As String) As Boolean ' Comments : Deletes the named reference ' Parameters: strName - name of the reference to delete ' Returns : true/false - success/failure ' Dim refDelete As Reference Dim fOK As Boolean Dim intSaveErr As Integer Dim strSaveErr As String Dim strMesg As String On Error GoTo PROC_ERR ' Assume failure fOK = False strName = Me!lstRefs.Value If MsgBox("Delete reference to " & UCase(strName) & "?", 36) _ = 6 Then DoCmd.Hourglass True Set refDelete = Application.References(strName) On Error Resume Next Application.References.Remove refDelete intSaveErr = Err.Number strSaveErr = Err.Description On Error GoTo 0 If intSaveErr <> 0 Then fOK = False Beep strMesg = "Could not delete the reference to " & vbCrLf & _ UCase(strName) & vbCrLf & "Error was: " & _ strSaveErr MsgBox strMesg, vbExclamation, gstrAppName_RL Else fOK = True End If DoCmd.Hourglass False End If fDeleteRef = fOK PROC_EXIT: Exit Function PROC_ERR: fDeleteRef = False Resume PROC_EXIT End Function
Access 95/97 Application Startup Techniques
Access 95 and Access 97 have a wealth of startup options. You can use these options to control not only how your application start, but also how it is presented to the user, and other aspects of its operations. This section outlines these options.
Setting Startup Options
With your database open, select Tools, Startup from the menu. The following form is displayed:
The following properties can be created/set to control how an application starts and behaves:
There are additional properties and settings you can use. Search Access online help for "Startup Options" for more information.
Error Handling
All applications should have some form of error handling. Applications that you distribute have an even more acute need for error handling. Think about it for moment: if you are going to distribute an application to a site other than your own, you are going to have troubleshoot over the phone. This is not enviable taskoften the user you are talking with is a computer novice, and is not going to be much help in debugging your code.
Therefore, your application needs to handle unanticipated errors gracefully, and log enough information to make it possible for you to debug and troubleshoot the problem. This section covers a strategy for providing robust and informative error handling and logging.
Step 1 Create an Error Stack
The first step is to create an array in your application's VBA code to keep track of which procedures and operations are running. Whenever a form opens, or a VBA procedure is invoked, the basic information is popped onto this array to form a call stack. This call stack can be logged to a file or table when an error occurs, giving you full information about what happened when the user encountered the error. The following code (compatible with Access 2.0, 95 and 97) illustrates how to do this:
Place the following code in the Declarations Section of a new module:
' Define the maximum number of procedure names held in the stack Const ERH_MAXITEMS_TSB = 20 ' Add two for our own handling Const ERH_USEITEMS_TSB = ERH_MAXITEMS_TSB + 2 ' Array to simulate a stack of procedure names Dim aERH_Stack_TSB(0 To ERH_USEITEMS_TSB) As String ' Pointer to the current position in the stack Dim intERH_Pointer_TSB As Integer ' Set the following to True to included expanded system information in the log file Const ERH_EXPANDEDINFO = True ' Set the following to True to display an error dialog in the event of an error Const ERH_DISPLAY_ERROR = True ' Set the following to true to log errors to a table Const ERH_LOG_TO_TABLE = True ' Set the following to the table to log errors to ' (if ERH_LOG_TO_TABLE is True) Const ERH_LOG_TABLE = "tblErrors_TSB" ' Set the following the path and name of the file to log errors to ' (if ERH_LOG_TO_TABLE is False) Const ERH_LOG_FILE = "C:\ERRORS.TXT"
The following procedure initializes the error handling. Call it once when your application starts.
Function ERH_Initialize_TSB () As Integer ' Comments : Initializes the error handler ' Parameters: None ' Returns : True if successful, False otherwise ' On Error GoTo ERH_Initialize_TSB_ERR ' Initialize the stack Erase aERH_Stack_TSB intERH_Pointer_TSB = LBound(aERH_Stack_TSB) ERH_Initialize_TSB = True ERH_Initialize_TSB_EXIT: Exit Function ERH_Initialize_TSB_ERR: ERH_Initialize_TSB = False Resume ERH_Initialize_TSB_EXIT End Function
This procedure adds the name of the procedure of operation to the stack. Call it whenever you initiate a "starting" operation such as entering a procedure, or opening a form or report.
Sub ERH_PushStack_TSB (strProc As String) ' Comments : Pushes the supplied procedure name onto the error handling stack ' Parameters: strProc - name of the currently executing procedure ' Returns : Nothing ' intERH_Pointer_TSB = intERH_Pointer_TSB + 1 If intERH_Pointer_TSB <= ERH_MAXITEMS_TSB Then aERH_Stack_TSB(intERH_Pointer_TSB) = strProc Else aERH_Stack_TSB(intERH_Pointer_TSB + 2) = strProc End If End Sub
This procedure removes the last string that was placed on the stack. Call this procedure whenever you end an operation, such as exiting a procedure, or closing an object. Note that for this technique to work properly, you must match "Push" and "Pop" operations into pairs. For example, if you push the name of form onto the stack with the Push procedure, be sure to pop the form name off of the stack when the form closes.
Sub ERH_PopStack_TSB () ' Comments : Pops the current procedure name off the ' error handling stack ' Parameters: None ' Returns : Nothing ' If intERH_Pointer_TSB <= ERH_MAXITEMS_TSB Then aERH_Stack_TSB(intERH_Pointer_TSB) = "" End If intERH_Pointer_TSB = intERH_Pointer_TSB - 1 If intERH_Pointer_TSB < 0 Then intERH_Pointer_TSB = 0 End If End Sub
When an error occurs, call the following procedure:
Sub ERH_Handler_TSB () ' Comments : the master error handler ' Parameters: none ' Returns : nothing ' Dim strError As String Dim strTemp As String Dim intError As Integer Const MB_STOP = 16 ' Get the current error information strError = Error$ intError = Err ' Since we don't know what state we are in, disable error trapping On Error Resume Next ' Log the error If ERH_LOG_TO_TABLE Then Call ERH_LogErrorToTable_TSB(strError, intError, Erl, ERH_LOG_TABLE, ERH_EXPANDEDINFO) Else Call ERH_LogErrorToFile_TSB(strError, intError, Erl, ERH_LOG_FILE, ERH_EXPANDEDINFO) End If If ERH_DISPLAY_ERROR Then Beep strTemp = "The following error has occurred in procedure: " & Chr$(13) & Chr$(10) strTemp = strTemp & aERH_Stack_TSB(intERH_Pointer_TSB) strTemp = strTemp & " on line " & IIf(IsNull(Erl), "<unknown>", Erl) & Chr$(13) & Chr$(10) strTemp = strTemp & "Error (" & intError & ") " & strError MsgBox strTemp, MB_STOP, "Database Error" End If End Sub
This procedure logs the error information to a file:
Sub ERH_LogErrorToFile_TSB (strError As String, intError As Integer, intErl As Integer, strFile As String, fExpandedStats As Integer) ' Comments : Logs the most recent error to a file ' Parameters: strError - error string ' intError - error number ' intErl - error line number ' strFile - name of the file to log the errors to ' fExpandedStats - True to include additional system information, False otherwise ' Returns : nothing ' Dim dbsCurrent As Database Dim intFile As Integer Dim intCounter As Integer Dim varTemp As Variant Set dbsCurrent = CurrentDB() intFile = FreeFile Open strFile For Append As intFile Print #intFile, "Access Application Error Information" Print #intFile, "====================================" Print #intFile, "Current Time : " & Now Print #intFile, "Error String : "; Left$(strError, 255) Print #intFile, "Error Number : " & intError Print #intFile, "Error Line : " & intErl Print #intFile, "Error Procedure: " & aERH_Stack_TSB(intERH_Pointer_TSB) Print #intFile, Print #intFile, "Procedure Stack" Print #intFile, "---------------" For intCounter = LBound(aERH_Stack_TSB) To UBound(aERH_Stack_TSB) If aERH_Stack_TSB(intCounter) <> "" Then varTemp = varTemp & Format(intCounter, "00") & Space$(intCounter * 2) & aERH_Stack_TSB(intCounter) & Chr$(13) & Chr$(10) End If Next intCounter Print #intFile, varTemp If fExpandedStats Then Print #intFile, Print #intFile, "Additional Information" Print #intFile, "----------------------" varTemp = "Current User : " & CurrentUser() & Chr$(13) & Chr$(10) varTemp = varTemp & "Access Version : " & SysCmd(SYSCMD_ACCESSVER) & Chr$(13) & Chr$(10) varTemp = varTemp & "Jet Version : " & DBENgine.Version & Chr$(13) & Chr$(10) varTemp = varTemp & "LoginTimeout : " & DBENgine.LoginTimeout & Chr$(13) & Chr$(10) varTemp = varTemp & "Current Diretory : " & CurDir & Chr$(13) & Chr$(10) varTemp = varTemp & "Access Directory : " & SysCmd(SYSCMD_ACCESSDIR) & Chr$(13) & Chr$(10) varTemp = varTemp & "Runtime Version : " & SysCmd(SYSCMD_RUNTIME) & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Path/Name : " & dbsCurrent.name & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Version : " & dbsCurrent.Version & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB CollatingOrder: " & dbsCurrent.CollatingOrder & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Connect : " & dbsCurrent.Connect & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB QueryTimeout : " & dbsCurrent.Querytimeout & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Transations : " & dbsCurrent.Transactions & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Updatable : " & dbsCurrent.Updatable & Chr$(13) & Chr$(10) Print #intFile, varTemp End If ' Close the objects Close #intFile dbsCurrent.Close End Sub
This procedure logs the error information to a table:
Sub ERH_LogErrorToTable_TSB (strError As String, intError As Integer, _ intErl As Integer, strTable As String, fExpandedStats As Integer) ' Comments : Logs the most recent error to a table ' Parameters: strError - error string ' intError - error number ' intErl - error line number ' strTable - name of the table in the current ' database to log errors to ' fExpandedStats - True to include additional system ' information, False otherwise ' Returns : nothing ' Dim dbsCurrent As Database Dim rstErrors As Recordset Dim intCounter As Integer Dim varTemp As Variant Set dbsCurrent = CurrentDB() Set rstErrors = dbsCurrent.OpenRecordset(strTable, DB_OPEN_DYNASET, _ DB_APPENDONLY) rstErrors.AddNew rstErrors![ErrorDate] = Now rstErrors![ErrorString] = Left$(strError, 255) rstErrors![ErrorNumber] = intError rstErrors![ErrorLine] = intErl rstErrors![ErrorProc] = aERH_Stack_TSB(intERH_Pointer_TSB) For intCounter = LBound(aERH_Stack_TSB) To UBound(aERH_Stack_TSB) If aERH_Stack_TSB(intCounter) <> "" Then varTemp = varTemp & Format(intCounter, "00") & Space$(intCounter * 2) & aERH_Stack_TSB(intCounter) & Chr$(13) & Chr$(10) End If Next intCounter rstErrors![CallStack] = varTemp If fExpandedStats Then varTemp = "Current User: " & CurrentUser() & Chr$(13) & Chr$(10) varTemp = varTemp & "Access Version: " & SysCmd(SYSCMD_ACCESSVER) & Chr$(13) & Chr$(10) varTemp = varTemp & "Jet Version: " & DBENgine.Version & Chr$(13) & Chr$(10) varTemp = varTemp & "LoginTimeout: " & DBENgine.LoginTimeout & Chr$(13) & Chr$(10) varTemp = varTemp & "Current Diretory: " & CurDir & Chr$(13) & Chr$(10) varTemp = varTemp & "Access Directory: " & SysCmd(SYSCMD_ACCESSDIR) & Chr$(13) & Chr$(10) varTemp = varTemp & "Runtime Version: " & SysCmd(SYSCMD_RUNTIME) & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Path/Name: " & dbsCurrent.name & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Version: " & dbsCurrent.Version & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB CollatingOrder: " & dbsCurrent.CollatingOrder & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Connect: " & dbsCurrent.Connect & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB QueryTimeout: " & dbsCurrent.Querytimeout & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Transations: " & dbsCurrent.Transactions & Chr$(13) & Chr$(10) varTemp = varTemp & "CurrentDB Updatable: " & dbsCurrent.Updatable & Chr$(13) & Chr$(10) rstErrors![AdditionalInfo] = varTemp End If rstErrors.Update ' Close the objects rstErrors.Close dbsCurrent.Close End Sub
The following steps detail the use of the above code:
To implement the error handling procedures in this category, follow these steps:
Declarations for Error Handling procedures
This code contains the elements necessary for the error-handling procedures to work. The following constants and variables are defined:
Conclusion
Although developing robust applications can require a substantial amount of additional work, the end result is well worth it. End users base their impressions of your application on a number of factors, any number of which may cause problems. By applying the techniques outlined in this session, you should be able to create applications that avoid these problems.