Previous Page TOC Next Page


14 — Creating Multi-User Applications

by Michael Amundsen

In this chapter, you'll learn about issues you'll encounter when you design, build, and install Visual Basic applications in a multiple-user environment. The topic of creating multi-user applications is very broad, so I'll concentrate on a handful of issues in this chapter. The focus will be the following:

Before I get into the details of coding multi-user features into your Visual Basic applications, I first want to touch on some points concerning multi-user programming in general.

Looking at General Issues in Multi-User Programming

Designing and creating multi-user applications is more than just making sure that you add error handlers to trap for database update conflicts. Good multi-user applications are designed with the user in mind. Applications that are well-designed, multi-user programs enable users to modify the program environment to fit their needs and preferences. Good multi-user applications also are aware of the division between shared and localized resources. Some configuration data might be shared with all users (location of needed files, system-level control parameters, and so on). Some other configuration data might be local to the logged-in users (their security profile, the last project data they were working on, and so on). Still other configuration data might be local to the physical workstation (type of hardware available, location of non-shared resources on the workstation, and so on). Good multi-user applications take all these factors into account.

After you design your multi-user application, you need to distribute and install it. You must decide whether your application will operate only in a multi-user setting or be allowed to function as a stand-alone program. You also need to make sure your program can locate and use all the needed resources, such as printers, modems, and disk drives. Finally, you need to make sure your program can locate and use any shared or local configuration information that might be stored on the workstation or at the network level.

Designing for Multiple Users

Before you begin coding your multi-user application, you should take time to think about the various multi-user aspects of your program. Will your application use a set of centrally located files? If so, you need to provide pointers in your program that will identify the location of these files. If you hard code these values into your program, you'll have trouble when you install it somewhere else.

Typically, "pointer gathering" is done at the start of the program using INI or registry settings and internal variables. If your application opens a central database file, for example, you can load the location for an INI or registry setting into your internal variable.


NOTE

Throughout this chapter, I refer to INI settings instead of registry values. The examples in this chapter all use the Visual Basic 4.0 GetSetting and SaveSetting statements. In 32-bit environments, these statements modify the Windows registry file. In 16-bit environments, these statements modify named INI files. Throughout the rest of this chapter, you'll see the words "INI settings." This will refer to the values stored in the 16-bit INI files and the values stored in the 32-bit registry file.

Listing 14.1 shows a typical technique for loading data from an INI setting into an internal variable. This routine usually is executed at the start of the program to establish the basic environment for the application.


NOTE

Listing 14.1 assumes the existence of some global variables and some predefined functions that use the INI-related statements. This code is part of a sample program that is covered in the section "Writing User-Configurable Applications," later in this chapter.

Public Sub LoadINIVars()

    '

    ' read ini values into internal variables

    '

    ' attempt to access settings

    If OpenINI() = False Then

        Unload Me   ' oops!

    End If

    '

    cDatabase = GetIniStr("System", "Database", "c:\source\chap14\vbu1401.mdb")

    cLocalPrinter = GetIniStr("System", "LocalPrinter", "No")

    cLocalModem = GetIniStr("System", "LocalModem", "No")

    '

    ' get form size and location info

    '

    nFormWidth = GetIniStr("Forms", Me.Name + ".Width", "6800")

    nFormHeight = GetIniStr("Forms", Me.Name + ".Height", "4550")

    nFormLeft = GetIniStr("Forms", Me.Name + ".Left", "1200")

    nFormTop = GetIniStr("Forms", Me.Name + ".Top", "1300")

    '

    ' get confirmation flag

    gblConfirm = UCase(GetIniStr("system", "Confirm", "YES"))

    '

    ' get color set

    gblColorSet = UCase(GetIniStr("system", "ColorSet", "Default"))

    '

End Sub

If your software will be using other system resources, such as modems or printers, you need to make sure that your program can locate the resources and that it provides error handlers to take care of instances when these resources are not responding. It is usually a good idea to test for the presence of these shared resources at the start of your program. If you need a modem to perform communications tasks with your program, for example, you should attempt to open and initialize the modem at startup. If it is not responding, you can tell users about the problem before they enter a great deal of data and click the Send key.


NOTE

Throughout the chapter, the term Windows is used to refer to all current flavors of the Microsoft Windows operating system—including Windows 3.1, Windows for Workgroups, Windows NT, and Windows 95. In cases where differences in the various flavors of Windows systems are important, the specific Windows version is mentioned in the text. Otherwise, you can assume that the behavior or feature discussed works the same in all cases.

Lastly, your application should allow individual users to customize their program environment to fit their own preferences. Whenever possible, you should let users choose the colors and fonts they prefer. The resulting selections should be stored by the users so that each user can have his or her own set of preferences. The values typically are stored in a local INI file in the same directory that contains the Windows executable.

However, there might be cases where this is not an acceptable solution. There are times when some configuration data must be stored in a central location in order to give application administrators access to the data. In this case, the configuration data can be stored in an INI-type file in a central location. This INI-type file can contain header sections that identify the configuration data by user. The sample INI file shown in Listing 14.2 is an example of storing multiple user data in a single INI file.

[UserA.Settings]

LocalPrinter=Yes

ModemFound=No

ColorMonitor=Yes

[UserB.Settings]

LocalPrinter=No

ModemFound=Yes

ColorMonitor=No

Distributing and Installing Multiple-User Programs

When you finish coding your multi-user application, there are some additional items you should plan for when you distribute and install the program. If your application assumes the existence of INI files or other resources, you need to ship the application with a default INI file or add routines to your application that will create these INI files. The configuration file creation can be a special configuration applet or a part of the user program.

Using a Configuration Applet after Installation

If you must create files that will be used by all users, it usually is best to take care of this with a special configuration program that runs independently of the user application. A good example of this is a routine to create a set of new data tables at installation time. You can write a small applet that creates the database and then creates empty tables.

The advantage of creating files of this type on-site, instead of shipping them with the application, is that users now have a method for re-creating these files without having to perform a complete installation. Also, if users want to perform an installation to refresh some damaged programs, they don't need to worry about destroying their existing data at the same time.

Listing 14.3 shows how you might write a routine to create all your data tables. This routine can be part of an initial configuration program that is run immediately after installation.

Public Sub NewData()

    '

    ' create a new database

    '

    Dim dbFile As Database

    Dim cDBFile As String

    Dim cTable1 As String

    Dim cTable2 As String

    Dim nTemp As Integer

    '

    ' set vars

    cDBFile = "c:\source\chap14\ch1401.mdb"

    cTable1 = "CREATE TABLE Table1 (CustID TEXT(10),CustName TEXT(30),CustType

    _TEXT(10));"

    cTable2 = "CREATE TABLE Table2 (CustType TEXT(10),TypeName TEXT(20));"

    '

    ' kill any current database

    nTemp = MsgBox("Ready to Delete Any Existing Database?", vbInformation +

    _vbYesNo, "Create Database")

    If nTemp = vbNo Then

        MsgBox "Create Database Canceled"

    Else

        On Error Resume Next

        Kill cDBFile

        On Error GoTo 0

        '

        ' create empty DB

        Set dbFile = DBEngine.CreateDatabase(cDBFile, dbLangGeneral)

        '

        ' create tables

        dbFile.Execute cTable1

        dbFile.Execute cTable2

        '

        ' add additional tables, indexes, relations, etc.

        '

        MsgBox "Database has been Created"

    End If

End Sub
Creating User Configuration Information within the Program

For configuration data that will be specific to each user, it usually is a good idea to add configuration routines to the user program. When the program is run for the first time at a workstation, these routines should create a configuration INI file that contains default values. The user then can modify these values at any time to customize the application. Because this configuration file is local, any changes made by one user will not affect other users at other workstations.

A simple method for incorporating auto-generation of local configuration files is to automatically create and populate the local INI file at initial startup. You can store default values for all INI settings in your program or in an ASCII file that you ship with the installation. These default values can be used at startup to create any missing settings. After the settings have been created, users should be able to use their version of the program to modify the settings they want.

Listing 14.4 shows how to write an INI-file read function that automatically creates missing entries by checking the return value from the GetSettings statement. If the return value is the same as the default value, the routine assumes that Windows could not find the INI setting and immediately calls the INI-file write function.


NOTE

This routine assumes that the proper user-defined functions and global variables exist. This code is taken from an example program reviewed in the section "Writing User-Configurable Applications," later in this chapter.

Function GetIniStr(cSection As String, ByVal cItem As String, ByVal cDefault As 

_String)

    '

    ' this routine looks for cItem

    ' in section cSection. If it

    ' is not found, the item is

    ' written using the cDefault

    ' value.

    '

    Dim cTemp As String

    '

    ' try to get it

    cTemp = GetSetting(gblIniFile, cSection, cItem, cDefault)

    '

    ' returned default? may be missing, write it to be sure

    If Trim(UCase(cTemp)) = Trim(UCase(cDefault)) Then

        SaveSetting gblIniFile, cSection, cItem, cDefault

    End If

    '

    GetIniStr = cTemp   ' set return value

    '

End Function

Looking at Local versus Shared Resources

Some portions of a multi-user application must be shared among all users, such as central database files. Other portions, such as the local user preference INI file, must exist on the local workstation. Still other parts of an application, such as common DLL files or other program modules, can exist in either location. You should consider the advantages and disadvantages when determining the final placement of shared application resources.

Sharing Program Code from a Network Device

Placing shared resources in a central location makes it easy to maintain an application and can reduce the total amount of storage resources occupied by your application. Suppose that your application has a total of 1.5 MB of executable modules (EXEs and DLLs). You need to roll out this application to 50 users on the same network server. If you place a single copy of the application on a shared network disk, your total program storage is only 1.5 MB. If you store the program at each workstation, however, you'll be using 75 MB of disk space for the same program.

