Access Programming
Access Programming
INFOTECH
TRAINING CENTRE
MS-ACCESS
PROGRAMMING
Creating an Application..........................................................................................................................................3
Creating an Application on Your Own....................................................................................................................4
Command Bars: Menu Bars, Toolbars, and Shortcut Menus..................................................................................7
Adding Menus and Submenus to Command Bars..................................................................................................9
Working with Button Images on Command Bars.................................................................................................16
Visual Basic..........................................................................................................................................................21
Writing and Editing Code.....................................................................................................................................21
Creating Your First Function................................................................................................................................23
Visual Basic Fundamentals...................................................................................................................................27
Creating and Calling Procedures..........................................................................................................................29
IF Then Else..........................................................................................................................................................33
Do..Loops..............................................................................................................................................................33
Assigning Values to Controls and Properties at Run Time...................................................................................37
Using Pop-up Forms and Dialog Boxes...............................................................................................................38
Using a Custom Dialog Box to Collect Information............................................................................................39
Filtering and Sorting Data in Forms and Reports.................................................................................................42
Combo Boxes........................................................................................................................................................45
Populating Controls on a Form.............................................................................................................................48
Assigning Values to Controls on the Orders Form...............................................................................................48
Adding a Row to a Combo Box List.....................................................................................................................50
Working with Variables, Data Types, and Constants............................................................................................58
Declaring Variables...........................................................................................................................................58
Assigning and Retrieving Values..........................................................................................................................71
Working with Objects and Collections.................................................................................................................78
Working with Objects and Collections.................................................................................................................81
Declaring and Assigning Object Variables...........................................................................................................82
Working with Properties and Methods.................................................................................................................88
Responding to Events...........................................................................................................................................91
Opening and Closing a Form................................................................................................................................94
Using Data........................................................................................................................................................96
Working with Records and Fields.........................................................................................................................96
Creating a Recordset Object Variable...................................................................................................................98
Sorting and Filtering Records.............................................................................................................................102
Displaying Values of Controls and Properties....................................................................................................112
Assigning Values to Controls, Properties, and Variables....................................................................................114
Handling Run-Time Errors.................................................................................................................................116
Errors and Error Handling...................................................................................................................................118
Using Error Events..............................................................................................................................................119
Using On Error Statements.................................................................................................................................120
Creating an Application
After you become familiar with the Microsoft Access interface and learn how to create tables, queries, forms, and
reports, you can make your database easier to use by tying these objects together in an application. This chapter
explains what a Microsoft Access application is and presents an overview of the steps required to build one, either
with the Database Wizard or on your own. When you finish reading this chapter, you'll be ready to create your
first Microsoft Access application.
What Is a Microsoft Access Application?
People use a database to perform data management tasks, such as storing, retrieving, and analyzing data about
orders and customers. A Microsoft Access application is made up of the same objects as a Microsoft Access
database tables, queries, forms, reports, macros, and modules. The objects are stored in one or more Microsoft
Access database (.mdb) files. What makes an application different from a database is that the objects are tied
together into a coherent system. An application organizes related tasks so that the user can focus on the job at
hand, not on how the application works or on the program used to develop the application.
The keys to a Microsoft Access application are its objects, their properties, and the events that occur on forms.
Here's how it works:
An application consists of objects. Your application is made up of objects that users see and use directly (forms
and reports) and supporting objects that control how the forms and reports work (tables, queries, macros, and
modules). You build the forms and other objects in their respective Design views.
Objects have properties you can set You set objects' properties to make them look and behave the way you want.
For example, all forms have a Default View property that specifies whether a form should appear in Form or
Datasheet view. Once you set the property, the form opens automatically in the correct view. By setting properties,
you make your objects behave more intelligently.
Forms respond automatically to events When people use the forms in your application, their actions changing
data in a field, clicking a command button, moving the mouse are recognized by Microsoft Access as events.
Microsoft Access responds to these events automatically. For example, when a user changes the data in a text box,
Microsoft Access checks to make sure that the data is the correct data type. When a user clicks a command button,
Microsoft Access displays the button so it appears pressed in.
You can add your own, custom response to an event You can use either a macro or an event procedure to add the
response you want to an event. An event procedure is a Visual Basic procedure you write that's attached to a form,
report, or control; Microsoft Access runs it when a specified event occurs. You specify in the event procedure or
macro what you want to take place when the event occurs. For example, you can change object properties, open
or close objects, or manipulate data. You use event properties to determine whether Microsoft Access runs a
macro or an event procedure in response to an event. For example, to have a macro run in response to a command
button's Click event, you set the button's OnClick event property to the name of the macro.
You can extend Visual Basic with external libraries In addition to writing your own event procedures, you can
use Visual Basic to call external procedures in Microsoft Access library databases (MDAs) and in dynamic-link
libraries (DLLs). For example, you can enable or disable menu commands by calling functions in the DLLs that
are part of Microsoft Windows.
Where Is My Application's "Brain"?
If you've developed database applications using other products, you may expect to write a Main program in the
Visual Basic language that makes your application work. The Main program would be the application's brain;
you'd use it to tell the objects how to appear and react, and how to process the data, much as the manager of an
office delegates different projects.
With Microsoft Access, the objects manage themselves by responding to events that occur within your
application. There is no Main program. For example, suppose you want something to happen when the user clicks
a button. You don't need code that checks to see whether the user clicks the button. You attach the code to the
button so it's run when the Click event occurs. When the event occurs, Microsoft Access runs your code
automatically.
Tip When you name the tables, fields, and other objects in your database, keep in mind that you'll use these
names to refer to the objects elsewhere in your application. Although descriptive names for objects with spaces
are easier to recognize than more compact names, they can be difficult to use in expressions, SQL statements, and
Visual Basic code. If you are creating a database that uses these advanced features, you may want to use short,
consistent names that don't contain spaces and are easier to remember and type for example, field names such as
LastName and Phone.
To refer to an object or a value, you start with an object or a collection of objects and identify each element in
turn. A collection groups objects, such as the forms or controls in the current database, as shown in the following
illustration.
To refer to an element of a collection, such as the Forms collection, use the ! operator. To refer to the Orders form,
for example, use the following expression:
Forms!Orders
Each form contains a collection of controls. To refer to the OrderID control on the Orders form, for example, use
the following expression:
Forms!Orders!OrderID
You refer to a control to get, set, or pass its value.
To refer to a property, use the . (dot) operator before the property name. You use this operator before properties,
methods, actions, and collections. For example, to refer to the Visible property of the Orders form, use the
following expression:
Forms!Orders.Visible
To refer to the OrderID control's Visible property on the Orders form, use the following expression:
Forms!Orders!OrderID.Visible
Tip If you want help referring to an object or property, use the Expression Builder. With the Expression Builder,
you can simply select the object you want from a list, and the Expression Builder writes the reference for you
with all the operators in the correct places. To display the Expression Builder, right-click where you want to enter
the expression, and then click Build on the shortcut menu.
Providing Navigation to Tasks and Objects
As an application developer, you determine how people navigate through your applications and complete their
tasks. Navigating in a Microsoft Access application usually means moving from control to control within a form
or switching between forms.
Navigation within a form Within a form, navigation from control to control should follow the natural flow of the
task your users shouldn't have to bounce from the top of the form to the bottom and then back to the top again in
order to complete one task. Group controls logically so that users can focus on one area of the form at a time.
Define how a user moves from control to control on a form with the keyboard by setting the AutoTab, TabStop,
and TabIndex properties. If controls are used to find or filter records, place them in the form header or footer to
show they're separate from the other fields in the current record.
Navigation from one form to another form and completing tasks Nothing says you must use one approach over
another to navigate from form to form or to complete a task; however, it's a good idea to provide your users with
ways to navigate that are common to other Windows-based applications. It's also a good idea to be consistent
within your application to do similar tasks on different forms in similar ways. Here are some common navigation
devices used in Windows-based applications:
Command buttons
You can use a command button on a form to carry out a task (for example, saving the current record) or to open
another form used for a related task. Command buttons have the advantage of being highly visible. For most
common actions, you can create buttons on your forms by using the Command Button Wizard, which creates an
event procedure for you. If you want to change or add to what the button does, you can edit the event procedure
the wizard creates.
Hyperlinks
As an alternative to command buttons that use macros or event procedures for navigation, you can use hyperlinks
to navigate to other forms or objects in your database. To create a hyperlink, you set the HyperlinkAddress and
HyperlinkSubAddress properties of a label, image control, or command button. Once you set these properties,
users can click the text, picture, or button to navigate to the specified object. You can also use hyperlinks to open
documents and other files on a local hard drive, a local area network, an internal Web (intranet), or the Internet. If
you use hyperlinks to navigate to objects within a Microsoft Access database or to other Microsoft Office
documents, your users can use the Web toolbar to navigate between documents and objects they've previously
opened with hyperlinks.
Menu commands
You can put your own menus and commands on a form or report's menu bar. Commands on menus occupy less
room than command buttons on forms, but they're also less visible users must open the menu to see them. You can
also put common commands on shortcut menus, which users access by right-clicking a form or a control on a
form.
Toolbar buttons
You'll often want to create buttons that provide shortcuts to the commands on your menu bar. You can add custom
toolbar buttons to your toolbars, as shown in the following illustration.
Key assignments
You can associate a macro with a key combination so that Microsoft Access runs the macro whenever users press
that key combination in your application. This approach is even less visible than menu commands your users must
know the key assignment in order to use it. However, it has the advantage of being available throughout your
application, not just on a particular form. If there's an action your users may want to perform anywhere in your
application (for example, printing the current record), you can give them a consistent way to do it by using a key
assignment.
only commands. In this respect, command bars can be too flexible. Avoid confusing the users of your application
by putting menu bar and toolbar controls in unusual locations or configurations. Instead, model your custom
command bars after the ones used in Microsoft Access.
The simplest way to work with command bars is by using the Customize dialog box (View menu, Toolbars
submenu). You can use the Customize dialog box to customize existing command bars or to create new ones. New
command bars can contain existing commands or new commands that run the event procedures or macros you
define.
By default, users can customize command bars. Users can also make menu bars and toolbars free-standing by
dragging them into the work area, or they can dock them to the sides or the bottom of the work area. In addition,
users can resize or hide menu bars and toolbars. You can prevent users from customizing all command bars in
your application in the Startup dialog box (Tools menu). You can prevent users from customizing, moving, or
resizing an individual menu bar or toolbar by setting options in the Toolbar Properties dialog box, which is
available from the Customize dialog box.
You can also use the objects, methods, and properties of the CommandBars collection in Visual Basic code to
create and work with command bars.
Changes to existing command bars are always stored in the Windows Registry in the
\HKEY_CURRENT_USER\Software\Microsoft\Office\8.0\Access\Settings \CommandBars key. When you create
a new command bar, it is saved in a system table in the current database and is only available in that database.
However, if you create an add-in database and store new command bars in it, they are available from any
installation of Microsoft Access that has the add-in installed.
Once you have customized an existing menu bar or toolbar, or created a new one, you can attach it to a form or
report by specifying it in the MenuBar or Toolbar property for the form or report. To attach a shortcut menu to a
form, report, or control, specify it in the ShortcutMenuBar property for the form, report, or control. You can also
specify a global menu bar or a global shortcut menu to be available throughout your application by using the
Startup dialog box (Tools menu).
Note Previous versions of Microsoft Access use the AddMenu and DoMenuItem actions in macros to create
custom menu bars and shortcut menus and to carry out standard Microsoft Access menu commands. If you
convert a database created in a previous version of Microsoft Access to Microsoft Access 97, the macros that
contain the AddMenu and DoMenuItem actions will still run; however, Microsoft Access 97 converts
DoMenuItem actions in the macros to the new RunCommand action. For more information on the RunCommand
action, search the Help index for "RunCommand action."
Although these menu bar macros created with a previous version of Microsoft Access will run from the forms,
reports, or controls they are attached to, they won't be available in the Customize dialog box. However, you can
create a Microsoft Access 97-style menu bar or shortcut menu from a menu bar macro created in a previous
version of Microsoft Access. For more information on how to do this, search the Help index for "macros, using to
work with menus."
Creating New Menu Bars, Toolbars, and Shortcut Menus
You create all command bars, whether they are menu bars, toolbars, or shortcut menus, by using the Customize
dialog box (View menu, Toolbars submenu). You create the different kinds of command bars by setting their
properties and, if necessary, by setting properties for the commands within them to control how they appear and
behave. The following procedures show how to create new menu bars, toolbars, and shortcut menus.
Creating New Command Bars and Setting Their Properties
The first step in creating a new menu bar, toolbar, or shortcut menu is to create and name an empty command bar,
set its type, and set other properties that control how it can be used.
10
1 If the Customize dialog box isn't open, point to Toolbars on the View menu, and then click Customize.
2 If the menu bar, toolbar, or shortcut menu you want to work with isn't displayed, open it.
Note To display a custom shortcut menu, select the Shortcut Menu check box in the Toolbars list of the
Customize dialog box. On the Shortcut Menu toolbar, click Custom and then click the name of your custom
shortcut menu.
3 In the Customize dialog box, click the Commands tab.
4 In the Categories box, click New Menu.
New Menu appears in the Commands box.
5 Drag New Menu from the Commands box to your menu bar or toolbar:
To create a top-level menu, drag New Menu to the top row of your menu bar or toolbar.
To create a submenu, you must have an existing top-level menu, or you must be adding a new menu to a shortcut
menu. Drag and hold New Menu over a top-level menu (or shortcut menu) until it drops down, then drag New
Menu to the location you want and release the mouse.
6 Right-click New Menu on your menu or toolbar, and then type the name for your menu in the Name box.
Tip You can create an access key for your menu names so that users can access your menus with the keyboard.
To do so, type an ampersand (&) in front of the letter you want to use. For example, to use F as the access key for
a menu named File, type &File. The F in your menu name is underlined and users can open the menu by pressing
ALT+F.
7 To further customize your new menu or submenu, set other properties in the Control Properties dialog box. To
display the Control Properties dialog box, right-click the new menu or submenu, and then click Properties.
Once you have added all the menus and submenus to your command bar, you have the basic framework to contain
the commands that you want to be available. If you are creating a menu bar, you have top-level menus and
perhaps some submenus. If you are creating a toolbar, you may have added top-level menu buttons and possibly
submenus within them. If you are creating a shortcut menu, you may have added submenus. The next step is to
add commands to your menus and submenus, or buttons that carry out commands to your toolbar.
Adding Existing Menu Commands, Buttons, and Other Controls to Command Bars
By using the Customize dialog box, you can add any existing Microsoft Access menu command, toolbar button,
or other control to your new command bar. This includes all standard Microsoft Access menu commands and
toolbar buttons, as well as drop-down combo box controls, such as the Font box, and special formatting controls,
such as the Fill/Back Color and Special Effect controls.
To add an existing Microsoft Access menu command, button, or other control
1 If the Customize dialog box isn't open, point to Toolbars on the View menu, and then click Customize.
2 If the menu bar, toolbar, or shortcut menu you want to work with isn't displayed, open it.
3 In the Customize dialog box, click the Commands tab.
4 In the Categories box, click the category that contains the menu command, button, or other control you want to
add to your command bar. For example, to add a command that appears on the File menu, click File. To add an
entire menu of commands at once, click Built-in Menus.
11
5 Drag the menu, menu command, button, or other control you want from the Commands box to the appropriate
location on your command bar.
To add the command or control to a menu or submenu, drag it and hold the mouse over the menu or submenu
name until it drops down, and then drag the command or control where you want it on the menu or submenu and
release the mouse.
Note If you place the command or control in the wrong location, you can drag it to the correct location.
6 To further customize your command bar, you can change the images that appear on toolbar buttons and next to
menu commands, and you can set other properties that determine how your menu commands, buttons, and other
controls appear and work. To display a menu of customization options, right-click the menu command, button, or
control.
12
differences. With all its improvements from earlier versions of Basic, Visual Basic retains its English-like flavor
and ease of use.
When to Use Visual Basic Instead of Macros
With Microsoft Access, you can accomplish many tasks with macros or through the user interface that require
programming in other database systems. So, when do you turn to Visual Basic? It depends on what you want to
do.
Why Use Visual Basic?
You'll want to use Visual Basic instead of macros if you want to do any of the following:
Make your application easier to maintain Because macros are separate objects from the forms and reports that
use them, an application containing a large number of macros that respond to events on forms and reports can
become difficult for you, the application developer, to maintain. In contrast, when you use Visual Basic to respond
to events, your code is built into the form or report's definition. If you move a form or report from one database to
another, the Visual Basic code built into the form or report moves with it. (Code is a general term for the
statements you write in a programming language.)
Create your own functions Microsoft Access includes many built-in, or intrinsic, functions such as the IPmt
function that calculates an interest payment. You can use these functions to perform calculations without having to
create complicated expressions. Using Visual Basic, you can also create your own functions to either perform
calculations that exceed the capability of an expression or replace complex expressions you've written in your
application.
Mask error messages When something unexpected happens in your application and Microsoft Access displays an
error message, the message can be quite mysterious to your application's users, especially if they aren't familiar
with Microsoft Access. Using Visual Basic, you can detect the error when it occurs and display your own
message, or you can have your application do something else. Applications used by a variety of people almost
always require some Visual Basic code for handling errors.
Create or manipulate objects In most cases, you'll find that it's easiest to create and modify an object in that
object's Design view. In some situations, however, you may want to manipulate the definition of an object in
code. Using Visual Basic, you can manipulate all the objects in a database, including the database itself. A
Microsoft Access wizard is a good example of an application that creates and modifies objects using code. For
example, the Form Wizard is a collection of Visual Basic functions that creates a form according to the
specifications supplied by the user.
Perform system-level actions You can use the RunApp action in a macro to run another Windows-based or MSDOS-based application from your Microsoft Access application, but you can't use a macro to do much else
outside Microsoft Access. Using Visual Basic, you can check to see if a file exists on the system, use Automation
or dynamic data exchange (DDE) to communicate with other Windows-based applications such as Microsoft
Excel, and call functions in Windows dynamic-link libraries (DLLs).
Manipulate records one at a time You can use Visual Basic to step through a set of records one record at a time
and perform an operation on each record. In contrast, macros work with entire sets of records at once.
Pass arguments to your code An argument is a value that supplies the additional information that some actions
require. You set arguments for macro actions in the lower part of the Macro window when you create the macro;
you can't change them when the macro is running. With Visual Basic, however, you can pass arguments to your
code at the time it runs. You can even use variables for arguments something you can't do in macros. This gives
you a great deal of flexibility in how your code runs.
Tip Although you can have both macros and Visual Basic code in your application, you may find it easier to use
Visual Basic exclusively once you get started programming. If you have macros in your application, Microsoft
13
Access can automatically convert them to event procedures or modules that perform all the equivalent actions in
Visual Basic code.
In form or report Design view, use the Convert Macros To Visual Basic command (Tools menu, Macro submenu).
For global macros that aren't attached to a specific form or report, use the Save As/Export command (File menu)
to save the macro as a module. For more information, search the Help index for "macros, converting."
Why Use Macros?
After reading all the reasons for using Visual Basic, you may wonder if there are any reasons left for using
macros. However, macros do have their place in many applications. Macros are an easy way to take care of
simple details such as opening and closing forms, showing and hiding toolbars, and running reports. Because you
specify options for each action in the lower part of the Macro window, there's little syntax to remember, and
developing applications can often be faster than with Visual Basic.
In addition to the ease of use macros provide, creating a macro is the only way to make global key assignments.
How an Event-Driven Application Works
An event is an action recognized by a form, report, or control. Each type of object in Microsoft Access
automatically recognizes a predefined set of events. When you want a form, report, or control to respond to an
event in a particular way, you can write a Visual Basic event procedure for that event.
Here's what happens in a typical event-driven application:
1. A user starts the application and Microsoft Access automatically opens the startup form specified in the Startup
dialog box.
2. The startup form, or a control on the startup form, receives an event. The event can be caused by the user (for
example, a keystroke), or by your code (for example, an Open event when your code opens a form).
3. If there is an event procedure corresponding to that event, it runs.
4. The application waits for the next event.
Note Some events automatically trigger other events. For example, when the MouseDown event occurs, the
Click and MouseUp events immediately follow.
Event-Driven vs. Traditional Programming
In a traditional procedural program, the application rather than an event controls the portions of code that are run.
It begins with the first line of code and follows a defined pathway through the application, calling procedures as
needed.
In event-driven applications, a user action or system event runs an event procedure. Thus, the order in which your
code is run depends on the order in which events occur; the order in which events occur is determined by the
user's actions. This is the essence of graphical user interfaces and event-driven programming: The user is in
charge, and your code responds accordingly.
Because you can't predict what the user will do, your code must make a few assumptions about "the state of the
world" when it runs. It is important that you either test these assumptions before running your code or try to
structure your application so that the assumptions are always valid. For example, if your application assumes that
a text box has text in it before the user clicks a command button, you can write code to enable the command
button only when the Change event for the text box occurs.
Creating Your First Event Procedure
14
You write Visual Basic code in units called procedures. A procedure contains a series of Visual Basic statements
that perform an operation or calculate a value. An event procedure is a procedure that runs in response to an event.
This section shows you how to create a simple event procedure that makes a command button and a text box work
together on a form. The following illustration provides an example of how this interface may appear to the user.
Note This example assumes that you have control wizards turned off in the form's Design view. To do this, make
sure the Control Wizards tool in the toolbox is not pressed in.
Because an event procedure is part of the design of the form or report that runs it, the first step is to create the
form and add the controls. In this case, you create a form that isn't based on a table or query and add a text box
and a command button. Your code will refer to these controls by name, so it's a good idea to set the Name
property of each control on your form to something more descriptive than the default settings that Microsoft
Access gives them. For example, Microsoft Access names the text box Text0 and the command button
Command1. To be more descriptive, you could name the text box Message and the command button OK.
Tip When you name the tables, fields, and other objects in your database, keep in mind that you'll use these
names to refer to the objects elsewhere in your application. Although descriptive names for objects with spaces
are easier to recognize than more compact names, they can be difficult to use in expressions, SQL statements, and
Visual Basic code. Consider using short, consistent names that don't contain spaces and are easy to remember and
type for example, field names such as LastName and Phone.
After you've created your form and its controls and set their properties, you're ready to write your first event
procedure in the Module window.
To write the event procedure for the OK command button
1 In Design view, right-click the object (form, report, section, or control) for which you want to write an event
procedure, in this case, the OK command button. On the shortcut menu, click Build Event.
Microsoft Access displays the Choose Builder dialog box.
2 In the list box, click Code Builder, and then click OK.
Microsoft Access opens the Module window and creates a template for the default event procedure of the object
you selected, in this case, the Click event procedure. (The default event procedure is the one for which Microsoft
Access thinks you're most likely to add code.) The template for the OK command button's Click event procedure
is shown in the following illustration.
3 Enter the code for the event procedure between the Sub and End Sub statements. For the OK command
button's event procedure, enter the following code:
Message = "Hello, World!"
This line of code sets the Message text box to the text string, "Hello, World!"
4 Save and close the module.
When you save the module, Microsoft Access sets the command button's OnClick event property to [Event
Procedure], as shown in the following illustration.
Now that you've written the event procedure, you're ready to run it. To do this, you make the event happen on the
form.
To run the OK_Click event procedure
15
Click the Form View button on the toolbar to switch to Form view.
2 Click OK.
The event procedure runs and the text "Hello, World!" appears in the text box.
Working with a Form or Report Module
The "Hello, World!" example in the previous section shows you how to create a new event procedure by using the
Build Event command on an object's shortcut menu. In addition to this method, Microsoft Access provides a
variety of other ways to open a form or report module and create or modify its event procedures.
To open a form or report and its module at the same time
In the Database window, select the form or report, and then click the Code button on the toolbar.
Microsoft Access opens the form or report and its module. You can also use the Code button in a form or report's
Design view to open its module.
In the previous section, you learned how to open the default event procedure. You can create or open any event
procedure directly from the property sheet.
To create or open any event procedure
1 Open the form or report in Design view.
2 Display the property sheet by right-clicking the form, report, section, or control, and then clicking Properties
on the shortcut menu.
3 In the property sheet, click the Event tab.
4 Select the property box for the event procedure you want to open.
5
If the event property already has an event procedure associated with it, Microsoft Access opens the Module
window and displays the event procedure.
If the event property is empty, Microsoft Access displays the Choose Builder dialog box. In the list box, click
Code Builder, and then click OK. Microsoft Access opens the Module window and creates a template for the
event procedure.
Creating a Command or Button That Runs a Visual Basic Function Procedure
For the greatest flexibility, you can create a Visual Basic Function procedure and run it from a menu command or
toolbar button. To do so, add the Custom command to your command bar, and then customize it to run your
Function procedure.
Important You can run only Visual Basic Function procedures from a command bar, not Sub procedures.
To add a custom command or button that runs a Visual Basic Function procedure
1 Create a Visual Basic Function procedure that performs the action you want.
2 If the Customize dialog box isn't open, point to Toolbars on the View menu, and then click Customize.
3 If the menu bar, toolbar, or shortcut menu you want to work with isn't displayed, open it.
16
17
2 If the menu bar, toolbar, or shortcut menu that contains the control you want to work with isn't displayed, open
it.
Right-click the control that has the image you want to use, and then click Copy Button Image. Right-click the
control whose image you are customizing, and then click Paste Button Image.
Copy and paste an image from a graphics program
Open the image you want to copy in a graphics program. Select and copy the image (preferably a 16 x 16 pixel
image or portion). Switch back to Microsoft Access. Right-click the control, and then click Paste Button Image.
Edit the control's current button image
Right-click the control, and then click Edit Button Image. In the Button Editor dialog box, you can change the
color and shape of the image, adjust the image's position on the control, and preview your changes to the image.
When you have finished editing the button image, click OK.
Reset a control to use its original button image
Right-click the control and then click Reset Button Image.
4 When you have finished working with the button image, click Close.
Setting Properties for Command Bar Controls
Microsoft Access provides some additional menu and control properties that you can use to further customize
menus, menu commands, and toolbar buttons. You set each of these properties in the Control Properties dialog
box.
Note Depending on the kind of control you're working with, some properties will not be available.
To set control properties for a menu, a menu command, or a toolbar button
1 If the Customize dialog box isn't open, point to Toolbars on the View menu, and then click Customize.
2 If the menu bar, toolbar, or shortcut menu that contains the control you want to work with isn't displayed, open
it.
3 Right-click the control, and then click Properties.
4 In the Control Properties dialog box, set the properties you want. The following table describes each property.
The name that is displayed for the command. This is identical to the text entered in the Name box on a menu or
control's shortcut menu.
Shortcut Text
The text that is displayed next to a menu command and that indicates its shortcut key; for example, CTRL+P.
This property only creates display text to prompt the user. To define the shortcut key, you must create an
AutoKeys macro as described in "Making Key Assignments" later in this chapter.
ToolTip
The text of the ToolTip that appears when a user rests the pointer on the control. If this setting is blank, Microsoft
Access uses the text from the Caption property as the ToolTip.
On Action
The name of a macro or Visual Basic Function procedure that runs when a user clicks the control. When using a
Function procedure, you must enter the name of procedure as an expression, using the syntax =functionname().
Style
Controls how a command is displayed. The Style property settings are also available from a control's shortcut
menu.
18
The help file that contains the What's This Tip topic specified by the Help ContextID property.
Help ContextID
The context ID of the topic to display as a What's This Tip for this command.
Parameter
An optional string associated with the control that your application can reference or set. For example, the Visual
Basic Function procedure specified in the On Action property can refer to the Parameter property to determine
how it works, or the Parameter property can be used to store information about the control, much like the Tag
property. The Parameter property isn't generally used by built-in menu and toolbar controls. However, the
Parameter property for a menu command or toolbar button used to add an ActiveX control is set to the ActiveX
control's class identifier(CLSID), which is the Registry value that uniquely identifies that control. If you delete or
modify the CLSID, the command or button won't work. Similarly, the Parameter property for a menu command or
toolbar button used to open a particular database object is set to the name of the object.
Tag
An optional string that can be used later in event procedures.
Begin A Group
Select this check box to indicate the beginning of a group of controls. On menus, a separator bar appears above a
command that has this property set. On toolbars, a vertical separator bar appears in front of the command. If you
resize a floating toolbar and the entire group of controls doesn't fit on the current line, the whole group is bumped
to a new line.
You can also set and read each of the properties in Visual Basic code. Most of the corresponding Visual Basic
property names are the same as those listed in the preceding table, although the words are concatenated; for
example, ShortcutText property. There are two exceptions: the Visual Basic properties that correspond to the
ToolTip and Begin A Group properties in the Control Properties dialog box are ToolTipText and BeginGroup.
Attaching a Custom Menu Bar to a Form or Report
The easiest way to create a menu bar that's attached to a form or report is to create a new menu bar and then
specify that menu bar in the form or report's MenuBar property, so Microsoft Access displays the menu bar
whenever the form or report is active.
To attach a custom menu bar to a form or report
1 Create a custom menu bar as described earlier in this chapter.
2 Open the form or report in Design view.
3
4 In the MenuBar property box, enter the name of the menu bar you created in step 1.
You can attach the same menu bar to more than one form or report.
Attaching a Custom Shortcut Menu to a Form, a Control on a Form, or a Report
You can attach custom shortcut menus to a form, a control on a form, or a report. After you create the shortcut
menu, set the ShortcutMenuBar property for the form, control, or report to the name of the shortcut menu.
Microsoft Access displays the custom shortcut menu whenever a user right-clicks the form, control, or report.
To attach a custom shortcut menu to a form, a control on a form, or a report
1 Create a custom shortcut menu as described earlier in this chapter.
19
5 In the ShortcutMenuBar property box, enter the name of the shortcut menu you created in step 1.
You can attach the same shortcut menu to more than one form, control, or report.
Specifying a Global Menu Bar or Shortcut Menu
You can specify a menu bar to use throughout your application by using the Startup dialog box.
To specify a global menu bar to display when your application starts
1 Create a custom menu bar as described earlier in this chapter.
2 On the Tools menu, click Startup.
3 In the Menu Bar box, enter the name of the menu you created in step 1.
4 Click OK.
The next time you start your application, Microsoft Access displays your custom menu bar instead of the default
menu bar.
You can change the global menu bar while your application is running, without having to restart your computer.
To do so, set the MenuBar property of the Application object to the name of the menu bar.
Using Custom Toolbars
You can use one or more custom toolbars in an application. Create the toolbars you want, and then use the
appropriate method to display your custom toolbars:
If your application has only one custom toolbar, simply use the Toolbars command (View menu) to display it
and it will appear each time your application starts.
If your application has different custom toolbars for different forms or reports, you can specify a toolbar for each
form or report in the form or report's Toolbar property.
If you need to work with more than one custom toolbar for a form or report, or if you want to hide or show
Microsoft Access built-in toolbars, you can use the Visible property of the CommandBar object in Visual Basic
code or the ShowToolbar action in macros to hide and show the toolbars.
If you want your application to display only custom toolbars, you can hide all built-in toolbars by clicking the
Startup command (Tools menu) and clearing the Allow Built-in Toolbars check box.
Attaching a Custom Toolbar to a Form
When using the Orders application, Northwind sales representatives want to click a button on the toolbar to print
the invoice for the current order. You can create a custom toolbar for the Orders form with a button that prints
invoices, and use the custom toolbar instead of the built-in toolbar.
20
Step One: Create the custom toolbar Create a custom toolbar for the Orders form that includes a button that runs
the PrintInvoice macro, as well as any other commands you want to provide, such as the Cut, Copy, and Paste
commands in the Edit category of the Customize dialog box (View menu, Toolbars submenu). Name this toolbar
Orders Form Toolbar.
Note The custom toolbar attached to the Orders form in the Orders sample application includes a Design View
button. You can use this button to easily switch between Form view and Design view while you're looking at the
sample application. However, if you don't want users to switch to Design view in your own application, don't put
the Design View button on your custom toolbars.
Step Two: Set the form's Toolbar property to the name of the custom toolbar Open the Orders form in Design
view, open the property sheet for the form, and then enter Orders Form Toolbar in the Toolbar property box.
Note There is no need to create event procedures for the Activate and Deactivate events of the form to show and
hide toolbars as was required in previous versions of Microsoft Access. Setting the Toolbar property to a custom
toolbar automatically hides the built-in Form View toolbar when your form is opened, and hides your custom
toolbar when a user closes the form or switches to another form.
Preventing Users from Customizing Your Application's Command Bars
You can control whether users can add or remove commands on all of the menus and toolbars in your application.
To prevent users from customizing all command bars in an application
1 On the Tools menu, click Startup.
2 In the Startup dialog box, clear the Allow Toolbar/Menu Changes check box.
3 Click OK.
The next time your application starts, users won't be able to add or delete menu or toolbar commands. However,
users will still be able to move and resize toolbars.
Note If you want to prevent users from customizing an individual command bar, you can clear the Allow
Customizing check box in the Toolbar Properties dialog box. For more information, see "Creating New Command
Bars and Setting Their Properties" earlier in this chapter.
Working with Command Bars in Code
You can work with menu bars, toolbars, and shortcut menus in Visual Basic code by using the properties and
methods of the CommandBars collection and the objects associated with it. In the command bars object model,
each menu is a CommandBar object. This is true of menus and submenus on all three types of command bars. For
example, to refer to the Tools menu on the standard menu bar, use the following statement:
CommandBars!Tools
The following code uses the Add method and several command bar control properties to add a new, hidden Print
Invoice command to the bottom of the Tools menu.
Private Sub AddInvoiceCommand()
Dim cb As CommandBar, ctl As CommandBarControl
' Set a reference to the Tools menu.
Set cb = CommandBars!Tools
' Create new CommandBarControl object on the Tools menu
' and set a reference to it.
Set ctl = cb.Controls.Add(Type:=msoControlButton)
21
Visual Basic
When you're looking for a level of power and control over your application that goes beyond what you can find in
the Microsoft Access interface, Visual Basic is the place to find it. This chapter shows you how to use Visual
Basic to respond to events on forms and reports, and how to create custom functions. It also introduces you to the
fundamentals of the Visual Basic programming language.
22
enter a method or property, Microsoft Access automatically displays a list of the methods and properties that
apply to the object. (You use methods to perform operations on an object. You use properties to determine or
change characteristics of an object.)
To complete the statement you're typing, you can either click an item in the list or continue typing your code. If
you continue typing code, the list displays the closest match to what you've typed. To enter the selected item in
the list at any time, press TAB. To make the list disappear, press ESC.
Note You can press ENTER to enter the selected item in the list. However, pressing ENTER also moves the
cursor to the next line, so you'll have to return to the line to enter any arguments or additional information.
To use automatic statement building, select the Auto List Members check box on the Module tab of the Options
dialog box (Tools menu).
When the Auto List Members check box is selected, the statement-building lists appear automatically as you type.
However, you may also want to explicitly cause Microsoft Access to display them, especially for lines of code
you typed previously and want to edit. To display the list of methods and properties for an existing object, rightclick the existing method or property name (to the right of the period) and then click List Properties/Methods on
the shortcut menu.
Some methods take a constant as an argument. To display a list of available constants when you're entering
arguments for a method, right-click in the Module window and then click List Constants on the shortcut menu.
If you've typed part of a property, method, or constant and want Microsoft Access to finish typing it for you, click
Complete Word on the Edit menu.
Quick Info When you type a procedure or method name (followed by a space or an opening parenthesis), a tip
automatically appears underneath the line of code you're writing. The tip gives syntax information about the
procedure, such as the arguments you need to type to use it.
To display syntax tips, select the Auto Quick Info check box on the Module tab of the Options dialog box (Tools
menu).
In addition, you can view information on any variable, constant, or procedure in your code by right-clicking the
item and then clicking Quick Info or Parameter Info on the shortcut menu.
Automatic syntax checking As you move the insertion point off a line, Visual Basic checks the syntax of that
line and displays a message if it finds an error. To enable automatic syntax checking, select the Auto Syntax
Check check box on the Module tab of the Options dialog box (Tools menu).
Drag and drop If you want to move code you've written from one place to another in a module or between
windows, you don't need to bother with the Copy and Paste commands. Just select the code you want to move,
and then drag it to the new location.
To enable drag-and-drop editing, select the Drag-Drop In Editor check box on the Module tab of the Options
dialog box (Tools menu).
Undo command
Microsoft Access keeps track of the changes you make when editing code. By clicking the Undo button one or
more times, you can reverse any changes you've made to a module since you opened it.
Note When several people are using the same database, they use separate versions of the forms, reports, and
modules in the database. If one person changes code, the others must close and reopen the database in order to see
those changes. More than one person can edit the same form, report, or module at the same time. If you attempt to
save a form, report, or module that has already been changed by someone else, Microsoft Access warns you that
the module has changed since you opened the database.
23
24
This section shows you how to create a simple function that calculates the date of the first day of the next month.
You'll use this function to set the value of the BillingDate text box on the Orders form in the Orders sample
application.
If you want to use this function in other forms and reports, you'll want to create a standard module to store it in.
You create a standard module in the same way you create and open other database objects.
To create a standard module
In the Database window, click the Modules tab, and then click New.
Microsoft Access displays a new module in the Module window.
Note When you open a new module, Microsoft Access automatically includes two Option statements in the
Declarations section, as shown in the preceding illustration. These statements tell Microsoft Access how to sort
data when running code and whether to warn you if you don't declare variables. For more information, search the
Help index for "Option."
To create a new function
1 Below the Option Explicit statement (or any empty line in a module), type Function followed by a space and
the name you want to give the function. In this case, name your new function FirstOfNextMonth.
Note It's a good idea to give your functions relatively short names that describe their purpose or the value they
return. Function names can't contain spaces or punctuation marks. For more information on names in Visual
Basic, see "Naming Conventions" later in this chapter.
2 Press ENTER.
When you press ENTER, Microsoft Access scans your typing, checks it for obvious errors, formats it according to
a consistent set of rules for capitalization and spacing, and displays it again. This occurs every time you enter a
new line in the Module window. Microsoft Access also adds a blank line and an End Function statement. The End
Function statement is always the last line in a function.
Note that Microsoft Access adds a set of parentheses after the name of the function. Use these parentheses to
enclose any arguments the function takes, if you decide that the function should take arguments.
Performing Calculations in a Visual Basic Function
You perform calculations in Visual Basic the same way you perform calculations elsewhere in Microsoft Access
by using an expression. The difference is in the way you specify where the result of the expression goes. When
you create an expression for a control on a form or for a field in a query, the result of that expression is assigned
to that control or that field.
When you perform a calculation in Visual Basic, however, it isn't obvious where the results should go. You have
to explicitly assign a destination to the expression. In the case of a function, you want the result of the calculation
to be the value returned by the function, so you assign the calculation to the name of the function.
To make a function return the result of a calculation, add an expression to the function that assigns the calculation
to the name of the function. For the FirstOfNextMonth function, you add the following line of code between the
Function and End Function statements.
FirstOfNextMonth = DateSerial(Year(Now), Month(Now) + 1, 1)
Compiling Your Procedure
25
Before you can run a procedure you've written, Microsoft Access must compile it. When it compiles a procedure,
Microsoft Access makes a final check for errors and converts the procedure into executable format. Because
Microsoft Access checks the syntax of each line as you enter it, your procedures compile very quickly.
You don't have to explicitly compile your procedures. If you've written a Function procedure, you can simply use
it in an expression. Then, when Microsoft Access evaluates the expression, it makes sure all the functions in the
expression have been compiled, compiling any uncompiled functions. If any of those functions use other
uncompiled procedures, Microsoft Access compiles those as well, and so on, until it has compiled all the code
required for it to evaluate the expression. If Microsoft Access discovers an error at any point during the
compilation process, it stops compiling and displays an error message.
Although automatic compiling is convenient, you can encounter error messages when you aren't expecting them.
For example, if you write a function and then use it in a form without compiling it first, you may not discover an
error in the function until Microsoft Access attempts to compile it when you try to view data in the form.
To make sure that a procedure has been compiled, you can explicitly compile the code in your database.
To compile code in all currently open forms, reports, and modules
On the Debug menu in the Module window, click Compile Loaded Modules.
Microsoft Access compiles all procedures that are in open modules. If it encounters an error, Microsoft Access
stops compiling, displays a message, and highlights the line of code that contains the error.
To compile all code in the current database
On the Debug menu in the Module window, click Compile All Modules or Compile And Save All Modules.
Microsoft Access compiles all the procedures in the database. This may take time if you have a large number of
procedures or modules.
Tip If you click Compile And Save All Modules, Microsoft Access saves all the code in your database in its
compiled form. It's a good idea to save modules after you compile them, because this allows Microsoft Access to
run them more quickly when you first open them in
the future.
Using Your Function
If you've followed the steps in this section, you now have a working function that you can use in an expression
almost anywhere in Microsoft Access. You may want to use your new function:
In other Visual Basic procedures that you write.
In the expression that defines a calculated field in a form, report, or query.
In the expression that defines the criteria in a query or the condition in a macro.
The following procedure shows you how to create a calculated text box on the Orders form that shows the billing
date of the order. When order takers take a new order, this text box will use the FirstOfNextMonth function to
automatically display the first day of the next month as the order's billing date.
To display the result of a function in a calculated text box
1 Open the Orders form in Design view.
2 Add an unbound text box to the Orders form, and set its Name property to BillingDate.
26
27
In a simple application, you may need to use Visual Basic only to create event procedures and simple functions, as
shown in the previous sections. However, as your applications get larger and more sophisticated, you'll want to
use the full power of the Visual Basic language. This section lays out the fundamental rules for writing Visual
Basic code in Microsoft Access.
What's In a Module?
A module can contain:
Declarations
These are statements that define variables, constants, user-defined types, and external procedures. The
Declarations section of a module is separate from the procedures, and declarations in this section apply to every
procedure in the module. You can also define variables and constants within a procedure, in which case they apply
only to the procedure they're in.
Event procedures
28
These are Sub procedures that apply to a specific object; they run in response to a user or system event, such as a
mouse click. Event procedures are always stored with a form or report in the form or report module.
General procedures
These are procedures that aren't directly associated with an object or event. You can include general procedures in
a standard module or a class module. General procedures can be either Sub procedures (procedures that don't
return a value) or Function procedures (procedures that do return a value).
Event Procedures
When Microsoft Access recognizes that an event has occurred on a form, report, or control, it automatically runs
the event procedure named for the object and event. If you want to run code in response to a particular event, you
add code to the event procedure for that event.
When you create an event procedure (by using the procedures described earlier in this chapter), Microsoft Access
automatically creates a code template for the event and adds it to the form or report module. The name of an event
procedure for a form or report is a combination of the word "Form" or "Report," an underscore (_), and the event
name. For example, if you want a form to run an event procedure when it's clicked, use the procedure
Form_Click.
An event procedure for a control uses the same naming convention. For example, if you want a command button
named MyButton to run an event procedure when it's clicked, use the procedure MyButton_Click. If a control
name contains characters other than numbers or letters, such as spaces, Microsoft Access replaces those characters
with an underscore (_) in any event procedures for the control.
Important If you want to change the names of your controls, it's a good idea to do so before you start writing
event procedures for them. If you change the name of a control after attaching a procedure to it, you also must
change the name of the procedure to match the control's new name. Otherwise, Visual Basic can't match the
control to the procedure. When a procedure name doesn't match a control name, Microsoft Access makes it a
general procedure. You can find general procedures in the Module window by clicking (General) in the Object
box, and then clicking the procedure name in the Procedure box.
General Procedures
Microsoft Access runs event procedures in response to a particular event on a form, report, or control. A general
procedure, in contrast, runs only when you explicitly call it. A function, for example, is a type of general
procedure.
Why use general procedures? One reason is to create your own functions to automate tasks you perform
frequently. For example, you can create a function and then either create a custom menu command or custom
toolbar button that runs the function, or use the function in an expression.
Another reason to use general procedures is that you may want several different event procedures to perform the
same actions. A good programming strategy is to put common code in a separate general procedure and have
event procedures call it. This eliminates the need to duplicate code, making the application easier to maintain.
You can create general procedures either in a form or report module or in a standard module. If you want a
general procedure that's always available from anywhere in your application, place it in a standard module. If a
procedure applies primarily to a specific form or report, place it in the module for that form or report.
29
Sub procedures perform operations, but they don't return a value and can't be used in expressions. Sub
procedures can accept arguments. An event procedure is a Sub procedure that's attached to a form or report. When
Microsoft Access recognizes that an event has occurred on a form, report, or control, it automatically runs the
event procedure named for the object and event. For example, you can write an event procedure that sets the focus
to a specified control when the user exits another control.
Function procedures return a value, such as the result of a calculation. Because they return values, Function
procedures can be used in expressions. Like Sub procedures, Function procedures can accept arguments. For
example, you can write a function that calculates the first day of the month that follows a date you pass the
function in an argument. Then you can use that function in an expression on a form or report.
Sub Procedures
The syntax for a Sub procedure is:
[Private|Public] [Static] Sub procedurename [(arguments)]
statements
End Sub
The statements are the Visual Basic statements that make up the code you want to run each time the procedure is
called. The arguments are argument names, separated by commas if there are more than one. Each argument looks
like a variable declaration and acts like a variable in the procedure. The syntax for each argument is:
[Optional] [ByVal] variablename [( )] [As type]
Type can be any of the fundamental data types: Byte, Integer, Long, Single, Double, Currency, String, Boolean,
Date, Object, or Variant. If you don't provide a type, the argument takes the Variant type and can contain any kind
of data. Parentheses after variablename indicate that the argument is an array.
By default, arguments to a procedure are passed by reference, meaning that changing the value of the variable
changes it in the calling procedure as well. To pass arguments by value rather than by reference, use the ByVal
keyword.
When you call a Sub procedure, you specify the arguments you want the procedure to use. For example, the
following Sub procedure makes a beep sound the number of times you specify with the intBeeps argument.
Sub MultiBeep(intBeeps As Integer)
Dim intX As Integer, lngY As Long
For intX = 1 To intBeeps
Beep
For lngY = 1 To 100000
' Short delay between beeps.
Next lngY
Next intX
End Sub
The following statement calls the MultiBeep Sub procedure by using an intBeeps argument of 3, making a beep
sound three times.
MultiBeep 3
You don't enclose arguments in parentheses when you call a Sub procedure, as you do when you declare one.
Note To make your code more readable, you can pass arguments to Sub or Function procedures by name. For
example, the following call to the MultiBeep Sub procedure passes the intBeeps argument by name:
30
MultiBeep intBeeps:=3
When you pass multiple arguments by name, you can include them in any order you want. For more information
on passing arguments by name, search the Help index for "named arguments."
Function Procedures
The syntax for a Function procedure is:
[Private|Public] [Static] Function procedurename [(arguments)] [As type]
statements
End Function
The arguments for a Function procedure work in exactly the same way as the arguments for a Sub procedure, and
have the same syntax. Function procedures differ from Sub procedures in three ways:
You enclose arguments in parentheses both when declaring and when calling a Function procedure.
Function procedures, like variables, have data types that determine the type of the return value.
You return a value by assigning it to the procedurename itself. The value returned by the Function procedure can
then be used as part of a larger expression.
For example, you could write a Function procedure that calculates the third side, or hypotenuse, of a right triangle
given the other two sides.
Function Hypotenuse (dblA As Double, dblB As Double) As Double
Hypotenuse = Sqr(dblA ^ 2 + dblB ^ 2)
End Function
You call a Function procedure the same way you call any of the built-in functions in Visual Basic. For example:
dblResult = Hypotenuse(dblWidth, dblHeight)
Tip If you aren't interested in the result of a Function procedure, you can call it without including parentheses or
assigning it to a variable, as you would a Sub procedure. For example, you can use the following code to call a
function called DisplayForm and ignore its return value:
DisplayForm strMessage
Using a Variable Number of Arguments
You can declare optional arguments in a procedure definition with the Optional keyword. An optional argument is
one that doesn't have to be passed every time you call the procedure. You must declare optional arguments after
any required arguments in the argument list. They can be of any type.
If you include an optional argument in a procedure definition, then you need to consider what happens in the
procedure when the argument is not passed. You can initialize an optional argument to a default value when you
declare the argument, so that if the optional argument is not included when the procedure is called, the default
value is used. If you don't initialize the argument to a default value, Microsoft Access initializes it as it would
initialize a variable of that type. If the argument is a number type, then it is initialized to zero. If it is a string, then
it is initialized to a zero-length string ("").
31
In the following example, if a value is not passed for the optional argument, this argument is assigned the default
value of 100.
Sub DisplayError(strText As String, Optional intNumber As Integer = 100)
If intNumber = 100 Then
MsgBox strText
Else
MsgBox intNumber & ": " & strText
End If
End Sub
You can call the procedure with either of the following lines of code.
DisplayError "Invalid Entry"
DisplayError "Invalid Entry", 250
Note If an optional argument is of type Variant, then you can use the IsMissing function to determine whether an
optional argument was included when the procedure was called. The IsMissing function only works with
arguments of type Variant.
To write a procedure that accepts an arbitrary number of arguments, use the ParamArray keyword to pass an array
of arguments with the Variant data type. With the ParamArray keyword, you can write functions like Sum, which
calculates the sum of an arbitrary number of arguments.
Function Sum(ParamArray varNumbers() As Variant) As Double
Dim dblTotal As Double, var As Variant
For Each var In varNumbers
dblTotal = dblTotal + var
Next var
Sum = dblTotal
End Function
You can call the Sum function with the following line of code.
dblSum = Sum(1, 3, 5, 7, 8)
Calling Procedures from Other Modules
Unless you specify otherwise, general procedures you create are public, meaning that you can call them from
anywhere in your application.
Tip If you know you will use a procedure only within its module, you should declare it with the Private keyword
to avoid confusion and to speed up compilation of your code. Event procedures are always declared with the
Private keyword, because they normally apply only to the form or report in which they are stored.
When you call a procedure that isn't in the current module, Microsoft Access searches other modules and runs the
first public procedure it finds that has the name you called. If the name of a public procedure isn't unique in the
database, you can specify the module it's in when you call the procedure. For example, to run a Sub procedure
called DisplayMsg that's stored in a module called Utility, you use the following code:
Utility.DisplayMsg
You can call procedures in a class module from other modules as well. To do this, specify the name of the class
module along with the procedure name. For example, to run a Function procedure called AddValues in a class
module called Class1 and print the result to the Debug window, use the following code:
Debug.Print Class1.AddValues
32
Because form and report modules are also class modules, you call a procedure in a form or report module in the
same way. To call a procedure in a form or report module, specify the name of the form or report module along
with the procedure name. The name of the form or report module includes the qualification Form_ or Report_
followed by the name of the form or report. For example, to run a Sub procedure called DisplayRecords that's
stored with the Orders form, use the following code:
Form_Orders.DisplayRecords
Alternatively, you can call a procedure in a class module or a form or report module by referring to an object
variable that points to an instance of either the class or the form or report. For example, the following code opens
an instance of the Orders form, and then runs the DisplayRecords procedure.
Dim frmOrders As New Form_Orders
' Declare an object variable.
frmOrders.Visible = True
' Open and display the Orders form.
frmOrders.DisplayRecords
' Call the form's procedure.
.
Set frmOrders = Nothing
' Close the Orders form.
By storing the DisplayRecords procedure in the Orders form module and making it public, you in effect create a
custom method of the Orders form.
Sub DisplayRecords
' This procedure can be called from another form
' to cause the Orders form to update itself.
.
.
.
End Sub
Using Variables
Often you store values temporarily when performing calculations with Visual Basic. For example, you may want
to calculate several values, compare them, and perform different operations on them, depending on the result of
the comparison. You want to retain the values so you can compare them, but because you need to store them only
while your code is running, you don't want to store them in a table.
Visual Basic uses variables to store values. Variables are like fields except that they exist within Visual Basic
rather than in a table. Like a field, a variable has a name, which you use to refer to the value the variable contains,
and a data type, which determines the kind of data the variable can store. Before you use a variable, it's a good
idea to declare it with a Dim statement, which tells Microsoft Access to set aside space for the variable.
For example, in the following procedure, dtmAny, dtmYear, and dtmMonth are variables with the Date data type.
Function DueDate(dtmAny As Date)
Dim dtmYear As Date, dtmMonth As Date
dtmYear = Year(dtmAny)
dtmMonth = Month(dtmAny) + 1
DueDate = DateSerial(dtmYear, dtmMonth, 1)
End Function
Naming Conventions
While you are writing your Visual Basic code, you declare and name many elements (Sub and Function
procedures, variables and constants, and so forth). The names of the procedures, variables, and constants you
declare in your Visual Basic code must:
33
IF Then Else
To run code conditionally, use the following statements:
If...Then
If...Then...Else
Select Case
To run one or more lines of code repetitively, use the following statements:
Do..Loops
Do...Loop
For...Next
While...Wend
Using the DoCmd Object to Perform Macro Actions
Many common actions you perform in an application don't have a corresponding command in the Visual Basic
language. To perform the equivalent of a macro action, use methods of the DoCmd object. The syntax for the
DoCmd object is:
[Application.]DoCmd.method [arguments]
Replace method with the name of a macro action. How many and what kind of arguments come after method
depends on the specific macro action you want to run. Specifying the Application object is optional; you can start
a line with the DoCmd object.
For example, the Close method, which corresponds to the Close action, takes two arguments that specify the type
and name of the database object you want to close. You use commas to separate the arguments when a method
takes multiple arguments.
DoCmd.Close acForm, "Add Products"
34
The first argument, acForm, is a Microsoft Access intrinsic constant specifying that the object to be closed is a
form. Microsoft Access automatically declares a number of intrinsic constants that you can use to represent a
variety of objects, actions, or data types. For example, you often use intrinsic constants with methods of the
DoCmd object to specify action arguments that you can enter in the lower part of the Macro window.
Some methods of the DoCmd object take optional arguments. If you leave these arguments unspecified, Microsoft
Access uses their default values. For example, if you leave both arguments for the Close method unspecified,
Microsoft Access closes the active database object (whatever it may be).
DoCmd.Close
If you omit an optional argument but specify an argument that follows that argument, you must include a comma
as a placeholder for the omitted argument. For example, the syntax for the MoveSize method is:
DoCmd.MoveSize [right] [, down] [, width] [, height]
The following code uses the default (current) settings for its right and down arguments, while using the specified
values for its width and height arguments.
DoCmd.MoveSize , , 5000, 3000
You can use methods of the DoCmd object to perform most macro actions, including the RunMacro action (which
runs an existing macro as if it were a procedure). However, eight macro actions have no equivalent methods of the
DoCmd object. In most cases, Visual Basic provides equivalent functionality with built-in statements or functions.
Using the RunCommand Method to Perform Menu Commands
Occasionally, you may want your application to perform a command that's on a Microsoft Access menu or
toolbar. To perform a built-in command just as if the user clicked it, use the RunCommand method. The syntax for
the RunCommand
method is:
RunCommand command
Command is a constant that corresponds to the Microsoft Access command you want to run. For example, the
following line of code performs the Options command, causing Microsoft Access to display the Options dialog
box:
RunCommand acCmdOptions
35
procedure. For example, you can use the Command Button Wizard to create a button that opens a form, and then
you can edit the event procedure so that it opens the form for read-only access instead of for editing data.
The event procedure that the Command Button Wizard creates includes simple Visual Basic statements that
perform the action you specify. In addition, the wizard adds a simple method of error handling to respond
appropriately to run-time errors. If an error occurs, the error-handling code displays the Microsoft Access error
message and then exits the event procedure. The following illustration shows the event procedure the Command
Button Wizard creates for a command button that opens a form.
To change or enhance the way the button works, you edit the lines of code that are highlighted in the previous
illustration. Unless you want to change the way the event procedure handles errors, you don't need to edit any
other lines in the procedure.
Using Variables to Refer to Object Names
You may have noticed that the Command Button Wizard's event procedure uses a Dim statement to declare a
variable that holds the name of the form the event procedure opens. A variable is a placeholder that temporarily
stores data, such as the name of an object or the value in a field, while your procedure runs. Once you declare a
variable and assign it a value, you can use the variable throughout your procedure to refer to the value.
' Creates a variable that can hold a string value.
Dim strDocName As String
' Assigns the form name to it.
strDocName = "ProductsPopup"
' Opens the ProductsPopup form.
DoCmd.OpenForm strDocName
You don't have to use a variable to open a form. The following line of code performs the exact same action as the
previous three lines.
DoCmd.OpenForm "ProductsPopup"
If you refer to a form only once in a procedure, it's probably easiest to just use the form name directly, as in the
preceding line of code. Use a variable when you want to perform more than one operation on an object. In these
cases, using a variable makes your code easier to write and maintain. If you want to change the form that the
procedure opens, for example, you just change the form name once in the statement that assigns it to the variable.
Customizing a Command Button That Opens a Form
Customers sometimes ask Northwind sales representatives for further information about a product on their order.
You can provide a command button on the Orders form to open a pop-up form that shows details about the
product selected in the Orders subform.
Step One: Create the button that opens the form The Orders sample application includes a form named
ProductsPopup that displays details about Northwind products. You can open the Orders form in Design view and
use the Command Button Wizard to add a command button named Details that opens the ProductsPopup form.
The Command Button Wizard creates the command button and writes an event procedure for the command
button's Click event.
36
However, the pop-up form displays details about a value on the subform, not the main form. Because the wizard
can't filter based on a subform value, you'll need to modify the command button's event procedure yourself so the
pop-up form shows information from the correct product.
Step Two: Open the event procedure In Design view, right-click the command button and then click Build Event
on the shortcut menu. Microsoft Access opens the form module and displays the button's Click event procedure.
Step Three: Change the code so it filters records You want to filter the records in the ProductsPopup form to
show the record whose ProductID is equal to the ProductID of the current record in the Orders subform. To do
this, you need to apply a filter to the form when you open it.
The Command Button Wizard automatically creates a variable in the Click event procedure that makes it easy for
you to set a filter for the form you're opening. The following line of code sets the strLinkCriteria variable to the
appropriate filter string.
strLinkCriteria = "ProductID = Forms!Orders!OrdersSubform!ProductID"
Add this line of code to the Click event procedure that opens the form, as shown in the following example:
Dim strDocName As String
Dim strLinkCriteria As String
strDocName = "ProductsPopup"
strLinkCriteria = "ProductID = Forms!Orders!OrdersSubform!ProductID"
DoCmd.OpenForm strDocName, , , strLinkCriteria
This event procedure passes the filter string you added (strLinkCriteria) as the wherecondition argument of the
OpenForm method that opens the form. Microsoft Access then automatically sets a filter on the ProductsPopup
form and makes the form display the correct record.
Compile your edited code and test it to make sure it works as expected. Now when you click the command button
on the Orders form, the ProductsPopup form opens, showing values in the record for the current product in the
Orders subform.
Note When the Details_Click event procedure opens the ProductsPopup form, the ProductsPopup form becomes
the active form. In addition to the code that opens the ProductsPopup form, the Details_Click event procedure
contains code that moves the focus from the pop-up form to the Orders form, so that sales representatives can
continue filling out the order without having to click the Orders form. This code uses the SetFocus method, first to
set the focus to the Orders form, and then to set the focus to the Orders subform rather than to the Details
command button (the last control on the form to have the focus). To see this code, open the Orders form in Design
view. Right-click the Details command button, and then click Build Event on the shortcut menu.
Keeping Two Forms Synchronized
You can keep the ProductsPopup form synchronized with the Orders form, so that when you move from record to
record in the Orders subform, the ProductsPopup form always shows details about the current product in the
subform. To do this, write an event procedure for the subform's Current event that sets the pop-up form's Filter
property. (You can also do this by writing a macro you specify as the subform's OnCurrent event property setting.)
To see this event procedure in the Orders sample application, look at the Form_Current event procedure for the
Orders subform. Open the Orders subform in Design view, and then click Code on the View menu to open its
form module. In the Object box, click Form, and then click Current in the Procedure box.
37
For example, after a user selects a customer for an order, you may want to fill in the controls that contain the
shipping address with the customer's address. You do this by assigning values to the shipping controls. Or you
may want to disable some controls on the form in response to values the user enters in other controls. You do this
by setting the Enabled property of each control to False when the user enters the values.
In a macro, use the SetValue action, as shown in the following illustrations.
If the control to which you assign a value is located on a form or report other than the one that runs the macro,
you must enter its full identifier as the Item argument.
If you want to use Visual Basic to assign a value to a control or property, use a simple assignment statement. To
refer to a control that is located on the form or report from which you're running the event procedure, you don't
need to use its full identifier.
ShipCity = City
Details.Enabled = False
To refer to a control on a different form or report, use its full identifier.
Forms!ShipForm!ShipCity = City
Note Although properties with more than one word appear in the property sheet with spaces between words, you
must concatenate them when you refer to them in Visual Basic. For example, to refer to the RecordSource
property, which appears in the property sheet as Record Source, you would use the following line of code:
Forms!Orders.RecordSource = "OrdersQuery"
Disabling a Command Button at Run Time
As described previously in this chapter, the Orders form in the Orders sample application includes a command
button named Details that opens the ProductsPopup form. This pop-up form shows details about the current
product in the Orders subform. The Details button works fine as long as the ProductID field in the current record
of the subform contains a value. But if the user clicks in the new record of the Orders subform or starts a new
order, then the wherecondition argument in the button's DoCmd.OpenForm statement returns no record, and the
ProductsPopup form opens with no record in it. To avoid having the ProductsPopup form show no information,
you can disable the Details button when there's no value in the ProductID field for the current record of the
subform, and enable it when there is a value in the ProductID field.
For example, the Current event of the Orders subform occurs when the Orders form first opens, every time the
focus moves from one record to another on the subform, and every time you requery the subform. You can write
an event procedure for the Current event that disables the Details button when there's no value in the ProductID
field for the current record of the subform, and enables it when there is one.
Private Sub Form_Current()
If IsNull(ProductID) Then
Me.Parent!Details.Enabled = False
Else
Me.Parent!Details.Enabled = True
End If
End Sub
The statements in this event procedure use the Parent property of the Orders subform to refer to the Orders form.
If there's no value in the ProductID field in the subform, the first statement sets the Enabled property of the
Details button on the Orders form to False (the same as No in the property sheet). If there is a value in the field,
the second statement sets the Enabled property to True (the same as Yes in the property sheet).
38
When you use the Parent property to refer to the main form from an event procedure attached to a subform, the
code runs only when the main form is open. If you open the subform by itself as a separate form, the code that
refers to the main form doesn't work.
Note As described previously in this chapter, the Form_Current event procedure for the Orders subform also
contains code that updates the record values in the ProductsPopup form to match the current record in the
subform.
39
When you use this technique, Microsoft Access suspends execution of the code or macro until the dialog box
form is hidden or closed.
Tip If you simply want to display a short message to the user or prompt the user for short text input, the easiest
approach is to use a message box or input box instead of a pop-up form. For more information on displaying a
message in a message box, see "Using a Message Box to Display a Message" later in this chapter. For more
information on displaying an input box to prompt the user for text input, search the Help index for "InputBox
function."
Creating a Pop-up Form to Display Details About an Item in a Subform
The Orders form in the Orders sample application includes a command button named Details that opens a form
showing details about the current product in the subform.
Because you want users to be able to continue filling out an order while answering customers' questions about the
product, you need the form opened by the Details button to stay on top of the Orders form.
Step One: Create the pop-up form Use the Form Wizard to create the pop-up form, specifying the product,
supplier, and category information you want to display. Name the form ProductsPopup.
Step Two: Set the form's properties Open the form in Design view and set properties to make the form look and
work like a dialog box. The following table shows the property settings for the form.
Because users can't edit data on this form, a shortcut menu isn't needed.
Note Menu commands the user chooses when a pop-up form is open don't apply to the pop-up form. For
example, when the ProductsPopup form is open on top of the Orders form, menu commands can perform
operations on the Orders form and its data, but they don't affect the ProductsPopup form. A pop-up form can't
have a custom menu bar associated with it.
40
Northwind sales representatives frequently print an invoice for an order immediately after taking the order. You
can create a dialog box to make it easier for the sales representatives to print the invoice.
Users open the dialog box using a button on a custom toolbar attached to the Orders form.
Step One: Create the query for the list box If the list box will display values from more than one table, you must
first create a query that displays the appropriate rows. Create a query called OrderList that includes three fields
from the Customers and Orders tables, as shown in the following illustration.
Step Two: Create the dialog box and the list box Create a blank form and add an unbound list box to it using the
List Box Wizard. Specify the OrderList query as the row source for the list box, and include all three fields as
columns in the list box. Select OrderID as the list box's bound column. Adjust the column widths so the data in all
three columns is visible at the same time. The List Box Wizard creates the list box and sets its properties. The
following table shows some of the properties for the list box in the Orders sample application.
Step Three: Create buttons for the dialog box Add three command buttons to the form using the Command
Button Wizard: a Preview button, a Print button, and a Cancel button. For the first button, tell the wizard that you
want it to preview the Invoice report. Tell the wizard to display the text "Preview" instead of a picture on the
button. Follow the same steps to create a Print button that prints the report and a Cancel button that closes the
form.
Step Four: Modify the event procedures that the wizard creates When the user chooses to preview or print the
report, you want to hide the dialog box before opening the report. Additionally, you want the report to print only
the record the user selects in the dialog box.
Open the event procedures for the Preview and Print buttons. In the procedures, hide the dialog box by setting its
Visible property to False. To filter records in the report so only the selected record is included, pass the criteria
identifying the record in the wherecondition argument of the OpenReport method.
The event procedure for the Preview command button with the new lines of code is:
Private Sub Preview_Click()
On Error GoTo Err_Print_Click
Dim strDocName As String
Dim strLinkCriteria As String
Me.Visible = False
strDocName = "Invoice"
strLinkCriteria = "OrderID = " & OrderID
DoCmd.OpenReport strDocName, acViewPreview, , strLinkCriteria
.
.
.
End Sub
The event procedure for the Print command button is identical, except that it passes the acViewNormal constant
(instead of acViewPreview) to the OpenReport method.
Note If you prefer, you can use macros instead of event procedures to make the dialog box work. In the macros
for the Preview and Print buttons, use the SetValue action to set the dialog box's Visible property to No, and use
the OpenReport action to preview or print the report, setting the Where Condition argument as follows:
OrderID = Forms!PrintInvoiceDialog!OrderID
In the macro for the Cancel button, use the Close action to close the dialog box. In the macro for the list box's
DblClick event, use the RunMacro action to run the Preview button's macro.
41
Step Five: Set properties to make the form behave like a dialog box Set the form's PopUp and Modal properties
to Yes, and then save the form with the name PrintInvoiceDialog.
Prevents users from working with other objects while the dialog box is visible.
Displays the form with thick borders that can't be resized and with no Minimize or Maximize buttons. (This is the
standard border style for a dialog box.)
Step Six: Create the macro that opens the dialog box Create a macro to open the dialog box. Add two actions to
it: an OpenForm action that opens the PrintInvoiceDialog form, and a SetValue action that sets the OrderID list
box on the form to the ID number of the current order on the Orders form. Save the macro with the settings as
shown in the following illustration, and name it PrintInvoice.
[Forms]![Orders]![OrderID]
Step Seven: Add a toolbar button to run the macro Create a custom toolbar for the Orders form, and add a button
that runs the PrintInvoice macro.
When a user clicks the button, the macro opens the dialog box. If you want, you can also include the Print Invoice
command on a custom menu bar for the form. To see an example of a custom menu bar, use the Customize dialog
box (View menu, Toolbars submenu) to view the OrdersMenuBar toolbar in the Orders sample application.
Tip To make the dialog box even easier to use, you can write an event procedure that lets users double-click an
order in the list box to choose the order and the Preview button at the same time. To do this, write an event
procedure for the list box's DblClick event that calls the Preview command button's Click event procedure, as
follows:
Private Sub OrderID_DblClick(Cancel As Integer)
Preview_Click
End Sub
Using a Message Box to Display a Message
When you want to display a brief message such as an error, warning, or alert, you can use a predefined dialog box
called a message box. You specify the message to display and when the message box is to appear. The following
illustration shows a typical message box.
Note When a user has the Office Assistant displayed, message box information displays in a speech balloon
associated with the Assistant. If the Office Assistant is hidden or wasn't installed, a message box displays as a
dialog box, as shown in the preceding illustration.
You can use an action or a function to display a message box. The following table describes each method.
Display a message in a message box using a macro.
MsgBox function
Display a message in a message box using Visual Basic. It can return a value to Visual Basic indicating which
command button a user has clicked.
To display the message box in the previous illustration using a macro, you add a MsgBox action to the macro and
set the arguments as shown in the following table.
To display the same message with Visual Basic, use the following code:
42
43
Apply Filter/Sort
Remove Filter/Sort
Sort Ascending
Sort Descending
Using these commands, users of your application can easily filter and sort records themselves. When a user
applies a filter or sort order using these commands, Microsoft Access sets the Filter, FilterOn, OrderBy, and
OrderByOn properties for the form accordingly, and requeries records on the form.
Note When a user changes the filter or sort order of a form and then closes the form, Microsoft Access saves this
information. The last sort order saved is reapplied automatically the next time the form is opened, and the user can
reapply the last filter saved by clicking the Apply Filter button .
Disabling Filter and Sort Features
In some cases, you don't want users of your application to filter records. To disable the standard filtering
commands on a form, set the AllowFilters property to No.
If you want to disable sorting, or if you want to disable some filtering features while allowing others, create
custom menu bars and toolbars for your form that include the commands and buttons you want and leave off the
ones you don't want to make available.
Responding to Filter Events
If you want to change the way that standard filtering and sorting commands work, you can write event procedures
for the Filter and ApplyFilter events. For example, you may want to display a message each time a user uses the
Filter By Form or the Advanced Filter/Sort command for a form to remind the user to specify criteria and then
apply the filter.
If you want to display a message or take other action when a user uses the Filter By Form or Advanced Filter/Sort
command, write an event procedure for the Filter event. To help you respond to each command, the Filter event
procedure has a FilterType argument that tells you which of these commands was selected.
If you want to cancel the filtering command the user chose, you can set the Cancel argument for the event
procedure to True.
The following event procedure compares the FilterType argument to the acFilterByForm and acFilterAdvanced
constants to display a different message to the user depending on which filtering command was chosen.
Private Sub Form_Filter(Cancel As Integer, FilterType As Integer)
Dim strMsg As String
Select Case FilterType
Case acFilterByForm
strMsg = "Specify the records you want to see by choosing "
strMsg = strMsg & "from the lists, then click Apply Filter."
Case acFilterAdvanced
strMsg = "Drag fields to the filter design grid, specify "
strMsg = strMsg & "criteria and sort order, then click "
strMsg = strMsg & "Apply Filter."
End Select
MsgBox strMsg
' Display the message.
End Sub
44
If you want to display a message or take other action when a user applies or changes a filter, write an event
procedure for the ApplyFilter event. This event occurs whenever a user chooses the Apply Filter/Sort, Remove
Filter/Sort, or Filter By Selection command, and whenever the user closes the Filter window without applying the
filter. The ApplyFilter event procedure has an ApplyType argument that tells you which of these actions was taken
so you can respond in different ways.
If you want to cancel the filtering command the user chose, you can set the Cancel argument for the event
procedure to True, and the filter won't be applied.
The following event procedure displays a message if the user is applying a filter. The message shows the setting
of the Filter property and gives the user a chance to cancel the operation.
Private Sub Form_ApplyFilter(Cancel As Integer, ApplyType As Integer)
Dim strMsg As String, intResponse As Integer
If ApplyType = acApplyFilter Then
strMsg = "You've chosen to filter for the following criteria:"
strMsg = strMsg & vbCrLf & Me.Filter
End If
' Display the message box and get an OK or Cancel response.
intResponse = MsgBox(strMsg, vbOkCancel + vbQuestion)
If intResponse = vbCancel Then Cancel = True
End Sub
Customizing the Standard Filtering Interface
When a Northwind sales representative starts the Orders application, the Orders form opens for data entry (no
existing records are available). To find an existing order in the database, the employee clicks the Filter Orders
button on the Orders toolbar.
You want the filtering interface on the Orders form to be as straightforward as possible. Because most fields aren't
appropriate for setting criteria, you don't want the employee to worry about them. To make them less obtrusive,
you can disable all the fields except those you expect a user to set criteria for.
The Filter event procedure for the Orders form disables all but three fields (BillTo, EmployeeID, and OrderDate).
Private Sub Form_Filter(Cancel As Integer, FilterType As Integer)
' If Filter By Form, disable all but three fields.
If FilterType = acFilterByForm Then
BillTo.SetFocus
Dim ctl As Control
For Each ctl In Me.Controls
Select Case ctl.ControlType
Case acTextBox, acComboBox, acOptionGroup, acSubForm
Select Case ctl.Name
Case "BillTo", "EmployeeID", "OrderDate"
Case Else
ctl.Enabled = False
End Select
End Select
Next ctl
DoCmd.ShowToolbar "Orders Form Toolbar", acToolbarNo
End If
End Sub
If you set properties in the Filter event procedure, be sure to reset them in the ApplyFilter event procedure.
45
Combo Boxes
46
OrderID = [Forms]![PrintInvoiceDialog]![OrderID]
The wherecondition argument is applied only by the event procedure or macro specified for the OnClick event of
the button that runs the OpenForm or OpenReport method or action. This gives you the flexibility of using any
number of different dialog boxes to open the same form or report and applying different sets of criteria depending
on what the user wants to do. For example, the user may want to print an invoice for a certain customer or view
orders only for a certain product. If users open the form or report in the Database window rather than through
your dialog box, however, no criteria are applied to the query and all its records are displayed or printed. To
prevent this, you can hide the Database window by clearing the Display Database Window check box in the
Startup dialog box (Tools menu).
You can use the wherecondition argument to set criteria for more than one field, but if you do, the argument
setting quickly becomes long and complicated. In those situations, specifying criteria in a query may be easier.
Using a Query as a Filter
A separate query, sometimes called a filter query, can refer to the controls on your dialog box to get its criteria.
Using this approach, you filter the records in a form or report by setting the filtername argument of the OpenForm
or OpenReport method or action to the name of the filter query you create. The filter query must include all the
tables in the record source of the form or report you're opening. Additionally, the filter query must either include
all the fields in the form or report you're opening, or you must set its OutputAllFields property to Yes.
For example, to create a filter query for the Invoice report, make a copy of the report's underlying query and save
it under another name. Then, add criteria to the OrderID field in the filter query that refers to the control on the
dialog box. (If the filter query's OutputAllFields property is set to Yes, this is the only field you need to include in
the filter query as long as you include all the tables that contain fields on the report.)
After you create and save the query you'll use as a filter, set the filtername argument of the OpenReport method or
action to the name of the filter query. The filtername argument applies the specified filter query each time the
OpenReport method or action runs.
Using a query as a filter to set the criteria has advantages similar to using the wherecondition argument of the
OpenForm or OpenReport method or action. A filter query gives you the same flexibility of using more than one
dialog box to open the same form or report and applying different sets of criteria depending on what a user wants
to do. If users open the form or report in the Database window rather than through your dialog box, however, no
criteria are applied to the query and all its records are displayed or printed. To prevent this, you can hide the
Database window by clearing the Display Database Window check box in the Startup dialog box (Tools menu).
Directly Referring to Dialog Box Controls in a Form or Report's Underlying Query
You can also refer to the dialog box controls directly in the form or report's underlying query instead of through
the arguments of the OpenForm or OpenReport method or action. For example, instead of referring to the control
on the PrintInvoiceDialog form in a filter query's criteria as shown in the previous illustration, you can set the
exact same criteria in the Invoice report's underlying query, Invoices.
Using this approach, the OpenForm or OpenReport method or action requires no wherecondition or filtername
argument. Instead, each time you open a form or report, its underlying query looks for the dialog box to get its
criteria. However, if a user opens the form or report in the Database window rather than through your dialog box,
Microsoft Access displays a parameter box prompting the user for the dialog box value. To prevent this, you can
hide the Database window by clearing the Display Database Window check box in the Startup dialog box (Tools
menu).
Changing the Filter or Sort Order of a Form or Report
47
After a form or report is open, you can change the filter or sort order in response to users' actions by setting form
and report properties in Visual Basic code or in macros. For example, you may want to provide a menu command
or a toolbar button that users can use to change the records that are displayed. Or you may have an option group
control on a form that users can use to select from common sorting options.
To set the filter of a form or report, set its Filter property to the appropriate wherecondition argument, and then set
the FilterOn property to True. To set the sort order, set the OrderBy property to the field or fields you want to sort
on, and then set the OrderByOn property to True. If a filter or sort order is already applied on a form, you can
change it simply by setting the Filter or OrderBy properties.
When you apply or change the filter or sort order by setting these properties, Microsoft Access automatically
requeries the records in the form or report. For example, the following code changes the sort order of a form
based on a user's selection in an option group:
Private Sub SortOptionGrp_AfterUpdate()
Const conName = 0, conDate = 1
Select Case SortOptionGrp
Case conName
Me.OrderBy = "LastName, FirstName" ' Sort by two fields.
Case conDate
Me.OrderBy = "HireDate DESC"
' Sort by descending date.
End Select
Me.OrderByOn = True
' Apply the sort order.
End Sub
Whether the filter and sort order get set in code or by the user, you can apply or remove them by setting the
FilterOn and OrderByOn properties to True or False. For example, you could add a button to a report's custom
toolbar that runs the following macro to apply or remove a filter you specified when opening the report.
Synchronizing Records by Changing the Filter
The Orders sample application controls which record appears in the ProductsPopup form by setting the form's
Filter property. You can keep the ProductsPopup form synchronized with the Orders form; this will make sure the
ProductsPopup form always shows details about the current product when you move from record to record in the
Orders subform.
To do this, write an event procedure for the subform's Current event that sets the pop-up form's Filter property.
(You can also do this by writing a macro you specify as the subform's OnCurrent event property setting.) The
following code example shows the event procedure for the Current event of the Orders subform. This event
procedure uses the IsLoaded function from the UtilityFunctions module that is included with the Orders sample
application.
Private Sub Form_Current()
.
.
.
If IsLoaded("ProductsPopup") Then
' If there's no current product record, display a blank pop-up window,
' otherwise filter to show the current product.
If IsNull(ProductID) Then
strFilter = "ProductID = 0"
Else
Forms!ProductsPopup.Filter = "ProductID = " & ProductID
End If
Forms!Orders.SetFocus
' Set focus back to Orders subform.
Forms!Orders!OrdersSubform.SetFocus
48
End If
.
.
.
End Sub
Each time a user changes records in the subform, the procedure resets the Filter property of the pop-up form,
causing it to display the corresponding record.
49
Step Two: Assign a control a value from a table The UnitPrice control in the Orders subform is bound to the
UnitPrice field in the Order Details table. Once the user selects a product, you want to fill in the UnitPrice control
in the subform with the current value from the Products table.
Because the UnitPrice field in the Products table isn't on the form or in its record source, you can't refer to it
directly. Instead, you can use the DLookup function to look up the value in the Products table and assign it to the
UnitPrice control in the subform. Use the following event procedure for the AfterUpdate event of the ProductID
control on the subform:
Private Sub ProductID_AfterUpdate()
.
.
.
' Look up product's price and assign it to the UnitPrice control.
strFilter = "ProductID = Forms!Orders!OrdersSubform!ProductID"
Me!UnitPrice = DLookup("UnitPrice", "Products", strFilter)
.
.
.
End Sub
The DLookup function has three arguments. The first specifies the field you're looking up (UnitPrice); the second
specifies the table (Products); and the third specifies which value to find (the value for the record where the
ProductID is the same as the ProductID on the current record in the Orders subform).
Note Because each call to the DLookup function causes Microsoft Access to run a query, you may want to limit
how often you use it. You can often avoid using the DLookup function by including the field you want to look up
in the underlying query or record source for the form or report. In the previous example, you could include the
UnitPrice field from the Products table in the record source for the subform. Because it would involve joining the
two tables, this strategy would cause the subform to open more slowly; however, it would allow you to look up
the UnitPrice value much more quickly and easily than with the DLookup function.
Using AutoLookup to Fill In Customer Information on the Orders Form
The Orders form in the Orders sample application displays the customer's billing address and contact information,
which is stored in the Customers table. By including the fields from the Customers table in the underlying record
source for the Orders form, you can have the billing information fill in automatically when the user selects the
customer from the BillTo combo box.
If the Orders and Customers tables are set up correctly, it's easy to create this type of form with the Form Wizard.
Because the Orders table includes a Lookup field that relates data in the two tables, the Form Wizard sets up the
lookup automatically. When the wizard adds the CustomerID Lookup field to the form, it creates a combo box
control that displays the customer's name, but is bound to the CustomerID field in the Orders table.
When you create the form, just tell the Form Wizard to include all fields in both the Orders and Customers tables.
After you create the form with the Form Wizard, resize and arrange the fields as you see fit. Additionally, rename
the CustomerID Lookup field (and its label) to BillTo, so you can distinguish it from the CustomerID field bound
to the Customers table.
Because the Customers table is on the "one" side of the one-to-many relationship between the Customers and
Orders tables, you don't have to do anything special to make the text boxes display the information from the
Customers table. When the user picks a customer from the combo box, which is bound to the CustomerID field in
50
the Orders table, Microsoft Access uses AutoLookup to automatically look up the information in the Customers
table and display it in the text boxes. If users change the data in the customer fields, their changes are saved in the
Customers table when they save the order.
Because all the customer fields are included on the Orders form, users can add or modify customer information
directly on the Orders form, without having to go to a separate Customers form.
Note To create this form without the Form Wizard, create a query that includes all the fields from the Orders and
Customers tables. Then, create a form based on the query and add the fields from both tables.
Additionally, you can use AutoLookup even if you don't have a Lookup field in the underlying table by adding a
combo box bound to the CustomerID field in the Orders table, and then setting its properties so that it displays
customer names.
51
52
CustomerID.SetFocus
' Enable the other customer information controls.
Address.Enabled = True
City.Enabled = True
Region.Enabled = True
City.Enabled = True
PostalCode.Enabled = True
Country.Enabled = True
ContactName.Enabled = True
ContactTitle.Enabled = True
Phone.Enabled = True
Fax.Enabled = True
MsgBox "Enter the new customer's ID, address, and contact information."
' Continue without displaying default error message.
Response = acDataErrContinue
Else
' Display the default error message.
Response = acDataErrDisplay
End If
Else
' User has already picked a customer; display a message and undo the field.
strMsg = "To modify this customer's company name, edit the name in the "
strMsg = strMsg & "box below the Bill To combo box. To add a new customer, "
strMsg = strMsg & "click Undo Record on the Records menu and then type the "
strMsg = strMsg & "new company name in the Bill To combo box."
MsgBox strMsg
BillTo.Undo
' Continue without displaying default error message.
Response = acDataErrContinue
End If
End Sub
In this code, you use the MsgBox function to ask if the user wants to add a new customer. If the user chooses Yes,
the event procedure uses the Undo method to remove the text from the combo box. It then uses the NewData
argument to assign the text the user entered in the combo box to the CompanyName control and the ShipName
control. With the value cleared from the combo box, you can move the focus down to the customer controls.
Because the user can't use the Orders form to change the CustomerID for an existing customer, the CustomerID
control is normally locked, disabled, and displayed with a background that matches the form's background, so it
doesn't look like a control the user can edit. Now that the user is entering a new customer, your code unlocks and
enables the control, and displays it with a white background and borders. It also enables the other customer
information controls. An event procedure for the form's AfterUpdate event, which occurs after the record is saved,
locks and disables the CustomerID control again, and displays it without a white background.
Note In the Orders form in the Orders sample application, all the fields from the Customers table are located on
the Orders form, so users can add a complete record for a new customer directly in the fields on the Orders form.
If you don't want to include all the fields from the underlying table on your form, you can still let users add a new
record to it. When the user wants to add a row to a combo box, display a separate form with all the fields from the
underlying table on it. After the user saves and closes this separate form, you can requery the combo box so the
new item appears in its list. For an example of this approach, see the Developer Solutions sample application.
Step Two: Write the event procedure that updates the combo box Your NotInList event procedure lets the user
add a new customer and a new order at the same time. Once the order is saved and the new customer record is in
the Customers table, you can update the BillTo combo box so it includes the new customer. You do this by writing
an event procedure for the form's AfterUpdate event that requeries the combo box using the Requery method, as
follows:
53
In the toolbox, click the Tab Control tool and then click the form where you want to place the control.
Microsoft Access adds a tab control with two pages. The Page1 tab is on top.
3 Add controls to Page1 of the tab control by clicking a tool in the toolbox, and then click on Page1 where you
want to place the control. You can add any type of control except another tab control.
Note You can also copy controls from another part of a form or from another page and paste them onto a tab
control page. However, you can't drag controls from another part of a form or from another page.
4 To add controls to Page2, click the Page2 tab, and then use the toolbox to add the controls.
5 Use the following table for other tasks you may want to perform.
Right-click the tab whose name you want to change, click Properties on the shortcut menu, and then specify a
new name in the Caption property. If you don't specify a name in the Caption property, Microsoft Access uses the
text in the Name property.
Add or delete pages, or change the page order of tabs
54
Right-click the tab, and then click Insert Page, Delete Page, or Page Order on the shortcut menu.
You can also insert a page by copying and pasting an existing page. This copies the entire page, including the
controls on it. You can also delete a page by clicking the page, and then pressing DELETE.
Change the tab order of controls on a page
Right-click the page and click Tab Order on the shortcut menu.
Change the font name, font size, or font style (weight, italic, and/or underline) of all pages
Right-click the border of the tab control, click Properties on the shortcut menu, and then set the appropriate
properties. The property settings you select apply to the fonts on all pages of the tab control.
Note The font color is determined by the Color setting for 3D Objects specified on the Appearance tab of the
Display Properties dialog box, which is available from Windows Control Panel.
6 Size the tab control as appropriate. Click each tab to make sure all the controls fit well within each tab.
Note Microsoft Access won't crop controls when you size the tab control. You may need to move controls before
you make the tab control smaller.
7 Switch to Form view and test the tab control.
Additional Tab Control and Tab Control Page Properties
You can further customize how a tab control and its pages look and work by setting their properties. Tab control
properties affect the way the tab control as a whole looks and works, and in many cases apply to all the pages
within the control. For example, you can set the TabFixedHeight and TabFixedWidth properties to set the size of
all tabs on a tab control. You can set most tab control properties in the tab control property sheet; however, some
properties can only be set or referenced by using Visual Basic. To display the tab control property sheet, rightclick the border of the tab control and click Properties on the shortcut menu.
The following table lists the most commonly used tab control properties. For information on other properties,
press F1 when the insertion point is in the property box.
Specifies whether a tab control can have more than one row of tabs. If the MultiRow property is set to No,
Microsoft Access truncates the tabs if they exceed the width of the tab control and adds a scroll bar. The default
setting is No.
Specifies whether the pages in the tab control are transparent. When set to Normal, the color of pages is
determined by the 3D Objects color specified on the Appearance tab of the Display Properties dialog box, which
is available from Windows Control Panel. When set to Transparent, the color of pages is determined by the
BackColor property of the detail section and the Picture property of the form (if any) showing through them. The
tabs in a tab control are always solid and use the 3D Objects color set in the Windows Control Panel. The default
setting is Normal.
Specifies what to display at the top of the tab control. You can display tabs, command buttons (in the same
positions as tabs), or nothing. The default setting is Tabs.
You may want to display nothing if you want to use command buttons on the form outside the tab control to
determine which page has the focus. To do this, set the tab control's Style property to None. Then add an event
procedure to the button's OnClick event that sets the tab control's Value property to the page you want to display.
Specifies the height of tabs in inches. When set to 0, each tab is tall enough to fit its contents. The minimum
height is .05 inches. The default setting is 0.
55
Specifies the width of tabs in inches. When set to 0, each tab is wide enough to fit its contents and, if there is more
than one row of tabs, the width of each tab is increased so that each row of tabs spans the width of the tab control.
If the setting is greater than 0, all tabs have an identical width as specified by this property. The minimum width is
0.5 inches. The default setting is 0.
In addition to the properties that apply to the tab control as a whole, there are also properties that apply to
individual pages. Tab control page properties affect the way a page looks and works. All tab control page
properties can be set in the page property sheet. To display the page property sheet, right-click the tab, and then
click Properties on the shortcut menu.
The following table lists the most commonly used tab control page properties. For information on other
properties, press F1 when the insertion point is in the property box.
Specifies the name of the page. Use this name when referring to a tab control page in Visual Basic. The default
name is Page1 for the first page, Page2 for the second page, and so on.
Specifies the display text that appears on a tab. If you don't specify a name in the Caption property, Microsoft
Access uses the text in the Name property.
Use to add a graphic to a tab. The graphic is displayed to the left of the tab name specified in the Caption
property. If you want to display only a picture and no name, enter a space in the Caption property.
If you're going to use a tab control in a custom dialog box, you may want to set additional properties so that your
form looks and works like a Windows dialog box.
Referring to Tab Control Objects in Visual Basic
In most ways, a tab control works like other controls on a form and can be referred to as a member of a form's
Controls collection. For example, to refer to a tab control named TabControl1 on a form named Form1, you can
use the following expression:
Form1.Controls!TabControl1
However, because the Controls collection is the default collection of the Form object, you don't have to explicitly
refer to the Controls collection. That is, you can omit the reference to the Controls collection from the expression,
like this:
Form1!TabControl1
Referring to the Pages Collection
A tab control contains one or more pages. Each page in a tab control is referenced as a member of the tab control's
Pages collection. Each page in the Pages collection can be referred to by either its PageIndex property setting
(which reflects the page's position in the collection starting with 0), or by the page's Name property setting. There
is no default collection for the TabControl object, so when referring to items in the Pages collection by their index
value, or to properties of the Pages collection, you must explicitly refer to the Pages collection.
For example, to change the value of the Caption property for the first page of a tab control named TabControl1 by
referring to its index value in the Pages collection, you can use the following statement:
TabControl1.Pages(0).Caption = "First Page"
Because each page is a member of the form's Controls collection, you can refer to a page solely by its Name
property without referring to the tab control's name or its Pages collection. For example, to change the value of
the Caption property of a page with its Name property set to Page1, use the following statement:
56
57
same form. You can refer to controls on a tab control page by using the same syntax used for controls on a form
without a tab control. For example, a fully qualified reference to the HomePhone text box on the Personal Info tab
of the Employees form in the Northwind sample application would read as follows.
Forms!Employees!HomePhone
Because each control on a form has its own Controls collection, you can also refer to the controls on a tab control
as members of its Controls collection. For example, the following code enumerates (lists) all the controls on the
tab control of the Employees form in the Northwind sample application. Because the EmployeeName text box in
the header section of the form is not a member of this collection, it isn't listed.
Sub ListTabControlControls()
' Declare object variables.
Dim tabCtl As TabControl, ctl As Control
' Return reference to tab control on Employees form.
Set tabCtl = Forms!Employees!TabCtl0
' List all controls on the tab control in the Debug window.
For Each ctl In tabCtl
Debug.Print ctl.Name
Next ctl
End Sub
Additionally, each page on a tab control has its own Controls collection. By using a page's Controls collection,
you can refer to controls on each page. The following code enumerates the controls for each page of the tab
control on the Employees form in the Northwind sample application.
Sub ListPageControls()
' Declare object variables.
Dim tabCtl As TabControl, pg As Page, ctl As Control, intPageNum As Integer
' Return reference to tab control on Employees form.
Set tabCtl = Forms!Employees!TabCtl0
' List all controls for each page on the tab control in the Debug window.
For Each pg In tabCtl.Pages
intPageNum = intPageNum + 1
Debug.Print "Page " & intPageNum & " Controls:"
For Each ctl In pg.Controls
Debug.Print ctl.Name
Next ctl
Debug.Print
Next pg
End Sub
Declaring Variables
As you saw earlier," you use variables to store values while your Visual Basic code is running. Within a procedure
or module, you declare a variable with the Dim statement. The syntax for the Dim statement is:
58
59
To avoid the problem of misspelling variable names, specify an Option Explicit statement in the Declarations
section of your modules. The Option Explicit statement forces the explicit declaration of all variables in a module.
If Visual Basic encounters a name not explicitly declared as a variable, it generates an error message.
If you had specified an Option Explicit statement for the module containing the SafeSqr function shown earlier,
Visual Basic would have recognized dblTemp and dblTmp as undeclared variables and would have generated
errors for both of them. You would then explicitly declare dblTemp, as shown in the following code:
Function SafeSqr(ByVal dblNum As Double) As Double
Dim dblTemp As Double
dblTemp = Abs(dblNum)
SafeSqr = Sqr(dblTmp)
End Function
Now Microsoft Access would display an error message for the incorrectly spelled dblTmp and the problem would
be clear. Because the Option Explicit statement helps you catch these kinds of errors, you should use it with all
your code.
You can also force variables to be explicitly declared in any new modules by clicking Options (Tools menu),
clicking the Module tab, and then selecting the Require Variable Declaration check box. This automatically inserts
the Option Explicit statement in any new modules.
Note The Option Explicit statement operates on a per-module basis; it must be placed in the Declarations section
of every form, report, and standard module for which you want Visual Basic to enforce explicit variable
declarations. If you select the Require Variable Declaration check box, Visual Basic inserts the Option Explicit
statement in all subsequent form, report, and standard modules, but doesn't add it to existing code. You must
manually add the Option Explicit statement to any existing modules within an application.
Scope and Lifetime of Variables
When you declare a variable within a procedure, only code within that procedure can read or change the value of
that variable: Its scope is local to that procedure. Sometimes, however, you want to use a variable with a broader
scope, one whose value is available to all procedures within the same module, or even to all the procedures in all
modules. With Visual Basic, you can specify the scope of a variable when you declare it.
Scope of Variables
Depending on how you declare a variable, it's either a procedure-level or a module-level variable.
Procedure-level variables are recognized only in the procedure in which they're declared. These are also known as
local variables. You declare them with the Dim or Static keywords, as follows:
Dim intTemp As Integer
Static intPermanent As Integer
Local variables declared with the Dim keyword exist only as long as the procedure is running. Local variables
declared with the Static keyword exist the entire time your application is running.
Because of the advantages they provide, consider using local variables for any kind of temporary calculation. For
example, you can create a dozen different procedures containing a variable called intTemp. As long as each
intTemp is declared as a local variable, each procedure recognizes only its own version of intTemp. Any one
procedure can alter the value in its local intTemp without affecting intTemp variables in other procedures.
Note Implicitly declared variables always have a local scope.
Variables Used Within a Module
60
By default, a module-level variable is available to all the procedures in that module, but not to code in other
modules. You create module-level variables in form, report, and standard modules by declaring them with the
Dim or Private keywords in the Declarations section at the top of the module. For example, use either of the
following statements:
Dim intTemp As Integer
Private intTemp As Integer
At the module level, there is no difference between Private and Dim. However, using Private is recommended
because it readily contrasts with Public and makes your code easier to read.
Variables Used by All Modules
To make a module-level variable available to other modules, use the Public keyword to declare the variable. (The
Public statement has replaced the Global statement used for declaring variables in Microsoft Access version 2.0.)
The values in public variables are available to all procedures in all modules in your application. Like all modulelevel variables, public variables are declared in the Declarations section at the top of the module, as shown in the
following example:
Public intX As Integer
Note You can't declare public variables within a procedure.
In a form, report, or standard module, if you want to refer to a public variable in a different form or report
module, you must qualify the reference by using the name of the form or report, such as Forms!Orders.intX.
You can declare public variables in any module. Generally speaking, each public variable should be located in the
module of which it's a logical member. However, if you have public variables that are used for purely global
purposes and are not specifically related to a particular module, you can put them all in one module. This makes
the variables easier to find and your code easier to read.
Scope and Variable Names
A variable can't change scope while your code is running. However, you can use the same name for different
variables under certain conditions.
If public variables in different modules share the same name, it's possible to differentiate between them in code.
For example, if a public Integer variable intX is declared in both of the standard modules Module1 and Module2,
you can refer to them as Module1.intX and Module2.intX to get the correct values.
Public vs. Local Variables
You can also have a variable with the same name at a different scope. For example, you could have a public
variable named intX and then, within a procedure, declare a local variable named intX. References to the name
intX within the procedure would access the local variable, while references to intX outside the procedure would
access the public variable. The module-level variable can be accessed from within the procedure by qualifying the
variable with the module name.
' This code is in the form module for Form1. Within the Test Sub
' procedure, the name intX always refers to the local variable, not
' to the variable declared in the Public statement at the top.
Public intX As Integer
Sub Test()
Dim intX As Integer
intX = 2
' intX has a value of 2 (even though
61
MsgBox Forms!Form1.intX
' MsgBox shows the value of intX in
End Sub
' Form1, which is 1).
Private Sub Form_Load()
intX = 1
End Sub
Private Sub Command1_Click()
Test
' intX has a value of 2.
End Sub
In general, when variables have the same name but a different scope, the more local variable shadows (is accessed
in preference to) less local variables. So, if you also had a private module-level variable named intX, it would
shadow the public variable intX within that module (and the local intX would shadow the private module-level
intX within that procedure).
Using Variables and Properties with the Same Name
Shadowing rules treat form and report properties and controls, user-defined types, constants, and procedures as
module-level variables in the form or report module. Thus, all module-level shadowing rules apply to each of
these. You can't have a form or report property or control with the same name as a module-level variable,
constant, user-defined type, or procedure because both are in the same scope.
Within the form or report module, local variables with the same names as controls on the form or report shadow
the controls. You need to either qualify the control with a reference to the form or report, or you need to use the
Me keyword to set or get its value or any of its properties. The following code shows one way to do this:
Private Sub Form_Click()
Dim Text1, Caption
' Assume there is also a control on the form called Text1.
Text1 = "Variable"
' Variable shadows control.
Me.Text1 = "Control"
' Must qualify with Me to get control.
Text1.Top = 0
' This causes an error!
Me.Text1.Top = 0
' Must qualify with Me to get control.
Caption = "Orders"
' Variable shadows property.
Me.Caption = "Orders"
' Must qualify with Me to get form property.
End Sub
Using Variables and Procedures with the Same Name
You may also have conflicts with the names of your private module-level and public module-level variables and
the names of your procedures. A variable in the module can't have the same name as any procedures or types
defined in the module. It can, however, have the same name as public procedures, types, or variables defined in
other modules. In this case, when the variable is accessed from another module, it must be qualified with the
module name.
While these shadowing rules are not complex, shadowing can be confusing and lead to subtle problems in your
code. To avoid these problems, it's good programming practice to keep the names of your variables distinct from
each other. For example, in form or report modules, try to have unique names for properties that are different from
names of controls on those forms or reports. This applies to procedure names as well.
Lifetime of Variables
In addition to scope, variables also have a lifetime, which is the time during which a variable retains its value. The
values in module-level and public variables are preserved as long as the database is open (unless you reinitialize
your code). This is true for form and report module-level variables even if you close the form or report. However,
local variables declared with the Dim keyword exist only while the procedure in which they are declared is
running. Usually, when a procedure has finished, the values of its local variables are discarded and the memory
62
used by the local variables is reclaimed. The next time the procedure is run, all of its local variables are
reinitialized. However, you can make Visual Basic preserve the value of a local variable by making the variable
static.
Static Variables
To make a local variable in a procedure static, use the Static keyword to declare the variable exactly as you would
using the Dim statement.
Static intX As Integer
For example, the following function calculates a running total by adding a new value to the total of previous
values stored in the static variable dblAccumulate.
Function RunningTotal(ByVal dblNum As Double) As Double
Static dblAccumulate As Double
dblAccumulate = dblAccumulate + dblNum
RunningTotal = dblAccumulate
End Function
If you declare dblAccumulate with the Dim keyword instead of the Static keyword, the previous accumulated
values aren't preserved across calls to the function, and the function simply returns the value with which it was
called.
You can produce the same result by declaring dblAccumulate in the Declarations section of the module, making it
a module-level variable. However, after you change the scope of a variable in this way, the procedure no longer
has exclusive access to that variable. If other procedures access the value of the variable and change it, the
running totals would be unreliable.
Declaring All Local Variables as Static
To make all local variables in a procedure static, place the Static keyword at the beginning of a procedure
declaration, as shown in the following example:
Static Function RunningTotal(ByVal dblNum As Double) As Double
This makes all the local variables in the procedure static, regardless of whether they are declared with the Static or
Dim keywords, or declared implicitly. You can place the Static keyword in front of any Function or Sub procedure
heading, including event procedures and those that are also declared as Private.
Fundamental Variable Data Types
When you declare a variable, you can also supply a data type for the variable. All variables have a data type that
determines what kind of data they can store. By default, if you don't supply a data type (or if you declare the
variable implicitly), the variable is given the Variant data type.
The Variant Data Type
The Variant data type can store many kinds of data. Like a text box control on a form, a Variant variable is equally
capable of storing numbers, strings of text, dates and times, or the Null value. You don't have to convert between
these types of data when assigning them to a Variant variable; Visual Basic automatically performs any necessary
conversion, as shown in the following example:
Dim varX
varX = "17"
varX = varX - 15
63
Although you can perform operations on Variant variables without much concern for what kind of data they
actually contain, there are some pitfalls you'll want to avoid:
If you perform arithmetic operations on a Variant, or use a Variant in an arithmetic function, the variable must
contain a valid number. For example, you can't perform any arithmetic operations on the value "U2" even though
it contains a numeric character, because the entire value isn't a valid number. Likewise, you can't perform any
calculations on the value "1040EZ". However, you can perform numeric calculations on the values "+10" and "1.7E62" because they are valid numbers.
You can use the IsNumeric function to determine if the value contained by a Variant variable can be used as a
valid number in an expression. For example:
If IsNumeric(varX) And IsNumeric(varY) Then
varZ = varX * varY
Else
varZ = Null
End If
If you're concatenating strings, use the & operator instead of the + operator. The result of the + operator can be
ambiguous when used with two Variant values.
If both of the Variant values contain numbers, then the + operator performs addition. If both of the Variant values
contain strings, then the + operator performs string concatenation. However, if one of the values is a number and
the other is a string, the situation becomes more complicated. Visual Basic first attempts to convert the string into
a number. If the conversion is successful, then the + operator adds the two values; if unsuccessful, it generates a
"Type mismatch" error.
To make sure that concatenation occurs, regardless of the representation of the value in the variables, use the &
operator. For example, the following code:
Sub Test()
Dim varX As Variant, varY As Variant
varX = "6"
varY = "7"
Debug.Print varX + varY, varX & varY
varX = 6
Debug.Print varX + varY, varX & varY
End Sub
produces the following result in the Debug window:
Important When typing your code, it's important to leave a space between any variable name and the & operator.
If you don't leave a space, Visual Basic assumes you intended to use the & as the type-declaration character for
the variable name.
In addition to strings and numbers, Variant variables can also contain date/time values. For example:
Function Century() As Integer
Dim varToday As Variant
varToday = Now
If varToday >= #1/1/2001# Then
Century = 21
Else
64
Century = 20
End If
End Function
In the same way that you can use the IsNumeric function to determine if a Variant variable contains a valid
numeric value, you can use the IsDate function to determine if a Variant variable contains a valid date/time value.
For example:
Function Century (ByVal varDate As Variant) As Variant
If IsDate(varDate) Then
Century = ((Year(varDate) - 1) \ 100) + 1
Else
Century = Null
End If
End Function
Objects, including Automation objects, can also be stored in Variant variables. This can be useful when you need
to handle a variety of data types, including objects. For example, all the elements in an array must have the same
data type. Setting the data type of an array to Variant allows you to store objects alongside other data types in an
array.
The Empty Value
Sometimes you need to know if a Variant variable has ever been assigned a value since the variable was created.
A Variant variable has the Empty value before it's assigned a value. The Empty value is a special value different
from 0, a zero-length string (""), or the Null value. You can use the IsEmpty function to determine if a Variant
variable has the Empty value.
If IsEmpty(varX) Then varX = 0
When you use a Variant in an expression, an Empty value is treated as either 0 or a zero-length string, depending
on the expression. The Empty value disappears as soon as any value is assigned to a Variant variable (including
the value of 0, the zero-length string, and the Null value). You can set a Variant variable back to the Empty value
by assigning the Empty keyword to the Variant.
The Null Value
The Variant data type can contain one other special value: Null. Null is commonly used in database applications to
indicate unknown or missing data. Fields and controls that haven't been initialized have a default value of Null.
You can use the IsNull function to determine if a Variant variable contains the Null value.
If IsNull(varX) And IsNull(varY) Then
varZ = Null
Else
varZ = 0
End If
A Null value has some unique characteristics:
Expressions involving Null always result in Null. Thus, Null is said to propagate through expressions; if any part
of the expression evaluates to Null, the entire expression evaluates to Null.
Most functions return Null if you pass them Null, a Variant containing Null, or an expression that evaluates to
Null as an argument.
Null values propagate through intrinsic (built-in) functions that return Variant data types.
65
You can assign a Null value by using the Null keyword. For example:
varZ = Null
Only Variant variables can contain Null values. If you assign Null to a variable of any data type other than
Variant, a trappable error occurs. Assigning Null to a Variant variable doesn't cause an error, and Null will
propagate through expressions involving Variant variables (though Null doesn't propagate through certain
functions). For example, the following code:
Sub Test()
Dim varX As Variant, varY As Variant
varX = "6"
varY = Null
Debug.Print varX + varY, varX & varY
End Sub
produces this result in the Debug window:
Null 6
In addition, you can return Null from any Function procedure that has a Variant return value.
Tip The fact that Null propagates makes it useful as an error value. If you write Function procedures that return
Null when an error occurs, and then combine these functions in expressions, you can use the IsNull function to
test the final result of the expression to see if an error has occurred. Because Null propagates, the final result is
Null if an error has occurred in any of the functions; you don't have to test the result of each function separately.
Other Fundamental Data Types
The Variant data type handles all types of fundamental data and converts between them automatically. However,
you can usually create more concise, faster code by using other data types where appropriate. For example, if a
variable will always contain small integer values, you can save several bytes, and significantly increase the speed
of arithmetic operations on the variable, by declaring that variable to be Integer instead of Variant.
The following table lists the fundamental data types in Visual Basic,
including Variant.
Date/time, floating-point number, integer, string, or object. 16 bytes, plus 1 byte for each character if a string
value.
Date values: January 1, 100 to December 31, 9999
Numeric values: same range as Double
String values: same range as String
When you declare a variable by using a Dim, Public, Private, or Static statement, you use the As type clause to
specify the data type of the variable. For example, the following statements declare Integer, Currency, Double,
and String variables, respectively:
Dim intX As Integer
Public curBillsPaid As Currency
Private dblAmt As Double
Static strName As String
A declaration statement can combine multiple declarations, as in the following statements:
66
67
68
69
Declare the argument by using the ByVal keyword to specify that the argument is passed by value rather than by
reference. When you pass a variable by value, changes to the variable in the procedure don't affect its value in the
calling procedure.
For example, you can't pass a Variant by reference to a string argument. Thus, the following code produces an
error:
Dim varTest As Variant
varTest = "Testing"
Debug.Print Reverse(varTest, 4)
One way to avoid this problem is to pass an expression, rather than a Variant, for an argument. Visual Basic then
evaluates the expression and, if it can, passes it as the required data type. The simplest way to turn a variable into
an expression is to enclose it in parentheses. For example:
Debug.Print Reverse((varTest), 4) ' Makes the variable an expression.
However, the best way to ensure that arguments are passed correctly is to declare the arguments with the ByVal
keyword, as illustrated by the second argument, ByVal intChars As Integer, in the preceding Reverse function
example. Thus, you can pass a Variant as the second argument to the Reverse function. For example:
Dim strTest As String, varTest As Variant
strTest = "Testing"
varTest = "2"
Debug.Print Reverse(strTest, varTest)
' Works!
When you pass a variable to a procedure by reference, the variable's value can be changed by that procedure. On
the other hand, when you pass the variable by value, only a copy of the variable is passed to the procedure;
therefore, if the procedure changes that value, the change affects only the copy and not the variable itself. This is
important in the Reverse function; if the second argument isn't declared with the ByVal keyword, bugs could
appear in the code. For example, suppose the second argument wasn't declared with the ByVal keyword, and you
called it as follows:
Dim intTest As Integer, strTest As String
intTest = 10
strTest = "Testing"
Debug.Print Reverse(strTest, intTest) ' Now intTest = 7 (length of strTest).
You don't usually expect a function to modify its arguments, as happens here. To avoid this kind of side effect in
any procedures that modify their arguments, declare those arguments with the ByVal keyword.
Function Return Data Types
The value returned by a function has a data type. When you define the function, you can declare the data type of
the value the function returns. For example, the Reverse function in the preceding section returns a String.
As with variables, Visual Basic can work more efficiently with functions if you explicitly declare a data type for
the values they return. If you don't declare a data type, functions use the Variant data type. For example, if you
don't set a return value for a function (by assigning a value to the name of the function), the function returns a
Variant containing the Empty value.
If you declare the function to return a String, as in the Reverse function example, the function returns a zerolength string ("") if you don't assign a return value. If you declare the function to return a numeric data type, such
as Integer or Double, it returns zero if you don't explicitly assign a return value.
70
71
You can access the values in an array within a user-defined type in the same way that you access the property of
an object. For example:
Dim sysMine As SystemInfo
sysMine.strDiskDrives(0) = "1.44 MB"
You can also declare an array of user-defined types:
Dim sysAll(100) As SystemInfo
Follow the same rules to access the components of each element in the array:
sysAll(5).varCPU = "386SX"
sysAll(intX).strDiskDrives(2) = "100M SCSI"
Declaring Procedure Arguments
You can declare procedure arguments with a user-defined type.
Sub FillSystem(sysAny As SystemInfo)
sysAny.varCPU = "486"
sysAny.lngMemory = "24"
sysAny.curCost = "$3000.00"
sysAny.dtePurchase = Now
End Sub
Note If you want to pass a user-defined type in a form or report module, the procedure must be private.
You can return user-defined types from functions, and you can pass a user-defined type variable to a procedure as
one of the arguments. Because user-defined types are always passed by reference, the procedure can modify the
argument and return it to the calling procedure, as illustrated in the previous example.
User-Defined Types That Contain Objects
User-defined types can also contain objects. For example:
Private Type AccountPack
frmInput As Form
72
dbsPayRollAccount As Database
End Type
Tip Because the Variant data type can store many different types of data, a Variant array can be used in many
situations where you may expect to use a user-defined type. A Variant array is actually more flexible than a userdefined type, because you can change the type of data you store in each element at any time, and you can make
the array dynamic so that you can change its size as necessary. However, a Variant array always uses more
memory than an equivalent user-defined type.
Nesting Data Structures
Nesting data structures can get as complex as you want. In fact, user-defined types can contain other user-defined
types, as shown in the following example. To make your code more readable and easier to debug, try to keep all
the code that defines user-defined types in one module. For example:
Type DriveInfo
Type As String
Size As Long
End Type
Type SystemInfo
varCPU As Variant
lngMemory As Long
strDiskDrives(26) As DriveInfo
curCost As Currency
dtePurchase As Variant
End Type
The following code demonstrates how you can refer to a nested user-defined type:
Dim sysAll(100) As SystemInfo
sysAll(1).strDiskDrives(0).Type = "Floppy"
Constants
Your code may contain unchanging values that appear over and over. Or your code may depend on certain
numbers that are difficult to remember numbers that, in and of themselves, have no obvious meaning.
In these cases you can greatly improve the readability of your code and make it easier to maintain by using
constants. A constant is a meaningful name that takes the place of a number or string that doesn't change. You
can't modify a constant or assign a new value to it as you can to a variable. Constants have one of two sources:
Intrinsic or system-defined constants are provided by applications. Microsoft Access, Visual Basic, and Data
Access Objects (DAO) constants are all available in Microsoft Access, along with intrinsic constants from other
applications that provide object libraries.
Symbolic or user-defined constants are declared by using the Const statement.
Creating Your Own Constants
You use the Const statement to create your own symbolic constants. The syntax for the Const statement is:
[Public | Private] Const constantname [As type] = expression
The argument constantname is a valid symbolic name (the rules are the same as the rules for creating variable
names), and expression is composed of numeric or string constants and operators (you can't use function calls in
expression).
73
74
Note You can't define a public constant in a form or report module, only in a standard module. You can,
however, declare a private constant in a form or report module.
Intrinsic Constants
In addition to the constants you declare with the Const statement, Microsoft Access automatically declares a
number of intrinsic constants. These constants are available in all modules.
Because you can't disable intrinsic constants, the constants you create can't have the same names as the intrinsic
constants. Also, you can't redeclare or set intrinsic constants to different values.
Important Because the values represented by the intrinsic constants may change in future versions of Microsoft
Access, you should use the constants instead of their actual values.
You can use intrinsic constants wherever you can use user-defined constants, including in expressions. The
following example shows how you might use the intrinsic constant vbCurrency to determine whether varTest is a
Variant of VarType 6 (Currency).
If VarType(varTest) = vbCurrency Then
Debug.Print "varTest contains Currency data."
Else
Debug.Print "varTest does not contain Currency data."
End If
The intrinsic constants for Microsoft Access, as well as those for the Visual Basic for Applications and Data
Access Objects (DAO) libraries, are listed in the Object Browser. Other applications that provide object libraries,
such as Microsoft Excel and Microsoft Project, also provide a list of constants you can use with their objects,
methods, and properties. To see these constants, click the appropriate object library in the Project/Library box of
the Object Browser, and then click Constants in the Classes box.
Important In order for constants from other applications to appear in the Object Browser, you must set a
reference to the application's object library. Open a module, and then in the References dialog box (Tools menu),
select the check box for the object library you want to set a reference for.
Intrinsic constant names are in a mixed-case format, with a two-character prefix indicating the object library that
defines the constant. Constants from Microsoft Access are prefaced with "ac"; for example, acDataErrContinue.
Constants from the VBA object library are prefaced with "vb" ; for example, vbTileHorizontal. Constants from
the DAO object library are prefaced with "db" ; for example, dbRelationUnique. All of these intrinsic constants
are available in Microsoft Access without having to be declared.
Note In Microsoft Access versions 2.0 and earlier, constant names appeared in all capital letters with underscores
for example, A_REFRESH. Microsoft Access 97 supports intrinsic constants from these early versions. To see
them, click the Access object library in the Project/Library box of the Object Browser, and then click
OldConstants in the Classes box.
Arrays
If you have programmed in other languages, you're probably familiar with the concept of arrays. You use arrays to
refer to a series of variables by the same name while using a number (an index) to tell them apart. This helps you
create smaller and simpler code in many situations because you can set up loops that deal efficiently with multiple
cases by using the index number. Arrays have both upper and lower bounds, and the elements of the array are
contiguous within those bounds. Because Visual Basic allocates space for each index number, avoid declaring an
array larger than you need it to be.
75
All the elements in an array have the same data type. Of course, when the data type is Variant, the individual
elements may contain different kinds of data (strings, numbers, date/time values, or objects). You can declare an
array with any of the fundamental data types, including user-defined types, and object variables.
Declaring Fixed-Size Arrays
You can declare an ordinary (fixed-size) array in three ways, depending on the scope you want the array to have:
To create a public array, use the Public statement in the Declarations section of a module to declare the array.
To create a module-level array, use the Private or Dim statement in the Declarations section of a module to
declare the array.
To create a local array, use the Dim or Static statement within a procedure to declare the array.
There are additional rules when you create a dynamic array (an array whose size can change at run time).
Setting Upper and Lower Bounds
When declaring an array, follow the array name with the upper bound in parentheses. The upper bound must be a
Long data type (in the range -231 to 231). For example, the following array declarations can appear in the
Declarations section of a module:
Dim intCount(14) As Integer
' Declares an array with 15
' elements (0 through 14).
Dim dblSum(20) As Double
' Declares an array with 21
' elements (0 through 20).
To create a public array, you use Public in place of Dim (or Private):
Public intCount(14) As Integer
Public dblSum(20) As Double
To create a local array, use the Dim (or Static) statement:
Dim intCount(14) As Integer
Static dblSum(20) As Double
The first declaration creates an array with 15 elements, with index numbers running from 0 through 14. The
second creates an array with 21 elements, with index numbers running from 0 through 20. The default lower
bound is 0. However, you can change the default lower bound to 1 by placing the following Option Base
statement in the Declarations section of a module:
Option Base 1
Another way to specify the lower bound is to provide it explicitly by using the To keyword. For example:
Dim intCount(1 To 15) As Integer
Dim dblSum(100 To 120) As Double
In the preceding declarations, the index numbers of intCount run from 1 through 15 (15 elements), and the index
numbers of dblSum run from 100 through 120 (21 elements).
Note With some versions of Basic, you can use an array without first declaring it. With Visual Basic, this isn't
possible; you must declare an array before using it.
76
Multidimensional Arrays
With Visual Basic, you can declare arrays of up to 60 dimensions. For example, the following statement declares
a two-dimensional 10-by-10 array within a procedure:
Static dblMatrix(9, 9) As Double
Either or both dimensions can be declared with explicit lower bounds:
Static dblMatrix(1 To 10, 1 To 10) As Double
You can extend this to more than two dimensions, as in the following example:
Dim intMultiD(3, 1 To 10, 1 To 15) As Integer
This declaration creates a three-dimensional 4-by-10-by-15 array. The total number of elements is the product of
these three dimensions, or 600.
Note Because the total storage needed by the array increases dramatically when you start adding dimensions to
it, be sure to use multidimensional arrays with care. Be especially careful with Variant arrays, because Variant
arrays are larger than arrays containing other data types.
Using Loops to Manipulate Arrays
Loops often provide an efficient way to manipulate arrays. For example, the following loop initializes all
elements in the array to 5:
Static intCount(1 To 15) As Integer
Dim intX As Integer
For intX = 1 To 15
intCount(intX) = 5
Next intX
You can efficiently process a multidimensional array by using nested For loops. For example, the following
statements initialize every element in dblMatrix to a value based on its location in the array:
Dim intX As Integer, intY As Integer
Static dblMatrix(1 To 10, 1 To 10) As Double
For intX = 1 To 10
For intY = 1 To 10
dblMatrix(intX, intY) = intX * 10 + intY
Next intY
Next intX
Dynamic Arrays
Sometimes you may not know exactly how large to make an array. You may want to be able to change the size of
the array at run time.
A dynamic array can be resized at any time. Dynamic arrays are among the most flexible and convenient features
in Visual Basic, and they help you to manage memory efficiently. For example, you can use a large array for a
short time and then free up memory to the system when you're no longer using the array.
The alternative is to declare an array with the largest anticipated size and then to ignore array elements you don't
need. However, if overused, this approach may cause Microsoft Access to run low on memory.
77
78
79
80
The Visual Basic for Applications object library provides three objects to Microsoft Access, but these objects
aren't organized in an object hierarchy. None of them belong to a collection of other objects. The following table
describes the objects provided by the Visual Basic for Applications object library.
Information about Visual Basic errors
Microsoft Office Objects
The Microsoft Office 8.0 object library provides objects you can use to customize the appearance of your
application or to implement some features common to Microsoft Office applications. For example, you can create
customized toolbars and menu bars in code by using the CommandBar object. You can perform custom file
searches by using the FileSearch object. You can also customize the Office Assistant to respond to the user's
actions.
Note In order to use objects in the Microsoft Office 8.0 object library from Visual Basic, you must first set a
reference to the object library. When you set a reference to an object library, you notify Visual Basic that you may
want to use the objects in that library. To set a reference to the Microsoft Office 8.0 object library, open a module
and click References on the Tools menu. Then select the Microsoft Office 8.0 Object Library check box in the
Available References box.
Not all of the objects in the Microsoft Office 8.0 object library are useful in Microsoft Access. The following table
describes some of the objects in the Microsoft Office 8.0 object library which Microsoft Access developers may
find useful.
Files found through file search operation
Note The Office Assistant is not available in Microsoft Access run-time applications.
81
82
Forms!Employees!LastName
You can also use the full reference to the control, as shown in the following line of code:
Forms!Employees.Controls!LastName
Databases
83
variable from the object to which it's been pointing once you are no longer using it. For example, if you are no
longer using an object variable that points to a Form object, you can free that variable as follows:
Set frm = Nothing
When you set an object variable to the Nothing keyword, you are no longer storing a reference to a particular
object. The variable still exists, and you can assign another object to it when you need it.
Using Objects and Collections in Code
Once you understand how to refer to objects in Visual Basic and how to create object variables to represent them,
you can begin using objects in code. The following sections present concepts that may be useful to you as you
begin working with objects and collections.
Navigating the Object Hierarchy
As explained earlier in this chapter, in order to work with an object that belongs to a collection, you must refer to
that object in its collection. Since objects are related to one another in an object hierarchy, you must also make
clear where the object and collection exist in the overall hierarchy. In other words, if the object is a member of a
collection, you must qualify the object with the name of its collection. If that collection belongs to another object,
you must qualify the collection with the name of that object, and so on.
When you create an object variable and assign an object to it, the information about that object's position within
the object hierarchy is stored with the variable. An object variable becomes a sort of shorthand for all the objects
preceding the one you want to work with in the object hierarchy.
The following example shows how you can work within the Microsoft Access object hierarchy to access
individual objects. The procedure returns a reference to the Employees Form object, which is a member of the
Forms collection, and assigns it to an object variable. Then it returns a reference to the LastName Control object,
which is a member of the Controls collection of the Form object, and assigns it to an object variable. Finally it
uses the ControlType property of the Control object to determine what type of control this is. If the control is a
text box, the procedure sets its Locked property to True.
Sub LockControl()
Dim frm As Form, ctl As Control
' Declare object variables.
Set frm = Forms!Employees
' Return reference to Form object.
Set ctl = frm!LastName
' Return reference to Control object.
If ctl.ControlType = acTextBox Then
' Check ControlType property.
ctl.Locked = True
' Lock control if it's a text box.
End If
Set frm = Nothing
End Sub
Although the Forms collection is a member of the Microsoft Access Application object, you don't need to refer to
the Application object when you refer to the Forms collection or to other Microsoft Access objects and
collections. The Application object is implicitly understood.
You work with objects and collections in the DAO object hierarchy in a similar manner. The next example
navigates through the DAO object hierarchy and prints the name of each field in the Employees table.
Sub ListTableFields()
' Declare object variables.
Dim dbs As Database, tdf As TableDef, fld As Field
' Return reference to current database.
Set dbs = CurrentDb
' Return reference to Employees table.
84
85
DAO objects and Microsoft Access Form, Report, and Control objects all contain a Properties collection. Each
Property object in the Properties collection corresponds to a property of the object. You can use an object's
Properties collection either to determine which properties apply to a particular object or to return their settings.
For example, the following procedure loops through the properties that apply to the Database object, which
represents the current database, and to the Employees Form object. The procedure displays the name of each
property in the Debug window.
Sub DisplayProperties()
' Declare variables.
Dim dbs As Database, frm As Form, prp As Property
' Return reference to current database.
Set dbs = CurrentDb
Debug.Print "Current Database Properties"
' Enumerate Properties collection.
For Each prp In dbs.Properties
Debug.Print prp.Name
Next prp
' Print blank line.
Debug.Print
Debug.Print "Employees Form Properties"
' Open Employees form in Form view.
DoCmd.OpenForm "Employees", acWindowNormal
' Return reference to Employees form.
Set frm = Forms!Employees
' Enumerate Properties collection.
For Each prp In frm.Properties
Debug.Print prp.Name
Next prp
Set frm = Nothing
Set dbs = Nothing
End Sub
Note If you're looping through the Properties collection of a table or query, some properties aren't displayed
because they're added to the collection only when they have a value.
Working with CommandBar Objects
Creating new CommandBar objects is somewhat different from creating other new objects in Microsoft Access.
To create a new CommandBar object, you use the Add method of the CommandBars collection. The following
example creates a new CommandBar object and adds a button to it:
Sub CreateNewCommandBar()
Dim cmb As CommandBar, cbc As CommandBarControl
' Create new CommandBar object and return reference to it.
Set cmb = CommandBars.Add("NewCommandBar", msoBarFloating, msoBarTypeNormal)
' Create new CommandBarControl object and return reference to it.
Set cbc = cmb.Controls.Add(msoControlButton)
' Set properties of new command bar control.
With cbc
.Caption = "Button1"
.DescriptionText = "First button in NewCommandBar"
.TooltipText = "Button1"
.Visible = True
End With
' Make command bar visible.
86
cmb.Visible = True
Set cmb = Nothing
End Sub
Note In order to use objects in the Microsoft Office 8.0 object library from Visual Basic, you must first set a
reference to the object library. When you set a reference to an object library, you notify Visual Basic that you may
want to use the objects in that library. To set a reference to the Microsoft Office 8.0 object library, open a module
and click References on the Tools menu. Then select the Microsoft Office 8.0 Object Library check box in the
Available References box.
Creating New Objects with Class Modules
Every object that you use in Microsoft Access is derived from a unique definition for that object. The definition
for an object includes its name, its inherent characteristics, and its properties, methods, and events. The definition
for an object is known as a class.
To simplify the concept of a class, you can think of a class as a cookie cutter, and an object as the cookie that it
makes. You can create multiple objects from a single class, just as you can make multiple cookies with a single
cookie cutter. Each individual object has the same characteristics, just as each cookie has the same shape and
pattern.
An individual object can also be referred to as an instance of a class. An instance of a class is like a single cookie
cut from the cookie cutter. When you create an instance of a class, you create a new object and return an object
reference to it. You then work with the instance by setting its properties and applying its methods.
In addition to the objects provided by Microsoft Access and its associated object libraries, you can define your
own custom objects in class modules. A class module is a module that can contain the definition for a new object.
To create a definition for a new object in a class module
1 Define the purpose for your new object. Think of the object in terms of the methods and properties it should
have. For example, calling functions in a dynamic-link library (DLL) is often tricky. You can create an object that
has methods that contain those function calls. Then, when you want to call a particular function, you can simply
call the method that contains it, rather than calling the complex function.
2 Create a new class module by clicking Class Module on the Insert menu. Choose a name for your class and
save the class module with that name.
3 Add procedures to the class module. Any Sub or Function procedures that you define in a class module
become custom methods of your new object. Any Property Get, Property Let, or Property Set procedures that you
define become custom properties of your new object.
4 If you want certain code to run when an instance of the class is created, add that code to the class's Initialize
event procedure. If you want certain code to run when an instance of the class is removed from memory, add it to
the class's Terminate event procedure.
5 Test the new class by creating an instance of it and applying its methods and setting its properties. To create an
instance of your class, use the New keyword to declare an object variable of type classname, where classname
represents the name of your class. The New keyword creates a new instance of the class.
For example, if your class is named NewClass, you can declare a new instance of it as shown in the following line
of code:
Dim obj As New NewClass
If you've defined a method called ListNames within the class module, you can then apply that method as follows:
87
Obj.ListNames
You can view the new class and its variables, methods, and properties in the Object Browser, which is available
through the View menu. In the Project/Library box, click the name of your project, and then click the name of the
class in the Classes box. You can determine the name of your project by checking the value in the Project Name
box on the Advanced tab of the Options dialog box (Tools menu).
Creating Multiple Instances of Forms and Reports
Form modules and report modules are also class modules. They are identical to the class modules on the Modules
tab of the Database window, except that they are associated with forms and reports. Since form and report
modules are class modules, you can create one or more instances of a form or report class. This is useful if you
want to display more than one instance of a form or report at a time.
When you create a new instance of a form or report class, the new instance has all the properties and methods of a
Form or Report object, and its properties are set to the same values as those in the original Form or Report object.
Additionally, any procedures that you have written in the form or report class module behave as methods and
properties of the new instance.
To create a new instance of a form or report class, you declare a new object variable with the name of the form or
report's class module and the New keyword. The name of the class module appears in the title bar of the module.
It indicates whether the class is associated with a form or a report and includes the name of the form or report. For
example, the class name for an Orders form is Form_Orders. The following line of code creates a new instance of
the Orders form:
Dim frmInstance As New Form_Orders
By creating multiple instances of a Orders form class, you could show information about one order on one form
instance, and information about another order on another form instance.
When you create an instance of a form class by using the New keyword, it is hidden. To show the form, set the
Visible property to True.
You should declare the variable that represents the new instance of a form class at the module level. If you declare
the variable at the procedure level, the variable goes out of scope when the procedure finishes running, and the
new instance is removed from memory. The instance exists in memory only as long as the variable to which it is
assigned remains in scope.
Note When you create a new form or report in Microsoft Access, the form or report doesn't automatically have
an associated module. Forms and reports without associated modules load more quickly. If you're working in form
or report Design view, Microsoft Access automatically creates the form or report module when you click Code on
the View menu. Once you enter code in the module, Microsoft Access saves the module with the form or report.
Whether or not the form or report module exists is determined by the setting of the HasModule property. When a
form or report is created, the HasModule property is automatically set to False. When you create a form or report
module by clicking Code on the View menu, Microsoft Access sets the HasModule property to True. If you refer
to the Module property of a form or report, the HasModule property is also automatically set to True. For more
information on the HasModule property, search the Help index for "HasModule property."
88
Because a collection is also an object, each collection in Microsoft Access has its own properties and methods.
You can set or read the properties of a collection, or apply its methods, in the same manner that you would for any
object.
Setting and Reading Properties
Visual Basic provides a standard syntax for setting and reading properties in code. When you set a property, you
give it a new value. You can use the following syntax to set a property for any type of object:
object.property = setting
The following line of code sets the Caption property of the Employees form:
Forms!Employees.Caption = "Employees Form"
When you read the value of a property, you determine its current value. In order to read the property, you can
assign its value to a variable or to another property, or you can display it in the Debug window, in a dialog box, or
in a control on a form or report. The following example assigns the value of the Caption property to a variable and
then displays the value of that property in a dialog box.
Dim strCaption As String
strCaption = Forms!Employees.Caption
MsgBox strCaption
Properties That Return Objects
Sometimes you may want your code to refer to whatever object happens to be in a particular state at the time a
procedure is running, rather than to a specific object. Writing code in this way can make your application more
flexible. For instance, you may want to change the caption of the active form, without knowing the form's name.
Or you may want to hide the control that has just lost the focus.
Rather than determining an object's characteristics, some properties of an object represent another object that is
related in some way. These properties return an object reference that you can work with directly or assign to an
object variable, just as you would any object reference.
The Section Property
The Section property returns a reference to a section of a form or report. For example, you can use the Section
property to return a reference to the detail section of a form. Once you've returned a reference to a section, you
can set the section's properties. The following example uses the Section property to set a property of the detail
section on an Employees form.
Forms!Employees.Section(acDetail).Visible = False
The Me Property
The Me property returns an object reference to the Form or Report object in which the code is currently running.
You can use the Me property to refer to a Form or Report object from within that object, without needing to know
the name of the form or report. You can also use it to pass a Form or Report object to a procedure that takes an
argument of type Form or Report.
For example, the following code uses the Me property to return a reference to the Employees form, the form in
which the code is running. It then passes this reference to the ChangeDetailColor procedure which it calls when
the form's Current event occurs. It also uses the Me property to return references to the Employees form in order
to set a property and to return the values of the FirstName and LastName controls on the form. Note that the .
(dot) operator is used to set the property, and the ! operator is used to refer to the controls on the form.
89
90
91
Responding to Events
Rather than running entire programs line by line, Microsoft Access applications run macros and event procedures
in response to specific events for particular objects, such as a change to data in a field, or a mouse click on a
command button. Understanding the events Microsoft Access recognizes can help you create powerful, flexible
applications. This chapter describes the Microsoft Access event model and shows you how to manage events in
your applications.
Working with Events
Microsoft Access applications are event-driven; this means that the way they behave depends on how objects are
designed to respond to events. Objects in Microsoft Access respond to the following types of events:
Mouse clicks
Changes in data
Keystrokes a user types
Objects receiving or losing the focus
Forms being opened, closed, or resized
Reports being printed or formatted
Run-time errors
The focus is the application's ability to receive input or respond to a user's mouse or keyboard actions. In
Microsoft Windows, only one item at a time can have the focus. For example, when a user types, characters
appear in a text box only if the text box has the focus. Which object or control receives the focus is determined by
a user's actions, such as clicking in a text box or pressing TAB to move to a control. Before a user acts, settings
made at design time determine which control has the focus. For example, when a user first opens or switches to a
form, the control that has the focus is the one with the lowest TabIndex property setting. You can also explicitly
set the focus in code by using the SetFocus method.
You can see the events that are generated in Microsoft Access by opening the ShowEvents form in the Orders
sample application. The ShowEvents form, a special version of the Orders form, records each event as it occurs.
An accompanying EventHistory form lists the name of the event and the type or name of the object on which the
event occurred, using the format object_event. For example, if a Click event occurs on the ShowEvents subform,
the line "[Subform]Form_Click" is added to the Events list on the EventHistory form.
The EventHistory form lists events in reverse order, with the most recent event at the top of the list. The form
lists all events except MouseMove events, which occur each time you move the mouse pointer, and would quickly
fill up the list if they were included.
By default, Microsoft Access automatically responds to events with built-in behaviors defined for each object. For
example, when a user enters or changes data in a text box, Microsoft Access automatically checks to make sure
the data is of the right type.
In addition, each object in Microsoft Access has a set of event properties that correspond to each event to which
the object can respond. For example, the following table lists some of the event properties and corresponding
events for a check box.
92
You can specify a further response to an event by setting the object's corresponding event property. When an
event occurs that an object can respond to, Microsoft Access uses the setting of the object's corresponding event
property to determine how to respond:
If the event property is blank, Microsoft Access responds to the event only with its built-in behavior.
If the event property is set to the name of a macro, and the event can't be canceled, Microsoft Access first
performs the built-in behavior, and then runs the macro. If the event property is set to the name of a macro and the
event can be canceled, Microsoft Access first runs the macro, and then performs the built-in behavior.
If the event property is set to [Event Procedure], and the event can't be canceled, Microsoft Access first performs
the built-in behavior, and then calls the appropriate event procedure. If the event property is set to [Event
Procedure], and the event can be canceled, Microsoft Access first runs the macro, and then performs the built-in
behavior.
For example, when you click a command button whose OnClick event property is set to a macro, Microsoft
Access:
1. Makes the button appear pressed in momentarily the built-in behavior when a Click event occurs on a
command button. Note that the Click event can't be canceled.
2. Runs the macro.
When the event property is set to [Event Procedure], Microsoft Access responds to the event by running the
appropriate event procedure in addition to performing its built-in behavior. Event procedures are named for the
event and the object for which they occur, in the format object_event. For example, if a user clicks the Products
command button, Microsoft Access:
1. Makes the Products command button appear pressed in momentarily its built-in behavior.
2. Runs the Products_Click event procedure.
Note When you create an event procedure, Microsoft Access automatically sets the appropriate event property to
[Event Procedure] if the property doesn't already have a setting. As an alternative, you can set the property to
[Event Procedure], and then create the event procedure separately. For more information on creating an event
procedure, see Chapter 2, "Introducing Visual Basic," or search the Help index for "event procedures, creating."
You can also have Microsoft Access call a function in response to an event. To do so, add to the appropriate event
procedure an expression that calls the function, or type an equal sign (=) followed by the function name as the
event property setting in the property sheet.
For example, to call the CheckValues function when a form opens, you can type the following OnOpen property
setting in the form's property sheet:
=CheckValues()
Note Using an expression that calls a function as an event property setting for a form or control is useful when
you want to use code and the form's HasModule property is set to No so that it loads more quickly. For more
information, see "Optimizing Form Loading and Paging" in Chapter 13, "Optimizing Your Application."
The macros and Visual Basic code that Microsoft Access runs in response to events control how the objects in
your application work together. By managing events, and the macros and Visual Basic code that Microsoft Access
runs in response to events, you can create powerful, flexible database applications.
93
Microsoft Access or the Jet database engine encounters an error, or a specified time interval passes.
Working with Forms and Controls
Opening a form triggers a sequence of events, including the Open, Load, Resize, and Activate events. In addition,
if no control on the form can receive the focus, a GotFocus event occurs for the form itself. Other events occur as
you work with the form and its controls. You can write macros or Visual Basic code for any of these events, so
you have a fine degree of control over how your application behaves.
Because opening forms, moving between forms, and working with controls are some of the most common
operations in a Microsoft Access application, understanding the order of these events is one of the keys to
effective application development. This section describes the sequence of events for some common form and
control operations.
94
If there are no active controls on the form, Microsoft Access triggers a LostFocus event for the form after the
Unload event, but before the Deactivate event.
Entering and Exiting a Control
When you open a form that contains one or more active controls, an Enter event occurs, followed by a GotFocus
event, for the control receiving the focus. These events occur after the form's Activate and Current events:
Both events occur when a control first receives the focus. If you switch to a different form and then return to the
same control on the first form, Microsoft Access triggers a GotFocus event for the control, but not an Enter event.
When you exit a control for example, when you select another control on the same form the following events
occur for the control:
Switching Between Open Forms
When you switch between two open forms that contain active controls, Microsoft Access triggers a Deactivate
event on the first form and an Activate event on the second form:
Note An Open event doesn't occur on a form that is already open but not activated, whether you switch to the
form or run a macro that specifies the form in an OpenForm action. If you want your application to run the code
in a form's Open event procedure when the form is already open, you can:
Add the code to the form's Activate event procedure instead of the Open event procedure, if timing isn't critical.
The Activate event occurs both when you open a form and when you make it the active form, so the code is sure
to run.
Determine if the form is open by checking the value returned by the IsLoaded function in the UtilityFunctions
module of the Orders sample application. Do this before running the macro that contains the OpenForm action.
If there are no active controls on the forms, Microsoft Access also triggers the LostFocus and GotFocus events:
Switching Between Controls on Different Forms
This example shows the sequence of events that are triggered in a typical scenario while you work with forms and
controls.
Step One: Open a form Open the form Form1, whose first active control is Control1.
Step Two: Open a second form Open the form Form2, whose first active control is Control2.
There is no Exit(Control1) event, because the object that receives the focus is on a different form.
Step Three: Return to the first form Click on the first form.
Control1 now has the focus. There is no Enter(Control1) event because Control1 had the focus when Form1 was
last active.
Step Four: Click another control on the first form Click a different control, Control3, on the second form.
Step Five: Click another control on the second form Click a different control, Control4, on the second form.
95
Responding to Keystrokes
When you press a key, Microsoft Access triggers the KeyDown, KeyPress, and KeyUp events for the form or
control that has the focus. For example, when a control has the focus, you'll normally want the control to receive
all keystrokes when changing data in a text box.
In some cases, however, you'll want to respond to specific keys pressed in a form, regardless of which control has
the focus. For example, you may want to perform some action whenever the user presses a key combination such
as CTRL+Y. You can make sure that the form receives all key events, even those that occur in controls, by setting
the KeyPreview property for the form to Yes. With this property setting, all key events occur first for the form,
and then for the control that has the focus.
You can respond to specific keys in the form's KeyPress, KeyDown, or KeyUp events. The KeyPress event
responds only to the ANSI characters generated by the keyboard. ANSI characters are generated by the following
keys and key combinations: any printable keyboard character, CTRL+A through CTRL+Z, ENTER,
CTRL+ENTER, BACKSPACE, CTRL+BACKSPACE, and TAB. The KeyPress event ignores all other
keystrokes. In most cases, it is simplest to use only the KeyPress event to respond to keyboard events.
The following sample code demonstrates how to respond to the CTRL+Y key combination in a form by using the
KeyPress event. Note that you can prevent the control from getting keystrokes you respond to by setting the
KeyAscii variable to zero.
Private Sub Form_KeyPress (KeyAscii As Integer)
Const conCtrlYCode = 25
' ANSI character code for CTRL+Y.
If KeyAscii = conCtrlYCode Then
MsgBox "You pressed Ctrl+Y"
KeyAscii = 0
' Do not send key on to control.
End If
End Sub
The KeyDown and KeyUp events work on a lower level by responding to events generated by the keys
themselves being pressed and released. Use KeyDown and KeyUp events if you need to respond to keys that don't
generate ANSI characters, such as the function keys (F1 through F12), or if you need to respond to key
combinations that include the SHIFT, ALT, and CTRL keys (except the CTRL key combinations that respond to
the KeyPress event).
Working with Data
You can use data events in your application to respond to many types of changes to records and data. For
example, the application can run a macro or an event procedure in response to:
Changes to text in a text box or combo box.
Updates to data in a control or record.
Insertions or deletions of records, either before or after the record is inserted or deleted, or before or after a
deletion is confirmed.
Note Some events do not occur when you use Visual Basic code to manipulate data in your application for
example, the Change event, the BeforeInsert event, and the AfterInsert event.
Changing Text in a Text Box or Combo Box
When you change text in a text box or combo box, a Change event occurs. The event occurs whenever the
contents of a control changes, but before you move to a different control or record. For example, when you delete
96
a character in a text box by pressing the DELETE key, Microsoft Access triggers the following sequence of
events:
If you then type one or more characters in the text box, Microsoft Access recognizes the same sequence of events
for each keystroke.
The Change event doesn't occur when a value changes in a calculated control, or when you select an item from a
combo box list.
Using Data
Working with Records and Fields
The Microsoft Jet database engine supports a rich set of Data Access Objects (DAO) features for organizing,
sorting, searching, updating, adding, and deleting data. The Recordset object alone provides 24 methods and 26
properties that give you a great deal of control over records in a database. With the Recordset object's Fields
collection and the properties and methods of the Field object, you can manipulate data at the field level. This
chapter describes how to manipulate records and fields by using the DAO Recordset and Field objects.
Using Recordset Objects
A Recordset object represents the records in a base table or the records that result from running a query. You use
Recordset objects to manipulate the data in a database at the record level.
Note You use Field objects to manipulate the data in a database at the field level. For more information, see
"Using Field Objects" later in this chapter.
The four types of Recordset objects table, dynaset, snapshot, and forward-only differ from each other in
significant ways:
A table-type Recordset object can be created from a table in a Microsoft Access database, but not from an Open
Database Connectivity (ODBC) or a linked table. When you create a table-type Recordset, the Jet database engine
opens the actual table, and your subsequent data manipulations operate directly on base-table data. A table-type
Recordset can be opened on only one table; it cannot be opened on a union query or a select query with a join.
One of the biggest advantages of this type of Recordset object is that you can index it by using an index created
for the underlying table. This allows much faster sorting and searching than is possible with the other types. To
locate specific records, use the Seek method, which is faster than the Find methods.
A dynaset-type Recordset object can be created from either a local or a linked table, or with a row-returning
query that joins tables. It's actually a set of references to records in one or more tables. With a dynaset, you can
extract and update data from more than one table, including linked tables from other databases. Heterogeneous
updatable joins are a unique feature of dynasets they enable you to use updatable queries against tables in
different types of databases.
One of the main benefits of this type is that a dynaset and its underlying tables update each other. Changes made
to records in the dynaset are also made in the underlying table, and changes made by other users to data in the
underlying tables while the dynaset is open are reflected in the dynaset. The dynaset is the most flexible and
powerful type of Recordset object, although searches and other manipulations may not run as fast as with a tabletype Recordset.
A snapshot-type Recordset object is a static copy of a set of records as it exists at the time the snapshot is
created. A snapshot-type Recordset object can contain fields from one or more tables in a database. You can't
update a snapshot.
97
The main advantage of a snapshot is that it creates less processing overhead than the other types, so it can run
queries and return data faster, especially when working with ODBC data sources.
Note For .mdb files, OLE Object and Memo fields are represented in a snapshot by pointers, rather than the
actual data. For more information on OLE Object and Memo fields, see "The OLE Object and Memo Data Types"
later in this chapter.
A forward-only-type Recordset object, sometimes referred to as a forward-scrolling snapshot or a forward-only
snapshot, provides a subset of the capabilities of a snapshot. With forward-only snapshots, you can move only in a
forward direction through the records. Recordset objects of this type cannot be cloned and only support the Move
and MoveNext methods. Like snapshots, you can't update a forward-only-type Recordset object.
The advantage of a forward-only-type Recordset object is that it usually provides the greatest speed. It does,
however, offer the least functionality of any Recordset.
Note A snapshot stores a copy of the entire record (except for OLE Object and Memo fields). A dynaset stores
just the primary key for each record, copying the full record only when it's needed for editing or display purposes.
Since a snapshot stores a complete copy of all the records in a table, a snapshot may perform more slowly than a
dynaset if the number of records is large. To determine whether a snapshot or dynaset is faster, you can open the
Recordset as a dynaset and then open it as a snapshot to see which provides faster performance.
The type of Recordset object you use depends on what you want to do and whether you want to change or simply
view the data. For example, if you must sort the data or work with indexes, use a table. Because table-type
Recordset objects are indexed, they also provide the fastest way to locate data. If you want to be able to update a
set of records selected by a query, use a dynaset. If the table-type is unavailable and you only need to scan
through a set of records, using a forward-only snapshot may improve performance.
All other things being equal, if a table-type Recordset object is available, using it almost always results in the best
performance.
Note In this chapter, the terms table, snapshot, and dynaset are often used for the sake of simplicity. However,
keep in mind that these are all types of Recordset objects. For example, the term dynaset refers to a dynaset-type
Recordset object, not the obsolete DAO Dynaset object.
98
SQL query or statement. When you create a new Recordset object from a TableDef, QueryDef, or existing
Recordset object, the object itself provides the data source for the new Recordset.
The type argument is an intrinsic constant that specifies the kind of Recordset object that you want to create. You
can use the following constants:
dbOpenTable
dbOpenDynaset
dbOpenSnapshot
dbOpenForwardOnly
Note The dbOpenForwardOnly type constant replaces the dbForwardOnly type constant that was available in
previous versions of DAO. You can still use the dbForwardOnly constant, but it's provided only for backward
compatibility.
The following sections discuss the type, options, and lockedits arguments in detail.
Default Recordset Types
Because DAO automatically chooses the default Recordset type depending on the data source and how the
Recordset is opened, you don't need to specify a Recordset type. However, you can specify a type different from
the default by using a type argument in the OpenRecordset method.
The following list describes the available types and the default type, depending on how you open the Recordset
object:
Using the OpenRecordset method with a Database object:
Set rstNew = dbs.OpenRecordset("Data Source")
If Data Source is a table local to the database, all four types are available, and the table-type Recordset object is
the default. If Data Source is anything else, only dynaset- and snapshot-type Recordset objects are available, and
the dynaset type is the default.
Using the OpenRecordset method with a TableDef object:
Set rstNew = tdfTableData.OpenRecordset
If tdfTableData refers to a table in a Microsoft Access database (.mdb) or to an installable ISAM database opened
directly, then all four types are available and the table-type Recordset object is the default type. If tdfTableData is
in an ODBC database or is a linked table in an external database, only dynaset- and snapshot-type Recordset
objects are available, and the dynaset type is the default.
Using the OpenRecordset method with a QueryDef object:
Set rstNew = qdfQueryData.OpenRecordset
Only dynaset- and snapshot-type Recordset objects are available, and the dynaset type is the default.
Using the OpenRecordset method with an existing Recordset object:
Set rstNew = rstExisting.OpenRecordset
99
Only dynaset- and snapshot-type Recordset objects are available. The default is the type of the existing Recordset,
in this case, the type of rstExisting.
OpenRecordset Options
With the options argument of the OpenRecordset method, you can specify a number of other features for a
Recordset object. You can use the following constants:
dbAppendOnly Users can append new records to the Recordset, but they cannot edit or delete existing records.
This is useful in applications that collect and archive data (dynaset only).
dbReadOnly No changes can be made to the Recordset. This argument is provided only for backward
compatibility. Use the dbReadOnly constant in the lockedits argument instead.
dbSeeChanges If another user changes data in a record on which this Recordset has invoked the Edit method,
but before it has invoked the Update method, a run-time error occurs. This is useful in applications where multiple
users have simultaneous read/write permission on the same data (dynaset and table only).
dbDenyWrite When used on a dynaset or snapshot, this option prevents other users from adding or modifying
records, although they can still read data. When used on a table, no other user can open any type of Recordset
from an underlying table.
dbDenyRead Other users cannot read data in the table (table only).
dbForwardOnly This option creates a forward-only snapshot. It is provided only for backward compatibility.
Use the dbOpenForwardOnly constant in the type argument instead.
dbSQLPassThrough When the source argument is an SQL statement, use this constant to pass the SQL
statement to an ODBC database for processing. If used with a dynaset, data isn't updatable (dynaset and snapshot
only).
dbConsistent (Default) Only consistent updates are allowed (dynaset only). You can't use this constant with the
dbInconsistent constant.
dbInconsistent Inconsistent updates are allowed. This is the opposite of dbConsistent (dynaset only). You can't
use this constant with the dbConsistent constant.
With the lockedits argument of the OpenRecordset method, you can control how locking is handled for a
Recordset object. You can use the following constants:
dbReadOnly No changes can be made to the Recordset. This constant replaces the dbReadOnly constant that
was used in the options argument in previous versions of DAO.
dbPessimistic (Default) Microsoft Jet uses pessimistic locking to determine how changes are made to the
Recordset in a multiuser environment.
dbOptimistic Microsoft Jet uses optimistic locking to determine how changes are made to the Recordset in a
multiuser environment.
The default value is dbPessimistic. The only effect of using dbPessimistic or dbOptimistic is to preset the value of
the Recordset object's LockEdits property.
Important Setting both the lockedits argument and the options argument to dbReadOnly generates a run-time
error.
Creating a Recordset Object from a Form
100
You can create a Recordset object based on a Microsoft Access form. To do so, use the RecordsetClone property
of the form. This creates a dynaset-type Recordset that refers to the same underlying query or data as the form. If
a form is based on a query, referring to the RecordsetClone property is the equivalent of creating a dynaset with
the same query. You can use the RecordsetClone property when you want to apply a method that can't be used
with forms, such as the FindFirst method. The RecordsetClone property provides access to all the methods and
properties that you can use with a dynaset. The syntax for the RecordsetClone property is:
Set variable = form.RecordsetClone
The variable argument is the name of an existing Recordset object. The form argument is the name of a Microsoft
Access form. The following example shows how to assign a Recordset object to the records in the Orders form:
Dim rstOrders As Recordset
Set rstOrders = Forms!Orders.RecordsetClone
This code always creates the type of Recordset being cloned (the type of Recordset on which the form is based);
no other types are available.
Creating a Recordset Object from a Table
The method you use to create a Recordset object from a table depends on whether the table is local to the current
database or is a linked table in another database. The following discussion explains the differences and provides
examples for each type of table.
Creating a Recordset from a Table in a Local Microsoft Access Database
The following example uses the OpenRecordset method to create a table-type Recordset object for a table in the
current database:
Dim dbs As Database, rstCustomers As Recordset
Set dbs = CurrentDb
Set rstCustomers = dbs.OpenRecordset("Customers")
Notice that you don't need to use the dbOpenTable constant to create a table-type Recordset. If you omit the type
constant, as discussed in "Default Recordset Types" earlier in this chapter, DAO chooses the highest-functionality
Recordset type available, depending on the object in which the Recordset is created, and the data source. Because
the table type is available when you open a Recordset from a local table, DAO uses it.
Creating a Recordset from a Linked Table in a Different Database Format
The following example creates a dynaset-type Recordset object for a linked Paradox version 3.x table. Because
the table type isn't available when you open a Recordset from a linked table in a database other than a Microsoft
Access database, DAO selects the next most efficient type, opening a dynaset-type Recordset.
Dim dbs As Database
Dim tdf As TableDef
Dim rstTableData As Recordset
' Get current database.
Set dbs = CurrentDb
Set tdf = dbs.CreateTableDef("PDXAuthor")
' Connect to the Paradox table Author in the database
' C:\PDX\Publish.
tdf.Connect = "Paradox 3.X;DATABASE=C:\PDX\Publish"
tdf.SourceTableName = "Author"
' Link the table.
101
dbs.TableDefs.Append tdf
' Create a dynaset-type Recordset for the table.
Set rstTableData = tdf.OpenRecordset()
You can also open a Paradox table directly by first opening the Paradox database.
Using an Index on a Table-Type Recordset Object
You can order records in a table-type Recordset object by setting its Index property. Any Index object in the
Indexes collection of the Recordset object's underlying table definition can be specified with the Index property.
The following example creates a table-type Recordset object based on the Customers table, by using an existing
index called City:
Dim dbs As Database, rstTableData As Recordset
Set dbs = CurrentDb
Set rstTableData = dbs.OpenRecordset("Customers", dbOpenTable)
' Move to the first record.
rstTableData.MoveFirst
' First record with no index set.
MsgBox rstTableData!CompanyName
rstTableData.Index = "City"
' Select the City index.
rstTableData.MoveFirst
' Move to the first record.
MsgBox rstTableData!CompanyName
rstTableData.Close
If you set the Index property to an index that doesn't exist, a trappable run-time error occurs. If you want to sort
records according to an index that doesn't exist, either create the index first, or create a dynaset- or snapshot-type
Recordset by using a query that returns records in a specified order.
Important You must set the Index property before using the Seek method. For information on using the Seek
method to locate records that satisfy criteria that you specify, see "Finding a Record in a Table-Type Recordset
Object" later in this chapter.
Creating a Recordset Object from a Query
You can also create a Recordset object based on a stored select query. In the following example, Current Product
List is an existing select query stored in the current database:
Dim dbs As Database, rstProducts As Recordset
Set dbs = CurrentDb
Set rstProducts = dbs.OpenRecordset("Current Product List")
If a stored select query doesn't already exist, the OpenRecordset method also accepts an SQL string instead of the
name of a query. The previous example can be rewritten as follows:
Dim dbs As Database, rstProducts As Recordset
Dim strQuerySQL As String
Set dbs = CurrentDb
strQuerySQL = "SELECT * FROM Products WHERE Discontinued = No " _
& "ORDER BY ProductName;"
Set rstProducts = dbs.OpenRecordset(strQuerySQL)
The disadvantage of this approach is that the query string must be compiled each time it's run, whereas the stored
query is compiled the first time it's saved, which usually results in slightly better performance.
102
Note When you create a Recordset object by using an SQL string or a stored query, your code doesn't continue
running until the query returns the first row in the Recordset.
103
Sub AddQuery()
Dim dbs As Database
Dim qdf As QueryDef
Dim rstSalesReps As Recordset
Set dbs = CurrentDb
Set qdf = dbs.CreateQueryDef("SalesRepQuery")
qdf.SQL = "SELECT * FROM Employees WHERE Title = 'Sales Representative'"
Set rstSalesReps = qdf.OpenRecordset()
' Call the function to add a constraint.
AddQueryFilter rstSalesReps
' Return database to original.
dbs.QueryDefs.Delete "SalesRepQuery"
rstSalesReps.Close
End Sub
Function AddQueryFilter(rst As Recordset)
Dim qdf As QueryDef
Dim strNewFilter As String, strRightSQL As String
Set qdf = rst.CopyQueryDef
' Try "LastName LIKE 'D*'".
strNewFilter = InputBox("Enter new criteria")
strRightSQL = Right(qdf.SQL, 1)
' Strip characters from the end of the query,
' as needed.
Do While strRightSQL = " " Or strRightSQL = ";" Or strRightSQL = vbCR Or _
strRightSQL = vbLF
qdf.SQL = Left(qdf.SQL, Len(qdf.SQL) - 1)
strRightSQL = Right(qdf.SQL, 1)
Loop
qdf.SQL = qdf.SQL & " AND " & strNewFilter & ";"
rst.Requery qdf
' Requery the Recordset.
rst.MoveLast
' Populate the Recordset.
' "Lastname LIKE 'D*'" should return 2 records.
MsgBox "Number returned = " & rst.RecordCount
End Function
Note To use the Requery method, the Restartable property of the Recordset object must be set to True. The
Restartable property is always set to True when the Recordset is created from a query other than a crosstab query
against tables in a Microsoft Access database. You can't restart SQL pass-through queries. You may or may not be
able to restart queries against linked tables in another database format. To determine whether a Recordset object
can rerun its query, check the Restartable property. For more information on the Restartable property, search the
Help index for "Restartable property."
The DAO Sort and Filter Properties
Another approach to sorting and filtering Recordset objects is to set the DAO Sort and Filter properties on an
existing Recordset, and then open a new Recordset from the existing one. However, this is usually much slower
than just including the sort and filter criteria in the original query or changing the query parameters and running it
again with the Requery method. The DAO Sort and Filter properties are useful when you want to allow a user to
sort or restrict a result set, but the original data source is unavailable for a new query for example, when a
Recordset object variable is passed to a function, and the function must reorder records or restrict the records in
the set. With this approach, performance is likely to be slow if the Recordset has more than 100 records. Using the
CopyQueryDef method described in the previous section is preferable.
Moving Through a Recordset Object
104
A Recordset object usually has a current position, most often at a record. When you refer to the fields in a
Recordset, you obtain values from the record at the current position, which is known as the current record.
However, the current position can also be immediately before the first record in a Recordset or immediately after
the last record. In certain circumstances, the current position is undefined.
You can use the following Move methods to loop through the records in a Recordset:
The MoveFirst method moves to the first record.
The MoveLast method moves to the last record.
The MoveNext method moves to the next record.
The MovePrevious method moves to the previous record.
The Move [n] method moves forward or backward the number of records you specify in its syntax.
You can use each of these methods on table-, dynaset-, and snapshot-type Recordset objects. On a forward-onlytype Recordset object, you can use only the MoveNext and Move methods. If you use the Move method on a
forward-only-type Recordset, the argument specifying the number of rows to move must be a positive integer.
The following example opens a Recordset object on the Employees table containing all of the records that have a
Null value in the ReportsTo field. The function then updates the records to indicate that these employees are
temporary employees. For each record in the Recordset, the example changes the Title and Notes fields, and saves
the changes with the Update method. It uses the MoveNext method to move to the next record.
Function UpdateEmployees()
Dim dbs As Database, rstEmployees As Recordset, strQuery As String
Dim intI As Integer
On Error GoTo ErrorHandler
Set dbs = CurrentDb
' Open a recordset on all records from the Employees table that have
' a Null value in the ReportsTo field.
strQuery = "SELECT * FROM Employees WHERE ReportsTo IS NULL;"
Set rstEmployees = dbs.OpenRecordset(strQuery, dbOpenDynaset)
' If the recordset is empty, exit.
If rstEmployees.EOF Then Exit Function
intI = 1
With rstEmployees
Do Until .EOF
.Edit
![ReportsTo] = 5
![Title] = "Temporary"
![Notes] = rstEmployees![Notes] & "Temp #" & intI
.Update
.MoveNext
intI = intI + 1
Loop
.Close
End With
ErrorHandler:
Select Case Err
Case 0
Exit Function
Case Else
105
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
Exit Function
End Select
End Function
Note The previous example is provided only for the purposes of illustrating the Update and MoveNext methods.
It would be much faster to perform this bulk operation with an SQL UPDATE query.
Detecting the Limits of a Recordset Object
In a Recordset object, if you try to move too far in one direction, a run-time error occurs. For example, if you try
to use the MoveNext method when you're already beyond the end of the Recordset, a trappable error occurs. For
this reason, it's helpful to know the limits of the Recordset object.
The BOF property indicates whether the current position is at the beginning of the Recordset. If BOF is True, the
current position is before the first record in the Recordset. The BOF property is also True if there are no records in
the Recordset when it's opened. Similarly, the EOF property is True if the current position is after the last record
in the Recordset, or if there are no records.
The following example shows you how to use the BOF and EOF properties to detect the beginning and end of a
Recordset object. This code fragment creates a table-type Recordset based on the Orders table from the current
database. It moves through the records, first from the beginning of the Recordset to the end, and then from the end
of the Recordset to the beginning.
Dim dbs As Database, rstOrders As Recordset
Set dbs = CurrentDb
Set rstOrders = dbs.OpenRecordset("Orders", dbOpenTable)
Do Until rstOrders.EOF
.
. ' Manipulate data.
.
rstOrders.MoveNext
' Move to the next record.
Loop
rstOrders.MoveLast
' Move to the last record.
' Do Until beginning of file.
Do Until rstOrders.BOF
.
. ' Manipulate data.
.
' Move to the previous record.
rstOrders.MovePrevious
Loop
rstOrders.Close
' Close the Recordset.
Notice that there's no current record immediately following the first loop. The BOF and EOF properties both have
the following characteristics:
If the Recordset contains no records when you open it, both BOF and EOF are True.
When BOF or EOF is True, the property remains True until you move to an existing record, at which time the
value of BOF or EOF becomes False.
When BOF or EOF is False, and the only record in a Recordset is deleted, the property remains False until you
try to move to another record, at which time both BOF and EOF become True.
106
At the moment you create or open a Recordset that contains at least one record, the first record is the current
record, and both BOF and EOF are False.
If the first record is the current record when you use the MovePrevious method, BOF is set to True. If you use
MovePrevious while BOF is True, a run-time error occurs. When this happens, BOF remains True and there is no
current record.
Similarly, moving past the last record in the Recordset changes the value of the EOF property to True. If you use
the MoveNext method while EOF is True, a run-time error occurs. When this happens, EOF remains True and
there is no current record.
The following illustration shows the settings of the BOF and EOF properties for all possible current positions in a
Recordset.
Counting the Number of Records in a Recordset Object
You may want to know the number of records in a Recordset object. For example, you may want to create a form
that shows how many records are in each of the tables in a database. Or you may want to change the appearance
of a form or report based on the number of records it includes.
The RecordCount property contains the number of records in a table-type Recordset or the total number of
records accessed in a dynaset- or snapshot-type Recordset. A Recordset object with no records has a RecordCount
property value of 0.
Note The value of the RecordCount property equals the number of records that have actually been accessed. For
example, when you first create a dynaset or snapshot, you have accessed (or visited) only one record. If you check
the RecordCount property immediately after creating the dynaset or snapshot (assuming it has at least one record),
the value is 1. To visit all the records, use the MoveLast method immediately after opening the Recordset, then
use MoveFirst to return to the first record. This isn't done automatically because it may be slow, especially for
large result sets.
When you open a table-type Recordset object, you effectively visit all of the records in the underlying table, and
the value of the RecordCount property totals the number of records in the table as soon as the Recordset is
opened. Canceled transactions may make the value of the RecordCount property out-of-date in some multiuser
situations. Compacting the database restores the table's record count to the correct value.
The following example creates a snapshot-type Recordset object, and then determines the number of records in
the Recordset:
Function RecCount(strSQL As String) As Long
Dim rstCount As Recordset
Dim dbs As Database
On Error GoTo ErrorHandler
Set dbs = CurrentDb
Set rstCount = dbs.OpenRecordset(strSQL)
If rstCount.EOF Then
rstCount.Close
RecCount = 0
Exit Function
Else
rstCount.MoveLast
RecCount = rstCount.RecordCount
rstCount.Close
Exit Function
End If
107
ErrorHandler:
Select Case Err
Case 0
Exit Function
Case Else
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
Exit Function
End Select
End Function
As your application deletes records in a dynaset-type Recordset, the value of the RecordCount property decreases.
However, in a multiuser environment, records deleted by other users aren't reflected in the value of the
RecordCount property until the current record is positioned on a deleted record. At that time, the setting of the
RecordCount property decreases by one. Using the Requery method on a Recordset, followed by the MoveLast
method, sets the RecordCount property to the current total number of records in the Recordset.
A snapshot-type Recordset object is static and the value of its RecordCount property doesn't change when you add
or delete records in the snapshot's underlying table.
Finding the Current Position in a Recordset Object
In some situations, you need to determine how far through a Recordset object you have moved the current record
position, and perhaps indicate the current record position to a user. For example, you may want to indicate the
current position on a dial, meter, or similar type of control. Two properties are available to indicate the current
position: the AbsolutePosition property and the PercentPosition property.
The AbsolutePosition property value is the position of the current record relative to 0. However, don't think of this
property as a record number; if the current record is undefined, the AbsolutePosition property returns - 1. In
addition, there is no assurance that a record will have the same absolute position if the Recordset object is recreated because the order of individual records within a Recordset object isn't guaranteed unless it's created with
an SQL statement that includes an ORDER BY clause.
The PercentPosition property shows the current position expressed as a percentage of the total number of records
indicated by the RecordCount property. Because the RecordCount property doesn't reflect the total number of
records in the Recordset object until the Recordset has been fully populated, the PercentPosition property only
reflects the current record position as a percentage of the number of records that have been accessed since the
Recordset was opened. To make sure that the PercentPosition property reflects the current record position relative
to the entire Recordset, use the MoveLast and MoveFirst methods immediately after opening the Recordset. This
fully populates the Recordset object before you use the PercentPosition property. If you have a large result set,
using the MoveLast method may take a long time for Recordsets that aren't of type table.
Important The PercentPosition property is only an approximation and shouldn't be used as a critical parameter.
This property is best suited for driving an indicator that marks a user's progress while moving though a set of
records. For example, you may want a control that indicates the percent of records completed. For more
information on the PercentPosition property, search the Help index for "PercentPosition property."
The following example opens a Recordset object on a table called Employees. The procedure then moves through
the Employees table and uses the SysCmd function to display a progress bar showing the percentage of the table
that's been processed. If the hire date of the employee is before Jan. 1, 1993, the text "Senior Staff" is appended to
the Notes field.
Function PercentPos()
Dim dbs As Database, strMsg As String, rstEmployees As Recordset, intRet As Integer
Dim intCount As Integer, strQuery As String, sngPercent As Single
Dim varReturn As Variant
Dim lngEmpID() As Long
108
109
Inserting Records
When you enter data in a new record by way of the user interface, Microsoft Access triggers a BeforeInsert event
when you first enter data in the record, and an AfterInsert event when the record is saved.
Entering Data in a New Record
This example shows the sequence of events that Microsoft Access triggers in a typical scenario when you enter
data in a new record.
Step One: Enter text in the first field of a new record After clicking Data Entry on the Records menu of a form to
display a blank record, type a character in a text box (TB1).
Step Two: Move to another field of the same record and enter text Click another text box (TB2) on the form and
type a character.
Step Three: Save the new record Click Save Record on the Records menu.
Deleting Records
When you select a record and delete it (either by pressing the DELETE key or by clicking Delete on the Edit
menu), Microsoft Access triggers the Delete event, and then the Current event. If you select multiple records and
delete them, the Delete event occurs once for each record that you have selected, and then Microsoft Access
triggers the Current event. Unless you cancel the Delete event, Microsoft Access also triggers BeforeDelConfirm
and AfterDelConfirm events. You use these events to control how record deletions are confirmed.
For example, when you select a record on a form and delete it, Microsoft Access by default:
Finding a Specific Record
The previous section, "Moving Through a Recordset Object," explores ways you can use the Move methods
MoveFirst, MoveLast, MoveNext, MovePrevious, and Move to loop through records in a Recordset object.
However, in most cases it's more efficient to search for a specific record.
For example, you may want to find a particular employee based on an employee number, or you may want to find
all of the detail records that belong to a specific order. In these cases, looping through all of the employee or order
detail records could be time consuming. Instead, you can use the Seek method with table-type Recordset objects,
and the Find methods with dynaset- and snapshot-type Recordset objects to locate records. Since the forwardonly-type Recordset object doesn't support the Seek method or any of the Find methods, you cannot search for
records in a forward-only-type Recordset.
Finding a Record in a Table-Type Recordset Object
You use the Seek method to locate a record in a table-type Recordset object.
When you use the Seek method to locate a record, the Microsoft Jet database engine uses the table's current index,
as defined by the Index property.
Important If you use the Seek method on a table-type Recordset object without first setting the current index, a
run-time error occurs.
The syntax for the Seek method is:
table.Seek comparison, key1, key2 ...
110
The table argument is the table-type Recordset object you're searching through. The comparison argument is a
string that determines the kind of comparison that is being performed. The following table lists the comparison
strings you can use with the Seek method.
Greater than the specified key values
Less than the specified key values
The keyn arguments are a series of one or more values that correspond to the field or fields that make up the
current index of the Recordset. Microsoft Jet compares these values to values in the corresponding fields of the
Recordset object's records.
The following example opens a table-type Recordset object called Employees, and uses the Seek method to locate
the record containing a value of lngEmpID in the EmployeeID field. It returns the hire date for the specified
employee.
Function intGetHireDate(lngEmpID As Long, varHireDate As Variant) As Integer
Dim rstEmployees As Recordset, dbs As Database
Const conFilePath As String = "C:\Program Files\Microsoft Office\Office\Samples\"
On Error GoTo ErrorHandler
Set dbs = OpenDatabase(conFilePath & "Northwind")
Set rstEmployees = dbs.OpenRecordset("Employees", dbOpenTable)
rstEmployees.Index = "PrimaryKey"
' The index name for Employee ID.
rstEmployees.Seek "=", lngEmpID
If rstEmployees.NoMatch Then
varHireDate = Null
' The constants conErrNoMatch, conSuccess, and conFailed are defined at
' the module level as public constants with Integer values of
' -32,761, 0, and -32,737 respectively.
intGetHireDate = conErrNoMatch
Exit Function
Else
varHireDate = rstEmployees!HireDate
intGetHireDate = conSuccess
Exit Function
End If
ErrorHandler:
Select Case Err
Case 0
Exit Function
Case Else
varHireDate = Null
intGetHireDate = conFailed
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
Exit Function
End Select
End Function
The Seek method always starts searching for records at the beginning of the Recordset object. If you use the Seek
method with the same arguments more than once on the same Recordset, it finds the same record.
You can use the NoMatch property on the Recordset object to test whether a record matching the search criteria
was found. If the record matching the criteria was found, the NoMatch property will be False; otherwise it will be
True.
Triggers the following sequence of events:
111
112
Note No event occurs for an object by default. The default event is only the event procedure that Microsoft
Access displays when you click Build Event on the shortcut menu. Microsoft Access always runs the procedure
associated with an object and the event that actually occurs, regardless of the object's default event.
The following table shows the default events for Microsoft Access objects that have them.
113
when you don't need to use them. You can also use conditional compilation to specify when to include these
statements and when to ignore them during compilation and at run time.
Note The Debug window doesn't open automatically when Visual Basic encounters a Debug.Print statement. If
you don't have the Debug window open, you won't see the values displayed by the Debug.Print statement. You
can open the Debug window quickly by pressing CTRL+G.
Tips on Using the Immediate Pane
You can use the following shortcuts in the Immediate pane:
Once you've run a statement in the Immediate pane, you can run it again by putting the insertion point anywhere
in the statement and pressing ENTER.
Before pressing ENTER, you can edit the statement in which the insertion point appears.
You can use the mouse or the arrow keys to move the insertion point in the Immediate pane. Press ENTER only
if the insertion point is on a statement you want to run.
You can use the PAGE UP and PAGE DOWN keys within the Immediate pane to move through your code one
page at a time. Pressing CTRL+ END moves the insertion point to the end of the Immediate pane.
You can use the HOME key to move the insertion point to the beginning of the current line and the END key to
move the insertion point to the end of the current line.
114
The Calls dialog box lists all the active procedure calls in a series of nested procedure calls. It places the earliest
active procedure call at the bottom of the list and adds subsequent procedure calls to the top. The information
given for each procedure begins with the name of the database and module that contain the procedure, followed
by the name of the called procedure.
You can use the Show button to display the statement that calls the next procedure listed in the Calls dialog box. If
you choose the current (top) procedure in the Calls dialog box and then click Show, Visual Basic displays the
current statement (the statement at which execution is suspended).
Debugging Event Procedures
Certain events that are a normal part of using Microsoft Windows can pose special challenges when you're
debugging an application that uses event procedures. It's important to be aware of these issues so they don't
confuse you or complicate the debugging process.
If you keep in mind that suspending execution can put events at odds with what your application expects, you can
usually find solutions. You may need to use the Print method of the Debug object instead of breakpoints to
monitor values of properties or variables. You may also need to change the values of variables that depend on the
sequence of events.
If you suspend execution during a MouseDown event procedure, you can release the mouse button or use the
mouse to do any number of tasks. However, when you continue execution, the application assumes that the mouse
button is still pressed down. A MouseUp event doesn't occur until you press the mouse button down again and
release it.
When you press the mouse button down after you continue execution, you suspend execution in the MouseDown
event procedure again if it contains a breakpoint. In this scenario, the MouseUp event never occurs. The typical
solution is to remove the breakpoint in the MouseDown event procedure.
If you suspend execution during a KeyDown event procedure, considerations similar to those during a
MouseDown event procedure apply. If you retain a breakpoint in a KeyDown event procedure, a KeyUp event
may never occur.
If you suspend execution during a GotFocus or LostFocus event procedure, the timing of system messages may
cause inconsistent results. You can avoid this problem by using the Print method of the Debug object instead of
suspending execution in GotFocus or LostFocus event procedures.
Using Conditional Compilation
You use conditional compilation to specify the parts of your program you want Microsoft Access to include or
ignore when compiling and running. Using conditional compilation, you can maintain a single version of your
application that behaves differently under certain conditions. For example, you may use conditional compilation
to:
Include specific features of your program in different versions of your application. For example, you may want
to design your application to run on different platforms.
Change the date and currency display filters for an application distributed in several different languages.
Include or exclude code used for debugging your application.
Structuring Code for Conditional Compilation
115
Visual Basic provides special statements called directives for creating conditional compilation constructs. You use
the #Const directive to declare a Boolean conditional compilation constant. You then evaluate this constant within
the #If...Then...#Else directive.
To conditionally compile a portion of your code, enclose it between #If...Then and #End If statements, using the
conditional compilation constant as the branching test. When you want this segment of code to be compiled and
run, set the value of the constant to True (-1). Otherwise, set the constant to False (0).
For example, suppose you want to include a portion of code only in an administrator's copy of your application.
Start by wrapping this segment in an If...Then statement preceded by a number sign (#).
#If Admn Then
.
. ' Insert code to be compiled and run only for an administrator's
. ' version.
#End If
If the value of the constant Admn is set to True at compile time, Visual Basic compiles and runs the conditional
code. Otherwise, Visual Basic ignores it.
Note Unlike regular Visual Basic code, you can't include other statements on the same line as a conditional
compilation statement by separating those statements with colons.
Declaring Conditional Compilation Constants
You can declare conditional compilation constants by setting the Conditional Compilation Arguments option on
the Advanced tab of the Options dialog box (Tools menu). The list should contain simple assignment statements
separated by colons. For example:
Admn = True : Ansi = 0
Alternatively, you can explicitly declare conditional compilation constants in the Declarations section of the
module containing the #If...Then and #Else statements, as follows:
#Const Admn = True
Conditional compilation constants have a special scope and cannot be accessed from standard code. While
constants declared with the #Const statement are private to the module in which they are declared, constants
declared in the Options dialog box are public to all modules in your application.
Only conditional compilation constants and literals can be used in expressions that you specify by way of the user
interface or with the #Const statement. Any undeclared identifier used in a conditional compilation expression
generates a compile error.
Using Conditional Compilation for Debugging
You can use conditional compilation to remove debugging statements from the application you distribute to users.
To do this, use conditional compilation to include these statements during development, and then ignore these
statements in the version of your application that you distribute to users.
The following example procedure uses these statements to display an assertion message when a function is passed
a value it isn't designed to handle. You may use a function like this one while writing and debugging your code.
Once you've finished debugging your code and you're ready to distribute your application to users, you no longer
need the function.
Sub Assertion(blnExpr As Boolean, strMsg As String)
116
117
The Err Object, the Errors Collection, and the Error Function
Microsoft Access provides the Err object and the Errors collection to help you retrieve information about run-time
errors in your applications, and regenerate errors when necessary.
Microsoft Access uses the Err object to store information about the most recent run-time error that has occurred.
When a run-time error occurs, the properties of the Err object are filled with information that uniquely identifies
the error's number, description, and source. Depending on the type of error that is generated, you may also be able
to obtain other information about the error.
You can also use the Err object to regenerate errors that have occurred in your application.
The Errors collection is contained by the DBEngine object. It contains one Error object for each error that is
generated by a single operation involving Data Access Objects (DAO). If multiple errors occur during a single
operation, then the collection contains more than one Error object. When another operation generates an error or
errors, the existing collection is cleared and refreshed with a new set of Error objects. Unlike other collections, the
Errors collection doesn't append objects to existing objects as subsequent operations occur.
In addition to the Error object and the Errors collection, there is also an Error function. The Error function returns
the error description that corresponds to a given error number. The Error function and the Description property of
the Err object return the same value.
118
error message in the Find What box, and then click Find First. You can also base a query or report on the table, or
sort the Error String column.
The Err Object, the Errors Collection, and the Error Function
Microsoft Access provides the Err object and the Errors collection to help you retrieve information about run-time
errors in your applications, and regenerate errors when necessary.
Microsoft Access uses the Err object to store information about the most recent run-time error that has occurred.
When a run-time error occurs, the properties of the Err object are filled with information that uniquely identifies
the error's number, description, and source. Depending on the type of error that is generated, you may also be able
to obtain other information about the error.
You can also use the Err object to regenerate errors that have occurred in your application.
The Errors collection is contained by the DBEngine object. It contains one Error object for each error that is
generated by a single operation involving Data Access Objects (DAO). If multiple errors occur during a single
operation, then the collection contains more than one Error object. When another operation generates an error or
errors, the existing collection is cleared and refreshed with a new set of Error objects. Unlike other collections, the
Errors collection doesn't append objects to existing objects as subsequent operations occur.
In addition to the Error object and the Errors collection, there is also an Error function. The Error function returns
the error description that corresponds to a given error number. The Error function and the Description property of
the Err object return the same value.
119
120
If your application doesn't handle Visual Basic run-time errors, users may be surprised if a run-time error
suddenly halts the application. It's especially important to handle Visual Basic errors if you're creating a run-time
version of your application. A run-time application shuts down if an untrapped error occurs.
For example, the following procedure doesn't contain error-handling code. It returns True (- 1) if the specified file
exists and False (0) if it doesn't exist.
Function FileExists (ByVal strFileName As String) As Boolean
FileExists = (Dir(strFileName) <> "")
End Function
The Dir function returns the first file matching the specified file name, and returns a zero-length string ("") if no
matching file is found. The code appears to cover either of the possible outcomes of the Dir call. However, if the
drive letter specified in the argument isn't a valid drive, the run-time error message "Device unavailable" is
displayed. If the specified drive is a floppy disk drive, this function works correctly only if a disk is in the drive
and the drive door is closed. If not, the run-time error "Disk not ready" occurs. In both cases, Microsoft Access
displays the error message and halts execution of the code.
To avoid this situation, you can use an On Error statement to respond to Visual Basic errors and take corrective
action. For example, device problems such as an invalid drive or an empty floppy disk drive can be handled by
the following code:
Function FileExists (ByVal strFileName As String) As Boolean
Dim strMsg As String
On Error GoTo CheckError
' Turn on error handling.
FileExists = (Dir(strFileName) <> "") ' Use Dir function to see
' if file exists.
Exit Function
' Avoid running error-handling
' code if no error occurs.
CheckError:
' Run following code
' if error occurs.
' Declare constants to represent Visual Basic error codes.
Const conErrDiskNotReady = 71, conErrDeviceUnavailable = 68
' vbExclamation, vbOK, vbCancel, vbCritical, and vbOKCancel are
' intrinsic constants that don't need to be declared.
If (Err.Number = conErrDiskNotReady) Then
' Display message box with an exclamation point icon and with
' OK and Cancel buttons.
strMsg = "Put a floppy disk in the drive and close the drive door."
If MsgBox(strMsg, vbExclamation + vbOKCancel) = vbOK Then
Resume
Else
Resume Next
End If
ElseIf Err.Number = conErrDeviceUnavailable Then
strMsg = "This drive or path does not exist: " & strfilename
MsgBox strMsg, vbExclamation
Resume Next
Else
strMsg = "Error number " & Str(Err.Number) & " occurred: " & _
Err.Description
' Display message box with stop sign icon and OK button.
MsgBox strMsg, vbCritical
Stop
End If
Resume
121
End Function
This code uses properties of the Err object to return the error code number and the message string associated with
the run-time error that occurred.
When Visual Basic generates the error "Disk not ready," the FileExists function displays a message telling the
user to click one of two buttons, OK or Cancel. If the user clicks OK, the Resume statement returns program
control to the statement at which the error occurred and attempts to run that statement again. This statement
succeeds if the user has corrected the problem; otherwise, the program returns to the error-handling code.
If the user clicks Cancel, the Resume Next statement returns program control to the statement following the one at
which the error occurred, in this case, the Exit Function statement.
If the "Device unavailable" error occurs, Visual Basic displays a message describing the problem. The Resume
Next statement then returns program control to the statement following the one at which the error occurred.
If an unanticipated error occurs, Visual Basic displays an alternative message and halts the code at the Stop
statement.
The error-handling code in the preceding example involves three steps:
1. Turn on error handling, and set (enable) an error trap by telling the application where to branch (which errorhandling routine to run) when an error occurs.
The On Error statement in the FileExists function turns on error handling and directs the application to the
CheckError line label.
2. Write error-handling code that responds to all errors you can anticipate. If program control actually branches to
the error-handling code at some point, the trap is then said to be active.
The CheckError code handles the error by using an If...Then...Else statement that checks the value returned by the
Number property of the Err object. The Number property of the Err object returns an error code number
corresponding to the error message that Visual Basic generates. In the example, if the "Disk not ready" error is
generated, a message prompts the user to close the drive door. A different message is displayed if the "Device
unavailable" error occurs.
Your code should also determine what action to take if an unanticipated error occurs. In the previous FileExists
function, if any error other than "Disk not ready" or "Device unavailable" occurs, a general message is displayed
and the program stops.
3. Exit the error-handling code.
In the case of the "Disk not ready" error, the Resume statement passes program control back to the statement at
which the error occurred. Visual Basic then tries to run that statement again. If the situation hasn't changed, then
the same error occurs again, and execution branches back to the error-handling code.
In the case of the "Device unavailable" error, the Resume Next statement passes program control to the statement
following the one at which the error occurred.
Writing Error-Handling Code
Begin the error-handling code with the line label specified in the On Error statement. In the FileExists function,
the line label is CheckError. The colon is part of the label, although it isn't used in the On Error GoTo line
statement.
122
You'll usually include error-handling code at the end of a procedure, before the End Function or End Sub
statement. Enter an Exit Function, Exit Sub, or Exit Property statement at the end of the main procedure code, but
immediately preceding the error handler's line label. This prevents Visual Basic from running the error-handling
code at the conclusion of the main-procedure code if no error occurs.
The Number property of the Err object returns an error code number representing the most recent run-time error.
By using the Number property of the Err object in combination with the Select Case or If...Then...Else statement,
you can take specific action for any error that occurs.
Note The string returned by the Description property of the Err object always explains the error associated with
the current error code number. However, the exact wording of the message may vary among different versions of
Microsoft Access. Therefore, use the Number property rather than the Description property to identify the specific
error that occurred.
When designing your error-handling routine, include code that tells the user what the problem is and how to
proceed. Also, if the application can't continue after an error is encountered, it's a good idea to close open objects,
remote connections, and database files when you exit the application.
Exiting Error-Handling Code
The preceding FileExists function uses the Resume statement within the error-handling code to rerun the
statement that caused the error, and uses the Resume Next statement to resume execution at the statement
following the one at which the error occurred. Depending on the circumstances, there are other ways to exit errorhandling code; regardless of which way you exit, you should always tell the error handler what to do when its
execution is complete. To exit error-handling code, use any of the statements shown in the following table.
Resumes program execution at the label specified by line, where line is a line label that must be in the same
procedure as the error handler.
Err.Raise Number:= number
Triggers the most recent run-time error again. When this statement is run within error-handling code, Visual Basic
searches backward through the calls list for other error-handling code. The calls list is the chain of procedures that
lead to the current point of execution. For more information, see "Unanticipated Errors" later in this chapter.
The Resume and Resume Next Statements
You can use the Resume and Resume Next statements to perform similar functions. The Resume statement returns
program control to the statement that caused the error. You use it to rerun the statement after correcting the error.
The Resume Next statement returns program control to the statement immediately following the one that caused
the error. The difference between Resume and Resume Next is shown in the following illustration.
Generally, you use the Resume statement whenever the user must make a correction. Use the Resume Next
statement whenever a correction by the user isn't required, and you want to continue program execution without
attempting to rerun the statement that caused the error. You can also use the Resume Next statement if you
anticipate an error in a loop, and you want to start the loop operation again if an error occurs. With the Resume
Next statement, you can write error-handling code so that the existence of a run-time error isn't revealed to the
user.
For example, the following Divide function uses error handling to perform division on its numeric arguments
without revealing errors that have occurred. There are several errors that can occur in division. If the numerator is
not zero, but the denominator is zero, Visual Basic generates the "Division by zero" error; if both numerator and
denominator are zero in floating-point division, it generates the "Overflow" error; or if either the numerator or the
denominator is a nonnumeric value (or can't be considered a numeric value), Visual Basic displays an appropriate
error message. In all three cases, the Divide function traps these errors and returns the Null value.
123
124
Err.Raise Number:=intErrNum
' Regenerate the error.
End Select
' This jumps back to the StartHere label so the user can
' try another file name.
Resume StartHere
End Function
If a file matching the specification is found, the function returns the file name. If no matching file is found, the
function returns a zero-length string (""). If one of the anticipated errors occurs, a message appropriate to the error
is assigned to Msg and execution branches to the StartHere line label. This gives the user another chance to enter
a valid path and file specification.
Use the Resume line statement when you want to resume execution at a place other than the statement that caused
the error, or the line immediately after the statement that caused the error. The Resume line statement can be
especially useful if you want to exit the error-handling code and branch to a point just before an Exit statement in
a Function or Sub procedure.
Note Although branching to a line label can be useful in some circumstances, jumps to labels are often
considered throwbacks to a less structured style of programming. Too many Resume line statements can make
code difficult to understand and debug.
The following example illustrates how you can create a function that uses the Seek method to locate a record by
using a multiple-field index:
Function GetFirstPrice(lngOrderID As Long, lngProductID As Long) As Variant
Dim dbs As Database, rstOrderDetail As Recordset
On Error GoTo ErrorHandler
Set dbs = CurrentDb
Set rstOrderDetail = dbs.OpenRecordset("Order Details", dbOpenTable)
rstOrderDetail.Index = "PrimaryKey"
rstOrderDetail.Seek "=", lngOrderID, lngProductID
If rstOrderDetail.NoMatch Then
GetFirstPrice = Null
MsgBox "Couldn't find order detail record."
Else
GetFirstPrice = rstOrderDetail!UnitPrice
End If
rstOrderDetail.Close
ErrorHandler:
Select Case Err
Case 0
Exit Function
Case Else
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
Exit Function
End Select
End Function
In this example, the table's primary key consists of two fields: OrderID and ProductID. When you call the
GetFirstPrice function with a valid (existing) combination of OrderID and ProductID field values, the function
returns the unit price from the found record. If it can't find the combination of field values you want in the table,
the function returns the Null value.
If the current index is a multiple-field index, trailing key values can be omitted and are treated as Null values.
That is, you can leave off any number of key values from the end of a Seek method's key argument, but not from
125
the beginning or the middle. However, if you don't specify all values in the index, you can use only the ">" or "<"
comparison string with the Seek method.
Finding a Record in a Dynaset- or Snapshot-Type Recordset Object
You can use the Find methods to locate a record in a dynaset- or snapshot-type Recordset object. DAO provides
four Find methods:
The FindFirst method finds the first record satisfying the specified criteria.
The FindLast method finds the last record satisfying the specified criteria.
The FindNext method finds the next record satisfying the specified criteria.
The FindPrevious method finds the previous record satisfying the specified criteria.
Note To locate a record in a table-type Recordset object, use the Seek method, which is described in the previous
section.
When you use the Find methods, you specify the search criteria; typically an expression equating a field name
with a specific value.
You can locate the matching records in reverse order by finding the last occurrence with the FindLast method and
then using the FindPrevious method instead of the FindNext method.
DAO sets the NoMatch property to True whenever a Find method fails and the current record position is
undefined. There may be a current record, but you have no way to tell which one. If you want to be able to return
to the previous current record following a failed Find method, use a bookmark.
The NoMatch property is False whenever the operation succeeds. In this case, the current record position is the
record found by one of the Find methods.
The following example illustrates how you can use the FindNext method to find all orders in the Orders table that
have no corresponding records in the Order Details table and adds the value in the OrderID field to the array
lngOrderID().
Function FindEx(lngOrderID() As Long)
Dim dbs As Database, rstOrders As Recordset
Dim strQuery As String, rstOrderDetails As Recordset
Dim intIndex As Integer
On Error GoTo ErrorHandler
Set dbs = CurrentDb
' Open recordsets on the Orders and Order Details tables. If there are no
' records in either table, exit the function.
strQuery = "SELECT * FROM Orders ORDER BY OrderID;"
Set rstOrders = dbs.OpenRecordset(strQuery, dbOpenSnapshot)
If rstOrders.EOF Then Exit Function
strQuery = "SELECT * FROM [Order Details] ORDER BY OrderID;"
Set rstOrderDetails = dbs.OpenRecordset(strQuery, dbOpenSnapshot)
' For the first record in Orders, find the first matching record
' in OrderDetails. If no match, redimension the array of order IDs and
' add the order ID to the array.
rstOrderDetails.FindFirst "OrderID = " & rstOrders![OrderID]
If rstOrderDetails.NoMatch Then
ReDim Preserve lngOrderID(1 To intIndex)
lngOrderID(intIndex) = rstOrders![OrderID]
126
End If
' The first match has already been found, so use the FindNext method to find the
' next record satisfying the criteria.
intIndex = 0
Do Until rstOrders.EOF
rstOrderDetails.FindNext "OrderID = " & rstOrders![OrderID]
If rstOrderDetails.NoMatch Then
intIndex = intIndex + 1
ReDim Preserve lngOrderID(1 To intIndex)
lngOrderID(intIndex) = rstOrders![OrderID]
End If
rstOrders.MoveNext
Loop
ErrorHandler:
Select Case Err
Case 0
Exit Function
Case Else
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
Exit Function
End Select
End Function
Tip If you need to frequently search records in a dynaset, you may find it easier to create a temporary indexed
table and use the Seek method instead.
Marking Record Position with Bookmarks
A bookmark is a system-generated Byte array that uniquely identifies each record. The DAO Bookmark property
of a Recordset object changes each time you move to a new record. To identify a record, move to that record and
then assign the value of the DAO Bookmark property to a variable of type Variant. To return to the record, set the
DAO Bookmark property to the value of the variable.
The following example illustrates how you can use a bookmark to save the current record position. You can then
perform other operations on the Recordset object, and then return to the saved record position.
Function BookMarkEx() As Integer
Dim dbs As Database, rstProducts As Recordset
Dim vBookMark As Variant, sngRevenue As Single
Dim strQuery As String, rstCategories As Recordset, strCriteria As String
On Error GoTo ErrHandler
BookMarkEx = 0
strQuery = "SELECT * FROM Products WHERE UnitsOnOrder >= 40 ORDER BY " _
& "CategoryID, UnitsOnOrder DESC;"
Set dbs = CurrentDb
Set rstProducts = dbs.OpenRecordset(strQuery, dbOpenSnapshot)
Set rstCategories = dbs.OpenRecordset("SELECT CategoryID FROM " _
& "Categories ORDER BY CategoryID;", dbOpenSnapshot)
If rstProducts.NoMatch Then Exit Function
' For each category find the product generating the least revenue
' and the product generating the most revenue.
Do Until rstCategories.EOF
strCriteria = "CategoryID = " & rstCategories![CategoryID]
rstProducts.FindFirst strCriteria
sngRevenue = rstProducts![UnitPrice] * rstProducts![UnitsOnOrder]
If Not rstProducts.NoMatch Then
127
128
129
Instead of record numbers, DAO provides bookmarks to uniquely identify a particular record. A given record
retains its unique bookmark for the life of the Recordset.
Which Recordset Objects Don't Support Bookmarks?
Dynasets based on certain linked tables, such as Paradox tables that have no primary key, don't support
bookmarks, nor do forward-only-type Recordset objects.
You can determine whether a given Recordset object supports bookmarks by checking the value of the
Bookmarkable property, as in the following example:
If rstLinkedTable.Bookmarkable Then
MsgBox "The underlying table supports bookmarks."
Else
MsgBox "The underlying table doesn't support bookmarks."
End If
Important If you try to use bookmarks on a Recordset object that doesn't support bookmarks, a run-time error
occurs.
Changing Data
After you've created a table- or dynaset-type Recordset object, you can change, delete, or add new records. You
can't change, delete, or add records to a snapshot-type or forward-only-type Recordset object.
This section presents the methods and procedures for changing data in table- and dynaset-type Recordset objects.
Using Parameter Queries
A parameter query is a query that when run displays a dialog box that prompts the user for information, such as
criteria for retrieving records or a value to insert in a field. You can use stored parameter queries to accomplish
most of the database maintenance tasks described in the rest of this chapter.
In many situations, you'll want a user or another procedure to provide parameters you can use with your stored
queries and Recordset objects. Microsoft Jet provides the means to do this. First, create a stored query, specifying
which parameters the user needs to provide. When you open a Recordset against one of these queries, the
application opens a dialog box that prompts the user to enter a value, such as the criteria for a WHERE clause or
the field on which to sort the selected records.
The following example takes two strings that represent dates and creates a parameter query that returns all records
in the Orders table whose order date is between the two dates. It adds all values in the OrderID field in the query's
recordset to an array.
Function OrdersFromTo(strDateFrom As Variant, strDateTo As Variant, _
lngOrderIDs() As Long)
Dim dbs As Database, rstOrders As Recordset
Dim qdf As QueryDef, strSQL As String, intI As Integer
On Error GoTo ErrorHandler
Set dbs = CurrentDb
strSQL = "PARAMETERS [DateFrom] DateTime, [DateTo] DateTime; "
strSQL = strSQL & "SELECT * FROM Orders WHERE OrderDate BETWEEN "
strSQL = strSQL & "[DateFrom] AND [DateTo];"
' Create an unstored parameter query.
Set qdf = dbs.CreateQueryDef("", strSQL)
' Set the query parameters.
130
qdf.Parameters("DateFrom") = strDateFrom
qdf.Parameters("DateTo") = strDateTo
' Open a forward-only snapshot on the query.
Set rstOrders = qdf.OpenRecordset(dbOpenSnapshot, dbForwardOnly)
' Load all the OrderIDs in the query into an array that the caller
' of the function can use.
intI = 1
While rstOrders.EOF = False
ReDim lngOrderIDs(1 To intI)
lngOrderIDs(intI) = rstOrders!OrderID
intI = intI + 1
rstOrders.MoveNext
Wend
ErrorHandler:
Select Case Err
Case 0
Exit Function
Case Else
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
Exit Function
End Select
End Function
Making Bulk Changes
Many of the changes you may otherwise perform in a loop can be done more efficiently with an update or delete
query. The following example creates a QueryDef object to update the Employees table and then runs the query:
Dim dbs As Database, qdfChangeTitles As QueryDef
Set dbs = CurrentDb
Set qdfChangeTitles = dbs.CreateQueryDef("")
qdfChangeTitles.SQL = "UPDATE Employees SET Title = 'Account Executive' " _
& "WHERE Title = 'Sales Representative';"
qdfChangeTitles.Execute dbFailOnError
' Invoke query.
You can replace the entire SQL string in this example with a stored parameter query, in which case the procedure
would prompt the user for parameter values. The following example shows how the previous example may be
rewritten as a stored parameter query:
Dim dbs As Database, qdfChangeTitles As QueryDef
Dim strSQLUpdate As String, strOld As String
Dim strNew As String
Set dbs = CurrentDb
strSQLUpdate = "PARAMETERS [Old Title] Text, [New Title] Text; " _
& "UPDATE Employees SET Title = [New Title] WHERE Title = [Old Title];"
' Create the QueryDef object.
Set qdfChangeTitles = dbs.CreateQueryDef("", strSQLUpdate)
' Prompt for old title.
strOld = InputBox("Enter old job title")
' Prompt for new title.
strNew = InputBox("Enter new job title")
' Set parameters.
qdfChangeTitles.Parameters("Old Title") = strOld
qdfChangeTitles.Parameters("New Title") = strNew
' Invoke query.
qdfChangeTitles.Execute
131
Note A delete query is more efficient than code that loops through records looking for records to delete,
especially with databases created in Microsoft Access for Windows 95 or later.
Modifying an Existing Record
You can modify existing records in a table- or dynaset-type Recordset object by using the Edit and Update
methods.
To modify an existing record in a table- or dynaset-type Recordset object
1 Go to the record that you want to change.
2 Use the Edit method to prepare the current record for editing.
3 Make the necessary changes to the record.
4 Use the Update method to save the changes to the current record.
The following example illustrates how to change the job titles for all sales representatives in a table called
Employees:
Dim dbs As Database, rstEmployees As Recordset
Set dbs = CurrentDb
Set rstEmployees = dbs.OpenRecordset("Employees")
rstEmployees.MoveFirst
Do Until rstEmployees.EOF
If rstEmployees!Title = "Sales Representative" Then
rstEmployees.Edit
rstEmployees!Title = "Account Executive"
rstEmployees.Update
End If
rstEmployees.MoveNext
Loop
rstEmployees.Close
Important If you don't use the Edit method before you try to change a value in the current record, a run-time
error occurs. If you edit the current record and then move to another record or close the Recordset object without
first using the Update method, your changes are lost without warning. For example, omitting the Update method
from the preceding example results in no changes being made to the Employees table.
You can also terminate the Edit method and any pending transactions without saving changes by using the
CancelUpdate method. While you can terminate the Edit method just by moving off the current record, this isn't
practical when the current record is the first or last record in the Recordset, or is a new record. It's generally
simpler to use the CancelUpdate method.
Inconsistent Updates
Dynaset-type Recordset objects can be based on a multiple-table query containing tables with a one-to-many
relationship. For example, suppose you want to create a multiple-table query that combines fields from the Orders
and Order Details tables. Generally speaking, you can't change values in the Orders table because it's on the "one"
side of the relationship. Depending on your application, however, you may want to be able to make changes to the
Orders table. To make it possible to freely change the values on the "one" side of a one-to-many relationship, use
the dbInconsistent constant of the OpenRecordset method to create an inconsistent dynaset. For example:
Set rstTotalSales = dbs.OpenRecordset("Sales Totals" ,, _
132
dbInconsistent)
When you update an inconsistent dynaset, you can easily destroy the referential integrity of the data in the
dynaset. You must take care to understand how the data is related across the one-to-many relationship and to
update the values on both sides in a way that preserves data integrity.
The dbInconsistent constant is available only for dynaset-type Recordset objects. It's ignored for table-, snapshot-,
and forward-only-type Recordset objects, but no compile or run-time error is returned if the dbInconsistent
constant is used with those types of Recordset objects.
Even with an inconsistent Recordset, some fields may not be updatable. For example, you can't change the value
of an AutoNumber field, and a Recordset based on certain linked tables may not be updatable.
Deleting an Existing Record
You can delete an existing record in a table- or dynaset-type Recordset object by using the Delete method. You
can't delete records from a snapshot-type Recordset object. The following example deletes all the duplicate
records in the Shippers table:
Function DeleteDuplicateShippers() As Integer
Dim rstShippers As Recordset, strQuery As String, dbs As Database, strName As String
On Error GoTo ErrorHandler
strQuery = "SELECT * FROM Shippers ORDER BY CompanyName;"
Set dbs = CurrentDb
Set rstShippers = dbs.OpenRecordset(strQuery, dbOpenDynaset)
' If no records in Shippers table, exit.
If rstShippers.EOF Then Exit Function
strName = rstShippers![CompanyName]
rstShippers.MoveNext
Do Until rstShippers.EOF
If rstShippers![CompanyName] = strName Then
rstShippers.Delete
Else
strName = rstShippers![CompanyName]
End If
rstShippers.MoveNext
Loop
ErrorHandler:
Select Case Err
Case 0
' The constants conSuccess and conFailed are defined at
' the module level as public constants with Integer values of
' 0 and -32,737 respectively.
DeleteDuplicateShippers = conSuccess
Exit Function
Case Else
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
DeleteDuplicateShippers = conFailed
Exit Function
End Select
End Function
When you use the Delete method, Microsoft Jet immediately deletes the current record without any warning or
prompting. Deleting a record doesn't automatically cause the next record to become the current record; to move to
133
the next record you must use the MoveNext method. However, keep in mind that after you've moved off the
deleted record, you cannot move back to it.
If you try to access a record after deleting it on a table-type Recordset, you'll get error 3167, "Record is deleted."
On a dynaset, you'll get error 3021, "No current record."
If you have a Recordset clone positioned at the deleted record and you try to read its value, you'll get error 3167
regardless of the type of Recordset object. Trying to use a bookmark to move to a deleted record will also result in
error 3167.
Adding a New Record
You can add a new record to a table- or dynaset-type Recordset object by using the AddNew method.
To add a new record to a table- or dynaset-type Recordset object
1 Use the AddNew method to create a new record you can edit.
2 Assign values to each of the record's fields.
3 Use the Update method to save the new record.
The following example adds a new record to a table-type Recordset called Shippers:
Dim dbs As Database, rstShippers As Recordset
Set dbs = CurrentDb
Set rstShippers = dbs.OpenRecordset("Shippers")
rstShippers.AddNew
rstShippers!CompanyName = "Global Parcel Service"
.
. ' Set remaining fields.
.
rstShippers.Update
rstShippers.Close
When you use the AddNew method, Microsoft Jet prepares a new, blank record and makes it the current record.
When you use the Update method to save the new record, the record that was current before you used the
AddNew method becomes the current record again.
The new record's position in the Recordset depends on whether you added the record to a dynaset- or a table-type
Recordset object. If you add a record to a dynaset-type Recordset, the new record appears at the end of the
Recordset, no matter how the Recordset is sorted. To force the new record to appear in its properly sorted
position, you can either use the Requery method or re-create the Recordset object.
If you add a record to a table-type Recordset, the record appears positioned according to the current index, or at
the end of the table if there is no current index. Because Microsoft Jet version 3.0 or later allows multiple users to
create new records in a table simultaneously, your record may not appear right at the end of the Recordset as it did
in previous versions of Microsoft Jet. Be sure to use the LastModified property rather than the MoveLast method
to move to the record you just added.
Important If you use the AddNew method to add a new record, and then move to another record or close the
Recordset object without first using the Update method, your changes are lost without warning. For example,
omitting the Update method from the preceding example results in no changes being made to the Shippers table.
Caching ODBC Data with a Recordset
134
You can use the dynaset-type Recordset to create a local cache for ODBC data. This lets you retrieve records in
batches instead of one at a time as each record is requested, and makes much better use of your server connection,
thus improving performance.
The CacheSize and CacheStart properties establish the size and starting offset (expressed as a bookmark) for the
cache. For example, you may set the CacheSize property to 100 records. Then, using the FillCache method, you
can retrieve sufficient records to fill the cache.
Tracking Recordset Changes
You may need to determine when the underlying TableDef object of a table-type Recordset was created, or the last
time it was modified. The DateCreated and LastUpdated properties, respectively, give you this information. Both
properties return the date stamp applied to the table by the machine on which the table resided at the time it was
stamped. These properties are only updated when the table's design changes; they aren't affected by changes to
records in the table.
Microsoft Jet Transactions
A transaction is a set of operations bundled together and treated as a single unit of work. The work in a transaction
must be completed as a whole; if any part of the transaction fails, the entire transaction fails. Transactions offer
the developer the ability to enforce data integrity. With multiple database operations bundled into a single unit that
must succeed or fail as a whole, the database can't reach an inconsistent state. Transactions are common to most
database management systems.
The most common example of transaction processing involves a bank's automated teller machine (ATM). The
processes of dispensing cash and then debiting the user's account are considered a logical unit of work and are
wrapped in a transaction: The cash isn't dispensed unless the system is also able to debit the account. By using a
transaction, the entire operation either succeeds or fails. This maintains the consistent state of the ATM database.
You should consider using transactions if you want to make sure that each operation in a group of operations is
successful before all operations are committed. Keep in mind that all transactions are invisible to other
transactions. That is, no transaction can see another transaction's updates to the database until the transaction is
committed.
Note The behavior of transactions with Microsoft Access databases differs from the behavior of ODBC data
sources, such as Microsoft SQL Server. For example, if a database is connected to a file server, and the file
server stops before a transaction has had time to commit its changes, then your database could be left in an
inconsistent state. If you require true transaction support with respect to durability, you should investigate the use
of a client/server architecture. For more information on client/server architecture, see Chapter 19, "Developing
Client/Server Applications."
Using Transactions in Your Applications
Microsoft Jet supports transactions through the DAO BeginTrans, CommitTrans, and Rollback methods of the
Workspace object.
The following example changes the job title of all sales representatives in the Employees table of the Northwind
sample database. After the BeginTrans method starts a transaction that isolates all of the changes made to the
Employees table, the CommitTrans method saves the changes. Notice that you can use the Rollback method to
undo changes that you saved with the Update method.
Sub ChangeTitle()
Dim dbsSales As Database
Dim rstEmp As Recordset
Dim wrkCurrent As Workspace
135
136
database object variables. If you close a database object variable, any uncommitted transactions within the scope
of that database object variable are rolled back. You should be aware of this behavior when you write your code.
Never assume that the Jet database engine is going to commit your transaction for you.
Transactions on External Data Sources
Transactions aren't supported on external non-Microsoft Jet data sources, with the exception of ODBC data. For
example, if your database has linked FoxPro or dBASE tables, any transactions on those objects are ignored.
This means that the transaction doesn't fail or generate a run-time error, but it doesn't actually do anything either.
Note Microsoft Access version 2.0 databases are opened by Microsoft Access for Windows 95 and Microsoft
Access 97 as external installable ISAM databases. However, unlike other external data sources, the Jet database
engine does support transactions on Microsoft Access version 2.x databases.
To determine whether or not a Database or Recordset object supports transactions, you can check the value of its
Transactions property. A value of True indicates that the object does support transactions, and a value of False
indicates that the object doesn't support transactions.
Transactions and Performance
In previous versions of Microsoft Access, it was generally recommended that you use transactions as a
performance enhancement. Now all transactions for DAO add, update, and delete operations are performed
internally and automatically. In most situations, this automatic support provides your application with the best
possible performance. However, there may be situations where you want to fine-tune transaction behavior. You
can do this by creating and modifying various settings in the Windows Registry.
Extracting Data from a Record
After you've located a particular record or records, you may want to extract data to use in your application instead
of modifying the underlying source table.
Copying a Single Field
You can copy a single field of a record to a variable of the appropriate data type. The following example extracts
three fields from the first record in a Recordset object:
Dim dbs As Database, rstEmployees As Recordset
Dim strFirstName As String, strLastName As String
Dim strTitle As String
Set dbs = CurrentDb
Set rstEmployees = dbs.OpenRecordset("Employees")
rstEmployees.MoveFirst
strFirstName = rstEmployees!FirstName
strLastName = rstEmployees!LastName
strTitle = rstEmployees!Title
rstEmployees.Close
Copying Entire Records to an Array
To copy one or more entire records, you can create a two-dimensional array and copy records one at a time. You
increment the first subscript for each field and the second subscript for each record.
A fast way to do this is with the GetRows method. The GetRows method returns a two-dimensional array. The
first subscript identifies the field and the second identifies the row number, as follows:
137
varRecords(intField, intRecord)
The following example uses an SQL statement to retrieve three fields from a table called Employees into a
Recordset object. It then uses the GetRows method to retrieve the first three records of the Recordset, and it stores
the selected records in a two-dimensional array. It then prints each record, one field at a time, by using the two
array indexes to select specific fields and records.
To clearly illustrate how the array indexes are used, the following example uses a separate statement to identify
and print each field of each record. In practice, it would be more reliable to use two loops, one nested in the other,
and to provide integer variables for the indexes that step through both dimensions of the array.
Sub GetRowsTest()
Dim dbs As Database
Dim rstEmployees As Recordset
Dim varRecords As Variant
Dim intNumReturned As Integer
Dim intNumColumns As Integer
Dim intColumn As Integer, intRow As Integer
Set dbs = CurrentDb
Set rstEmployees = dbs.OpenRecordset("SELECT FirstName, LastName, Title " _
& "FROM Employees", dbOpenSnapshot)
varRecords = rstEmployees.GetRows(3)
intNumReturned = UBound(varRecords, 2) + 1
intNumColumns = UBound(varRecords, 1) + 1
For intRow = 0 To intNumReturned - 1
For intColumn = 0 To intNumColumns - 1
Debug.Print varRecords(intColumn, intRow)
Next intColumn
Next intRow
rstEmployees.Close
End Sub
You can use subsequent calls to the GetRows method if more records are available. Because the array is filled as
soon as you call the GetRows method, you can see why this approach is much faster than copying one field at a
time.
Notice also that you don't have to declare the Variant as an array, because this is done automatically when the
GetRows method returns records. This enables you to use fixed-length array dimensions without knowing how
many records or fields will be returned, instead of using variable-length dimensions that take up more memory.
If you're trying to retrieve all the rows by using multiple GetRows calls, use the EOF property to be sure that
you're at the end of the Recordset. The GetRows method may return fewer rows than you request. If you request
more that the remaining number of rows in a Recordset, for example, the GetRows method only returns the rows
that remain. Similarly, if it can't retrieve a row in the range requested, it doesn't return that row. For example, if
the fifth record cannot be retrieved in a group of ten records that you're trying to retrieve, the GetRows method
returns four records and leaves the current record position on the record that caused a problem and doesn't
generate a run-time error. This situation may occur if a record in a dynaset was deleted by another user. If it
returns fewer records than the number requested and you're not at the end of the file, you need to read each field
in the current record to determine what error the GetRows method encountered.
Because the GetRows method always returns all the fields in the Recordset object, you may want to create a query
that returns just the fields that you need. This is especially important for OLE Object and Memo fields.
Using Field Objects
138
The default collection of a Recordset object is its Fields collection. This collection includes a single Field object
that corresponds to each field (or column) in the Recordset. Each Field object has a set of properties that uniquely
identifies the field name, data type, and so on, as well as the value of the field in the current record. You use the
Field objects in a Recordset object to read and set values for the fields in the current record of the Recordset
object.
You manipulate a field by using a Field object and its methods and properties. For example, you can:
Use the OrdinalPosition property to get or set the position of a Field object relative to other fields in a Fields
collection.
Use the FieldSize property, the GetChunk method, or the AppendChunk method to get or set a value in an OLE
Object or Memo field of a Recordset object.
Read or set the DAO Value property of a Recordset object.
Read or set the DAO AllowZeroLength, Required, ValidationRule, ValidationText, or ValidateOnSet property
setting to find or specify validation conditions.
Read the SourceField and SourceTable property settings to determine the original source of the data.
Referring to Field Objects
You can identify a Field object by its DAO Name property, which corresponds to the column name in the table
from which the data in the field was retrieved. The Fields collection is the default collection of a Recordset object.
Therefore, you can refer to the LastName field in the rstEmployees Recordset in any of the following ways:
rstEmployees.Fields("LastName")
rstEmployees!LastName
rstEmployees![LastName]
When using the ! operator, you must include brackets around a field name when it contains spaces. For example,
the statement:
strEmp = rstEmployees!Last Name
will not compile, but the statement:
strEmp = rstEmployees![Last Name]
will compile with no problems.
Within the Fields collection, each Field object can also be identified by its index:
rstEmployees.Fields(0)
The index enables you to walk through the collection in a loop, replacing the index with a variable that is
incremented with each pass through the loop. Objects in a collection are numbered starting with zero, so the first
Field object in the Fields collection is number 0, the second is 1, and so on. The field order is determined by the
underlying table. Fields are usually numbered in the order retrieved when the Recordset object is opened. One
drawback to this approach is that you can't be certain which field will be referred to, because the underlying table
structure may change, fields may be added or deleted, and so on.
To help you determine the order of fields in a Fields collection, the Field object supports the OrdinalPosition
property, which you can use to get or set a field's position relative to other fields in the collection. You can set the
139
OrdinalPosition property to any positive integer to change the field order when data is displayed in a form, copied
to an array or a Microsoft Excel worksheet, and so on.
When writing code that refers to fields within a loop, it's more efficient to refer to Field objects rather than to refer
to fields by their names. The following example shows a more efficient way of writing the ChangeTitle procedure
discussed earlier in this chapter. Instead of referring to the Title field as rstEmp!Title, it refers to the field by its
object variable, which doesn't require that the field be looked up in the Fields collection every time it's referred to.
Sub ChangeTitle()
Dim dbsSales As Database
Dim rstEmp As Recordset, fldTitle As Field
Dim wrkCurrent As Workspace
Set wrkCurrent = DBEngine.Workspaces(0)
Set dbsSales = OpenDatabase("Northwind.mdb")
Set rstEmp = dbsSales.OpenRecordset("Employees", dbOpenTable)
Set fldTitle = rstEmp.Fields("Title")
wrkCurrent.BeginTrans
Do Until rstEmp.EOF
If fldTitle = "Sales Representative" Then
rstEmp.Edit
fldTitle = "Sales Associate"
rstEmp.Update
End If
rstEmp.MoveNext
Loop
If MsgBox("Save all changes?", vbQuestion + vbYesNo) = vbYes Then
wrkCurrent.CommitTrans
Else
wrkCurrent.Rollback
End If
rstEmp.Close
dbsSales.Close
End Sub
Field Data Types
For a Field object on a Recordset, the Type property is read-only. However, you must be aware of the field type
when copying data to or from a field in code or a "Type mismatch" error may occur. For example, you cannot
copy Text data to an Integer field.
The Type property of a Field object on a Recordset is determined by the underlying table from which the record
was retrieved. If you create the table and its fields by using DAO data-definition language (DDL) statements, you
can easily determine the data type of the table's fields.
If you're accessing external data through an installable ISAM driver, the data types within external tables may be
different from those defined within Microsoft Jet. The installable ISAM driver for the external data source
converts external data types into their closest equivalent DAO data type.
The GUID Data Type
The GUID data type is used to store a globally unique identifier, a unique string of digits that identifies OLE
objects, Microsoft SQL Server remote procedure calls, and other entities that need a unique reference
identification.
140
Note The GUID data type is also used in the Database object's ReplicaID property to identify a replica. For
information on replicas, see Chapter 20, "Using Replication in Your Application."
The Text Data Type
For Field objects declared as type Text, you must set the Size property, which indicates the length of the longest
string that can be stored in the field. All other types of Field objects have their Size property set automatically.
The Currency Data Type
If you need to store monetary values, use fields of type Currency. Don't use any of the number data types (such as
Single) for currency values, because numbers to the right of the decimal may be rounded during calculations. The
Currency data type always maintains a fixed number of digits to the right of the decimal.
The Long Data Type
In some tables, you'll want to store a series of sequential numbers to uniquely identify records. For example, you
may want to start customer order records at order number 1 and begin counting upward.
Microsoft Access can automatically insert unique numbers in a field, saving your application the effort of
generating unique identifiers to be used within a primary key field. To take advantage of this capability, define a
field with the Long data type and set the dbAutoIncrField constant in the Field object's Attributes property. Autoincrementing fields start at 1 and increment sequentially. Fields of this type are also referred to as AutoNumber
fields.
If you want to establish a primary key/foreign key relationship between two tables by using an AutoNumber field,
make sure that the foreign key field is also defined as Long.
You can also set the DAO DefaultValue property of a Field object on a TableDef object to a special value called
GenUniqueId( ). This causes a random number to be assigned to this field whenever a new record is added or
created. The field's Type property must be Long.
Note A Field object's data type is read/write before the field is appended to a table's Fields collection, and readonly after it's appended.
The OLE Object and Memo Data Types
OLE Object and Memo fields are collectively referred to as large value fields because they are typically much
larger than fields of other data types. OLE Object fields consist of binary data up to 1.2 gigabytes in size. This
type is used to store pictures, files, or other raw binary data. Memo fields are used to store lengthy text and
numbers, such as comments or explanations. The size of a Memo field is limited by the maximum size of the
database.
Records within a Recordset object must fit on the 2K data pages supported by the Microsoft Jet database engine.
Each Field object you include in the table definition counts toward this 2K total, including OLE Object and
Memo fields. However, the amount stored for OLE Object and Memo fields is only 14 bytes per non-null field,
and only 1 byte for null fields. The 14 bytes is used to store a pointer to the actual data for these fields, which is
located on additional 2K pages. The amount of data committed to each text field isn't set until you actually store
data in the field. You can overcommit a data page by defining more text fields than there would be room for, but
no more than about 2K of actual data can be stored in a record. For example, you can define fifteen 250-byte text
fields in a record, but the total number of characters stored must be less than 2K.
When you query tables containing large value fields, don't include those fields in the field list unless you need
them, because returning large value fields takes time. Also, be aware that you can't index large value fields.
141
A snapshot- or forward-only-type Recordset object opened against large value fields in an .mdb file doesn't
actually contain that data. Instead, the snapshot maintains references to the data in the original tables, the same
way a dynaset refers to all data.
Handling Large Value Data
Sometimes you'll need to read or copy data from a large value field where you don't have sufficient memory to
copy the entire field in a single statement. Instead, you have to break up the data into smaller units, or chunks, that
will fit available memory. The FieldSize property tells you how large the field is, measured in bytes. Then you can
use the GetChunk method to copy a specific number of bytes to a buffer, and use the AppendChunk method to
copy the buffer to the final location. You then continue using GetChunk and AppendChunk until the entire field is
copied.
Reading and Writing Data
When you read or write data to a field, you're actually reading or setting the DAO Value property of a Field
object. The DAO Value property is the default property of a Field object. Therefore, you can set the DAO Value
property of the LastName field in the rstEmployees Recordset in any of the following ways:
rstEmployees!LastName.Value = strName
rstEmployees!LastName = strName
rstEmployees![LastName] = strName
Write Permission
The tables underlying a Recordset object may not permit you to modify data, even though the Recordset is of type
dynaset or table, which are usually updatable. Check the Updatable property of the Recordset to determine
whether its data can be changed. If the property is True, the Recordset object can be updated.
Individual fields within an updatable Recordset object may not be updatable, and trying to write to these fields
generates a run-time error. To determine whether a given field is updatable, check the DataUpdatable property of
the corresponding Field object in the Fields collection of the Recordset. The following example returns True if all
fields in the dynaset created by strQuery are updatable and returns False otherwise.
Function blnUpdatable(strQuery As String) As Boolean
Dim dbs As Database, rstDynaset As Recordset, intI As Integer
On Error GoTo ErrorHandler
' Initialize the function's return value to True.
blnUpdatable = True
Set dbs = CurrentDb
Set rstDynaset = dbs.OpenRecordset(strQuery, dbOpenDynaset)
' If the entire dynaset isn't updatable, return False.
If rstDynaset.Updatable = False Then
blnUpdatable = False
Else
' If the dynaset is updatable, check if all fields in the dynaset are updatable.
' If one of the fields isn't updatable, return False.
For intI = 0 To rstDynaset.Fields.Count - 1
If rstDynaset.Fields(intI).DataUpdatable = False Then
blnUpdatable = False
Exit For
End If
Next intI
End If
ErrorHandler:
Select Case Err
142
Case 0
Exit Function
Case Else
MsgBox "Error " & Err & ": " & Error, vbOKOnly, "ERROR"
Exit Function
End Select
End Function
Criteria
Any single field can impose a number of criteria on data in that field when records are added or updated. These
criteria are defined by a handful of properties. The DAO AllowZeroLength property on a Text or Memo field
indicates whether or not the field will accept a zero-length string (""). The DAO Required property indicates
whether or not some value must be entered in the field, or if it instead can accept a Null value. For a Field object
on a Recordset, these properties are read-only; their state is determined by the underlying table.
Field-Level Data Validation
Validation is the process of determining whether data entered into a field's DAO Value property is within an
acceptable range. A Field object on a Recordset may have the DAO ValidationRule and ValidationText properties
set. The DAO ValidationRule property is simply a criteria expression, similar to the criteria of an SQL WHERE
clause, without the WHERE keyword. The DAO ValidationText property is a string that Microsoft Access
displays in an error message if you try to enter data in the field that's outside the limits of the DAO ValidationRule
property. If you're using DAO in your code, then you can use the DAO ValidationText for a message that you
want to display to the user.
Note The DAO ValidationRule and ValidationText properties also exist at the Recordset level. These are readonly properties, reflecting the table-level validation scheme established on the table from which the current record
is retrieved.
A Field object on a Recordset also features the ValidateOnSet property. When the ValidateOnSet property is set to
True, Microsoft Access checks validation as soon as the field's DAO Value property is set. When it's set to False
(the default), Microsoft Access checks validation only when the completed record is updated. For example, if
you're adding data to a record that contains a large Memo or OLE Object field and that has the DAO
ValidationRule property set, you should determine whether the new data violates the validation rule before trying
to write the data you should write the data when the field value is set. To do so, set the ValidateOnSet property to
True. If you wait to check validation until the entire record is written to disk, you may waste time trying to write
an invalid record to disk.
Tracing the Origin of Dynaset Fields
A dynaset-type Recordset object can include records from more than one source table. Also, within a single
record, fields from different tables can be joined into new records. Sometimes it's useful to know the table from
which a field originated. The SourceTable property of a Field object returns the name of the table from which the
field's current data was retrieved.
Within a query, a field can be renamed for display purposes. For example, in an SQL SELECT query, the AS
operator in the select field list can create an alias for one of the returned fields. In a Recordset based on an SQL
query, a field that has been aliased is represented by a Field object whose DAO Name property reflects the alias,
not the original field name. To find out the original field name, check the Field object's SourceField property.