Although saving space is nice, there is the matter of program performance to consider, too. Sharing an application over a networked disk can dramatically decrease overall performance—not just for your application, but for the entire network. If your program makes numerous calls to executables stored on the server, server traffic can skyrocket, causing even unrelated programs to become bogged down.

In situations where the installation requires that your program work in a diskless workstation environment, you have no choice but to locate your programs on the remote disk. However, because of the potential performance drain, it is recommended that you install all executables at the workstation level.

Storing DLLs and OCX/VBX Files in the Windows Directory

Whether you are working in a diskless environment or at a powerful desktop workstation, you always should store any DLL, OCX, and VBX files in the \WINDOWS\SYSTEM directory.


NOTE

It is possible that you will have workstations that do not have a \WINDOWS\SYSTEM directory. Users can use any name they want when they install the Windows operating system. You can use the GetWinDir API to determine the actual name of the Windows launch directory and then locate the SYSTEM folder below that. To simplify the text, this chapter refers to the \WINDOWS\SYSTEM directory or the \WINDOWS directory. Just remember that some workstations might use other names for these locations.

In fact, if you do not store this information in the \WINDOWS\SYSTEM directory, you might run into serious problems. Windows assumes that the needed DLLs and other components will be stored in the SYSTEM directory; it looks there first. If the needed item is not found in the SYSTEM directory, the directory that contains the EXE file that is currently running is checked next. If you store your components (DLLs and so on) in the same directory as the EXE file, youll be okay as long as no one installs a component with the same name (possibly a different version) in the SYSTEM directory. If this does happen, you'll spend hours trying to figure out why your program is acting weird or hanging up entirely.

Always store DLLs, OCXs, VBXs, and other similar components in the user's \WINDOW\SYSTEM directory. This greatly reduces the possibility of encountering unexplained and difficult-to-diagnose fatal errors in your multi-user applications.

Writing User-Configurable Applications

Writing programs that can be configured for user preferences is an important part of creating any multi-user application. Users should be able to configure their own colors and fonts, arrange and size forms when appropriate, and control some aspects of program behavior (skip confirmations on deletes, display or hide a toolbar on the main form, and so on).

Most of this type of information can be stored in local INI-type configuration files. In the next several sections, we'll cover the use of INI files and explore several examples of user-configurable options you should consider for your multi-user programs.

Using INI Files to Store User Configuration Information

Multi-user applications need to allow for individual customization of selected aspects of the program. In the 16-bit Windows environment (Windows for WorkGroups and Windows 3.1), details about these customizations can be stored in a related INI file in the user's \WINDOWS directory. In the 32-bit Windows environment (Windows 95 and Windows NT), this information is stored in the registry file. It is easy to read and write INI data using Visual Basic 4.0 GetSettings and SaveSettings statements. In some cases, you will also need the GetWindowsDirectory API call. These can form the basis of a valuable set of library routines you can use in all your Visual Basic programs.


NOTE

This section shows how to store configuration information using the GetSettings and SaveSettings statements of Visual Basic 4.0. In 16-bit systems, these Visual Basic statements will modify an associated INI file in the Windows launch directory. In 32-bit systems, these statements will modify associated registry settings in the HKEY_CURRENT_USERS\Software\VB and VBA Program Settings area. If you plan to store shared settings in a 32-bit environment, you should not use the GetSettings and SaveSettings statements because these will always modify the Windows registry on the local workstation.

Listing 14.5 shows a short set of routines that make up a library file you can use in any Visual Basic program to read and write INI entries.


NOTE

This code is used in several code examples throughout this chapter. A completed version of this code library can be found in the \source\chap14\ directory of the disc included with this book.

Open a new project in Visual Basic, add a BAS module to the project, and enter the code in Listing 14.5 in the declaration section of the module.

Option Explicit

'

' *** ini api declarations

'

#If Win16 Then

Declare Function OSGetWindowsDirectory% Lib "Kernel" Alias "GetWindowsDirectory"

_(ByVal a$, ByVal b%)

#End If

'

#If Win32 Then

Declare Function OSGetWindowsDirectory Lib "kernel32" Alias "GetWindowsDirectoryA"

_(ByVal lpBuffer As String, ByVal nSize As Long) As Long

#End If

'

Global gblIniFile As String ' name of INI file

Global gblWinDir As String  ' name of Windows launch directory

Now add the function that will return the Windows launch directory. Create a new function called GetWinDir and enter the code shown in Listing 14.6.

Function GetWinDir() As String

    '

    ' get the Windows launch dir

    '

    Dim cTemp As String

    Dim nTemp As Integer

    '

    cTemp = String$(255, 32)    ' reserve space

    nTemp = OSGetWindowsDirectory(cTemp, 255) ' call API

    GetWinDir = Left$(cTemp, nTemp) ' set return value

End Function

Now create a new function called WriteINIStr and enter the code shown in Listing 14.7.

Function WriteINIStr(ByVal cSection, ByVal cItem, ByVal cDefault) As Integer

    '

    ' write cKeyName into cSection

    SaveSetting gblIniFile, cSection, cItem, cDefault

    WriteINIStr = True  ' in case they ask!

    '

End Function

Now, create a new function called GetINIStr and enter the code shown in Listing 14.8.

Function GetIniStr(cSection As String, ByVal cItem As String, ByVal cDefault As

_String)

    '

    ' this routine looks for cItem

    ' in section cSection. If it

    ' is not found, the item is

    ' written using the cDefault

    ' value.

    '

    Dim cTemp As String

    '

    ' try to get it

    cTemp = GetSetting(gblIniFile, cSection, cItem, cDefault)

    '

    ' returned default? may be missing, write it to be sure

    If Trim(UCase(cTemp)) = Trim(UCase(cDefault)) Then

        SaveSetting gblIniFile, cSection, cItem, cDefault

    End If

    '

    GetIniStr = cTemp   ' set return value

    '

End Function

Notice that this routine does more than just read an INI entry. If the entry is determined to be missing, this routine automatically adds that entry, using the default value supplied. This makes it very easy to automatically create fully populated INI files when a user first starts up the newly installed application.

We need to add one more routine to our library. This routine will handle differences between the 16-bit and 32-bit versions of Windows. In 16-bit environments, all settings are stored in an ASCII text file. We will need to make sure that file is present and available to our 16-bit application. In the 32-bit environment, this routine just confirms the name of the registry key we will use.

Create a new function called OpenINI and add the following code, as in Listing 14.9.

Public Function OpenINI(Optional cININame As Variant)

    '

    ' For 32-bit

    '   set gblIniFile name for registry calls

    '

    ' For 16-bit

    '   set gblIniFile name for File calls

    '   if not there, create a new one

    '

    OpenINI = True  ' assume all goes OK

    '

    If IsMissing(cININame) Then ' no parm passed?

        cININame = App.EXEName  ' set to this app

    End If

    gblIniFile = Trim(cININame) ' clean up spaces

    '

    Dim nFile As Integer        ' for file channel

    Dim cFile As String         ' for file name

    '

    ' If we're running in 16-bit environment

    ' we have to get to a file in the Windows

    ' launch directory. We'll make sure it's

    ' there and create a new one if needed.

    '

    #If Win16 Then

        On Error Resume Next    ' keep quiet about errors

        nFile = FreeFile        ' get free file channel

        cFile = gblIniFile + ".INI" ' fill out file name

        Open cFile For Input As nFile   ' try to open it

        If Err <> 0 Then        ' some error

            If Err = 53 Then    ' was it 'FileNotFound?'

                Open cFile For Output As nFile  ' create it

            Else    ' uh-oh....

                MsgBox Error$, vbCritical, "Error opening INI File - " + Str(Err)

                OpenINI = False ' we failed!

            End If

        End If

        Close nFile 'close channel

    #End If

    '

End Function

Save this module with the name libINI.bas. Now let's add a bit of code to the main form of the project to show how the INI library works.

First add the form-level variables shown in Listing 14.10 to the form.

Option Explicit

'

' internal vars for INI values

'

Dim cDatabase As String

Dim cLocalPrinter As String

Dim cLocalModem As String

Next, create a subprocedure called LoadINIVars and add the code shown in Listing 14.11.

Public Sub LoadINIVars()

    '

    ' read ini values into internal variables

    '

    ' attempt to access settings

    If OpenINI() = False Then

        Unload Me   ' oops!

    End If

    '

    cDatabase = GetIniStr("System", "Database", "c:\source\chap14\vbu1401.mdb")

    cLocalPrinter = GetIniStr("System", "LocalPrinter", "No")

    cLocalModem = GetIniStr("System", "LocalModem", "No")

    '

    ' get form size and location info

    '

    nFormWidth = GetIniStr("Forms", Me.Name + ".Width", "6800")

    nFormHeight = GetIniStr("Forms", Me.Name + ".Height", "4550")

    nFormLeft = GetIniStr("Forms", Me.Name + ".Left", "1200")

    nFormTop = GetIniStr("Forms", Me.Name + ".Top", "1300")

    '

    ' get confirmation flag

    gblConfirm = UCase(GetIniStr("system", "Confirm", "YES"))

    '

    ' get color set

    gblColorSet = UCase(GetIniStr("system", "ColorSet", "Default"))

    '

End Sub

This routine attempts to open the INI file (16-bit only). If it is not found (and a new file couldn't be created), an error is returned and the program is halted. If OpenINI returns TRUE, the routine attempts to read three INI settings in the file. Because of the behavior of your GetINIStr function, if these settings are missing from the INI file, they will be created with the default values.

Now add one more routine to the form. This will display the INI readings captured in the LoadINIVars routine. Place the code shown in Listing 14.12 in the Form_Activate event.

Private Sub Form_Activate()

    LoadINIVars ' read INI stuff

    Me.Cls

    Me.Print "gblIniFile="; gblIniFile

    Me.Print "cDatabase="; cDatabase

    Me.Print "cLocalPrinter="; cLocalPrinter

    Me.Print "cLocalModem="; cLocalModem

    '

End Sub

This routine first calls the INI read routine, and then displays the results on the form. Save this form as vbu1401.frm and save the project as vbu1401.vbp. Now run the project and view the results. Your screen should look like the screen shown in Figure 14.1.


FIGURE 14.1. Displaying INI file results.

If you are running the 16-bit version of Visual Basic, you now have a new INI file in your \WINDOWS directory called vbu1401.ini. Use Notepad or some other ASCII editor to load and view this file. It should look like Listing 14.12.

If you are running the 32-bit version of Visual Basic, you now have a new set of entries in the Windows registry. Use RegEdit.EXE to open the registry and look under the key "HKEY_CURRENT_USERS\Software\VB and VBA Program Settings\vbu1401." You'll see the same values shown in Listing 14.13.

[System]

Database=c:\source\chap14\vbu14.mdb

LocalPrinter=No

LocalModem=No

Adjusting Window Placement and Size

A common user preference is the capability to remember the size and placement of program windows within the application. It always is annoying to have to continually rearrange windows on a screen each time you start the program. Listing 14.13 shows how this information can be stored and recalled the next time the program is started. Because this data is stored in the local INI file, multiple users can arrange their forms to their liking without affecting the forms arrangement for other users of the same application.

Add the form-level variables shown in Listing 14.14 to the declaration section of the form.

'

' form size/location

Dim nFormWidth As Integer

Dim nFormHeight As Integer

Dim nFormLeft As Integer

Dim nFormTop As Integer

Now add the code shown in Listing 14.15, which will read the new INI settings at the end of the LoadINIVars routine.

    '

    ' get form size and location info

    '

    nFormWidth = GetIniStr("Forms", Me.Name + ".Width", "6800")

    nFormHeight = GetIniStr("Forms", Me.Name + ".Height", "4550")

    nFormLeft = GetIniStr("Forms", Me.Name + ".Left", "1200")

    nFormTop = GetIniStr("Forms", Me.Name + ".Top", "1300")

    '

Notice that you are using the form name as part of the INI setting. This means that you can use this code to store different forms without changing your code.


WARNING

This code will not work properly if you are using multiple instances of the same form. Each of the instances will have the same name. Storing the form information using this routine would result in only one set of form values. You should consider modifying this routine to check for multi-instanced forms.

Now add the code that will use the variables to update the form properties. Add the lines shown in Listing 14.16 to the Form_Activate routine. Insert these lines right after the LoadINIVars line and before the Me.Cls line.

    '

    ' resize based on INI settings

    '

    Me.Left = nFormLeft

    Me.Width = nFormWidth

    Me.Height = nFormHeight

    Me.Top = nFormTop

Finally, add the code that will update the INI file with the current form properties (see Listing 14.17). You'll do this in the Form_Unload event.

Private Sub Form_Unload(Cancel As Integer)

    '

    ' store form size & location

    '

    Dim lTemp As Long

    Dim cForm As String

    '

    cForm = Me.Name

    '

    lTemp = WriteINIStr("Forms", cForm + ".Top", Str(Me.Top))

    lTemp = WriteINIStr("Forms", cForm + ".Left", Str(Me.Left))

    lTemp = WriteINIStr("Forms", cForm + ".Width", Str(Me.Width))

    lTemp = WriteINIStr("Forms", cForm + ".Height", Str(Me.Height))

    '

End Sub

Now save this form and run it. The first time it runs, you'll see no change in the form's size or location. Now use your mouse to resize and move the form, and then close the form (use the standard form buttons, not the Visual Basic End button). Now restart the form. This time, the form is sized and located in the same way you left it (see Figure 14.2).


FIGURE 14.2. Recalling the resized form.

This example performs the resizing and relocating within the Form_Activate event. To keep the code simple, you usually should perform this task in the Form_Load event so that the user will not see the form move or resize.

Now use Notepad to load and view the modified vbu1401.ini file or registry entries. Your values for these settings may differ from the ones shown in Listing 14.18.

[System]

Database=c:\source\chap14\vbu14.mdb

LocalPrinter=No

LocalModem=No

[Forms]

Form1.Width= 3210

Form1.Height= 3255

Form1.Left= 2715

Form1.Top= 1485

Customizing Colors

One of the most common user customizations is the capability to alter the colors displayed on the user's monitor. Providing this level of customization is essential for any good multi-user application. It also is a fairly straightforward task if you follow a short list of guidelines.

Using System Colors

The simplest way to provide users with the capability to customize the colors used within your program is to make sure your program uses the current Windows color scheme for all forms and controls. That way, whenever users change their Windows colors, your program follows suit. You can use a set of defined constants to get the current Windows color settings. Table 14.1 lists these defined constants, their values, and descriptions of what the constants represent in the Windows color scheme.

Constant


Value


Description


vbScrollBars

0x80000000

Scroll bar color

vbDesktop

0x80000001

Desktop color

vbActiveTitleBar

0x80000002

Color of title bar for active window

vbInactiveTitleBar

0x80000003

Color of title bar for inactive window

vbMenuBar

0x80000004

Menu background color

vbWindowBackground

0x80000005

Window background color

vbWindowFrame

0x80000006

Window frame color

vbMenuText

0x80000007

Color of text on menus

vbWindowText

0x80000008

Color of text in windows

vbTitleBarText

0x80000009

Color of text in caption, size box, and scroll arrow

vbActiveBorder

0x8000000A

Border color of active window

vbInactiveBorder

0x8000000B

Border color of inactive window

vbApplicationWorkspace

0x8000000C

Background color of multiple-document interface (MDI) applications

vbHighlight

0x8000000D

Background color of items selected in a control

vbHighlightText

0x8000000E

Text color of items selected in a control

vbButtonFace

0x8000000F

Color of shading on the face of command buttons

vbButtonShadow

0x80000010

Color of shading on the edge of command buttons

vbGrayText

0x80000011

Grayed (disabled) text

vbButtonText

0x80000012

Text color on pushbuttons

vbInactiveCaptionText

0x80000013

Color of text in an inactive caption

vb3Dhighlight

0x80000014

Highlight color for 3D display elements

vb3DDKShadow

0x80000015

Darkest shadow color for 3D display elements

vb3Dlight

0x80000016

Second lightest of the 3D colors after vb3DHighlight

vbInfoText

0x80000017

Color of text in ToolTips

vbInfoBackground

0x80000018

Background color of ToolTips

The values in Table 14.1 that you will most commonly use as the core of your color scheme are vbWindowBackground and vbWindowText for standard form background and form foreground values, respectively. These also can be used for standard control background and control foreground. You can use the vbApplicationWorkspace for the background color of your MDI forms. Use the vbHighlight and vbHightlighText values for any controls or text that should be highlighted (active input controls, selected values in a listbox, and so on). You can use any of the color values you want in your Visual Basic applications, but be sure you are not confusing the user by misapplying the color used by Windows (don't use the disabled text color (vbGrayText) for all your active input controls, for example).

Listing 14.19 shows how you can use the system color constants to set the colors for your forms. Create a new subprocedure in the vbu1401.vbp project and add this code.

Public Sub LoadSysColors()

    '

    ' load the colors from the current

    ' Windows color scheme

    '

    Dim ctlTemp As Control

    '

    ' set colors for all controls on form

    On Error Resume Next

    For Each ctlTemp In Me.Controls

        ctlTemp.BackColor = vbWindowBackground

        ctlTemp.ForeColor = vbWindowText

    Next

    On Error GoTo 0

    '

    ' set colors for form itself

    Me.BackColor = vbApplicationWorkspace

    Me.ForeColor = vbWindowText

    '

End Sub

The code in Listing 14.18 loops through all the controls on the form and attempts to set their foreground and background colors to match the standard colors for the current color scheme. Notice the On Error Resume Next and On Error Goto 0 statements. This is done to prevent error messages from occurring if you try to set colors for controls that have no ForeColor or BackColor properties. Because the idea is to have your application change whenever the Windows color scheme changes, you do not save the colors setting in the user's INI file.

Add a line to the Form_Load event that calls the LoadsysColors routine. Place this line right after the LoadINIVars line. Now save and run the project to see the slightly different color scheme you now have for the sample program.

Using Custom Colors

If you want to give users the capability to set and retain their own color values, you need to provide an input form for them to select their desired colors. In our example, we will stick to the standard foreground and background colors for the input and form controls. Use Figure 14.3 as a guide in laying out new controls on the existing form.


FIGURE 14.3. Laying out the custom color form.

Add a single command button to the form. Set its Name property to cmdColor and then use Copy and Paste to create a command button array with four members (0..3). After adding two text controls and two label controls, add a single common dialog control. You'll use that to call up the Windows colors dialog box.

First add the form-level variables shown in Listing 14.20 to the declaration section of the default form.

'

' colors

'

Dim lUserColor(4) As Long

Const vbuFormBG = 1

Const vbuFormFG = 2

Const vbuControlBG = 3

Const vbuControlFG = 4

Now add the code shown in Listing 14.21 to the cmdColor_Click event.

Private Sub cmdColor_Click(Index As Integer)

    '

    ' handle user color settings

    '

Select Case Index

        Case 0

            ' form back color

            CommonDialog1.DialogTitle = "Select Form Background Color"

            CommonDialog1.ShowColor

            lUserColor(Index + 1) = CommonDialog1.Color

        Case 1

            ' form fore color

            CommonDialog1.DialogTitle = "Select Form Foreground Color"

            CommonDialog1.ShowColor

            lUserColor(Index + 1) = CommonDialog1.Color

        Case 2

            ' control back color

            CommonDialog1.DialogTitle = "Select Control Background Color"

            CommonDialog1.ShowColor

            lUserColor(Index + 1) = CommonDialog1.Color

        Case 3

            ' control fore color

            CommonDialog1.DialogTitle = "Select Control Foreground Color"

            CommonDialog1.ShowColor

            lUserColor(Index + 1) = CommonDialog1.Color

    End Select

    '

    ' set object colors

    SetUserColors

    '

End Sub

Listing 14.21 calls the Color dialog box for each designated color value. The returned color is stored in the lUserColor array, and then the objects are updated with the current color settings.

Now add the routine that actually uses the values set by the user to set the Color properties. Create a new subprocedure called SetUserColors and add the code shown in Listing 14.22.

Public Sub SetUserColors()

    '

    ' set the form and controls

    ' to the selected colors

    '

    Dim ctlTemp As Control

    '

    ' first the form

    Me.BackColor = lUserColor(vbuFormBG)

    Me.ForeColor = lUserColor(vbuFormFG)

    '

    ' now all controls

    On Error Resume Next

    For Each ctlTemp In Form.Controls

        ctlTemp.BackColor = lUserColor(vbuControlBG)

        ctlTemp.ForeColor = lUserColor(vbuControlFG)

    Next

    On Error GoTo 0

    '

End Sub

Now add the code to the Form_Unload event that will write these values out to the INI file (see Listing 14.23).

    lTemp = WriteINIStr("forms", cForm + ".formBG", Str(lUserColor(lFormBG)))

    lTemp = WriteINIStr("forms", cForm + ".formFG", Str(lUserColor(lFormFG)))

    lTemp = WriteINIStr("forms", cForm + ".controlBG", Str(lUserColor(lControlBG)))

    lTemp = WriteINIStr("forms", cForm + ".controlFG", Str(lUserColor(lControlFG)))

    '

And finally, add the code that reads the INI settings at startup and sets the initial color values (see Listing 14.24).

Public Sub LoadUserColors()

    '

    ' load colors from ini file

    '

    Dim cTemp As String

    '

    cTemp = GetIniStr("Forms", Me.Name + ".formBG", Str(Me.BackColor))

    lUserColor(lFormBG) = Val(cTemp)

    '

    cTemp = GetIniStr("Forms", Me.Name + ".formFG", Str(Me.ForeColor))

    lUserColor(lFormFG) = Val(cTemp)

    '

    cTemp = GetIniStr("Forms", Me.Name + ".controlBG", Str(Text1.BackColor))

    lUserColor(lControlBG) = Val(cTemp)

    '

    cTemp = GetIniStr("Forms", Me.Name + ".controlFG", Str(Text1.ForeColor))

    lUserColor(lControlFG) = Val(cTemp)

    '

    SetUserColors

End Sub

Add the following line to the Form_Activate event right after the LoadSysColors line. This forces the user colors onto the form at startup:

LoadUserColors

Now save and run the vbu1401.vbp project. The first time you run this, you won't see any difference in the color scheme. After you use the command buttons to select your colors, exit the form (don't use the Visual Basic Stop button). The next time you start the program, you'll see the colors you selected instead of the original colors.

Controlling Program Behavior Options

Another way to make sure your multi-user applications are able to handle individual user configuration is to allow users to control some aspects of the program behavior. Advanced users might not need to see a confirmation message each time they attempt to update a value on-screen, for example, but novice users might like the feature. You can make this a user-configurable feature by adding a few lines of code and a new INI setting.

The code shown in Listing 14.25 adds a confirmation dialog box to the color setting command buttons. Add this code to the cmdColor_Click event. This code replaces the single SetUserColors line that is already in the routine.

    '

    ' check for confirmation first

    If gblConfirm = "YES" Then

        nTemp = MsgBox("Update Current Color Scheme?", vbInformation + vbYesNo,

                _"Color Configuration")

    Else

        nTemp = vbYes

    End If

    '

    ' if ok, then update colors

    If nTemp = vbYes Then

        SetUserColors   ' set colors

    End If

    '

This code first checks to see whether a confirmation message should be sent, and then acts accordingly. Be sure to add a line at the top of the routine to initialize the nTemp variable as an integer:

dim nTemp as integer

You also need to add a new line to the declaration section:

dim gblConfirm as string

Now add the following lines to the LoadINIVars routine:

    '

    ' get confirmation flag

    gblConfirm = UCase(GetIniStr("system", "Confirm", "YES"))

    '

Now save and run the project. When you attempt to change a color, you'll see a confirmation message (see Figure 14.4).


FIGURE 14.4. Displaying the optional confirmation message.

Load the vbu1401.ini file into Notepad, change the Confirm setting in the [System] section to No, and rerun the program. Now you will not see any confirmation when you attempt to change colors.

Usually applications provide check-off menu items to toggle these types of features on and off. The following listings add this menu feature to the current project. At the same time, the examples show another recommended feature for your user-configurable applications. The Preferences menu enables users to restore the original color scheme. This is highly recommended because users often will create unusable color schemes and will want to immediately restore the default values to clear out the unwanted selections.

Add one more line to the declarations section of the form. Place this line in the colors section of the listing:

Dim gblColorSet As String

Now create a subprocedure called LoadDefaultColors and enter the code shown in Listing 14.26. This routine resets the color variables to the initial startup values.

Public Sub LoadDefaultColors()

    '

    ' load the original color set

    '

    lUserColor(vbuFormBG) = &H8000000F

    lUserColor(vbuFormFG) = &H80000012

    lUserColor(vbuControlBG) = &H80000005

    lUserColor(vbuControlFG) = &H80000008

    '

    SetUserColors

End Sub

Now add the code that will read the new INI setting to the LoadINIVars routine:

    '

    ' get color set

    gblColorSet = UCase(GetIniStr("system", "ColorSet", "Default"))

    '

Then add the code to the Form_UnLoad event that will save the new INI value (see Listing 14.27). Also, wrap the lines that store the form color INI settings in an If...Then...End If structure. You only want to save the colors if they are the User settings.

If gblColorSet = "USER" Then

        lTemp = WriteINIStr("forms", cForm + ".formBG", Str(lUserColor(vbuFormBG)))

        lTemp = WriteINIStr("forms", cForm + ".formFG", Str(lUserColor(vbuFormFG)))

        lTemp = WriteINIStr("forms", cForm + ".controlBG",

        _Str(lUserColor(vbuControlBG)))

        lTemp = WriteINIStr("forms", cForm + ".controlFG",

        _Str(lUserColor(vbuControlFG)))

    End If

    '

    lTemp = WriteINIStr("System", "ColorSet", gblColorSet)

Now add the menu to the project. Use Table 14.2 as a guide.

Caption


Menu


&File

mnuFile

E&xit

mnuFileExit

&Preferences

mnuPref

&Default Colors

mnuPrefDefaultColor

&System Colors

mnuPrefSysColor

&UserColors

mnuPrefUserColor

Now add the code shown in Listing 14.28 for each of the menu items.

Private Sub mnuPrefDefaultColor_Click()

    '

    mnuPrefUserColor.Checked = False

    mnuPrefSysColor.Checked = False

    mnuprefdefaultcolor.Checked = True

    gblColorSet = "DEFAULT"

    LoadDefaultColors

    '

End Sub

Private Sub mnuPrefSysColor_Click()

    '

    mnuprefdefaultcolor.Checked = False

    mnuPrefUserColor.Checked = False

    mnuPrefSysColor.Checked = True

    gblColorSet = "SYSTEM"

    LoadSysColors

    '

End Sub

Private Sub mnuPrefUserColor_Click()

    '

    mnuprefdefaultcolor.Checked = False

    mnuPrefSysColor.Checked = False

    mnuPrefUserColor.Checked = True

    gblColorSet = "USER"

    LoadUserColors

    '

End Sub

Private Sub mnuFileExit_Click()

    Unload Me

End Sub

These routines set the proper menu check item, update the color INI variable, and update the color set itself. Now you need to modify the Form_Activate routine to initialize the proper color scheme at startup. Replace the calls to LoadSysColors and LoadUserColors with the lines of code shown in Listing 20.29.

    '

    Select Case gblColorSet

        Case "DEFAULT"

            mnuPrefDefaultColor_Click

        Case "SYSTEM"

            mnuPrefSysColor_Click

        Case "USER"

            mnuPrefUserColor_Click

     End Select

    '

Now the program will read the color set flag to determine the color set to use. If the color set flag is set to USER, the routine loads the user values from the INI file and updates the form.

Save and run the project. When you load the project, the default color scheme appears. You can change that by selecting a new scheme from the menu. You still can update the user color set and save that, too.

These are just a few examples of how you can design your application to allow multiple users to have their own customized version of your Visual Basic program. You can expand on each of the examples here to improve their flexibility and increase the number of options and features you allow your users to customize.

Examining Microsoft Jet Database Locking Issues

Probably the most frequently talked about multi-user programming issue for Visual Basic (or any other language) is that of database locking. Although all database programmers must deal with this issue in some fashion, Visual Basic programmers have the somewhat dubious honor of having to deal with database locking in a rather unique environment. The Microsoft Jet database engine provides three levels of locking within databases:

Those familiar with the common PC database packages (dBASE, FoxPro, Paradox, and so on) will notice that an important additional locking level is missing from this list: record-level locking. The Microsoft Jet database engine does not provide record-level locking for the native database format (Microsoft Access MDB-type databases) and only provides record-level locking when the Microsoft Jet is used to directly open compatible non-Microsoft Jet databases, such as FoxPro, Paradox, and so on.


NOTE

It is important to note that the Microsoft Jet database engine is not the only Microsoft data engine that currently lacks record-level locking. The most current version of Microsoft SQL Server as of this writing (6.0) is also a page-locking database engine.

In this section, you'll review the three locking levels provided by Microsoft Jet, focusing more time on the issues related to page-level locking. This chapter also contains three other techniques for reducing the occurrence of locking conflicts and speeding the processing of Visual Basic database programs. These other techniques include the use of Visual Basic Data Access Object Relation objects to improve cross-table integrity and locking, the use of a manual lock table to reduce unwanted lock conflicts when updating databases, and a method of employing temporary local batch input tables to speed data entry and reduce the need for constant access to busy database tables.

You May Not Need to Lock It

Although it is essential to understand how Microsoft Jet locking behavior affects your multi-user applications, it is even more important to know when locking is an appropriate way to solve your programming problems. Typically, locking methods are used whenever database updates (adds, edits, or deletes) occur. In these cases, the locking produces a static condition in the database until the update is completed. There are other times when your Visual Basic program might need to ensure a static database to properly perform critical operations, such as totals calculations or lengthy report production.

Although some of these problems can be solved by locking the required tables or the entire database during the operation, it is not always necessary to do so. Time-sensitive calculations and reporting can be performed on a private copy of the needed data tables. These private tables can be created at the moment the calculation or report is to be performed, and then destroyed at the end of the critical process.

Listing 14.30 shows how you can use table copies to increase integrity without using table locking when starting a lengthy set of reports for an accounting system.

Public Sub ProcMonthEndReports()

    '

    ' create copies of required tables

    '

    db.Execute "SELECT Master.* INTO MER_Master FROM Master;"

    db.Execute "SELECT Trans.* INTO MER_Trans FROM Trans;"

    db.Execute "SELECT Orders.* INTO MER_Orders FROM Orders;"

    '

    ' now run reports

    ReportMonthlyTrans' report transactions

    ReportMonthlyOrders' report orders

    '

    ' kill temporary tables

    db.Execute "DROP TABLE MER_Master;"

    db.Execute "DROP TABLE MER_Trans;"

    db.Execute "DROP TABLE MER_Orders;"

    '

End Sub

Using this technique often can reduce the number of database update conflicts you encounter in your program. It also can increase processing speed because users performing updates never experience any "hits" on their tables during the reporting process. Reports also can run faster for the same reason.

If you need to perform mass updates to database tables, you cannot use the copy method because you need to act on "live" data throughout the update process. In these cases, you should consider database or table-level locking for your solution.

High-Level Locking

High-level locking schemes available within the Microsoft Jet engine include locking at the database level and at the data table or recordset level. This locking level is handy when you need to perform mass updates on one or more tables and you want to reduce the chance of encountering update conflicts during the process.

Typically, database-level locking is used when your update operation will affect more than one table in the database. A good example of this kind of operation is an update that performs mass changes to the part numbers in an inventory database. It is likely that this part number appears in not only the master parts table, but also in pending order tables, return or reject item tables, and other control and report tables. Changing the part number in just one record can affect several tables within the database. If you have a busy application (a great deal of traffic and/or many users), it's quite likely that someone will be holding at least one record that contains the part number you are updating. If so, your entire update process could be halted unless you clear all users out of the system before you start the update.

Locking at the table or recordset level typically is used when your update operation will affect multiple records within the same table or recordset. A good example of this operation is the mass removal of all records for a specific customer from the history table of a revenue accounting system. Depending on the size of the data table, this operation could affect hundreds of records. You can reduce the chance that an operation of this type will fail by locking other users out of the table before you begin.

Database Locking

You should consider using database-level locking whenever you are about to perform a mass update (or delete) operation that will affect more than one table within the database. Keep in mind that the existence of enforced database integrity relation objects can widen the effects of even a simple single-record update. If you plan on deleting a single row from the master table of a database where you have well-defined and enforced relationships to several related transaction and history tables, for example, this single delete could affect tens of records in several tables within the database. If you plan on deleting several rows from the same master table, you easily could be up for deleting hundreds of records from the database.

To lock a database, you must open it for exclusive use. This prevents any other users from opening the database until you close the database completely. You can open a database for exclusive use by setting the Exclusive property of the data control to True or by using the optional Exclusive parameter in the OpenDatabase method (see Listing 14.31).

Private Sub Form_Load()

    Dim db As Database

    '

    ' open db exclusively

    Set db = WorkSpaces(0).OpenDatabase("c:\source\chap14\ch1402.mdb", True)

    MsgBox "Database Opened Successfully", vbInformation, "Open DB"

    '

End Sub

If you attempt to open a database exclusively and there is at least one other program that has the database open, you'll get an error 3356: Database is opened by user username on machine machinename (see Figure 14.5).

You may also get an error 3196, Couldn't use database name; database already in use, or an error 3045, Couldn't use name; file already in use.

You can add logic to your Visual Basic program that traps for these errors and tells users to attempt to open the database later. You should add this code to the startup of your application if you intend to use database-level locking anywhere in your program (see Listing 14.32).


FIGURE 14.5. Attempting to open a locked database.

Private Sub Form_Load()

    Dim db As Database

    '

    ' open db exclusively

    On Error GoTo FormLoadErr

    '

    Set db = OpenDatabase("c:\source\chap14\ch1402.mdb", True)

    MsgBox "Database Opened Successfully", vbInformation, "Open DB"

    '

    GoTo FormLoadExit

    '

FormLoadErr:

    If Err = 3356 or Err = 3196 or Err = 3045 Then

        MsgBox "Database Locked for Maintenance - Try again later", vbCritical,

        _"Open DB"

    Else

        MsgBox Error$, vbCritical, "Error " + Str(Err)

    End If

    Unload Me

    End

    '

FormLoadExit:

    '

End Sub

When you are using the Visual Basic data control to open the database with the Exclusive property set to True, you can trap for error 3196 by placing the error logic in the Error event of the data control, as shown in Listing 14.33.

Private Sub Data1_Error(DataErr As Integer, Response As Integer)

    If DataErr = 3356 or DataErr = 3196 or DataErr = 3045 Then

        MsgBox "Database Locked for Maintenance - Try again later", vbCritical,

        _"open DB"

    Else

        MsgBox Error$, vbCritical, "Error " + Str(Err)

        Unload Me

        End

    End If

End Sub
Table and Recordset Locking

Table- and recordset-level locking should be used whenever you plan to perform mass updates on a single table or recordset (see Listing 14.34). If you plan to update all the wholesale prices in the inventory file, for example, you should lock the inventory price table before you begin.

Public Sub UpdatePrices()

    '

    ' update all inventory wholesale prices

    '

    Dim dbFile As Database

    Dim rsTable As Recordset

    '

    ' open db

    Set dbFile = DBEngine.OpenDatabase("c:\source\chap14\ch1402.mdb")

    '

    ' open table exclusively

    Set rsTable = dbFile.OpenRecordset("InvPrice", dbOpenTable, dbDenyRead)

    '

    ' perform mass update

    '

    On Error Resume Next    ' ignore errors

    Workspaces(0).BeginTrans ' start trans tracking

    While Not rsTable.EOF   ' for every row

        rsTable.Edit        ' start edit

        rsTable.Fields("WholesalePrice") = rsTable.Fields("WholesalePrice") * 1.05

        rsTable.Update      ' end edit

    Wend                    ' get another

    If Err = 0 Then         ' no errors

        Workspaces(0).CommitTrans    ' final update

        MsgBox "Wholesale Prices Updated", vbInformation, "Inventory"

    Else                    ' trouble

        Workspaces(0).Rollback   ' undo all edits

        MsgBox "Wholesale Price Update Failed", vbCritical, "Error " + Str(Err)

    End If

    On Error GoTo 0         ' tell me about errors!

    '

    ' close and release lock

    dbFile.Close

End Sub

Notice the use of transaction processing in Listing 14.33. If any error occurs during the update, the entire process can be rolled back. The use of transaction keywords is highly recommended whenever you perform mass updates to databases.


TIP

You might have noticed that we used a While...Wend loop to perform the mass update instead of using a single SQL Update query like UPDATE InvPrice SET WholesalePrice = WholesalePrice * 1.05. The SQL line is easier to code and much faster than the program loop. However, if you open the table with a dbDenyRead option, Visual Basic will not let you Execute an update query against the same table.

You can open a table for exclusive use by using the Visual Basic data control, setting the Options property to 2 (dbDenyRead), and filling in the DataSource property with the name of the table you want to lock. This can be done at design time or at runtime with Visual Basic code.

The code example in Listing 14.35 sets the Options property of a data control in order to perform an exclusive table open.

 '

' set to open table exclusively

'

Data1.Options = 2

Data1.DatabaseName = c:\source\chap14\ch1402.mdb

Data1.RecordSource = InvPrice

If your Visual Basic program will perform any table locks, you should provide error-handling code in all your startup routines or any other location that attempts to open a table. You can trap for Error 3008, Table tablename is exclusively locked and Error 3009, Couldn't lock table name; currently in use. Add this code in the same module that contains the OpenRecordSet statement. If you are using the Visual Basic data control, add the error trapping logic to the Error event of the data control.

Locking at the recordset level affects all underlying tables used to make up the data set. If you have only one table as the source of the recordset, you will see the same effects you see when you perform a lock on a table object. However, if your recordset is the product of a complex query that contains JOINs, UNIONs, or subqueries, the lock could affect several tables at once.

For example, the query shown in Listing 14.36 contains a JOIN statement that combines two tables into a single query.

SELECT [Department Name],

FirstName & " " & LastName AS Name

FROM Departments LEFT JOIN Employees

ON Departments.[Department ID] = Employees.[Department ID]

ORDER BY [Department Name];

This query affects two tables (Departments and Employees). If this query were used to create a locked recordset, both tables would be locked to all users.

Although multi-table recordset locking is very useful when performing mass updates, it can be annoying in a multi-user environment. Because multiple tables are affected, it often is difficult to successfully open a locked recordset in a busy application. Also, once the recordset is locked, it is possible that many users will be adversely affected by the lock operation. This can lead to numerous access denials, attempted re-reads, and time-out conditions. All this might make users think your application is coming loose at the hinges! Because the effects of a recordset lock can be far-reaching, it is recommended that you use it sparingly, if at all. If you need to perform an update on a complex, multi-table recordset, consider performing a database lock before you begin the update process.

Low-Level Locking

The lowest level of locking provided by the Microsoft Jet database engine is at the page level. A page is always 2K in size. Page locking is automatic and cannot be turned on or off through Visual Basic code or via any property settings. Each time you attempt to save or edit a record, the Microsoft Jet automatically locks a page of data.

A data page can contain one or more records, depending on the size of the record. If the data record is 2K in size, each data page contains a single data record. If the data record is 200 bytes in size, then each data page contains 10 records. If a data record is larger than 2K, Microsoft Jet locks all the pages needed to lock an entire data record.

The Microsoft Jet database engine has two forms of low-level page locking: pessimistic (the default) and optimistic. Under pessimistic locking, data pages are locked as soon as a user begins to edit a record. Under optimistic locking, data pages are locked only at the moment a user attempts to save the updated record to the database. In both pessimistic and optimistic locking, the page lock stays active until the Edit operation is completed successfully or canceled by the user.

Page-Locking Option

You can set the page-locking option (pessimistic or optimistic) by setting the LockEdits property of the record set. When LockEdits = True, pessimistic locking is in place. When LockEdits = False, optimistic locking is in place. The code examples in Listing 14.37 show how this is done.

' setting page locking with the data control

Data1.Recordset.LockEdits = True ' pessimistic locking

Data2.Recordset.LockEdits = False ' optimistic locking

'

' setting page locking with Visual Basic code

rsObject1.LockEdits = True ' pessimistic locking

rsObject2.LockEdits = False ' optimistic locking

'
Pessimistic Locking

Pessimistic locking is the default page-locking method. Under the pessimistic locking scheme, pages are locked when the Edit method is invoked using Visual Basic code. If you are using the Visual Basic data control, the page is locked when the current record is selected using the reposition event. The lock stays in effect until the Visual Basic code Update or Cancel method is used or when the record pointer is advanced past the previously locked record using the data control.

Under pessimistic locking, the data pages stay locked the longest amount of time (from Edit to Update). Once USERA begins to edit a record, for example, that record cannot be edited by USERB until USERA commits an Udpate or Cancel operation to match the Edit that started the page lock. This ensures the greatest level of database integrity because both users cannot edit the same record at the same time.

The disadvantage of pessimistic locking is that the long locks increase the odds that users will encounter page-locking conflicts. This is especially true if your data tables have small record sizes. Because all locking is done in 2K increments, updates of small records result in page locks in several records at the same time. If a page contains twenty 100-byte records, for example, each time USERA edits record 1, USERB will not be able to edit records 1 through 100 (but USERB can edit record 101).

There are several possible errors that can occur within your Visual Basic program if you attempt to edit a record that is on a data page that is already locked. The most common errors you'll encounter are Error 3260 and Error 3197. Error 3260 occurs whenever you attempt to edit a record that is stored on a page that is already locked by another user or program. You can handle this error by attempting a read at another time. Error 3197 occurs when your program attempts to read a record that has been updated or deleted since you last refreshed the data set. You can handle this error by reloading the record into the data set (usually by invoking the Edit method again). Refer to Listing 14.38 as an example.

Public Sub PageLockErrors(LockErr As Integer)

    '

    ' handle errors due to page locks

    '

    Static nTries As Integer

    Dim nMaxTries As Integer

    Dim nLoop As Integer

    Dim nTemp As Integer

    '

    nMaxTries = 3

    '

    Select Case LockErr

        Case 3197

            ' data changed

            rsObject.Edit    ' re-read changed record

        Case 3260, 3816

            ' currently locked

            nTries = nTries + 1 ' try again

            If nTries > nMaxTries Then  ' too manytimes?

                nTemp = MsgBox(Error, vbRetryCancel, "Error " + Str(Err))

                If nTemp = vbRetry Then ' user said try again

                    nTries = 1  ' start all over

                End If

            End If

    End Select

    '

    DBEngine.Idle  ' free up old locks

    DoEvents            ' let messages catch up

    '

    For nLoop = 1 To 2000 ' 2000 millisecond pause

        ' empty loop

    Next nLoop

    Resume

    '

End Sub

Notice the use of the Idle and DoEvents methods in the error handler in Listing 14.38. These two lines allow Visual Basic and Windows to pass any backlogged messages to each other that may relate to old page locks that are no longer active.

The key to handling these errors is to trap for the error at the moment the user attempts the edit. If you are using the data control, you can place your error-handling logic in the Error event of the control. If you are using Visual Basic code, place the error handler in the same routine that invokes the Edit method (see Listing 14.39).

On error goto LocalErr

rsTable.Edit

'

' more code to edit record

'

rsTable.Update

'

Exit Sub

'

LocalErr:

Call PageLockErrors Err

If you are using the data control, you can place the same logic that is in the PageLockErrors routine directly into the Error event of the control, or you can call the error handler, passing the error code into the remote handler.

Optimistic Locking

Under the optimistic locking scheme, pages are locked only during the time the actual physical Update method is in operation. This is the shortest possible page-locking method. The advantage of this method is that the chance that two users will both perform updates on the same page at the same time is very small.

The main disadvantage of the optimistic locking scheme is that is it possible for two different users to edit the same record (not page) at the same time. This can result in locking errors at update time. If USERA begins to edit record 1 and then USERB begins to edit record 1, and then USERB saves record 1 back to the data set, USERA receives a locking error when attempting to save record 1 back to the data set. This is because USERB changed the data that USERA initially read; USERA has an outdated copy of record 1. The only way to remedy this situation is for USERA to abandon the update operation, read in USERB's version of the record, and start editing again (let's hope USERB doesn't do the same thing again!).

The most common locking errors received under optimistic locking are Errors 3186, 3260, and 3197. Error 3186 occurs when another user has locked the page you are attempting to update. Errors 3260 and 3197 were discussed in the preceding section, "Pessimistic Locking." Error 3186 should be handled in the same way 3260 is handled—clear any locks and try again.

Page Locks and Adding New Records

Regardless of which page-locking scheme you are using (pessimistic or optimistic), lock conflicts for adding new records always occur at the moment your program attempts to save the new record to the data set. This is the same scenario as updating an existing record under optimistic locking. Even though you might be using pessimistic locks for all existing records, you still will see update errors when users encounter page locks while adding new records.

If your application has several users entering data at the same time, you are likely to see many update errors. This is because all new records are added to the last data page of the data set. If you have five people entering new data, you have five people all attempting to lock the same data page. Also, the shorter the record, the more likely users are to see data page locks.

Sections later in the chapter show you a few techniques for reducing update conflicts.

Transactions and Locking

Page-level locking is affected by the use of the BeginTrans, CommitTrans, and RollBack methods in Visual Basic. When you are performing a transaction set, all page locks established during the transaction set are maintained from the time the BeginTrans method is invoked until the CommitTrans or Rollback method is invoked. This can lead to several page locks on one or more tables.

In Listing 14.40, every row of the data table is affected by the update within the transaction. This means that, in effect, you have created a table-level lock. If you attempt this while others have one or more pages locked, you will not be able to complete the transaction. Conversely, if you are successful, you will have locked the first page of the table for quite a long time (and the last page for a short time).

    WorkSpaces(0).BeginTrans ' start trans tracking

    While Not rsTable.EOF   ' for every row

        rsTable.Edit        ' start edit

        rsTable.Fields("Field1") = rsTable.Fields("Field1") * 1.05

        rsTable.Update      ' end edit

    Wend                    ' get another

    If Err = 0 Then         ' no errors

        WorkSpaces(0).CommitTrans    ' final update

        MsgBox "Table Updated", vbInformation

    Else                    ' trouble

        WorkSpaces(0).RollBack   ' undo all edits

        MsgBox "Table Update Failed", vbCritical, "Error " + Str(Err)

    End If

If you use nested transactions and/or transactions that span multiple tables, you must keep in mind that you will be placing and holding locks on all affected records until the CommitTrans or RollBack method is invoked.

Looking at Relation Objects and Locking

If you have a highly relational data design, your Microsoft Jet locking-scheme performance can be affected. If you have defined multiple relationships between tables within your database, even single-row updates can affect several rows in the database and require a number of locked pages in order to successfully complete the update operation. This can be both a blessing and a curse. It is a blessing to know that you can use Microsoft Jet to define and enforce multi-table update and delete integrity. It is a curse to know that you might be creating a page-locking demon when you build your database structure.

If you define enforced integrity relation objects within your database, you will be able to ensure that your update operations will cascade properly to all related tables within the database. This also points out the need to use transaction methods for all your critical database updates.

You can use the VisData sample application that ships with Visual Basic to create and maintain relation objects within your database (see Figure 14.6).


NOTE

You can find the VisData sample application in the Professional and Enterprise Editions of Visual Basic 4.0. It is stored in the "\sample\visdata" directory under the main Visual Basic install directory.


FIGURE 14.6. Using VisData to create a relation object.

You also can create and store relation objects using Visual Basic code. Listing 14.41 creates a new database, adds two tables with indexes, and then creates a relationship object. This particular relation object enforces cascading updates and deletes to make sure any changes to the master table are reflected in the related table.

Private Sub cmdRelation_Click()

    On Error Resume Next

    '

    Dim dbFile As DATABASE

    Dim tdTemp As TableDef

    Dim idxTemp As Index

    Dim fldTemp As Field

    Dim relTemp As Relation

    Dim proTemp As Property

    '

    Dim cDBName As String

    Dim cTblLookUp As String

    Dim cTblMaster As String

    Dim cIdxLookUp As String

    Dim cIdxMaster As String

    Dim cRelName As String

    '

    cDBName = "\source\chap14\ch1400.mdb"

    cTblLookUp = "ValidUnits"

    cTblMaster = "MasterTable"

    cIdxLookUp = "PKUnits"

    cIdxMaster = "PKMaster"

    cRelName = "relUnitMaster"

    '

    ' erase database if it's already there

    Kill cDBName

    '

    ' create database

    Set dbFile = CreateDatabase(cDBName, dbLangGeneral, dbVersion20)

    '

    ' create list table

    ' this has the stuff to lookup

    Set tdTemp = dbFile.CreateTableDef(cTblLookUp)

    '

    ' add fields to the table

    Set fldTemp = tdTemp.CreateField("UnitID", dbText, 10)

    tdTemp.Fields.Append fldTemp

    '

    Set fldTemp = tdTemp.CreateField("UnitDesc", dbText, 30)

    tdTemp.Fields.Append fldTemp

    '

    ' add main index to ValidUnits table

    Set idxTemp = tdTemp.CREATEINDEX(cIdxLookUp)

    idxTemp.PRIMARY = True

    idxTemp.Required = True

    Set fldTemp = tdTemp.CreateField("UnitID")

    idxTemp.Fields.Append fldTemp

    tdTemp.Indexes.Append idxTemp

    '

    ' append table def to database

    dbFile.TableDefs.Append tdTemp

    '

    ' now create master table

    ' this table will need a reference to lookup

    Set tdTemp = dbFile.CreateTableDef(cTblMaster)

    '

    ' now add some fields

    Set fldTemp = tdTemp.CreateField("MasterName", dbText, 20)

    tdTemp.Fields.Append fldTemp

    '

    Set fldTemp = tdTemp.CreateField("MstrUnitID", dbText, 10)

    tdTemp.Fields.Append fldTemp

    '

    ' add main index to master table

    Set idxTemp = tdTemp.CREATEINDEX(cIdxMaster)

    idxTemp.PRIMARY = True

    idxTemp.Required = True

    Set fldTemp = tdTemp.CreateField("MasterName")

    idxTemp.Fields.Append fldTemp

    tdTemp.Indexes.Append idxTemp

    '

    ' append table to db

    dbFile.TableDefs.Append tdTemp

    '

    ' now set a relationship

    Set relTemp = dbFile.CreateRelation(cRelName)

    relTemp.TABLE = cTblLookUp ' table for lookups

    relTemp.ForeignTable = cTblMaster ' table to check

    Set fldTemp = relTemp.CreateField("UnitID") ' field to lookup

    fldTemp.ForeignName = "MstrUnitID"  ' field to check

    relTemp.Fields.Append fldTemp   ' add field object to relation object

    relTemp.Attributes = dbRelationUpdateCascade ' for cacading updates

    dbFile.Relations.Append relTemp ' book the completed relation object

    '

    ' enumerate the relation object

    Me.Cls

    For Each relTemp In dbFile.Relations

        For Each proTemp In relTemp.Properties

            Me.Print proTemp.Name,

            Me.Print ">";

            Me.Print proTemp.VALUE;

            Me.Print "<"

        Next

        '

        Me.Print "Relation Fields:"

        For Each fldTemp In relTemp.Fields

            Me.Print "",

            Me.Print "Name: ";

            Me.Print fldTemp.Name,

            Me.Print "ForeignName: ";

            Me.Print fldTemp.ForeignName

        Next

    Next

    '

End Sub

The down side of this power is that you will need to be prepared for multiple page locks when you attempt to update or delete highly dependent data objects in your database. If you want to change the CustomerID value in a master table, for example, you probably will affect a number of related transaction and history tables within the database. This will call for several page locks. If you are a good database programmer, you'll encapsulate this action within a single transaction. If you create a long transaction that spans multiple tables, you'll commit many page locks and hold them for some time before you finally complete and commit the transaction.


WARNING

Relation objects cannot be enforced in non-Microsoft Jet database formats, including attached tables. This means that you will not be able to count on Microsoft Jet to lock and update related attached tables during your database updates. If you have attached tables in your database, you must handle any relational updates within your Visual Basic code.

Using a Lock Table for Multiple Table Locking

Most programmers prefer to use the optimistic page-locking scheme. This results in the shortest locks and the least number of lock conflicts at edit time. If your application experiences heavy data entry, however, it is possible that your users will experience many update lock conflicts during peak load times. You can reduce the number of conflicts users encounter by creating a Lock table that contains information on all current locked records. By forcing your code to check this table before beginning an edit operation, you can reduce the number of update lock conflicts your users encounter under optimistic locking.

Here's how you can establish a Lock table for your application. Create a single table in your database called AppLock. This table should have four fields: UserID, TableName, RecordID, and LockStamp. The LockStamp field is a date/time field, and the rest are text fields (set their length to about 10 to 20 characters each). Each time a user attempts to edit a record in a table, the program first checks to see whether that TableName/RecordID pair is already on file in the table. If so, the user is told that the record is locked and to try again later. You could even add the user who holds the record and the time that user started his or her lock operation). If the TableName/RecordID pair was not found in the AppLock table, a new record is added to the table containing the UserID, TableName, RecordID, and current time. When the user has completed the edit work, the program can remove the record from the lock file to allow others to read the record.


NOTE

Listings 14.42 and 14.43 assume the existence of a few primer routines and some form-level or global-level variables. In order to get these routines to work properly, you need to add a routine that opens the LockTable recordset and a routine that clears out old records once the edit is done.

Public Function CheckLockTable(cTable$, cRecID$) As Boolean

    '

    ' check lock table to see if

    ' we can edit this rec

    '

    rsLockTable.FindFirst "TableName=" + cTable$ + " and RecordID=" + cRecID$

    If rsLockTable.NoMatch = False Then

        MsgBox cTable$ + "." + cRecID$ + " is Locked - Try again later.",

        _vbCritical, "CheckLockTable"

        CheckLockTable = False

    Else

        CheckLockTable = True

    End If

    '

End Function

You can place CheckLockTable function right before the Edit method in Visual Basic code or as part of the Reposition event of the Visual Basic data control (see Listing 14.43).

Public Sub EditRec()

    '

    ' check locks, then edit

    '

    Dim cTable As String

    Dim cRecId As String

    '

    cTable = rsEditTable.Name ' get table to lock

    cRecId = rsEditTable.Field(0) ' first field is primary key

    '

    If CheckLockTable(cTable, cRecId) = True Then

        rsEditTable.Edit

        '

        ' perform edits

        '

        rsEditTable.udpate

        ClearLock cTable, cRecId

    End If

End Sub

The main drawback to this approach is that updating the lock table itself can result in locking conflicts. Also, this design essentially has tripled the number of database disk operations needed to save a single record (read lock record, write lock record, read edit record, write edit record, and delete lock record). A routine like this might be overkill in a tiny data-entry application, but it works well in a large application that experiences heavy data input from multiple users.

Reducing Locking Conflicts with SQL Statements

Another way to reduce page-locking conflicts is to perform data table updates via SQL statements. The basic concept here is to use snapshots to load data onto the workstation, and then use SQL statements to actually perform the update. Finally, after the update, requery the snapshot object to get an updated picture of the data. This method works well in an environment where the centrally located data file is some distance away and the updates are rare and/or small.

The code for this kind of process might look like Listing 14.44.

Public Sub SQLProcess()

    '

    ' perform updates via SQL statements

    '

    Dim db As Database

    Dim rs As Recordset

    Dim cSQL As String

    '

    Set db = DBEngine.OpenDatabase("c:\source\chap14\ch1403.mdb")

    Set rs = db.OpenRecordset("Table1", dbOpenSnapshot)

    '

    ' add new record

    cSQL = "INSERT INTO Table1 VALUES('MCA','Weather Ballon','FirstClass','RoundTrip');"

    db.Execute cSQL

    '

    db.Close

    '

 End Sub

The limitations of this system include possible wait time to load the data set and the chance that the data set will be too large to fit into workstation memory. Also, even though record adds and deletes are quite easy with this method, updates can be tougher because they involve a series of SQL UPDATE statements (one for each field). Still, although this is a somewhat restrictive system, many ODBC applications still use this basic model for reading and writing remote data sources.


Using Batch Updates for Transaction Tables

One of the most annoying problems in multi-user database work is handling heavy data entry loading from multiple users. In Visual Basic Microsoft Jet, all new records are added to the last physical page in the database. If you have several users all attempting to add records at the same time, it is very likely that users will see numerous update locking conflicts because they are all trying to add records to the same physical data page.

One way to reduce the update conflicts is to have users enter their data into private temporary tables. Then, when they have completed a batch of entries, this private table can be used to update the master database table. The update can be accomplished with a SQL APPEND query, and then the temporary table can be deleted.

Listing 14.45 shows how you could structure a batch-update application.

Public Sub BatchUpdate()

    '

    ' provide temporary table

    ' for batch loading of

    ' master table

    '

    Dim dbFile As Database

    Dim rsTable As Recordset

    Dim cSQL As String

    '

    Set dbFile = DBEngine.OpenDatabase("c:\source\chap14\ch1402.mdb")

    '

    cSQL = "SELECT InvTrans.* INTO MyTrans SELECT * FROM InvTrans WHERE

    _InvID='JUNK';"

    dbFile.Execute cSQL

    '

    Set rsTable = dbFile.OpenRecordset("MyTrans", dbOpenTable)

    '

    ' allow user to peform batch processing on local table

    '

    cSQL = "INSERT * INTO InvTrans FROM MyTrans;"

    dbFile.Execute cSQL

    '

End Sub

This method works very well on applications that handle a large volume of transaction-type data entry work (accounting entries, for example). This method does not work well for data tables that act as master records that must be related to other child tables in the system (such as customer master records).

Modifying Page-Level Lock Related Settings

There are several INI/registry settings that you can adjust to control the page-level locking behavior of the Microsoft Jet database engine. These settings control the number of times Microsoft Jet will attempt to lock a page, the number of pages held in memory, and the length of time Microsoft Jet will hold unused pages in memory. Usually the default values for these settings will work fine. However, if you are having problems with your database reads and writes over a network, you might want to experiment with these settings.

When working with Microsoft Jet 2.5 data format, these settings can be placed in the [ISAM] section of your VB.INI file or the INI file that has the same name as your executable Visual Basic program. For Microsoft Jet 3.0 databases, these values can be found in the JET\3.0\ENGINES\JET folder of the registry.


NOTE

Some of these settings also appear in the ODBC-related INI files ODBCISAM.INI and ODBCDCCP.INI. If you are using the ODBC drivers to access your data, you may need to adjust the values in the ODBC-related INI files, too.

Table 14.3 shows the INI settings that are used to control page-lock behavior. The first column contains the complete INI setting, as it would appear in your VB.INI file including the default value. The second column contains a brief description of the setting and its meaning.

INI Key


Comment


PageTimeOut=5

Release unused data pages after 500 milliseconds.

LockedPageTimeOut=5

Release used (locked) pages after 500 milliseconds.

MaxBufferSize=512

This is the total memory cache used by Microsoft Jet. Enlarging this cache can help speed ODBC processing.

LockRetry=20

Microsoft Jet will attempt to perform a page lock 20 times before reporting an error.

CommitLockRetry=20

Microsoft Jet will attempt to commit changes to a database 20 times before returning an error. Each "commit" attempt involves executing a lock. The attempt involves executing a lock. The number of locks attempted is controlled by the LockRetry setting. The default settings will result in 20 CommitLockRetries, each attempting 20 LockRetries before Microsoft Jet will return an error. This is 400 locking attempts in all.

ReadAheadPages=16

Microsoft Jet will preload 16 data pages when reading records. Increasing this number can speed sequential processing since it reduces the number of disk operations. Excessive pages in memory can also increase the chances of update errors in optimistic locking mode since users are preloading data pages that may be edited by other users.


NOTE

For more information on modifying locking-related settings, see the Visual Basic online help under the topic locking behavior.

Implementing Microsoft Jet Security

You can add user- and group-level security to your Microsoft Jet database Visual Basic applications by installing the SYSTEM.MDA security file created when you install Microsoft Access. This system security file contains information on defined users and groups along with permission profiles. You can use a single SYSTEM file to manage multiple databases or you can use a different security file for each database or application you use.


NOTE

The Microsoft Jet Security file is called SYSTEM.MDA in the 16-bit environment and SYSTEM.MDW in the 32-bit environment. When SYSTEM.MDA appears in this chapter, keep in mind that it also refers to the SYSTEM.MDW file where appropriate.

This section covers the advantages and disadvantages of using the SYSTEM security file; you'll review the security objects and permission levels available through the SYSTEM file and you'll see some examples of how you can use Visual Basic code and the VisData sample application to create and maintain Microsoft Jet security features.


NOTE

Microsoft Jet security operations do not work with non-Microsoft Jet format databases. However, if you create an attachment to a non-Microsoft Jet database, you can maintain security for the attachment. This section focuses on Microsoft Jet format databases.

Looking at the Advantages and Limitations of Microsoft Jet Security

There are several advantages and disadvantages to using Microsoft Jet security with your Visual Basic database applications. Some programmers love the SYSTEM.MDA, but others don't. It's up to you to determine whether the SYSTEM security file will work for you and how you will use it.

The main advantage of using the SYSTEM.MDA file is that you can use the Microsoft Jet security scheme as a basis for creating secure database applications. You can define users and groups that have carefully defined permissions and you easily can enforce this security scheme within your Visual Basic program.

The primary drawback of using Microsoft Jet security is that all the security features are stored in one single file. If you lose this file (it gets erased or corrupted), you might not be able to recover your data. Also, you easily can overwrite the security file with a clean security file that gives you (or anyone else) free access to all your secured data.

You also can manage multiple databases from within a single SYSTEM.MDA file. This is a real advantage for sites that have several Visual Basic or Microsoft Access programs in production. A system administrator easily can control access rights to all Microsoft Jet databases active on the server from one single point.

The default name for the Microsoft Jet security file is SYSTEM.MDA. However, if you want to define a special security scheme for each database installed at your site, you can rename the SYSTEM.MDA file to a name more appropriate like ACCOUNTING.MDA or INVENTORY.MDA.

When you use the SYSTEM.MDA file with Visual Basic, you can secure only table and QueryDef objects. You cannot set permission rights for forms, reports, or program modules.

Even if you have designed a tight security scheme, other non-Microsoft Jet data tools (ODBC-enabled query tools) might simply ignore the SYSTEM.MDA file altogether and allow users free access to the data within your secured database.

In summary, while the SYSTEM security file is useful, you should not count on the SYSTEM.MDA file alone to properly secure your databases. Also keep in mind that other software that needs access to the database might ignore the SYSTEM security file altogether. Still other applications might use the security file, and those users will need security profiles defined in the SYSTEM file.

Identifying the Microsoft Jet Security File

The name of the security file that will be loaded by your application at startup is determined by the SystemDB setting in the INI startup file. If you are running Microsoft Access, this is stored in the Microsoft Access INI file. If you are running a specific Visual Basic application, your application can be pointed to a specific security file by using the SystemDB INI setting in your applications INI file:

 [Options]

SystemDB=C:\MSOFFICE\ACCESS\SYSTEM.MDA

If your program will be running in a 32-bit environment, you can use the SystemDB property of the DBEngine to point to the SYSTEM security file:

    '

    DBEngine.SystemDB = "c:\msoffice\access\system.mda"

    '

NOTE

The SystemDB property of the DbEngine is available only for the 32-bit version of Visual Basic 4.0. If you attempt to refer to the SystemDB property while running the 16-bit version of Visual Basic 4.0, you'll get an error.

Installing Microsoft Jet Security

You cannot create a Microsoft Jet security file using Visual Basic. Only Microsoft Access can create the SYSTEM.MDA file needed to define and maintain database security profiles. If you have Microsoft Access, you can copy the SYSTEM.MDA data file to a new location (your application directory) and use that as the security file for your Visual Basic application.


WARNING

Before you begin to change the SYSTEM.MDA file, be sure to make a backup copy of the SYSTEM.MDA and all the databases secured by the SYSTEM.MDA. It is possible to secure yourself right out of the system. You might not be able to gain access to any of your databases if the SYSTEM.MDA file is lost or corrupted.

Once the SYSTEM security file is in place, you can use the VisData sample Visual Basic program to create and maintain user and group objects and set permission rights for all identified QueryDefs and Databases (see Figure 14.7).


FIGURE 14.7. Using VisData to create and maintain Microsoft Jet security.

After you copy a clean SYSTEM.MDA to the application or Visual Basic directory, you must add the SystemDB setting to your Visual Basic and application INI files in order to be able to access the security system (see Identifying the Microsoft Jet Security File, earlier in this chapter). In other words, unless you actually tell Visual Basic applications the exact name and location of the SYSTEM security file, no security features will be in place; your data will not be secure.


TIP

If you want to remove security features from an application, simply remove the SystemDB setting from the INI file. Real secure, right?

Now that you have a clean SYSTEM security file and it has been activated, you are ready to start creating users and groups for your Visual Basic application.

Logging In to a Secured Database

Once the security features are in place, users will be prompted to provide a Login Name and Password each time they attempt to open a secured database (see Figure 14.8).


Figure 14.8. Login dialog box for secured database.

You can bypass this dialog box by setting the DefaultUser and DefaultPassword properties of the DBEngine object before you execute an OpenDatabase statement. Listing 14.46 shows how this can be done.

Private Sub Form_Load()

    '

    ' set defaults before login

    '

    On Error GoTo FormLoadErr

    '

    DBEngine.DefaultUser = "MCA"

    DBEngine.DefaultPassword = "MyPassword"

    '

    Workspaces(0).OpenDatabase ("c:\source\chap14\ch1403.mdb")

    '

    Exit Sub

    '

FormLoadErr:

        MsgBox Error$, vbCritical, "Error Opening Database [" + Str(Err) + "]"

        End

End Sub

In Listing 14.46, the Login Name and Password are supplied via hard coding. These could also be stored in an INI setting/registry or in another database. However, storing login information in this manner can defeat the security system. An alternate approach might be to store the UserName parameter in an INI setting and prompt the user to provide the password.

Notice also that the routine in Listing 14.46 contains an error trap. If the UserName and Password parameters are not valid for this SYSTEM.MDA, an error will be reported and the database will not be opened (see Figure 14.9).


Figure 14.9. Error attempting to login to secured database.

Working with Microsoft Jet Database Security Objects and Levels

The Microsoft Jet allows you to secure two data objects: Tables and QueryDefs. You toggle on or off seven permission values:

There are several possible security-related error codes that Visual Basic could return. The most common ones are listed in Table 14.4.

Error Code


Error Text


3028

Can't start your application. The system database is missing or opened exclusively by another user.

3029

Not a valid account name or password.

3030

Account name isn't a valid account name.

3031

Not a valid password.

3032

Can't perform this operation.

3033

No permission for name.

You can trap for these errors in the Error event of the Visual Basic data control. You also can check for them during data table AddNew, Edit, Update, and Delete methods.

Summary

This chapter concentrated on four challenges you will encounter when you create multi-user applications using Visual Basic 4.0:

Previous Page TOC Next Page