You are on page 1of 332

Access Forms Masterclasses

Access Forms Masterclass #1


Custom Navigation Buttons
Published: 1 August 2013

Revised 17 August 2018

Author: Martin Green

Screenshots: Access 2010, Windows 7

For Access Versions: 2007, 2010, 2013, 2016

Why Add Custom Navigation Buttons?


Unless you specify otherwise, each Access form comes with its own set of built-in navigation buttons,

located in the lower left corner of the form (Fig. 1) so why bother creating your own custom

navigation buttons?

Fig. 1 A form's built-in Navigation Buttons.

When building applications for other people to use you soon learn that you should take nothing for

granted and, most importantly, you should never assume any particular level of knowledge or

expertise on the part of the user. Of course, you and I know what those little arrows at the bottom of

a form are for and how to use them but what about the people who are going to use the database?
Adding some clearly marked buttons might help them navigate through their records.

I have found that even knowledgeable users find custom buttons useful because on modern high

resolution computer screens the built-in navigation buttons appear very small and can be difficult to

see and operate.

I usually add my own custom navigation buttons to a form (Fig. 2) simply because it makes things

easier and more convenient for the user. Whether or not you choose to keep the built-in navigation

buttons is entirely up to you.


Fig. 2 Custom Navigation Buttons

In this Masterclass you will learn how to add a set of custom navigation buttons to a form and write

the VBA code to activate them. You can optionally remove the built-in navigation buttons and add

refinements such as Tool Tips and code to disable the buttons when they are not needed.

You can download a copy of the database containing the completed exercise used in this Masterclass

as well as a printable PDF of this Masterclass in the Download section at the bottom of this page.

View the video version of this Masterclass in my Access Video Tips section or on my YouTube channel

at www.youtube.com/martingreenvba.

^ top

Step 1: Add a Footer to the Form


First, you need to decide where to put your navigation buttons. I like to put them in the form's Footer

section. The footer is always visible in the form's window so, if the form is too big for the window, the

user won't have to scroll to find the navigation buttons.

When you create an Access form it doesn't have a footer by default so you have to add one. In the

form's Design View right-click in the background of the form (the Detail area) and choose Form

Header/Footer from the context menu (Fig. 3).


Fig. 3 Add a Footer to the form.

A Form Footer section appears at the bottom of the form (Fig. 4). Notice that a Form Header section is

also created at the top of the form.

Fig. 4 A Footer section is added to the form.

If you don't need a header you can remove it by reducing its height to zero. To do this point at the
upper edge of the bar labelled Detail so that the mouse pointer changes to a cross with a double
headed vertical arrow (Fig. 5) then drag the bar up as far as it will go. This hides the Header section

from view (Fig. 6).

Fig. 5 Adjust the height of the Header Section

Fig. 6 Hiding the Header by reducing its height to zero.

If at any time you want to restore the Header just reverse the process. An alternative way to do this is

to specify the Height property of the Header in the Property Sheet. Click on the bar labelled Form

Header then go to the Property Sheet (click the Property Sheet button on the Design tab of the

Ribbon or press [Alt]+[Enter] to open the Property Sheet if it is not already visible). Enter the

required size in the Height property on the Format tab of the Property Sheet. Enter a zero to hide
the Header or a number to specify the required size.

^ top

Step 2: Draw the Command Buttons


Having decided where to place them, the next step is to draw the required number of buttons on the

form. Click the Button tool on the Design tab of the Ribbon (Fig. 7) then click on the form Footer

approximately where you want the button to appear. I normally work with the Control Wizards tool

switched off because I prefer to write my own code rather than let Access do it. If you have the

Control Wizards tool switched on the Command Button Wizard will appear when you click on the
footer. You could use the wizard to achieve the task in hand but since the point of this exercise is to

teach you how to do these things yourself you can dismiss the wizard by clicking its Cancel button.

NOTE: If you want to turn off Control Wizards permanently (you can turn them on again at any time)

expand the Controls group on the Design tab of the Ribbon and de-select the Use Control Wizards

option.

Fig. 7 Use the Button tool to add a Command Button.

When you clicked on the footer Access created a Command Button and gave it a default name and

caption. In this example the button was given the name and caption Command16 (Fig. 8). Whenever

a control (an object on a form is called a Control) is added to a form Access automatically names it

and, if appropriate, captions or labels it with a name and sequential number. You will give the button a

sensible name and a meaningful caption later. First, you need to resize the button and position it

accurately.

Fig.8 The Button tool creates a new Command Button.

See that the when the Command Button control is selected it has a highlighted border with dots at the

corners and mid-way along its sides. These are handles for resizing or moving the control using the

mouse. Drag the small dots to change the size of the control. Drag the large dot at the upper left

corner of the control to move it. I prefer to use either the keyboard or the Property Sheet to
accurately size and position controls.
To use the keyboard to move a control, first click on the control to select it then press the [Left

Arrow], [Right Arrow], [Up Arrow] or [Down Arrow] keys. Each button press moves the control

one grid unit. For finer movements hold down the [Control] key whilst pressing the arrow key. This

moves the control one quarter of a grid unit for each key press and is useful for accurate placement of

items on the form.

To use the keyboard to change the size of a control, hold down the [Shift] key whilst pressing the

arrow key. [Shift]+[Left Arrow] reduces the width of the control whilst [Shift]+[Right

Arrow] increases the width. Similarly, [Shift]+[Up Arrow] reduces the height of a control

whilst [Shift]+[Down Arrow] increases the height. Hold down the [Control] key as well to achieve

fine changes.

Alternatively you can do it all using the Property Sheet. That's my preferred method. Select the

Command Button control and go to the Format tab of the Property Sheet. Because I'm English I'm

going to use centimetres but if you use inches just enter the nearest equivalent measurement that

suits you. With the command button selected, go to the Property Sheet and enter the following

property values (Table 1):

Table 1: Command Button "Back" Properties

Property Tab Value

Width: Format 3 cm

Height: Format 1 cm

Top: Format 0.5 cm

Left: Format 1 cm

There is no need to add "cm" or "in" as Access will do this automatically. You might also notice that,

particularly if using metric measurements, Access changes some of the dimensions. For example 0.5

gets changes to 0.501. Don't worry about this, it's just Access converting measurements behind the

scenes.

Give the command button a sensible caption by entering < Back in the Caption property, then switch

to the Other tab of the Property Sheet and change the Name property to cmdBack. You now have a
command button of the required size, in the desired position, with a meaningful name and caption

(Fig. 9).

Fig. 9 The command button is positioned, resized and captioned.

You need two more buttons, one for Next and another for New. Since these need to be the same size

as the one you have already made the simplest way to create them is to make copies of it. It doesn't

matter how you do this. I like to use keyboard shortcuts, so with the first command button selected I

press [Control]+[C] to copy it then [Control]+[V] twice to make two copies. Alternatively you

could right-click the button and choose Copy then Paste from the context menu, or use

the Copy and Paste commands on the Home tab of the Ribbon.

The new command buttons have been assigned new default names by Access but, like the original,

they are all captioned < Back. The next task is to change their names and captions and place them in

their correct locations. This can all be done from the Property Sheet.

Select the first of the new buttons and go to the Property Sheet and enter the following property

values (Table 2):

Table 2: Command Button "Next" Properties

Property Tab Value

Name: Other cmdNext

Caption: Format Next >

Top: Format 0.5 cm

Left: Format 4 cm
Select the second of the new buttons and go to the Property Sheet and enter the following property

values (Table 3):

Table 3: Command Button "New" Properties

Property Tab Value

Name: Other cmdNew

Caption: Format New

Top: Format 0.5 cm

Left: Format 7 cm

Depending on which method you used to create the new buttons, you might find that the Footer

section increased in height to accommodate them. If necessary adjust the height of the Footer section

by pointing at its bottom edge so that the mouse pointer changes to a cross with a double headed

vertical arrow (Fig. 10) then drag the border upwards to change the Footer to the desired size.

If you prefer, you can select the Footer by clicking the bar labelled Form Footer before entering the

desired value in the Height property on the Property Sheet.

Fig. 10 Resizing the Footer section.

Before proceeding, switch the form into Form View and take a look at your new buttons (Fig. 11). If

you want to change their size or position you can return to Design View to make any necessary

adjustments.
Fig 11 The new Navigation Buttons in Form View.

^ top

Step 3: Additional Design Refinements


Add Tool Tips

Users might find it helpful to have a little extra information about what the buttons do. You can easily

provide an additional hint by creating a Control Tip. This is a Tool Tip that appears when the user

points their mouse at a control (Fig. 12). Enter some text in the ControlTip Text property (located on

the Other tab of the Property Sheet) of each button. For example you might enter Go To Previous

Record for the cmdBack button, Go To Next Record for the cmdNext button and Create New Record for

the cmdNew button.

Fig. 12 A tool Tip helps users understand a control's function.

Check the Tab Order

A form's Tab Order is often overlooked by form designers. Many users navigate around a form by

using their [Tab] key. This should take the user from control to control in a logical sequence. A

control's position in the Tab Order is defined by the order in which that particular control was created

on the form so, unless you have created each control in the precise order in which the user would be

expected to visit them, the Tab Order will be incorrect. This can be very frustrating for someone who
is used to using the [Tab] key to navigate around a form. It is very simple to check the Tab Order

and, if necessary, correct it.


Each part of the form has its own Tab Order. To check the Tab Order of your new buttons, right-click

on the background of the Footer section and choose Tab Order from the context menu (Fig. 13).

Alternatively, select the Footer by clicking on the bar marked Form Footer and click the Tab

Order button on the Design tab of the Ribbon.

Fig. 13 Open the Tab Order dialog.

The Tab Order dialog (Fig. 14) shows the current order of controls in the selected Tab Order. To

change the order, select a list item by clicking the grey button next to its name. Release the mouse

then drag the selected item up or down the list to create the desired order. You can select multiple

items and move several at once if required.

Fig. 14 The Tab Order dialog.


You can use the Auto Order command to have Access automatically create a tab order. If there is

more than one column of controls on a form the automatic tab order will visit items left-to-right before

moving down.

You should always check the Tab Order after making changes to a form. A simple change such as

converting a Text Box control to a Combo Box will change its position within the Tab Order, moving it

to the end of the order even though its position on the form has not changed.

Add First and Last Record Buttons

I haven't included First and Last record buttons because I seldom use them myself, but it is a simple

matter to add them if you wish (Fig. 15).

Fig. 15 First and Last buttons can be added.

Proceed exactly as for the other buttons, supplying appropriate names, captions and, if you are using

them, tool tips for the command buttons. Remember to include them in the correct Tab Order.

Remove the Built-In Navigation Buttons

Now that you have your own custom navigation buttons you might like to remove the built-in ones.

It's a simple process. In Form Design view, open the Property Sheet and select Form from the drop-

down list at the top. Then, on the Format tab change the Navigation Buttons property to No. This

causes the navigation bar to be hidden on that particular form (Fig. 16).
Fig. 16 Hiding the form's built-in Navigation Buttons.

Bear in mind that removing the built-in navigation buttons also removes the Record Count and Search

boxes, so you might want to keep them in addition to your own. There are alternative methods for

searching and in the next Masterclass I show you how to display your own custom Record Counter

(Masterclass #2 - Custom Record Counter).

^ top

Step 4: Coding the Navigation Buttons


The buttons don't work yet. To activate them you need to write some VBA code. The code for each

button will take the form of an Event Procedure, that is a procedure (commonly called a Sub or VBA

Macro) that will run by itself when a particular event happens. In the case of our buttons that will be

the Click event, which fires when the user clicks a button or presses their [Enter] key when a button

is selected.

Coding the Back Button

In Form Design view select the cmdBack button and go to the Event tab of the Property Sheet. Click

in the text box next to On Click then click the Build button ([…]) to open the Choose Builder dialog

then select Code Builder (Fig. 17) and click OK.


Fig. 17 The Choose Builder dialog.

This takes you into the Visual Basic Editor where Access has created an empty event procedure for

the cmdBack_Click event, ready for you to add your code.

When writing VBA code you should always consider what might go wrong and cause an error when the

code runs. If, for example, the user clicks the Back button when they are already on the first record

this will cause the code to "crash". So, the first thing you need to do is add a simple error handler

telling Access to ignore an error if one should occur when the user clicks the button.

(NOTE: Error handlers usually need to be more sophisticated than this but in this case it will be safe to

simply ignore the error.)

Enter the code as follows:

1. Place the cursor between the lines beginning Private Sub… and End Sub and press [Tab] to
indent your typing by one tab-space. Type On Error Resume Next then press [Enter] to
create a new line.

2. Type DoCmd followed by a dot (.). When you type the dot Access shows you a list of all the
available relevant commands.

3. Scroll down the list and choose GoToRecord (HINT: Start typing the text you need and the list
will automatically scroll to it.). Either double-click the list item or select it and press [Tab] to
add it to your code.

4. Type a [Space]. Access shows a list of possible objects, but since we are referring to the
current form we don't need to specify this so skip this piece of information by typing a comma
(,).

5. Access now wants to know the name of the object but, again, since we are referring to the
current form we don't need to specify this, so skip this piece of information by typing another
comma (,).

6. Finally we get a list allowing us to specify which record we want to go to. Double-
click acPrevious to choose it.

Your finished code should look like this (Listing 1).

Listing 1:
Private Sub cmdBack_Click()
On Error Resume Next
DoCmd.GoToRecord , , acPrevious
End Sub

Before testing your code, first check your typing then open the Visual Basic Editor's Debug menu and

choose Compile… (the name of your database is shown). Compiling the code checks for any errors

you might have missed. If everything is OK (the Visual Basic Editor will tell you if it finds a problem)

then click the Save button. This is an important step because, if you test code without saving your

database first and an error causes Access to crash, you might lose some of your work.

Return to your database, put the form into Form View and test the Back button. Clicking it should

move you to the previous record unless you are on the first record, in which case nothing should

happen.

Coding the Next Button

Proceed exactly as described for the Back button. Create a Click event procedure for

the cmdNext button and enter the code, this time specifying acNext. Your code should look like this

(Listing 2):

Listing 2:

Private Sub cmdNext_Click()


On Error Resume Next
DoCmd.GoToRecord , , acNext
End Sub

Compile, save and test the code as before.

Coding the New Button

Proceed exactly as described for the Back button. Create a Click event procedure for

the cmdNew button and enter the code, this time specifying acNewRec. Your code should look like

this (Listing 3):

Listing 3:

Private Sub cmdNew_Click()


On Error Resume Next
DoCmd.GoToRecord , , acNewRec
End Sub

Compile, save and test the code as before.

^ top

Step 5: Additional Code Refinements


I suggested that you added a simple error handler in case the user pressed a button and gave Access

a command that it could not complete. The error handler dealt with any possible error by ignoring it.

But it is always good practice in programming to try to prevent errors happening in the first place.

Often errors can't be anticipated so error handlers are always a good idea, but in this case we can

prevent errors from happening by disabling those buttons whose function is not relevant at the time.

We can use the form's CurrentRecord property, which returns the index number of the record within

the current recordset, to know if we are on the first record (the CurrentRecord property will have a

value of 1).

The RecordCount property of the form's recordset can be used to find out how many records there are

and can be compared with the CurrentRecord to determine if we are on the last record or a new

record (if the value of the CurrentRecord property is equal to the value of the RecordCount property

then we are on the last record).

The form's NewRecord property can be used to determine if we are on a new record. It

returns True for a new record and False for an existing record.

With this information we can change the Enabled property of the buttons to True or False to enable or

disable the appropriate buttons so there is no chance of an error occurring.

To achieve this we make use of the form's Current event. This event fires whenever a new record is

displayed. This happens when the form opens and when the user moves from record to record, or asks

for a new record. To create the event procedure select the form itself by choosing Form from the

drop-down list at the top of the Property Sheet. Select OnCurrent and proceed as before, entering

code so that the finished procedure looks like this (Listing 4):

Listing 4:
Private Sub Form_Current()
On Error Resume Next
If Me.CurrentRecord = 1 Then
Me.cmdBack.Enabled = False
Else
Me.cmdBack.Enabled = True
End If
If Me.CurrentRecord >= Me.Recordset.RecordCount Then
Me.cmdNext.Enabled = False
Else
Me.cmdNext.Enabled = True
End If
If Me.NewRecord Then
Me.cmdNew.Enabled = False
Else
Me.cmdNew.Enabled = True
End If
End Sub

The code ensures that when the form is showing the first record the Back button is disabled; when

showing the last record the Next button is disabled; when showing a new record both the Next and

New buttons are disabled; and when showing any other record no buttons are disabled (Fig. 18).

Fig. 18 Buttons are enabled or disabled according to which record is showing.


If you have chosen to add First and Last buttons, remember to add code to their On Click event as

you did for the other buttons, using acFirst and acLast as appropriate.

First and Last buttons will also require some additional code in the Form_Current event if they are to

be enabled or disabled according to which record is being shown. Modify the code so the resulting

event procedure looks like this (Listing 5):

Listing 5:

Private Sub Form_Current()


On Error Resume Next
If Me.CurrentRecord = 1 Then
Me.cmdBack.Enabled = False
Me.cmdFirst.Enabled = False
Else
Me.cmdBack.Enabled = True
Me.cmdFirst.Enabled = True
End If
If Me.CurrentRecord = Me.Recordset.RecordCount Then
Me.cmdLast.Enabled = False
Else
Me.cmdLast.Enabled = True
End If
If Me.CurrentRecord >= Me.Recordset.RecordCount Then
Me.cmdNext.Enabled = False
Else
Me.cmdNext.Enabled = True
End If
If Me.NewRecord Then
Me.cmdNew.Enabled = False
Else
Me.cmdNew.Enabled = True
End If
End Sub

When the form is showing the first record the First button is disabled; when showing the last record

the Last button is disabled. Both buttons are enabled at all other times.
^ top

Coding Notes
If you are new to writing VBA code these notes should help you understand what the code in this

project does and how it works.

Event Procedures

Much of the code associated with Access forms takes the form of Event Procedures. A Procedure is just

another term describing a self-contained collection of VBA commands. Other terms commonly used

are Macro and Subroutine or Sub. The procedures used here are Event Procedures meaning they run

automatically when a particular event happens (or "fires" as we propellorheads like to say!).

The event procedures used in this project are the On Click event of each of the command buttons and

the On Current event of the form itself. The various parts of a form and its controls usually have many

different types of event for which a procedure can be created. To see a list of what is available, select

an item on a form in Design view and take a look at the Event tab of the Property Sheet.

The Visual Basic Editor automatically names event procedures, the names being a combination of the

object's name and the type of event, separated by an underscore, for

example cmdBack_Click or Form_Current.

Handling Errors

These event procedures all begin with an Error Handler. This tells Access what to do if something goes

wrong. Error handlers are often quite detailed, instructing Access how to proceed for each possible

error that might happen and what to do if an unforeseen error occurs. They usually also attempt to

rescue the situation if possible and, most importantly, try to prevent the program itself crashing.

Here, a very simple error handler is used, On Error Resume Next, which tells Access simply to
ignore any error that might occur and to proceed to the next command. This kind of error handler

should be used with caution since it is often not safe simply to ignore a problem. In these examples

however its use is perfectly safe and there is no need to create anything more specific.

The Button Click Events

The DoCmd method offers access to a wide range of commands in Access and mirrors much of what is
available in Access Macros. Here we have used DoCmd.GoToRecord which instructs Access to
move a specific data object, in this case our form, to a particular place in its recordset. It allows you

so specify the type and name of the data object in question but since we are dealing with the current
object those parameters can be left empty and only the parameter detailing where to go need be

supplied. All of the button click events work the same way (Listing 6).

Listing 6:

Private Sub cmdBack_Click()


On Error Resume Next
DoCmd.GoToRecord , , acPrevious
End Sub

As usual the Visual Basic Editor helpfully supplies a list of options to choose from.

The Form Current Event

As explained earlier, it is always better to try to avoid an error than to deal with one that has already

happened. For this reason a set of commands was created to disable those buttons whose function

might have caused a problem if they were clicked at a particular time. For example, if the user was to

click the Back button when on the first record.

Since the combination of available buttons changes according to which record is being displayed, the

best time to run a procedure that checks and then enables or disables buttons, is when the form

moves from one record to another. The form's Current event is suitable for this because it fires

whenever a new or different record is displayed. This happens when a form opens, when the user

moves from record to record, and when the user asks to create a new record.

The code used here consists of a number of If Statements. In VBA an If Statement can take a number

of different forms. Here, each If Statement consists of a condition and two parts, what to do if the
condition evaluates to True and what to do if it evaluates to False.

The code uses the keyword Me to refer to the current form. The form's CurrentRecord property
returns a number which represents the index number of the current record in the form's recordset. So,
for the first record it returns 1, the second record 2 and so on. If the form's CurrentRecord is 1
then it is displaying the first record so the first If Statement disables the First and Back buttons,

otherwise it enables those buttons (Listing 7).

Listing 7:

If Me.CurrentRecord = 1 Then
Me.cmdBack.Enabled = False
Me.cmdFirst.Enabled = False
Else
Me.cmdBack.Enabled = True
Me.cmdFirst.Enabled = True
End If

The second If Statement checks to see if the form is displaying the last record. But since the number

of records in the recordset is likely to change we have to ask if the index number of the displayed

record is the same as the number of records in the recordset. The If Statement uses
the RecordCount property of the form's Recordset property to ascertain the total number of records
and compares it with the CurrentRecord to decide whether or not to enable the Last button
(Listing 8).

Listing 8:

If Me.CurrentRecord = Me.Recordset.RecordCount Then


Me.cmdLast.Enabled = False
Else
Me.cmdLast.Enabled = True
End If

The If Statement for the Next button is very similar but instead of asking if
the CurrentRecord equals the RecordCount it asks if the CurrentRecord is greater than or
equal to the RecordCount. This is because if it is possible to move to a new record by using the
Next command from the last record in the recordset. This effectively creates a value for
the CurrentRecord of 1 greater than the number of existing records (e.g. record number 1085 of
1084 records!). (Listing 9)

Listing 9:

If Me.CurrentRecord >= Me.Recordset.RecordCount Then


Me.cmdNext.Enabled = False
Else
Me.cmdNext.Enabled = True
End If

Finally we make use of the form's NewRecord property which returns True if a new, unsaved record
is being displayed. Note that, when using a logical condition (one that could be either True or False) it
is not always necessary to specify =True. The statement If Me.NewRecord means the same
as If Me.NewRecord = True. (Listing 10)

Listing 10:

If Me.NewRecord Then
Me.cmdNew.Enabled = False
Else
Me.cmdNew.Enabled = True
End If

Summary
This concludes this Masterclass. In it you learned how to improve the database user's experience by

adding some useful buttons to an Access form for easily navigating through the form's records. You

were shown different ways to accurately specify the size and placements of objects on a form and how

to give them logical and meaningful names. Finally you added VBA code to power the buttons,

including some useful refinements along the way.

All the techniques described here have been tested successfully in Microsoft Access versions 2007,

2010, 2013 and 2016.

Download Example Database


You can download a sample database containing the completed exercise used in this Masterclass. You

can also download a printable PDF of this Masterclass.

To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

AccessFormsMasterclass1.zip [73KB]

AccessFormsMasterclass1_NavigationButtons.pdf [316KB]
Access Forms Masterclass #2
Custom Record Counter
Published: 5 August 2013

Revised 17 August 2018

Author: Martin Green

Screenshots: Access 2010, Windows 7

For Access Versions: 2007, 2010, 2013, 2016

Why Add Custom Custom Record Counter ?


A new Access form comes complete with a set of built-in navigation buttons and a record counter,

located in the lower-left corner of the form (Fig. 1).

Fig. 1 A form's built-in record counter and navigation buttons.

Whilst these are useful tools, on modern high-resolution screens they appear quite small and many

users find them hard to see and operate. The solution is to replace them with ones you can build

yourself, to your own design and to suit your users' needs.

The first Masterclass in this series showed you how to build a set of Custom Navigation Buttons

(Masterclass #1: Custom Navigation Buttons). If, having built them you want to remove the built-in
ones you will also lose the built-in record counter. This Masterclass will show you how to add a

replacement Record Counter of your own design (Fig. 2).

Fig. 2 Custom Record Counter and Navigation Buttons.


In this Masterclass you will learn how to add a Label Control to a form and write the necessary VBA

code to turn it into a custom Record Counter which you can position and format to suit your own

requirements.

^ top

Step 1: Add a Label to the Form


Draw the Label

When you want to display some text on a form you have a couple of methods to choose from. You can

use an unbound Text Box control (one that is not bound to a field in the form's underlying recordset)

then use some code to change its Value property, or you can use a Label control and use some code

to change its Caption property. I normally prefer the latter because, from the user's point of view, it is

effectively inert. They can't select it or otherwise interfere with it. If you use a Text Box the user

might find their way to it and try to enter something in it. Even if you lock a Text Box and exclude it

from the Tab Order the user can still click on it. Whilst that isn't a problem it might confuse them and

you don't want a confused user!

You can place the label on any part of the form. In this example I am going to locate the Record

Counter next to the Custom Navigation Buttons I created in the first Masterclass (Masterclass #1:

Custom Navigation Buttons).

To add a Label control to your form, put the form into Design View and click the Label tool in

the Controls section of the Design tab of the Ribbon (Fig. 3).

Fig. 3 Choose the Label tool from the Ribbon.

The mouse pointer changes to show that it is ready to add a Label. Click on the form approximately

where you want the Record Counter to appear (Fig. 4).


Fig. 4 Click on the form to add a new Label control.

It is important that you don't click anywhere else or press [Enter] at this point because Access will

immediately delete the empty label. To prevent the label from disappearing you must add some text

to it. Anything will do but you might as well enter something sensible so, immediately after adding the

label to the form, type Record Counter then press [Enter].

Your label now appears with the text you typed and is highlighted to indicate that it is selected. If you

click anywhere else is becomes de-selected. If necessary, simply click on it again to select it.

You will see that in the upper left corner of the selected label there is a small green triangle and

adjacent to the label is a small warning icon. This means that Access thinks there might be a problem

with the control you just added. If you point at the warning icon Access tells you that: This is a new

label and is not associated with a control (Fig. 5).

Fig. 5 Access warns that the new label is not associated with a control.

We know that this isn't a problem. The label is there just to display some information and not to

provide a label for another control so, to remove the warning, click the down-arrow next to the

warning icon and choose Ignore Error from the menu.

Rename the Label

Access will have automatically named the new label but we need to give it a meaningful name before

we refer to it in our code. In Design View select the Label control. If the Property Sheet is not
already open display it by right-clicking on the label and choosing Properties from the context menu.

Go to the Other tab of the Property Sheet and change the Name property to lblRecordCounter (Fig.

6).

Fig. 6 Give the label a meaningful name.

Format and Position the Label

Switch the form into Form View and take a look at the label. You can decide how large you want the

text to appear and where exactly to position it. In this example the default text appears quite small

and the default light grey colour is not sufficiently prominent for my users (Fig. 7) so I am going to

increase the font size and change its colour to something more visible.

Fig. 7 You may decide to enlarge the text and change its colour.

Return to Design View, click on the label to select it, and use the tools in the Font section of

the Format tab of the Ribbon to change the colour and size of the text to your preference. I have

chosen Black, Bold and 16 point text size.

Although the appearance of the text changes, the size of the label does not. To make the label the

correct size for the newly formatted text, double-click the dot in its lower-right corner (Fig. 8).
Fig. 8 Resize the label to fit the text.

This snaps the label to the correct height but it is probably not wide enough for the text that it will

display. I intend to display text that reads something like "Record 140 of 1084". You can move and

resize the label by dragging the dots that are located along its edges when it is selected, but for

accuracy I prefer to move and resize controls using the keyboard as follows.

With the label selected, use the [Arrow] keys to move the label to the required position. Then, hold

down [Shift] and click the [Right Arrow] key to increase its width by the required amount (Fig. 9).

Since the label has no visible border (unless you have chosen to add one) it is quite safe to make it

wider than necessary to ensure that it will be able to accommodate any text that might be created.

Fig. 9 Widen and position the label as required.

Check the label's finished appearance by switching to Form View (Fig. 10).

Fig. 10 The formatted label seen in Form View.

^ top
Step 2: Write VBA Code to Power the Record Counter
We are going to create an Event Procedure that will change the label's caption each time a different

record is displayed. An event procedure is a VBA macro that runs by itself whenever a specified event

happens. The appropriate one in this case is the form's Current event. This event fires when the form

opens and when the user moves from record to record, or asks for a new record.

To create the event procedure, in Design View select the form itself by choosing Form from the

drop-down list at the top of the Property Sheet. Choose the Event tab, then click in the text box next

to On Current then click the Build button ([…])*. In the Choose Builder dialog select Code

Builder (Fig. 11) and click OK.

Fig. 11 The Choose Builder dialog.

*NOTE: If a Form_Current event procedure already exists (such as the one you might have created

in Masterclass #1) then clicking the build button takes you directly to it in the Visual Basic Editor.

If one does not already exist, Access creates a new, empty Form_Current event procedure and places

the cursor between the Private Sub… and End Sub statements ready for you to add your code (Listing

1).

Listing 1:

Private Sub Form_Current()

End Sub

Our code will make use of some of the form's properties to create a string of text that is then applied

to the Caption property of the label each time a different record is displayed.

The form's CurrentRecord property has a value which represents the index number of the currently

selected record in the form's recordset. The first record has an index of 1, the second record 2 and so
on. The form's Recordset property has a RecordCount property which represents the total number of

saved records in the form's recordset.

I want the label to read Record x of y where x is the index number of the current record and y is the

total number of records, for example: Record 140 of 1084.

Enter the code as follows:

1. Place the cursor between the lines beginning Private Sub… and End Sub and press [Tab] to
indent your typing by one tab-space.

2. Type the text exactly as shown below (Listing 2).

Listing 2:

Me.lblrecordCounter.Caption = _
"Record " & Me.CurrentRecord & " of " &
Me.Recordset.RecordCount

NOTE: Immediately after the equals sign (=) I have added a space followed by the underscore

character. This allows me to break the code statement on to a second line. I am only doing this

because it makes the long line of code easier to read and can be omitted if you prefer to write the

code statement as a single line.

The completed event procedure looks like this (Listing 3) (in addition to any existing code you might

have there):

Listing 3:

Private Sub Form_Current()


Me.lblrecordCounter.Caption = _
"Record " & Me.CurrentRecord & " of " &
Me.Recordset.RecordCount
End Sub

Before testing your code, first check your typing then open the Visual Basic Editor's Debug menu and

choose Compile… (the name of your database is shown). Compiling the code checks for any errors

you might have missed. If everything is OK (the Visual Basic Editor will tell you if it finds a problem)

click the Save button. This is an important step because, if you test code without saving your
database first and an error causes Access to crash, you might lose some of your work.
Switch the form into Form View and use the navigation buttons to move through the recordset. In

this example (Fig. 12) I have retained the built in navigation buttons to allow me to check that the

code is working correctly, but I will remove them later.

Fig. 12 The finished Record Counter.

Step 3: Additional Code Refinements


An anomaly occurs when the form is moved to a new and as yet unsaved record. On a new record

the CurrentRecord property has a value of 1 greater than the value of the RecordCount property, even

though the new record has not yet been saved to the recordset (Fig. 13). In this example the current

record is shown as 1085 although the recordset contains only 1084 saved records.

Fig. 13 On a new record the numbers are shown incorrectly.

This happens too in the built-in counter but Access resolves this by treating the new, unsaved record

as if it were already part of the recordset and increases the record count by 1. So, in this example the

built-in counter reads 1085 of 1085.

If this anomaly bothers you can resolve it in a number of ways. You could have the counter behave

the same way as the built-in counter, alternatively you could have the counter display something else,

such as the text New Record. Both methods require the code to check whether or not the form is

displaying a new record. This can be done with an If Statement examining the value of the

form's NewRecord property.


If you want the label to display the same values for the record index number and the total record

count when a new, unsaved record is displayed, edit the code as follows (Listing 4):

Listing 4:

Private Sub Form_Current()


If Me.NewRecord Then
Me.lblrecordCounter.Caption = _
"Record " & Me.CurrentRecord & " of " &
Me.Recordset.RecordCount + 1
Else
Me.lblrecordCounter.Caption = _
"Record " & Me.CurrentRecord & " of " &
Me.Recordset.RecordCount
End If
End Sub

This changes the label's behaviour so that the record index number and total record count are the

same (Fig. 14).

Fig. 14 Both the record index number and total record count are the same.

If you want the label to display some text when a new, unsaved record is displayed, edit the code as

follows (Listing 5):

Listing 5:

Private Sub Form_Current()


If Me.NewRecord Then
Me.lblrecordCounter.Caption = "New Record"
Else
Me.lblrecordCounter.Caption = _
"Record " & Me.CurrentRecord & " of " &
Me.Recordset.RecordCount
End If
End Sub

This changes the label's behaviour so that the text New Record is displayed (Fig. 15).

Fig. 15 Text is displayed when the form shows a New record.

^ top

Step 4: Additional Design Refinements


If you have added your own Custom Navigation Buttons (see Masterclass #1 Custom Navigation

Buttons), now that you also have your own Custom Record Counter you might like to remove the

built-in one.

Remove the Built-In Navigation Buttons

This is a simple process. In Design View, open the Property Sheet and select Form from the drop-

down list at the top. Then, on the Format tab change the Navigation Buttons property to No. This

causes the navigation bar to be hidden on that particular form (Fig. 16).
Fig. 16 Hiding the form's built-in navigation buttons.

Summary
In this Masterclass you learned how to add a Label Control to an Access form, how to size and position

it accurately, and how to change its appearance. You then added VBA code to change the label's

caption so that it behaved as a Record Counter showing both the index number of the current record

and the total number of records in the form's recordset.

All the techniques described here have been tested successfully in Microsoft Access versions 2007,

2010, 2013 and 2016.

Download Example Database


You can download a sample database containing the completed exercise used in this Masterclass. You

can also download a printable PDF of this Masterclass.

To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

AccessFormsMasterclass2.zip [76KB]

AccessFormsMasterclass2_RecordCounter.pdf [251KB]

Access Forms Masterclass #3


Go To Record - A Custom Record Locator
Published: 16 August 2013

Revised: 17 August 2018

Author: Martin Green

Screenshots: Access 2010, Windows 7

For Access Versions: 2007, 2010, 2013, 2016

A Custom Record Locator


A common requirement for database users is to quickly locate a specific record. Whilst Access provides

a number of tools for sorting and filtering a form's recordset there isn't a built-in tool to help the user

quickly find and display a specific record. If you want one, and you almost certainly will, you will have

to build it yourself. I have built several different kinds of Go to Record tools to suit my users'

requirements and the complexity of the data. This is one of my favourites and is really easy to build

(Fig. 1).

Fig. 1 The Custom Record Locator.

How It Works
This Go to Record tool uses a combo box to display a list of items, each one identifying a specific

record, from which the user makes a choice. The form then jumps to that specific record.

Because the form simply moves to the specified record in the recordset no filtering is involved so the

user does not have to remember to remove the filter to see the full recordset again.

Any field or combination of fields can be used to identify the record so that might be, for example, a

company name or a person's first and last names. The process makes use of the record's primary key

(which the user does not need to know) to identify it and locate it in the recordset.

The Combo Box Wizard that runs when you add a combo box to a form includes an option to create a

tool similar in action to this but, as it stands, it is of limited flexibility. My Go to Record tool is powered

by VBA and SQL to offer as much flexibility as you could need for this task.

Prerequisites
There are only two prerequisites. First, that the recordset that the form is displaying includes the

primary key field. That field need not necessarily be displayed for the user to see as long as it is

included in the recordset. And second, that the recordset also includes one or more fields that will

allow the user to accurately specify a particular record.

Step 1: Add a Combo Box to the Form


You can place the combo box anywhere on the form. I usually locate it on the form's header or footer

section. A header and footer are not displayed by default on an Access form. If they are not visible

you will need to add them. In the form's Design View right-click on the Detail area of the form (the

form background) and choose Form Header/Footer from the context menu.

^ top

Add the Combo Box

In the form's Design View, click the Combo Box tool on the Controls section of the Design tab of

the ribbon (Fig. 2). The cursor changes to indicate that it is ready to place a combo box on the form.

Fig. 2 Select the Combo Box tool.

Click on the form approximately where you want the combo box to appear (Fig. 3). You can accurately

size and position it later.


Fig. 3 Add a combo box to the form.

Change the Name and Caption

Access has already assigned a default name to the combo box and captioned the accompanying label.

With the combo box selected go to the Property Sheet (if it is not visible right-click the combo box

and choose Properties from the menu) and on the Other tab change the Name property

to cboGoToRecord. At the same time you can optionally add some text, such as Go To Record to

the Control Tip Text property. This will cause a helpful Tool Tip to appear when the user points their

mouse at the combo box (Fig. 4).

Fig. 4 Edit the Name and Control Tip Text properties.

Select the label that Access placed next to the combo box and on the Format tab of the Property

Sheet change the Caption Property to Go To Record:. You will probably find that the label is now too

small to accommodate the new text. Drag the resizing handles around the perimeter of the selected

label to change its size. To move the label, drag the large grey dot in its upper-left corner.

You can also format the label, changing the font, size and colour using the tools on the Format tab of

the ribbon. Check the finished appearance of the combo box and its label by switching the form into

Form View (Fig. 5).


Fig. 5 The combo box and its formatted label.

The combo box does not have a list yet. That is the next task.

^ top

Step 2: Create the Combo Box List


The list that the combo box will display will be derived from the recordset that the form is displaying.

Decide which field, or combination of fields, the user will use to specify which record they want to see.

This may be the Primary Key field itself or another field or combination of fields. I usually use an

Autonumber field for the Primary Key, which will probably have little or no meaning to the user, but if

your primary key is meaningful to the user you could use that.

In Design View select the new combo box and go to the Data tab of its Property Sheet. Make sure

that the Row Source Type property is set to Table/Query then click in the Row Source property text

box and click the Build button […] (Fig. 6). This opens the Query Builder tool and displays the Show

Table dialog box.

Fig. 6 Click the Build button of the Row Source property.

Select the name of the table that the form's recordset is based upon (Fig. 7), click the Add button

then click Close to dismiss the dialog box.


Fig. 7 Choose the table to add.

The Query Builder is now displayed along with a field list for the table you chose. You are going to

choose a selection of fields that will be used to create the list that the combo box will display. One of

the fields must be the Primary Key field and this should be added first. In the field list it will have a

small icon of a Key against its name. Double-click the Primary Key field name to add it to the query

design grid (Fig. 8).

Fig. 8 Add the Primary Key field to the grid.


What you do next depends upon your data and the fields you need to identify specific records. If this

were, for example, a database of business customers for which there was a single entry for each

customer, you could use a single field such as the Company Name.

In this example the recordset contains a list of employees all of whom can be identified by the

combination of their first and last names. (NOTE: It is possible that there could be two people with the

same first and last name, in which case an additional field would be necessary to separately identify

them.)

I could add these two fields, in the order I wished to see them in the list, simply by double-clicking on

each as I did for the Primary Key. But I prefer to combine them into a single field (Fig. 9). I want my

list to show the person's Last Name followed by a comma and a space and then their First Name

e.g. Green, Martin.

To do this, instead of adding the fields to the grid, in the Field cell at the top of the empty column to

the right of the Primary Key in the grid, enter a name for the new field followed by a colon (:) and the

names of the fields to be combined as follows:

Fullname: [Lastname] & ", " & [Firstname]

You have now defined a new, calculated field. It is also important to sort the necessary fields into

ascending order to assist the user in finding the required item in the list. To do this click in

the Sort row of the appropriate column of the grid and choose Ascending from the list.

Fig. 9 Multiple fields can be added separately (Left) or combined (Right).

Finally, click the Run button on the Design tab of the ribbon to run the query and check that the list

it creates is correct (Fig. 10). If you need to alter anything return to the Query Builder by clicking

the Design View button on the Home tab of the ribbon and make the necessary changes.
Fig. 10 Run the query and check the list.

When you are satisfied with the list, close the Query Builder. Access displays a message asking you to

confirm that you want to return the SQL statement that the Query Builder created and use it to build

the combo box list (Fig. 11). Click Yes.

Fig. 11 Access asks for confirmation.

The SQL statement that the Query Builder generated is now written into the Row Source property of

the combo box.

If you switch the form into Form View and open the combo box list you will see that only the Primary

Key field is displayed (Fig. 12). This is because, unless you specify otherwise, a combo box displays

only the first field in the list's recordset.


Fig. 12 The combo box displays the first column.

You must tell Access how many columns you want the combo box to display and also define their

widths. If, as in this example, you don't want to display the Primary Key column simply set its width to

zero. The combo box will then display the first visible column instead, together with any others that

you have specified.

In Design View select the combo box and on the Data tab of the Property Sheet check that

the Bound Column property is set to 1. The value of the Bound Column becomes the Value of the

combo box when the user makes a selection (even if the Bound Column is not visible). We need the

Bound Column to be the Primary Key column.

Then, on the Format tab of the Property Sheet change the Column Count property to the total

number of columns that you defined in the Query Builder. In this example that value is 2 but if you

used more columns enter the appropriate number.

Next, in the Column Widths property enter the required width of each column, starting with a zero
(to hide the Primary Key column) and separating each with a semicolon (;). You can just type the

numbers, there is no need to enter cm or in (Fig. 13).


Fig. 13 Specify the Column Count and Column Widths.

This might involve some trial and error to get it right. Check the result in Form View and adjust the

measurements until it looks right. You might also need to adjust the Width property of the combo box

so that it is wide enough to accommodate the list (Fig. 14).

Fig. 14 The finished combo box.

Now we have a combo box that displays a list of items each of which can be used to identify a specific

record. The next step is to write the VBA code that will turn this combo box into

our Go To Record tool.

^ top

Step 3: Write the VBA Code


The next task is to create the code that will instruct the form to display the record whose Primary Key

field has the same value as the hidden column in the item chosen by the user in the combo box. The
appropriate place for this code is in the combo box's After Update event procedure which fires when

the user makes a choice from the combo box list.

In the form's Design View select the combo box and go to the Event tab of the Property Sheet.

Click on After Update then click the Build button ([…]) to open the Choose Builder dialog box, and

choose Code Builder (Fig. 15) then click OK.

Fig. 15 Open the Code Builder.

This takes you into the Visual Basic Editor where Access has created an empty event procedure and

placed your cursor inside, ready for you to enter the code (Listing 1).

Listing 1:

Private Sub cboGoToRecord_AfterUpdate()

End Sub

To start, we need an error handler just in case something goes wrong. A simple one will do.

Press [Tab] to indent your code then type:

On Error Resume Next

The code will make use of the form's RecordsetClone property. This is a copy of the form's underlying

recordset that we can manipulate with code. Since this is a VBA object it must be declared as an

object variable (which I shall call "rst") and defined by having a value assigned to it. To do this, enter

the following two code statements:

Dim rst As Object

Set rst = Me.RecordsetClone

The next statement instructs Access to find the record in the RecordsetClone that has a value in its
Primary Key field that matches the value of the hidden Primary Key column that was chosen by the
user in the combo box. This is done using the FindFirst method and a SQL expression that supplies the

appropriate field name and value. Enter the following code statement:

rst.FindFirst "EmployeeID = " & Me.cboGoToRecord.Value

Note: I have used EmployeeID which is the name of the Primary Key field in this example. Make sure

you enter the correct field name if yours is different.

When the above statement is executed the RecordsetClone moves to the specified record. Now the

code needs to synchronise the form's recordset with the RecordsetClone. This is done with using

the Bookmark property of each recordset. Enter the following:

Me.Bookmark = rst.Bookmark

Here is the completed procedure (Listing 2).

Listing 2:

Private Sub cboGoToRecord_AfterUpdate()


On Error Resume Next
Dim rst As Object
Set rst = Me.RecordsetClone
rst.FindFirst "EmployeeID = " & Me.cboGoToRecord.Value
Me.Bookmark = rst.Bookmark
End Sub

In the Visual Basic Editor open the Debug menu and choose Compile… to check the code. If any
errors are shown, correct them and compile again, then click the Save button. It is good practice to

compile and save your new code in this way before testing it in case an unforeseen error should cause

Access to crash.

Move back to Access, switch the form into Form View, and test the Go To Record tool. Each time you

select an item from the combo box the form should jump to that record.

There is one final refinement to add. Notice that if you navigate to a record by another method, such

as with the Navigation Buttons or by using the [Page Up] or [Page Down] keys, The combo box still

displays the last item you chose. This can be fixed by synchronising the combo box with the form.
To do this a line of code added to the form's Current event procedure can be used to set the value of

the combo box to the value of the Primary Key of the record being displayed. Since the

form's Current event fires each time a record is displayed this will make sure that the item shown in

the combo box will always be correct for the record being displayed.

In Design View go to the Property Sheet and choose Form from the dropdown list at the top. On

the Event tab click on On Current and go to the Code Builder as before. If you have followed earlier

Masterclasses in this series your form will already have code in its On Current event procedure, in

which case simply add the following code statement to it:

Me.cboGoToRecord.Value = Me.EmployeeID.Value

I have used the name of the Primary Key field employed in this example. Remember to use the name

of the Primary Key field in your form's recordset if it is different.

After a final Debug, Compile and Save, test your finished GoToRecord tool. Job done!

Summary
In this Masterclass you created a simple GoToRecord tool that helps the user find and jump quickly to

any record in the forms recordset. You added and formatted a combo box control and used the Query

Builder to create an SQL statement that defined its list. You then created a VBA event procedure for

the AfterUpdate event of the combo box that made use of the

form's RecordsetClone and Bookmarks to find and a specific record and instruct the form to display it.

Finally you added an instruction to the form's Current event procedure to keep the combo box

synchronised with the form itself.

All the techniques described here have been tested successfully in Microsoft Access versions 2007,

2010, 2013 and 2018.

Download Example Database


You can download a sample database containing the completed exercise used in this Masterclass. You

can also download a printable PDF of this Masterclass.

To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip
file icon then choose Extract All... and follow the instructions.
AccessFormsMasterclass3.zip [73KB]

AccessFormsMasterclass3_GoToRecord.pdf [263KB]

Access Forms Masterclass #4


A Pop-Up Search Tool
Published: 10 September 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

A Custom Search Tool for Your Access Database


I have added this custom search tool to many of the databases I have built. Access forms include

simple built-in search tools but, whilst useful, they are limited to simple text searches on individual

fields, but how many of your users know of the existence of the options on the right-click context

menu?

My search tool consists of a pop-up dialog box (Fig. 1) that offers the user the ability to search for a
specific record, through a choice of fields and in several different ways.
Fig. 1 A pop-up search tool.

The search tool is usually opened from a button on a form. It opens to display a complete list of
records. The user enters a search string into the tool’s Find text box then chooses which field or fields

they want to search, and how they want the search performed. The search string is not case sensitive.

It can be a single letter or string of letters, and it can include an asterisk (*) to represent any one or

more characters, or question mark (?) to represent any single character.

To perform the search the user simply clicks the Search button and the search tool’s list changes to

show the results. They can restore the original list by clicking the Reset button or perform another

search without needing to refresh the list.

If they fail to find the item they are looking for they can close the search tool by clicking

the Cancel button but if they have found the required item they can select it in the list then click
the OK button to close the dialog box and display the chosen record in their form.
In this Masterclass I am going to lead you through the steps of building a tool that will allow the user

to search for a specific record within their data. In this example we are looking for a person by name

by entering a string of characters and choosing whether to search for a First Name or Last Name. You

should be able to apply the same technique to search any kind of data, the only proviso being that the

data you are searching comes from a table that has a Primary Key field. You will see the importance of

this later.

You probably already know how to build a form in Access but, in case you don’t, I’m going to take you

through all the necessary steps. The are several tasks to perform but you can pause at any time. Just

remember to save your work when you do.

^top

Step 1: Build the Search Form


The search tool takes the form of a custom dialog box. A dialog box is basically an Access form that is

not linked to an underlying recordset and that has certain settings applied so that, usually, it floats in

front of the user’s workspace.

1.1 Open a New Blank Form

Click the Form Design button (Fig. 2) on the Create tab of the Access ribbon to create a new, empty

form.

Fig. 2 The Form Design tool.

I’m a firm believer in regularly saving my work in case something goes wrong so now is a good time

to start. Click the Save button in the upper left corner of the Access window or press [Ctrl]+S to

bring up the Save As dialog and give your new form a name. For this exercise I am using the

name frmSearch. From now on you can save your changes simply by repeating the Save command.

So far, the form consists solely of a Detail area (the form’s background) which you can colour and

resize as necessary. I like to colour the form’s background to suit the colour scheme of my database

at this stage. To do this, if the Property Sheet isn’t already open, right-click on the detail area and

choose Properties from the context menu. Go to the Format tab and choose a suitable colour for
the Back Color property. I have chosen Access Theme 2 (Fig. 3) a pale blue colour which is one of the
presets on the drop-down list. Alternatively click the build ([…]) button to apply any colour of your

choice.

Fig. 3 A new empty form.

1.2 Add a Text Box

The first thing to add to the form is a Text Box into which the user will enter a search string. They will

be able to enter a single letter, a string of letters or an entire name. Click the Text Box button

(Fig. 4) on the Design tab of the ribbon to activate the Text Box tool then click on the form,

somewhere near the centre of in the upper part of the Detail area to place a new Text Box and its

label on the form.

Fig. 4 The Text Box tool.


Enter some suitable text into the label (I used Find:) then move the text box and its label into the

upper left part of the detail area (Fig. 5).

Fig. 5 Position the Text Box and enter suitable text to the label.

TIP: To resize a selected control (items on a form are called controls) drag one of the dots on the

control’s border. To move a control, drag the border itself (not a dot). When a control has an

accompanying label the label also moves when you move the control. To move a control or its label

independently drag it by the large grey dot in its upper left corner. You can also use the keyboard to

move and resize a control: click on the control to select it then use the [Arrow] keys to move an item.

Hold down [Shift] as you do so to resize the item. Hold down [Ctrl] whilst moving or resizing for fine-

tuning.

1.3 Add the Option Groups

In this example the user will be searching through a list of names. They will be offered two sets of

choices for searching:

 Where: they can choose to search in the Firstname field, in the Lastname field or in both fields.

 How: they can look for a name that Starts With, Ends With or Contains the string that they
entered in the text box.

To achieve this, we are going to add two Option Groups. An Option Group consists of a frame with a

label. Option Buttons will be placed inside each frame, each button representing one of the choices.

When an Option Button is used alone on a form it has a value of True or False but when Option

Buttons are placed inside an Option Group frame they each have a numerical value and only one can

be selected at a time. The value of the Option Group becomes that of the selected Option Button.

Start by using the Option Group tool (Fig. 6 left) to draw a rectangle on the form, then use
the Option Button tool (Fig. 6 right) to place three option buttons inside the rectangle.
Fig. 6 The Option Group (left) and Option Button (right) tools.

Add a suitable title to the Option Group’s label (e.g. Where:) and to each option button (First Name,

Last Name, Both). In this example (Fig. 7) I have applied formatting to match the design and colour

scheme of my form, making the text bold and blue, and changing the Special Effect property of the

Option Group border from Etched to Flat, also colouring the border blue.

Fig. 7 Name and format the parts of the Option Group.

You need a second Option Group so repeat the process or, more easily, simply select your first Option

Group by using the mouse to drag a rectangle enclosing the entire Option Group, and

pressing [Ctrl]+C then [Ctrl]+V to Copy and Paste, making an exact copy of it.

Change the labels of the second Option Group, for example How: for the Option Group label

and Starts With, Ends With and Contains for the Option Buttons. Format your controls as you wish and

position them as you prefer on the form, then check out your design by switching the form into Form

View (Fig. 8) using the Views button on the Home tab of the ribbon.

TIP: Now would be a good time to save your work. Remember to save your changes regularly.
Fig. 8 Check the form's appearance in Form View.

You might notice that in my example I have chosen to place the Option Groups to the right of the

search Text Box and have also widened the Text Box and coloured its border blue to match my form’s

colour scheme.

Now we can set the properties of the controls we have placed on the form so far. Select the Text Box,

go to the Other tab of the Property Sheet and change its Name property to txtSearch. Select the

Frame of the first Option Group (not the whole group) and change its name to grpWhere. Name the

second Option Group grpHow.

It isn’t necessary to name the individual Option Buttons because we won’t be referring to them in the

code but you need to check the Option Value of each button on the Data tab of the Property Sheet.

Access numbers the buttons’ value consecutively as you add them to the group but it is best to check

anyway. They should be 1, 2 and 3 respectively in each group.

TIP: Sometimes I create a set of Option Buttons by placing one in the frame then copying it and

pasting as many times as I need buttons. If you do this remember to change their Option Values

because, being copies of the first one, they will all have the same value unless you change it.

Finally, set the Default Value property of each Option Group to 1 to make sure that one of the buttons

is already selected when the search tool opens. You won’t notice any difference by simply switching to

Form View but if you close, save and reopen the form you will see that the first option button of each

group is now selected. This avoids the possibility of the user trying to perform a search with no option

selected.
1.4 Add the List Box

The next task is to add a List Box to display the results of the search. Use the List Box tool to draw a

large rectangle on the form (Fig. 9). Don’t worry too much about the exact size of the box because

you can adjust that later. It doesn’t need its label so select the Label and remove it by pressing

the [Delete] key. Change the Name property of the List Box to lstSearch.

Fig. 9 Draw a large List Box on the form.

Your List Box can show as much information as you require. In this example the user is searching the

data in the tblEmployees table so I want to display the Firstname, Lastname and JobTitle fields. In

addition the List Box list must include the Primary Key field which, in this example, is the Autonumber

field EmployeeID.

To populate the list box select it then go to the Data tab of the Property Sheet, click in the Row

Source box then click the Build button ([…]) to open the Query Builder (Fig. 10).
Fig. 10 Open the Row Source Query Builder.

Just as you would when creating a regular Select query, add the fields you would like the user to see

in the search results List Box. I have chosen EmployeeID (the Primark Key field), Firstname,

Lastname and JobTitle. It is important that you include the Primary Key field in your query, and make

sure that it is the first field returned by the query. I also chose to sort my query

by Firstname then Lastname. You can do whatever you prefer.

You can run the query to check that it is working as you wish (Fig. 11). This is how the user will see

the data in the List Box except for the Primary Key field which will be present but hidden. When you

are satisfied with your query close the Query Builder window and confirm that you want to use the

query as the List Box Row Source when asked. You will see the query’s SQL statement written in the

Row Source box on the Property Sheet.

Tip: If you are familiar with SQL you can type a suitable SQL statement directly into the Row Source

box.
Fig. 11 Use the Query Builder to create a SQL statement for the List Box Row Source property.

If you were to check the result in Form View now you would see that the List Box is showing only the

first field that was returned by the query (EmployeeID). The next step is to configure the List Box so

that it displays all the required fields correctly.

You will see on the Property Sheet that the Bound Column property of the List Box is column 1. This

represents the first field returned by the query which, correctly, is our Primary Key field. Being

the Bound Column means that, when an item is selected, the value of this field is the value that the

List Box assumes.

NOTE: List Boxes can be set to allow the user to make a single selection (the default setting and that

used here) or multiple selections. If you are interested in using a List Box for multiple selections read

my tutorial Making Sense of List Boxes at http://www.fontstuff.com/access/acctut11.htm.

With the List Box selected, go to the Format tab of the Property Sheet and make the following

settings (Table 1):

Table 1: List Box Properties

Property Tab Value


Column Count Format 4

Column Widths Format 0cm;3cm;3cm;4cm

Column Heads Format Yes (optional)

Bound Column Data 1

The exact settings will depend upon your data and the width of your List Box. Check the result in Form

View. There is usually an amount of trial-and-error involved in getting it right.

Fig.12 Set the List Box properties.

I have chosen to show Column Heads (Fig. 12). Access uses the field names for this but, if your field

names are not appropriate or sufficiently meaningful you can choose not to show them. Alternatively,

you have a couple of choices: go to the design view of the source table and set the Caption property
of the field to something more appropriate. If you prefer not to get involved with the table design you

can modify your Row Source query to add a custom caption for each field. To do this type your chosen

caption, followed by a colon (:), in front of the field name in the grid as shown here (Fig. 13).

Fig. 13 You can customize the field titles in the Row Source query.

When you are satisfied with the layout of the data in your List Box (Fig. 14) save your changes.
Fig. 14 The completed List Box.

1.5 Add the Command Buttons

You need four command buttons:

 Search The user will click this button after entering their search criteria to perform the search
and display the results in the List Box.

 Reset Clicking this button will return the List Box list to its original state (i.e. showing the full
list). Although it will not be necessary to reset the list before running another search, it might be
convenient for the user to do so. You could omit this button if you wish.

 OK Having found and selected a suitable list entry the user will click this button to close the
search tool and return to their original form and view the chosen record.

 Cancel This button will close the search tool without performing any other task.

Add four command buttons to your form and give an appropriate name and caption to each one using

the Name and Caption properties on the Property Sheet. In this example I have

used Captions and Names as follows:

Search, cmdSearch; Reset, cmdReset; OK, cmdOK; Cancel, cmdCancel

TIP: You can write whatever you like for the caption but keep the name simple and relevant. It can be

very frustrating when writing code trying to remember what you called something when cryptic names

are used.

You can lay your form out in any way that pleases you. Here’s how mine looks after a few adjustments

(Fig. 15):
Fig. 15 The finished layout of the Search tool.

Next, we need to add some VBA code to power the command buttons.

^top

Step 2: Write Code to Power the Command Buttons


Having equipped the form with all the necessary controls the next task is to create the VBA code that

will make it work. Each of the command buttons requires a macro (called an Event Procedure) that will

run when the user clicks the button.

2.1 The Cancel Button

Let’s start with the simplest one. All that this button’s code needs to do is to close the form. Select

the Cancel button and go to the Event tab of the Properties Sheet. We need to have a macro run

when the user clicks this button so click in the On Click text box then click the Build button to display

the Choose Builder dialog. Choose Code Builder (Fig. 16) then click OK.
Fig. 16 Choose the Code Builder.

TIP: A quicker way to do this is to double-click in the Event text box (the entry [Event Procedure] will

appear in the box) then click the build button.

This takes you to the Visual Basic Editor with an empty Click event macro already written and your

cursor placed inside (Fig. 17) ready for you to enter the code.

Fig. 17 The Visual Basic Editor has created an empty Click event macro.

TIP: You can save time typing by selecting and copying the code you see here and pasting it directly

into the Visual Basic Editor.

Enter the code statement as follows (Listing 1):

Listing 1:

Private Sub cmdCancel_Click()


On Error Resume Next
DoCmd.Close acForm, "frmSearch"
End Sub

How it Works:
This simple code statement closes the named form which, in this case, is the current form.

NOTE: You will notice that, as you type, the Visual Basic Editor often provides a list of entries, relevant

to what you have typed. You can carry on typing but, if you prefer, you can select an item from the

list by double-clicking it. When appropriate, the Visual Basic Editor also displays a helper showing

what parameters are required, the entry in bold being the item it expects next.

You will see that I have included a simple error handler at the start of the macro. It is good practice to

always include an error handler in all your macros, just in case something goes wrong.

2.2 The Reset Button

The purpose of the Reset button is to return the List Box to its original state by returning its Row

Source to its original value. This can be done easily with code.

Start by making a copy of the Row Source property of the List box. To do this select the List Box then

go to the Data tab of the Property Sheet and click the label of the Row Source property. This

highlights everything in the Row Source text box (Fig. 18). Now either press [Ctrl]+C or right-

click on the highlighted text and choose Copy to copy it.

Fig. 18 Click the property label to highlight the text.

Create an Event Procedure for the On Click event of the Reset button exactly as you did in the

previous step and enter its code as shown below (Listing 2). I suggest you read the whole of this

section before you start typing.

Listing 2:

Private Sub cmdReset_Click()


On Error Resume Next
Dim strSQL As String
strSQL = "SELECT tblEmployees.EmployeeID, tblEmployees.Firstname,
" & _
"tblEmployees.Lastname, tblEmployees.JobTitle " & _
"FROM tblEmployees " & _
"ORDER BY tblEmployees.Firstname, tblEmployees.Lastname;"
With Me.lstSearch
.RowSource = strSQL
.Requery
End With
Me.grpHow.Value = 1
Me.grpWhere.Value = 1
End Sub

How it Works:

Following the Error Handler (which must always be at the very top of your macro) there is a statement

declaring the variable strSQL. This variable is simply a container to temporarily hold the text of the

query that forms the Row Source of the list box. The process of declaring a variable warns the code

engine that a particular piece of text (strSQL) is being used to represent that variable and also what

sort of data it will contain (here a text string).

The next statement places a value into the variable. To do this, after typing the equals sign, type an

opening quote mark then simply paste the SQL statement you copied from the Row Source property

earlier. Remember to type a closing quote mark at the end of the SQL. When you paste your SQL

statement you will see that is arrives as a long line of text running off the right side of the window.

This is OK but I prefer to enter it in a way that I can see the whole thing when viewing my code

(Fig. 19). Access doesn’t care as long as it is done correctly!


Fig. 19 Breaking a very long code statement to fit on the screen.

When you break lines of code like this you must do it correctly. Because we are breaking a text string

each part must be surrounded by quote marks and an ampersand (&) used to tell Access that the next

string of text continues directly from the current one. Following the ampersand is the Line Break

Character (a space followed by an underscore) which tells Access that the code statement continues

on the next line.

The code continues with two commands. The first command applies the SQL statement (in the form of

the strSQL variable) to the RowSource property of the List Box. This alone does not cause the

contents of the List Box to change, which is why we need the second command telling it

to Requery which makes it rebuild its list.

Finally, a pair of code statements return the two option groups to their default settings by making the
value of each = 1.

Now would be a good time to Compile and Save your code. In the Visual Basic Editor open

the Debug menu and choose Compile… (Fig. 20) then click the Save button.

Fig. 20 Regularly Compile and Save your code.


2.3 The Search Button

The Row Source property of the List Box takes the form of a SQL Statement that we generated with

the aid of the Query Builder. The task of the code behind the Search button is to replace this SQL

statement with one that generates a recordset that satisfies the user’s search requirements.

The code must read the Search string entered by the user as well as their choices of Where and How

to search. This information will be used to construct a suitable SQL statement which will define the

Row Source of the list box.

The way I approach this task varies according to the number and nature of the choices that I am

offering the user. In this example I am using two Case Statements to interpret the choices that the

user makes in the two Option Groups. A case statement works like an IF Statement in that it can

make a decision according to the information it is given. When there are more than a couple of options

I find that a Case Statement is much easier to write and understand than a complicated If Statement.

The process of building the SQL begins with a Case Statement that looks at the choice the user made

in the Where option group. It does this by examining the value of that group i.e. 1 = Firstname, 2 =

Lastname, 3 = Both (Firstname or Lastname).

For each of these choices there are another three possibilities as chosen by the user in

the How option group. For each of the three cases in my first Case Statement there is another

“nested” Case Statement examining the value of the second option group i.e. 1 = Starts With, 2 =

Ends With, 3 = Contains. The completed macro (Listing 3) is shown below.

Listing 3:

Private Sub cmdSearch_Click()


On Error Resume Next
Dim strSearch As String
Dim strSQL As String
strSearch = Me.txtSearch.Value
Select Case Me.grpWhere.Value
Case 1 'Firstname
Select Case Me.grpHow.Value
Case 1 'Starts With
strSearch = "[Firstname] Like " & Chr(34) & strSearch &
"*" & Chr(34)
Case 2 'Ends With
strSearch = "[Firstname] Like " & Chr(34) & "*" &
strSearch & Chr(34)
Case 3 'Contains
strSearch = "[Firstname] Like " & Chr(34) & "*" &
strSearch & "*" & Chr(34)
End Select
Case 2 'Lastname
Select Case Me.grpHow.Value
Case 1 'Starts With
strSearch = "[Lastname] Like " & Chr(34) & strSearch &
"*" & Chr(34)
Case 2 'Ends With
strSearch = "[Lastname] Like " & Chr(34) & "*" &
strSearch & Chr(34)
Case 3 'Contains
strSearch = "[Lastname] Like " & Chr(34) & "*" &
strSearch & "*" & Chr(34)
End Select
Case 3 'Both
Select Case Me.grpHow.Value
. Case 1 'Starts With
strSearch = "[Firstname] Like " & Chr(34) & strSearch &
"*" & Chr(34) & _
" OR [Lastname] Like " & Chr(34) &
strSearch & "*" & Chr(34)
Case 2 'Ends With
strSearch = "[Firstname] Like " & Chr(34) & "*" &
strSearch & Chr(34) & _
" OR [Lastname] Like " & Chr(34) & "*" &
strSearch & Chr(34)
Case 3 'Contains
strSearch = "[Firstname] Like " & Chr(34) & "*" &
strSearch & "*" & Chr(34) & _
" OR [Lastname] Like " & Chr(34) & "*" &
strSearch & "*" & Chr(34)
End Select
End Select
strSQL = "SELECT tblEmployees.EmployeeID, tblEmployees.Firstname, "
& _
"tblEmployees.Lastname, tblEmployees.JobTitle " & _
"FROM tblEmployees " & _
"WHERE " & strSearch & " " & _
"ORDER BY tblEmployees.Firstname, tblEmployees.Lastname;"
With Me.lstSearch
.RowSource = strSQL
.Requery
End With
End Sub

How it Works:

After the error handler two variables are declared, strSearch to hold the SQL string that will define the

WHERE clause of the Row Source, and strSQL which will hold the final SQL string that will make up the

Row Source itself. Next comes a statement that places the user’s entry from the Find text box into

the strSearch variable.

The first Case Statement checks the user’s choice from the Where option group (1, 2 or 3) and moves

to the relevant part of the Case Statement where it encounters a second Case Statement. It uses this

second Case Statement to check the user’s choice from the How option group (again 1, 2 or 3) and

moves to the relevant part of this case statement. Here it finds a text string that describes in SQL the
user’s choices from the Find text box, the Where option group and the How Option Group.

NOTE: You will see that in the VBA that creates the SQL I have used Chr(34) a number of times.

Chr(34) uses the Chr() function to represent the quote mark (“) which has the ASCII value 34. It is

used here because in VBA a string must be enclosed in quotes, but if that string itself includes quotes

an anomaly will occur e.g. Like “M*” becomes “Like “M*””. Some developers deal with this by nesting

different kinds of quotes e.g. “Like ‘M*’” but I have found that this can sometimes be misinterpreted

by the database engine so I always use Chr(34) instead. It takes a little extra care but will always

work correctly.

For example, if the user enters gr in the Find text box then chooses Last Name and Starts With the
code statement:
strSearch = "[Lastname] Like " & Chr(34) & strSearch & "*" & Chr(34)

generates the SQL:

[Lastname] Like "gr*"

Which is then added to the WHERE clause of the SQL statement that forms the Row Source of the List

Box as:

WHERE [Lastname] Like "gr*"

TIP: If you want to find the ASCII value of a particular character in VBA you can use the Asc() function

to do so. In the Visual Basic Editor open the Immediate window (Keys: [Ctrl]+G or View >

Immediate Window) and type a question mark followed by the Asc() function containing the

character in question enclosed in quotes e.g ?Asc(“@”). Press [Enter] and the ASCII value of the

character will be returned (in this case 64).

Finally, the search string held in the strSearch variable is added to the remaining SQL that will form

the Row Source of the List Box. To complete the process the completed SQL statement is applied to

the Row Source property of the List Box and the Requery command tells it to it refresh its list.

2.4 The OK Button

Having searched the database and found the item they were looking for, our user will select the item

in the List Box and click the OK button to take them to the relevant record.

In this example the records will be displayed on a form named frmEmployees. The code must identify
which record the user has chosen from the list then instruct the form to display that same record.

Create an Event Procedure for the On Click event of the OK button and enter the code as follows

(Listing 4):

Listing 4:

Private Sub cmdOK_Click()


On Error Resume Next
Dim rst As Object
DoCmd.OpenForm "frmEmployees"
Set rst = Forms![frmEmployees].RecordsetClone
rst.FindFirst "[EmployeeID]=" & Me.lstSearch.Value
Forms![frmEmployees].Bookmark = rst.Bookmark
DoCmd.Close acForm, "frmSearch"
End Sub

How it Works:

The code first declares an Object variable that I have named rst to represent a recordset. When using

an Object variable (as opposed to a data variable) you must use the Set keyword when assigning a

value to it.

A code statement opens the Employees form. If the form is already open it simply gets the focus. The

next statement defines the RecordsetClone of the Employees form as the rst recordset variable.

NOTE: A RecordsetClone is a copy of the form’s own recordset that can be used to navigate or operate

on a form's records independent of the form itself.

Using FindFirst the RecordsetClone is instructed to move to the first occurrence of the value of the

ListBox in the [EmployeeID] field. This is the Primary Key field of the recordset so we know that there

will be only one occurrence of that value. We also know that the specified value exists because the List

Box list is generated from the same recordset as is used by the form.

When moving through a recordset Access uses a bookmark to keep track of its position. That

bookmark now marks the position in the RecordsetClone of the item that the user selected from the

List Box. The form’s bookmark is currently on the record it happens to be displaying now. The next

code statement synchronises the form’s bookmark with that of the RecordsetClone which makes the

form move to that same record, so that the form now displays the record that the user chose in the
Search form.

The final code statement closes the Search form.

^top

Step 3: Turn the Search Form into a Dialog Box


Although it functions correctly, our Search tool still looks like a regular Access form so, to finish the

job, we need to make some settings for it to function as a dialog box.


With the form in Design View select the form itself by clicking the small rectangle in the upper left

corner of the design window where the rulers meet (Fig. 21) so that a black dot appears, and go to

the form’s Property Sheet.

Fig.21 Select the Form in Design View.

Make the following settings (Table 2):

Table 2: Form Properties

Property Tab Value

Pop Up Other Yes

Modal Other Yes (optional*)

Caption Format Search

Auto Center Format Yes

Border Style Format Dialog

Record Selectors Format No

Navigation Buttons Format No

Scroll bars Format No

*NOTE: The Modal property, when set to Yes, prevents the user working outside the dialog box whilst

it is open. I have marked this property as optional because, in this example, the tool closes after the

user asks to view the selected record. You might choose to omit the statement that closes the Search
tool so that the user can leave it open and search other records. In this case you might want to set

the Modal property to No (the default setting).


When you view your dialog box in Form View it should now look something like this (Fig. 22):

Fig.22 The completed Search dialog.

TIP: Having made these settings you might find that after viewing the Search tool in Form View the

View button on the Access Home tab is disabled, preventing you from switching the dialog box into

Design View. Instead, simply right-click on the dialog itself and choose Design View from the context
menu.

^top

Step 4: Add a Button to Open the Search Tool


The best way to open the Search tool is with a command button. I usually place one on the form that

will display the relevant records and perhaps another on the database’s Switchboard or Home screen.

The process is the same wherever you choose to put it.


Fig. 23 Add a command button to open the Search tool.

Draw a command button on the form on which you want it to appear, give it a suitable name and

caption (Fig. 23), and add the following code to its On Click event (Listing 5):

Listing 5:

Private Sub cmdSearchForEmployee_Click()


On Error Resume Next
DoCmd.OpenForm "frmSearch"
End Sub

Job done! At this point you have a fully functioning Search tool, but there are various things you can

do to refine and widen the scope of the tool. Here are a few refinements to consider…

^top

Additional Optional Refinements


A Non-Numeric Primary Key

I emphasised the need to include the Primary Key field in the Search tool’s List Box Row Source

because this is the most efficient way to search for a specific record. Every record will have a value in

its Primary Key field. I usually use an Autonumber data type for the Primary Key field but that is not

obligatory.

If your Primary Key field is not numeric you must modify the SQL statement to handle a text value.

You will need to modify the code that powers the OK button (Listing 4) so that instead of:

rst.FindFirst "[EmployeeID]=" & Me.lstSearch.Value

it reads:

rst.FindFirst "[EmployeeID]=" & Chr(34) & Me.lstSearch.Value &


Chr(34)

This simply places quote marks around the text value that comprises the text of your Primary Key.
The User Does Not Enter Anything in the Find Box

I included an error handler so that, in the event of the user clicking the Search button without

entering anything in the Find text box, nothing will happen. You might want to add a message

reminding the user that they need to make an entry in the Find text box first.

Modify the code behind the Search button (Listing 3) so that it includes the following (Listing 6):

Listing 6:

strSearch = Me.txtSearch.Value
If Len(strSearch) = 0 Then
MsgBox "You must enter something in the Find box.",
vbExclamation
Me.txtSearch.SetFocus
Exit Sub
End If

The If Statement checks the length of the string in the strSearch variable which returns zero if the

variable is empty indicating that the user did not enter anything. That being the case, the user is

shown a suitable message after which their cursor is returned to the Find text box and the macro is

cancelled.

The Selected Record Was Not Found

In this example the Search tool’s list was created from the same table as that being searched so we

can be certain that any of the items in the list will be found in the recordset we are searching.

However, if the Search tool’s list was created from a different recordset it’s possible that the chosen

record will not be found. The simple error handler prevents an error occurring if the specified value

isn’t found, and the bookmarks would simply synchronize at the end of the recordset. However, it

would be better to let the user know what has happened so, if this might apply to your data, modify

the code behind the OK button (Listing 4) as follows (Listing 7):

Listing 7:

rst.FindFirst "[EmployeeID]=" & Me.lstSearch.Value


If rst.EOF Then
MsgBox "A matching record was not found.", vbInformation
Exit Sub
End If

If Access moves through the entire recordset without finding the specified value it finds itself at the

end of the recordset, known in VBA as the End of File (EOF). The code uses an If Statement to check

whether, having searched the recordset, it finds itself at the End of File position. If that is true then

the user is shown a suitable message and the macro is cancelled.

Can I Use the Same Search Tool for Multiple Forms?

Yes! Your Search tool needs to know which form has opened it. To do this you need to create a Global

Variable (i.e. one that does not reside inside a macro so that its value can be written and read from

any macro).

In the Visual Basic Editor create a new Module and at the top enter the variable declaration:

Public strCallingForm As String

I have named the variable strCallingForm but you can give it any name you like. The code behind each

button that opens the Search tool on your various forms must now also place a value in this variable

which is the name of the form that you wish to search, for example (Listing 8):

Listing 8:

Private Sub cmdSearchForEmployee_Click()


On Error Resume Next
DoCmd.OpenForm "frmSearch"
strCallingForm = "frmEmployees"
End Sub

You also need to modify the code behind the Search tool’s OK button so that it knows which form to

search. Unfortunately you can’t simply replace the name of our original form with the name of our

variable. Instead, replace the original code with a case statement that adds a case for each possible

calling form (Listing 9), for example:

Listing 9:

Private Sub cmdOK_Click()


On Error Resume Next
Dim rst As Object
Select Case strCallingForm
Case "frmEmployees"
DoCmd.OpenForm "frmEmployees"
Set rst = Forms![frmEmployees].RecordsetClone
rst.FindFirst "[EmployeeID]=" & Me.lstSearch.Value
If rst.EOF Then
MsgBox "A matching record was not found.",
vbInformation
Exit Sub
End If
Forms![frmEmployees].Bookmark = rst.Bookmark
Case "frmEmployees2"
DoCmd.OpenForm "frmEmployees2"
Set rst = Forms![frmEmployees2].RecordsetClone
rst.FindFirst "[EmployeeID]=" & Me.lstSearch.Value
If rst.EOF Then
MsgBox "A matching record was not found.",
vbInformation
Exit Sub
End If
Forms![frmEmployees2].Bookmark = rst.Bookmark
End Select
DoCmd.Close acForm, "frmSearch"
End Sub

Sort the List Box List

If you are dealing with a large number of records it might be easier for the user to find what they are

looking for if you sort the list of results. I have included a standard ORDER BY clause in my example

(Listing 3) but you could allow the user to choose the sort order. I am going to keep this simple by

allowing the user to choose only one field.

To allow the user to choose which field to sort by the Search dialog needs another Option Group. I

rearranged the objects on the dialog box to accommodate the new item (Fig. 24). I named the new

Option Group grpSort and set its Default Value property to 1.


Fig. 24 Add a Sort option to the Search dialog.

Some modifications must be made to the original macro behind the Search button (Listing 3). First

add a statement declaring a new string variable to hold the text that will form the fields of the ORDER

BY clause of the SQL:

Dim strSort As String

Create a new Case Statement to create appropriate SQL statements according to the user’s choice

from the optSearch option group (Listing 10). You will see that I have chosen to sort by multiple

fields, starting with that chosen by the user. You can choose to do the same or simply sort by the one

field that the user selected:

Listing 10:

Select Case Me.grpSort


Case 1 'Firstname
strSort = " tblEmployees.[FirstName],tblEmployees.[Lastname]"
Case 2 'Lastname
strSort = " tblEmployees.[Lastname],tblEmployees.[Firstname]"
Case 3 'Jobtitle
strSort =
" tblEmployees.[Jobtitle],tblEmployees.[Firstname],tblEmployees.[LastNa
me]"
End Select
Finally, modify the SQL statement that will be used as the List Box Row Source by replacing the

existing ORDER BY fields with the strSort variable (Listing 11):

Listing 11:

strSQL = "SELECT tblEmployees.EmployeeID, tblEmployees.Firstname, " &


_
"tblEmployees.Lastname, tblEmployees.JobTitle " & _
"FROM tblEmployees " & _
"WHERE " & strSearch & " " & _
"ORDER BY " & strSort & ";"

Writing Safe Code

When creating this Masterclass I decided to build two Search dialogs, one with and one without the

above enhancements. I built the basic dialog box first then made a copy of it and added the

enhancements to the copy. The original form was named frmSearch so I named the copy frmSearch2.

But when I was testing the enhanced dialog I found that it didn’t close when it was supposed to. I had

forgotten to change the code that instructed the form, by name, to close. This reminded me why,

when writing a code statement in which a form refers to itself, instead of writing:

DoCmd.Close acForm, "frmSearch"

I usually write:

DoCmd.Close acForm, Me.Name

This has the advantage that, if the name of the form is changed, the code will still work. I could say

that, trying to keep things simple, I didn’t do it. But I just forgot. Even an “expert” can make

mistakes!

Download Example Database


You can download a sample database containing a completed example of the Search tool built in this

Masterclass. The database contains two copies of the Search tool: the basic dialog as described in the

main part of the Masterclass, and an enhanced version containing the Optional Refinements described

above. You can also download a printable PDF of this tutorial.

To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the
database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

AccessFormsMasterclass4.zip [110KB]

AccessFormsMasterclass4_PopUpSearchTool.pdf [557KB]

Access Forms Masterclass #5


Create Dynamic Titles for Your Forms
Published: 13 September 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

Add a Dynamic Title to Your Form


Most Access forms need some sort of title. The title can simply tell the user what the form is for, a

facility also provided by any text you add to the form’s caption and displayed on the tab, but it can

also provide a quick and easily visible reminder of which record is currently being displayed.

In this Masterclass I will lead you through the steps of creating a Dynamic Title for your form, one that

changes automatically as you move from record to record, and automatically updates when certain

data on the form is changed.

A title should be both visually appealing and useful. This Dynamic Form Title fulfills both those aims

(Fig. 1). It is very easy to create and in the process of building it you can learn a little VBA code.
Fig. 1 Add a Dynamic Title to a form.

Step 1: Create a Form Header


We need somewhere to put the title. It can go anywhere but, to make it prominent, I like to place it in

the Header section at the top of the form. If your form doesn’t already have a header, switch it to

Design View then right-click anywhere on the Detail area (the form’s background) and choose Form

Header/Footer from the context menu (Fig. 2).

Fig. 2 Open the form header.

NOTE: Don’t choose Page Header/Footer which is something different and not what we need for this

exercise.
Fig. 3 Access adds a Header and a Footer to the form.

When you do this Access opens both Header and Footer areas on the form (Fig. 3). They open at 2cm

in height, but you can easily adjust their size by pointing either at the base of the footer or at the

lower edge of the header when a double-headed arrow cursor will appear (Fig. 4). Use this to drag up

or down to change the section’s height.


Fig. 4 Use the resize cursor to change the height of the Header or Footer.

I like to make the header section of my forms a contrasting colour. To do this right-click on the

background of the Header and choose Fill/Back Color from the context menu, then pick a suitable

colour from the palette (Fig. 5). For a greater choice of colours right-click on the background of the

Header and choose Properties to open the Properties Sheet (if it isn’t already open) and go to

the Back Color property on the Format tab where you can choose one of the presets on the drop-down

list or click the Build button ([…]) to open a detailed colour chooser.

Fig. 5 Choose a background colour for the Header.

^top

Step 2: Add the Title


The purpose of the Dynamic Title is to show the user, at a glance, which record they are looking at, by

replicating some of the data on the form. In my example, the form is displaying the details of a
company employee so I want the title to clearly display their name by making use of the data in

the Firstname and Lastname fields. There are two ways to do this:

Method 1: Using an Unbound Text Box

This method does not require any coding and is very easy to do (but I prefer Method 2 for reasons I’ll

explain in a moment). Select the Text Box tool (Fig. 6) from the Design tab of the ribbon and use it to

draw a rectangle on the form’s Header. Usually a Text Box is linked (“bound”) to an underlying field

but this one will be “unbound” and display data that we will create.

Fig. 6 The text Box tool.

Click in the Text Box and enter a suitable expression such as:

=[Firstname] & " " & [Lastname]

This expression simply concatenates (joins together) the data in the Firstname and Lastname fields

with a space in between (Fig. 7). The space is text so must be enclosed in quotes.

Fig. 7 Add a suitable expression to create the Text Box entry.

The Text Box will need to be wide enough to accommodate the widest entry that it will have to

display, and its text should be a suitable colour and large enough to stand out. I made the following

changes using the tools on the Format tab of the ribbon (Table 1).

Table 1: Text Box Properties.

Tool Value
Font Size: 24 pt

Font Color: White

Shape Fill: Transparent

Shape Outline: Transparent

TIP: When you change the size of the text in your Text Box it probably won’t fit the box you drew. To

make the box snap to the size of the text it contains, select the Text Box then double-click the dot in

its lower-right corner. You can then change the width of the Text Box by dragging the dot at the

centre of its right edge.

Switch the form into Form View to view the result (Fig. 8). You will see that as you move from record

to record the title changes to reflect the values in the fields used in its expression.

Fig. 8 The Text Box displays the specified data.

Here comes the problem! I mentioned earlier that I preferred not to use this method. To see why, in

Form View click on the title. The text box is selected and if, like me you used white text, the title

disappears (Fig. 9).


Fig. 9 If the user clicks on the title its contents disappear.

If the user can select the Title they might think that they are supposed to enter its contents but, since

it is an unbound textbox containing an expression, Access won’t let them type there (nor do we want

them to!). Even if the text box is locked it can still be selected. It looks ugly and is guaranteed to

confuse the user, hence Method 2…

Method 2: Using a Label

As described in Method 1, but this time using the Label tool (Fig. 10) draw and format a rectangle and

enter some default text (the text of a Label control is called its caption) such as Employee Details.

Fig. 10 The Label tool.

TIP: if you have already created the title in the form of a Text Box you can save time re-drawing and

formatting a new object by simply changing the existing Text Box to a Label. To do this right-click on

the Text Box and from the context menu choose Change To then Label (Fig. 11).

Fig. 11 Change an existing Text Box control to a Label control.


After entering some default text check the result in Form View. It looks just as it did before (Fig. 12)

except the text does not change as you move through the records and, importantly, it can’t be

selected.

Fig. 12 The Form Title created using a Label control.

We are going to be referring to our new Label control in VBA code so we need to give it a meaningful

name. In Form Design view select the Label control and go to the Other tab of its Property Sheet and

change its Name property to, for example, lblFormTitle.

^top

Step 3: Program the Title


The next task is to make the caption of the Title Label change automatically to reflect the values in the

relevant fields when the user moves from record to record. To achieve this we need to write some VBA

code.

The VBA we are going to use is known as Event Driven programming. This means that we create

macros that run automatically when a particular Event happens. We need the Label’s caption to

update when the form opens and when it moves from record to record. Both of these cause the On

Current event to fire (“fire” is propellorhead language for a programmable event happening). We also

need the label’s caption to update when the Text of the appropriate fields changes (in my example

the Firstname and Lastname fields). To achieve this we can make use of the After Update event of

each of these fields.

With the form in Design View click the View Code button (Fig. 13) on the Design tab of the ribbon.

Fig. 13 The View Code button.


This takes you directly to the form’s code module in the Visual Basic Editor. We will start by creating a

macro that, when run, will update the Label’s caption. Type the following code statements (Listing 1):

Listing 1:

Private Sub UpdateTitle()


On Error Resume Next
Me.lblFormTitle.Caption = Me.Firstname.Value & " " &
Me.Lastname.Value
End Sub

In addition to the instruction to write the Label’s caption, I have added a simple error handler to

prevent problems in the unlikely event that something should go wrong. I have also made the macro

a Private Sub rather than just a Sub. Making it Private means it can only be run from within this

module.

Next, we need to write our Event Procedures that will instruct the UpdateTitle macro to run. This is

known as Calling the macro.

At the to of the code module window there are two drop-down lists. We can generate our empty Event

Procedures using these lists rather than having to return to the form’s design view each time.

Open the left-hand list (currently showing (General)) and choose Form. The Visual Basic Editor

immediately creates an empty Form_Load event procedure, which happens to be the default, but it

isn’t what we need. Instead, open the right-hand list and choose Current (Fig. 14) to create an

empty Form_Current event procedure.

Fig. 14 Choose items from the drop-downs to create an empty Event Procedure.

We don’t need to use the Form_Load event so select and delete the text that the Visual Basic Editor
created for it.
Edit the Form_Current code as follows (Listing 2):

Listing 2:

Private Sub Form_Current()


On Error Resume Next
Call UpdateTitle
End Sub

Repeat the process choosing Firstname and AfterUpdate then Lastname and Afterupdate (if you

have different field name use whatever is appropriate for your needs). Again the Visual Basic Editor,

trying to be helpful, supplies the BeforeUpdate event procedures so simply delete the unwanted text

as you did earlier.

You should now have three event procedures, one for when the form opens and moves through the

records, and one each for the fields used to create the Label Caption. Each one Calling the same

macro.

Before testing your code, open the Visual Basic Editor’s Debug menu and

choose Compile… (Fig. 15). This causes the Visual Basic Editor to check the code for any errors that

you might have missed when writing it.

Fig.15 Compile the code before testing.

Assuming everything is OK (if not then check your code and correct any mistakes) click the Visual

Basic Editor’s Save button or press [Ctrl]+S to save your changes.

Switch your form into Form View and see how the text of the Title changes when you move through

the recordset and when the value of the fields used to create it change.

Your form’s title is now dynamic.

^top

Some More Ideas for Dynamic Titles


My VBA method (Method 2) for defining a Dynamic Form Title builds a string that concatenates field

data with regular text. The original example:

Me.Firstname.Value & " " & Me.Lastname.Value

Uses the Firstname and Lastname fields separated by a space and concatenated with the ampersand

(&) character to create, for example:

Martin Green

You can add as many fields as the available space permits and include any text you want, such as:

Me.Firstname.Value & " " & Me.Lastname.Value & " (" &
Me.JobTitle.Value & ")"

which adds the JobTitle field enclosed in parentheses:

Martin Green (Chairman)

You can even include a calculation such as the next example which makes use of the Birthdate field to

calculate the person’s age:

Me.Firstname.Value & " " & Me.Lastname.Value & " - Age: " & Int((Date
- Me.BirthDate.Value) / 365.25)

resulting in something like:

Martin Green – Age: 67

Here is another example, this time using four of the form’s fields

(Lastname, Firstname, Department and Office) along with some text:

Me.Lastname.Value & ", " & Me.Firstname.Value & " : " &
Me.Department.Value & " Dept. " & Me.Office.Value

This generates a title in the format:

Green, Martin : Management Dept. London

NOTE: In the some of the code examples above the width of this document has forced the code to

flow on to a second line. Remember, when writing your code in the Visual Basic Editor that you should
keep each expression on a single line. If you choose to break a line of code you must use the line

break character (a space followed by an underscore) to tell the Visual Basic Editor that your code

statement continues on the next line, for example:

Me.Lastname.Value & ", " & Me.Firstname.Value & " : " &
Me.Department.Value _
& " Dept. " & Me.Office.Value

^top

Download Example Database


You can download a sample Access database containing completed examples of the form shown in this

Masterclass. The database contains three copies of the Form: one using an unbound Text Box control

to create the Title, another using a Label control driven by VBA code, and a third showing the

alternative examples described above. You can also download a printable PDF of this tutorial.

To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

AccessFormsMasterclass5.zip [101KB]

AccessFormsMasterclass5_DynamicTitle.pdf [364KB]

Access Forms Masterclass #6


A Push-Button Filter for Your Access Forms
Published: 5 October 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

I first came across this idea when learning Access 2 many years ago, so thanks to the author of

whatever book I found it in (and apologies for not being able to remember the name of the book or its
author!). Then it was called a Rolodex, referring to the well-known card index gadget found on so
many desks in those days. Not wanting to be sued by the Rolodex company for taking their excellent

product’s brand name in vain, I have chosen to call this tool a Push-Button Filter. The original tool was

powered by Access macros. In those early Microsoft office days there was not the Visual Basic Editor

we have today, and any code writing was in Access Basic. My version uses VBA and benefits from the

power and flexibility this offers.

The idea is very simple. The tool takes the form of a console consisting of a collection of 26 Toggle

Buttons, one for each letter of the alphabet (Fig. 1), enclosed in an Option Group.

Fig. 1 The Push-Button Filter tool.

The user clicks a button, and something happens. Exactly what depends upon you. I’m offering a

couple of suggestions. One idea is a Filter, so that the form’s recordset is filtered to show only those

records in which the text of a specific field starts with the chosen letter. Another idea makes use of a

sorted recordset, in which the form displays the first record that starts with the chosen letter. There

are lots more possibilities for a tool like this but once you understand the general principle of how it

works you can probably come up with other ideas yourself. Regardless of how it is to be used, the

method of building the Push-Button console is the same.

Step 1: Build the Push-Button Console


Depending upon how it will be used, the console can be placed on the form, or in its header or footer

sections. Move your form into Design View and, in an empty space, use the Option Group tool (Fig. 2)

to draw a rectangle large enough to enclose the required number of buttons (Fig. 3). Don’t worry too

much about the size, you can adjust it to fit as you go along.

Fig. 2 The Option Group tool.


Fig. 3 Draw a rectangle on the form.

The Option Group needs a sensible name so enter one (I called mine grpConsole) in

the Name textbox on the Other tab of its Property Sheet (open the Property Sheet

with [Alt]+[Enter]).

Next, select the Toggle Button tool on the Design tab of the ribbon (Fig. 4) then click inside the

rectangle you just created. As you hover over the Option Group frame its background turns black to

indicate that the item you are about to drop onto it will form part of an option group (Fig. 5).

Fig. 4 The Toggle Button tool.

NOTE: Like Option Buttons and Check Boxes, Toggle Buttons have two states: selected and not-

selected. When used alone on a form these states become True and False respectively, but when

these controls are placed inside a Frame as part of an Option Group they each have a numerical value

which, when selected, becomes the value of the Option Group. The numerical value is assigned

automatically as you add controls to the group, incrementing from 1, or it can be assigned manually

using the control’s Option Value property.


Fig. 5 Drop a Toggle Button onto the Frame.

Format the Toggle Button to suit the style of your own form. For this example, I used the following

settings (Table 1) on the Toggle Button’s Property Sheet (if necessary, open the Property Sheet by

selecting the Toggle Button and pressing [Alt]+[Enter]).

Table 1: Properties of the first Toggle Button.

Property Tab Value

Width: Format 1 cm

Height: Format 1 cm

Back Color: Format Access Theme 3 (optional)

Caption: Format A

Font Size: Format 18

Font Weight: Format Bold

Option Value Data 1

Having created the first Toggle Button (Fig. 6 left) you now need to add the remaining 25, one for

each letter of the alphabet. You could add them one at a time, but I prefer to save time by copying

and pasting. Simply click on the first Toggle Button to select it and press [Ctrl]+C then [Ctrl]+V to
create an exact copy. Move the copy so that it sits alongside the first, then repeat until you have built
the first row of buttons. Controls on a form will snap to the grid as you move or resize them so it’s

quite easy to align them neatly.

Fig. 6 Add the remaining Toggle Buttons to the Option Group.

Now you have a row of Toggle Buttons all labelled “A” so you need to change the Caption property of

each one as appropriate. The copy/paste process also confers the same Option Value on each button

so they currently all have the same value as the first one. Access has spotted this and has displayed a

green marker on each to warn you that the value is duplicated (Fig. 6 centre left). Change the Option

Value property of each button (B=2, C=3, D=4 and so on) to fix this (Fig. 6 centre right). As you do

this the green warning markers disappear.

If, like me, you choose to arrange your buttons in more than one row, you can create the first row

then copy it and paste the next row underneath. Change the captions and values as before until you

have an entire alphabet of buttons (Fig. 6 right).

TIP: When selecting multiple controls on a form, you can click on the first item then hold

down [Ctrl] or [Shift] whilst you click on each of the others. If the controls to be selected are

adjacent to each other, you can make a multiple selection by using the mouse to draw a box around

the items to be selected.

If you wish you can add a caption to the Option Group’s label. I preferred to delete the label (select

the label then press [Delete]). I formatted the outline of the Option Group’s frame to suit the design

of my form by changing its Special Effect property to Flat and choosing a suitable colour for its Border

Color property. I also adjusted the size of the frame to better suit the arrangement of the buttons

(Fig. 7).
Fig. 7 The completed Option Group in Design View and Form View.

Once created, the button console can be moved to a suitable location. Start by making a multiple

selection of the entire Option Group as described in the TIP above, then point at the outline of the

selection so that the Mover cursor appears (Fig. 8) then simply drag the whole thing to its desired

location.

Fig. 8 Drag the Option group to its desired location.

For accurate placement use the [Arrow] keys to move the selected items up, down, left or right. Hold

down the [Control] key whilst doing so for fine-tuning.

TIP: I have found the most useful command when building forms is Undo! It is so easy to accidentally
move, change or delete an object. Don’t panic! Just press [Ctrl]+Z until you have undone whatever it

is that you didn’t mean to do.


The Push-Button console is now complete. When in form view you will see that only one button can be

selected at a time. If you select another button the previously selected button will be deselected.

The next task is to write the VBA code that will turn it into a useful tool.

Step 2: Write the VBA Code


I am suggesting two different uses for this Push-Button console:

 Filter by Letter When a letter is chosen the form’s recordset is filtered by a particular field so
that only those records with entries starting with that letter are shown.

 GoTo by Letter This requires the recordset to be sorted alphabetically (A-Z) by the field in
question. When the user chooses a letter, the form moves to the first record that starts with that
letter.

A Filter by Letter Tool

1. Prepare the Form

To best appreciate this tool in action I have created a form that displays its records as Continuous

Forms. I designed the form with the fields arranged in a single row so that, in form view the form has

the appearance of a datasheet. So, why not use Datasheet View? Datasheet View does exactly what it

says and presents the data in the form of a datasheet and does not allow the addition of a header or

footer, so there is nowhere to place the Option Group. I can achieve the same effect with a little work

on a Continuous Forms form and still have somewhere to locate the console (Fig. 9, Fig. 10).

Note that I have also added a “Show All” button to allow the user to cancel the filter if they wish.
Fig. 9 The Continuous Forms form in design view.
Fig 10 The Continuous Forms form in form view.

2. Enter the Code

The macro that applies the filter must run when one of the console buttons is pressed. Pressing a
button results in the value of the Option Group changing, adopting the Option Value of the selected

button. The code is therefore attached to the AfterUpdate event of the Option Group control.

Select the Option Group by clicking on its frame (not on one of the buttons) and click in

the AfterUpdate text box on the Event tab of its Property Sheet. Click the Build button ([…]) and

choose Code Builder in the Choose Builder dialog then click OK to open to the Visual Basic Editor

where an empty AfterUpdate macro has been created. Add the following code (Listing 1) remembering

to change the name of the Option Group if you used something different:

Listing 1:

Private Sub grpConsole_AfterUpdate()


On Error Resume Next
Dim strLetter As String
strLetter = Chr(64 + Me.grpConsole.Value)
Me.OrderBy = "[Lastname], [Firstname]"
Me.OrderByOn = True
Me.Filter = "[Lastname] Like " & Chr(34) & strLetter & Chr(42) &
Chr(34)
Me.FilterOn = True
End Sub

3. How it Works

I can’t think of anything that might go wrong here but to be safe the code starts with a basic error

handler.

The code needs to convert the number that is the value of the Option Group into a letter. I am going

to store that letter in a string variable called strLetter so the next statement declares the variable.

The code needs to change the numerical value of the Option Group (e.g. A=1, B=2 etc.) into the letter

it represents, and makes use of the Chr() function to do this. The ASCII value of the letter “A” is 65.

These ASCII values increment by one through the alphabet, so that “Z” is character 90.

NOTE: Upper-case and lower-case letters have different ASCII values. Lower-case “a” is character 97,

through to “z” being character 122. Since the filter is not case sensitive it doesn’t matter which we

use. I have chosen upper-case.

I could have applied the actual ASCII values to the buttons, but it is a simple matter to convert them.

All that is needed is to add 64 to the value of the Option Group to get the ASCII value of the letter
corresponding to the button that was pressed. The resulting number is then put into the Chr() function

which is then passed to the strLetter variable (e.g. “A” = Chr(64+1), “B” = Chr(64+2) etc.).

The filter doesn’t require the recordset to be sorted but it makes sense to sort the results of the filter,

so the next two statements apply a sort order and switch it on.

Finally, two statements construct the filter, apply it and switch it on. To build the filter I have made

use of the Chr() function again to represent various characters (for example Chr(34) is the quote mark
(") and Chr(42) the asterisk (*))

TIP: The Chr() function is very useful when constructing text strings in VBA, especially when working
with SQL where quote marks, asterisks etc. are involved. To find out the ASCII value of any character
(i.e. the number you give the Chr() function to generate a character), open the Visual Basic Editor’s

Immediate Window (keys: [Ctrl]+G) and type, for example, ?Asc(“A”) and press [Enter]. The Asc()

function will return the ASCII value of the character you gave it.

4. Suggested Improvements

You could add a couple of simple improvements to this macro. If the user chooses a letter for which

there are no records the form will simply display a single empty record. Rather than leave it at that, I

prefer to let the user know that this is because no matching records were found. I use an If Statement

and a Message Box to achieve this (Listing 2).

Listing 2:

If Me.Recordset.RecordCount = 0 Then
MsgBox "No records starting with " & strLetter & " were found.",
vbInformation
End If

The If Statement checks the RecordCount property of the form’s Recordset property which holds the

number of records currently being displayed. When executed after the filter has been applied it will

give the number of records that remain after the filter was applied. If the result is zero, then a simple

message is displayed (Fig. 11).

Fig. 11 An optional message shows that nothing was found.


You might notice that, when a button is pressed, the screen flickers as the filter is applied and the

picture redrawn. Whilst this is a minor inconvenience user might find it distracting. It is easily cured

by hiding screen updates until the process has finished. Do this by turning Echo off then on again. The

completed macro including the message and Echo commands is shown here (Listing 3):

Listing 3:

Private Sub grpConsole_AfterUpdate()


On Error Resume Next
Dim strLetter As String
strLetter = Chr(64 + Me.grpConsole.Value)
DoCmd.Echo False
Me.OrderBy = "[Lastname], [Firstname]"
Me.OrderByOn = True
Me.Filter = "[Lastname] Like " & Chr(34) & strLetter & Chr(42) &
Chr(34)
Me.FilterOn = True
DoCmd.Echo True
If Me.Recordset.RecordCount = 0 Then
MsgBox "No records starting with " & strLetter & " were
found.", vbInformation
End If
End Sub

NOTE: When using DoCmd.Echo False you must remember to switch it on again before the end of the
macro. It is also vital that your code is protected by an error handler. A simple error handler like that

used here is fine for the simplest of macros where nothing serious could happen. When using a full

error handler, you must include DoCmd.Echo True in the Exit Routine to make sure that, in the event

of an error, screen updating is restored. Failure to do this could leave your user with a blank screen.

5. Add a Show All Button

Unless you set a default value for the Option Group it will have a value of zero when the form opens

and the form’s recordset will be unfiltered. If the user wants to remove the filter they can do so by

clicking the “Filtered” button next to the navigation buttons at the bottom of the screen, or the Toggle

Filter button on the Home tab of the ribbon. However, doing this does not affect the Option Group and
the last-chosen button will remain selected. This might give the impression that the recordset was
filtered when it wasn’t. You could attempt to synchronise the button console with the recordset but,

wanting to keep things simple, I have chosen to add a “Show All” button.

This is simply a command button (I named mine cmdShowAll) with a command to switch off and clear

the form’s filter applied to its OnClick event (Listing 4).

Listing 4:

Private Sub cmdShowAll_Click()


On Error Resume Next
Me.Filter = ""
Me.FilterOn = False
Me.grpConsole.Value = 0
End Sub

It also includes a command setting the value of the Option Group to zero so that no buttons appear

selected. Unlike the built-in commands, it doesn’t toggle the filter on and off, it simply makes sure it’s

off.

A GoTo by Letter Tool

This tool is intended for use on forms displaying their records in Single Form view. It also requires that

the recordset be sorted alphabetically (A-Z) but the code will take care of that. The console can be

placed anywhere on the form, or in its header or footer sections. In this example I have placed it in

the form’s footer (Fig. 12).


Fig. 12 The console on a Single Form form.

When the user clicks a button on the console the form will move to the first record in the recordset

whose entry in the specified field (in this case Lastname) starts with the chosen letter. The user can

then move through the records in the usual way (they will have been sorted alphabetically by the

macro) or select another letter.

As with the Filter by Letter tool the code runs on the AfterUpdate event of the Option Group

(Listing 5):

Listing 5:

Private Sub grpConsole_AfterUpdate()


On Error Resume Next
Dim strLetter As String
Dim rst As Object
strLetter = Chr(64 + Me.grpConsole.Value)
Me.OrderBy = "[Lastname] ASC"
Me.OrderByOn = True
Set rst = Me.RecordsetClone
rst.FindFirst "[Lastname] Like " & Chr(34) & strLetter & Chr(42) &
Chr(34)
If rst.NoMatch Then
MsgBox "No records starting with " & strLetter & " were
found.", vbInformation
Else
Me.Bookmark = rst.Bookmark
End If
Set rst = Nothing
End Sub

2. How It Works

As in the previous example the macro notes which button has been pressed and in the same way

stores it in the strLetter variable. It then sorts the form’s recordset by the Lastname field in ascending

(A-Z) order.

The code makes use of the form’s RecordsetClone property which it stores in an object variable

named rst.

NOTE: The Recordset Clone is an exact copy of the form’s recordset that can be manipulated in

memory without interfering with the recordset that the form is displaying. Having carried out any

necessary work on the clone it can be discarded. When assigning a value to an object variable, such

as the Recordset Clone, you must use the Set keyword.

The macro uses the FindFirst method to travel through the recordset until it finds a record that

satisfies the specified condition, in this case a record in which the Lastname field starts with the

chosen letter. The recordset must be in ascending sort order for this to work.

If the end of the recordset is reached without a matching record being found the

recordset’s NoMatch property is set to True. An If Statement checks to see whether this is the case

and if so displays a message stating that nothing was found. If, however, a suitable record is found

the FindFirst method stops on that record and its position in the recordset is bookmarked.

The position of the form’s bookmark is then synchronised with that of the Recordset Clone’s

bookmark, making the form display the found record. Finally, the rst variable is set to Nothing to clear
it from memory.
3. A suggested Improvement

Having found their chosen records, the user might move to other records in the recordset. As they do

so the console will continue to display the letter the user last chose, even though they might be

viewing a record with a Lastname starting with a different letter.

The solution is to synchronise the console with the record that is currently being displayed. The

following code runs on the form’s Current event (Listing 6). This event happens when the form opens

and each time a different record is displayed.

Listing 6:

Private Sub Form_Current()


On Error Resume Next
If IsNull(Me.Lastname.Value) Then
Me.grpConsole.Value = 0
Else
Me.grpConsole.Value = Asc(UCase(Left(Me.Lastname.Value, 1))) -
64
End If
End Sub

The code uses an If Statement to check if the Lastname field contains a value. If not (i.e. its value

is Null) the value of the Option Group is set to zero so that no buttons are pressed. If there is a value

in the Lastname field the code determines the ASCII value of the first (leftmost) letter then subtracts

64 to arrive at a number it can assign to the Option Group so that the correct button appears selected.

NOTE: when searching a recordset, text is normally not case sensitive, but when determining the

ASCII value of a character upper-case and lower-case letters have different values. To prevent

confusion, I have used the UCase function to ensure that, whatever case the Lastname data is written

in, the ASCII value of the upper-case version of the first letter is calculated.

Download Example Database


You can download a sample Access database containing completed examples of the form shown in this

Masterclass. The database contains two copies of the form: one equipped with the Filter by Letter tool

and the other with the GoTo by Letter tool.


To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

AccessFormsMasterclass6.zip [114KB]

AccessFormsMasterclass6_PushButtonFilter.pdf [354KB]

Access Database Management


Add an Audit Trail to your Access Database
Published: 23 April 2013

Author: Martin Green

Screenshots: Access 2010, Windows 7

For Access Versions: 2003, 2007, 2010

About This Tutorial


My Access tutorials are designed to help people learn about building databases and how to add functionality

to their databases with VBA programming. If you are just looking for some code to copy then skip down to

the relevant code listings and help yourself to whatever you need. But if you want to learn something so

that maybe next time you can figure it out for yourself, I urge you to read the accompanying notes that

explain how I have come to the decisions necessary to fulfil the task at hand. I believe that in addition to

telling people what to do, it helps to explain why they are doing it so my code listings are accompanied by

explanations of what the code does, how it works and why it is necessary. I have also included two ready-

made sample databases using the code shown here which you can download using the link at the bottom of

this page.

Why Use an Audit Trail?


If you need to keep track of changes to your data then you need an Audit Trail. A comprehensive Audit Trail

records not only what changes are made but when and by whom. Perhaps you are interested only in data

that is changed, or perhaps you also want to know about data that has been added or deleted. This is

potentially quite a daunting task. You may have many tables, each with a multitude of fields. In a busy

database this could result in a very large number of actions being logged so, whatever method you choose,
your Audit Trail has to be accurate, reliable, and it must not interfere with the smooth running of the

database.

In this tutorial I describe two variations of a simple Audit Trail tool. In one, only edits to the data are

recorded, with the option to ignore the addition of new records. In the other the approach is more

comprehensive with edits, additions and deletions recorded. Both record the date and time of the action and

the identity of the user responsible.

Valuable as it is, it's important to remember that an Audit Trail alone isn't the complete answer to your data

security issues. You should make regular backups of your database files and make full use of available

security provisions.

The Requirements
Here's what I want from an Audit Trail:

 I want my Audit Trail to keep a record of changes to my data. When a user edits a record I want to
know which fields were changed, what the values were before the change was made and what they
were changed to. I also want to know who made the changes and when they did it.

 I might want the option to ignore the addition of new records since that might involve duplication of
data. After all, if I want to know what data was added I just have to look at the database. And I can
record the user ID along with a timestamp when records are added.

 I would like the facility to specify which fields are included in the audit in case I decide that it is not
necessary to record changes to every field.

 I might also want the option to know about the deletion of records. This may involve storing the
details of the deleted record but, if I have incremental backups of my data, I will already have this
information to hand.

 For ease of implementation the Audit Trail should be easily applied to various different datasets
without having to re-write the code to suit each one.

The Provisos
I'm going to use VBA to build my Audit Trail tool and since code cannot be attached to a table I can't keep

track of changes made directly on the tables themselves. This means that I need to prevent users from

working in tables. In my opinion this is good practice anyway and I (almost) never allow users to see the

tables in the databases that I build. If you think that users might try to bypass the Audit Trail then prevent

them from working in the tables by either hiding them or not showing the Navigation Pane. More persistent

users can be thwarted by using code to disable shortcut keys so that they can not open the Navigation Pane.

Similarly, data added, changed or deleted by other methods such as queries might need to be tracked so, if

necessary, all manipulation of data should be achieved through your user interface (via forms in other

words) so that you can retain control of your data and make sure that everything you want recorded is.
I am not going to cover all the ins and outs of database security in this tutorial but, if you do nothing else,

hide the Navigation Pane and protect your code with a password. Build a Switchboard or Home Page to give

your users easy access to all the things you want to allow them to see.

The Plan
Capturing the Changes

Now that I have decided that my users will work in forms I have a number of easy ways to track what they

do. When figuring out my approach to this task my first thought was to make use of the Dirty property. The

form itself and controls that display data have a Dirty property that you can inspect with code to find out

whether the form or an individual control contains unsaved changes. There is also an OnDirty event that

fires when a change is made to the data. But I decided that, although useful in another context, I did not

need to make use of these.

I am only interested in changes that are saved, so the time for me to detect any changes is at the point of

saving. The form has a BeforeUpdate event which fires immediately before a record is saved. At this point

the data on the form is just about to be written to the underlying table so two versions of the record exist,

the old version which resides in the table and the new version which resides on the form. In addition to

their Value property form controls that display data have an OldValue property. The Value property

represents what you can see on the form, which may be the original value if no change was made.

The OldValue property represents the original value before any change was made. By comparing

the Value with the OldValue I can determine whether change was made and, if so, what the values were

before and after the change.

If changes have been made, a form automatically saves its data when the focus is moved to a different

record or when the form is closed. In addition, the user may choose to save the data manually by means of

one of the built-in commands or by clicking the record selector. Regardless of the method by which the data

is saved I can be confident that, provided the changes were made using a form, I can trap the event

using BeforeUpdate.

Deciding How to Save the Data

For simplicity I want to have a single Audit Trail table. The way the data is saved is determined by how

flexible the procedure needs to be and how the Audit Trail data is going to be used once it has been

collected. If the Audit Trail was only ever going to apply to one recordset then perhaps each field in the

recordset could be represented individually in the Audit Trail table. This could potentially cause problems

since each field in the recordset would require two fields in the Audit Trail table, one for the old value and
one for the new value. This could make for a lot of fields and also make interrogating the data quite

complicated.

Instead I decided that I needed just three fields: one for the name of the field that was changed; one for the

old value; and one for the new value. This means that if, for example, changes were made to five fields in a

record, five rows would be added to the Audit Trail table, one for each changed field. I would have to make

sure that each row had the same timestamp so that it was clear that all those fields were changed as part of

the same operation.

Identifying the Recordset

I don't want to have several different Audit Trails, one for each table, so I need some way to identify which

recordset the recorded changes belong to. I could use the form's RecordSource property but I decided it

would be simpler to record instead the name of the form, which I can read from its Name property. In a

multi-user environment where there may be numerous different forms working on the same recordset this

might also provide additional information about how or by whom the data was changed.

Identifying the Record

It is important to note which record was changed. The best way to do this is to identify the record by

its Primary Key field. I (almost) always use an AutoNumber for the Primary Key so this was the obvious

choice.

Identifying the User

In an Access 2003 database (including databases created in later versions but saved in the *.mdb format) it

is possible to use the user-level security tool to set up individual user accounts and user names. In this

case Application.CurrentUser could be used to return the name of the current user account. However, if

user-level security was not set up or if the database is in Access 2007-2010 format

(*.accdb) Application.CurrentUser simply returns "Admin". Fortunately, there are alternatives.

My method of choice is to use the Environ function Environ("USERNAME") which returns the ID of the

current user as identified by Windows, normally a person's Windows log-in name. There is some discussion

about how secure this method is since this information can be "spoofed" by someone trying to mask their

identity (and no, I'm not going to tell you how to do that!). If you think there is a possibility of this

happening then you have the option to talk directly to the Windows API. That is outside my personal skillset

but there are plenty of methods published, most of them requiring large amounts of code. The simplest way

I have found is to use CreateObject("WScript.Network").UserName. You can try the various choices and

select the one most suitable for your own particular setup.
Capturing Date and Time

The Now() function returns the date and time accurate to the second, which is suitable for auditing

purposes.

Specifying Which Fields to Check

Again, to make the process as flexible as possible I want to be able to designate specific fields to check. On

the form these may be represented by Text Boxes, Combo Boxes, Check Boxes or any of the other types of

bound controls. All form controls have a Tag property that can accept any text or numeric value. By

assigning a specific value to the tag property of the controls I want to record (I am going to use the word

"Audit") I can specify which controls get checked. I can also avoid code errors by trying to read a value from

the wrong sort of control such as a Label.

So, now we are ready to get started...

To build an Audit Trail tool that simply records edits, with the option to include or exclude new records you

should start here. If you want you Audit Trail tool to record edits, new records and deletions, you will find a

second version of the tool described lower down the page starting here.

Build the Audit Trail Table


The first task is to build the table that will receive the records of changes to the database. As I explained

earlier, it needs to record the date and time of the change, the identity of the user responsible, and details

of the change itself. You'll see that I don't like using spaces in my field names. If you use different field

names from the ones shown make sure you make the corresponding changes to the code. Here is a list of

the required fields and data types:

Field Name Data Type

AuditTrailID AutoNumber (Primary Key)

DateTime Date/Time

UserName Text

FormName Text

RecordID Text

FieldName Text

OldValue Text

NewValue Text
Note that all the field data types (apart from the Primary Key and the DateTime fields) are specified as text.

This is to allow the maximum amount of flexibility in the tool since whilst I know that

the UserName, FormName and FieldName data will be text, the RecordID, OldValue and NewValue fields

might have different data types. The safest way therefore is to store all data as text. It does mean,

however, that when interrogating the data you should take account of the fact that you might have dates or

numbers stored as text. I have named my table tblAuditTrail.

Write the Code that Records the Changes


The next task is to create the code that will record the changes to your data. There are a couple of things to

do first. Because I have designed the code to be used by any of my forms it does not reside in the form's

own code module. Instead it will be placed in a standard VBA module. This means that it will only have to be

created once and any form will be able to make use of it.

If you are not familiar with writing VBA code here's what you have to do. From anywhere in your database

use the keyboard shortcut [Alt]+[F11] to open the Visual Basic Editor. From the Insert menu

choose Module to create a new code module. You should see it appear in the Project Explorer window at

the left of the screen. The module has been automatically named Module1. I like to give my modules

meaningful names so I have renamed mine to basAudit so I can easily find my code later. This isn't really

important but if you want to do the same you can enter the new name by selecting the module and

changing its name in the Properties Window. (NOTE: If you can't see either the Project Explorer or the

Properties Window you can switch them on from the View menu.)

Next you have to set a reference to ADO (ActiveX Data Objects) because some of the code will be using this

subset of the VBA language. Setting a reference to the ADO code library will allow Access to understand that

part of your code. A reference to ADO was set by default in Access 2003 but not in Access 2007 or 2010.

Check to see if the reference is set by opening the Tools menu and choosing References. In

the References dialog box there will be several items with a tick against them at the top of the list. If there

isn't one named Microsoft ActiveX Data Objects 2.8 Library (the number may differ slightly e.g. 2.1 in

Access 2003) then scroll down the list to find it. Place a tick in the box then click OK to set the reference.
Double-click the module to open it in the main code editing window then add the code shown below

(Listing 1).

Listing 1. A procedure to write data changes to the Audit Trail table

Sub AuditChanges(IDField As String)


On Error GoTo AuditChanges_Err
Dim cnn As ADODB.Connection
Dim rst As ADODB.Recordset
Dim ctl As Control
Dim datTimeCheck As Date
Dim strUserID As String
Set cnn = CurrentProject.Connection
Set rst = New ADODB.Recordset
rst.Open "SELECT * FROM tblAuditTrail", cnn, adOpenDynamic,
adLockOptimistic
datTimeCheck = Now()
strUserID = Environ("USERNAME")
For Each ctl In Screen.ActiveForm.Controls
If ctl.Tag = "Audit" Then
If Nz(ctl.Value) <> Nz(ctl.OldValue) Then
With rst
.AddNew
![DateTime] = datTimeCheck
![UserName] = strUserID
![FormName] = Screen.ActiveForm.Name
![RecordID] = Screen.ActiveForm.Controls(IDField).Value
![FieldName] = ctl.ControlSource
![OldValue] = ctl.OldValue
![NewValue] = ctl.Value
.Update
End With
End If
End If
Next ctl
AuditChanges_Exit:
On Error Resume Next
rst.Close
cnn.Close
Set rst = Nothing
Set cnn = Nothing
Exit Sub
AuditChanges_Err:
MsgBox Err.Description, vbCritical, "ERROR!"
Resume AuditChanges_Exit
End Sub

You can type the code yourself or copy the text above and paste it into the code module. Make sure that, if

you have changed any field names, or given your table a different name, you modify the code accordingly.

HOW THE CODE WORKS

The AuditChanges code is a self-contained procedure that can be "called" from any form. Along

with the name of the procedure I have included a parameter which I have named IDField. When

calling the procedure you will need to supply a value for this parameter which will be the name

of the field that identifies the current record. The first line and the last 3 lines comprise an error

handler, telling Access what to do if something goes wrong. The first two Dim statements

declare the ADO variables that represent a recordset and a database connection. Three

more Dim statements declare variables representing the controls on the form, the timestamp

and the ID of the current user. The two Set statements assign values to the ADO variables, the

first representing the current database and the second a new recordset. Now things start

happening! The next command opens a recordset, described by an SQL statement, that

represents the Audit Trail table. At this point a note is made of the time, which is stored in

the datTimeCheck variable, and of the user ID which is stored in the strUserID variable. The

block of code starting For and ending with Next comprises a code loop which visits each control

on the currently active form (this means I don't have to specify a form by name). Then comes
an If statement that checks the control's Tag property and only proceeds if the Tag reads

"Audit". This is important because there are various controls, such as Labels, that don't carry

data and these have to be ignored. Assuming the control has been designated as one to be

checked, a second If statement then compares its Value property with its OldValue property.

Note that I have used the Nz (Null-to-Zero) function because Nulls can cause problems here. If

the values are different the a block of ADO code enclosed in a With statement adds a new record

to the recordset, writes the necessary information into each field, then saves the record before

moving to the next control. If the two values are the same the code simply moves straight to the

next control. When all the controls have been checked the code performs its exit routine, closing

the recordset and the database connection and un-setting their variables, before finally exiting.

Check the code carefully for spelling errors then open the Debug menu and choose Compile... This checks

the code for errors. If you get an error message then find and fix the problem before compiling again. If

everything is OK open the File menu and choose Save or click the Save button to save your code before

proceeding to the next step.

Prepare the Forms and Write their Code


There are two tasks to perform on each form whose changes you want to be audited. The controls to be

checked must be designated and the code that will call the AuditChanges procedure added.

Designate the Controls to be Checked

Open the form in design view then, for each control that you want included in the audit, enter the

word Audit in its Tag property. This is the last item on the Other tab of the control's Property Sheet.
Add Code to the Form's BeforeUpdate Event

Select the form itself by either clicking the small box where the rulers meet in the upper left corner of the

form design window, or by choosing Form from the drop-down list at the top of the Property Sheet. On

the Event tab double-click the box next to Before Update so that the text [Event Procedure] appears then

click the Build button ([...]) to open the form's code window.

The Visual Basic Editor opens showing the form's code module with and empty BeforeUpdate event

procedure ready for you to add the necessary code. You have two options here. If you want to audit all

changes, including the addition of new records, enter the first code statement shown below (Listing 2). You
need add only the second of the three lines shown here, don't duplicate the Private Sub and End

Sub statements. IMPORTANT: The example shows "EmployeeID" as the parameter value. Change this to the

name of the field that identifies the current record, usually the Primary Key field although you can use any

field that uniquely identifies the record.

Listing 2. A procedure to call the AuditChanges routine (including new records)

Private Sub Form_BeforeUpdate(Cancel As Integer)


Call AuditChanges("EmployeeID")
End Sub

If you want to exclude the addition of new records from the audit trail use this code statement instead

(Listing 3). Again, remember to change the parameter name from "CustomerID" to the name of a field that

uniquely identifies the current record.

Listing 3. A procedure to call the AuditChanges routine (excluding new records)

Private Sub Form_BeforeUpdate(Cancel As Integer)


If Not Me.NewRecord Then Call AuditChanges("CustomerID")
End Sub

Check, compile and save your code as described earlier, and you are ready to test your new Audit Trail.

HOW THE CODE WORKS

In the first example (Listing 2) This simple statement uses the Call keyword to instruct Access to

run the AuditChanges procedure. Some programmers omit the word Call, the instruction will still

be understood, but I prefer to use it since it makes it clear to someone reading the code that an

external macro is being run. The parameter value is supplied so that the current record can be

identified. The second example (Listing 3) is modified to include an If Statement that calls

the AuditChanges macro only if the current record is not a new one.

Job done! The Audit Trail has successfully been added to the database and is ready for use.

An Alternative Audit Trail Routine for Recording Additions,


Edits and Deletes
Create a Modified Audit Trail Table

This version of the Audit Trail tool, in addition to recording edits to existing data, records details of additions
to and deletions from the database. In order to do this an additional field is required in the Audit Trail table

to identify what type of action the record represents. I have named this field Action with the data

type Text. The modified table has the following fields:

Field Name Data Type

AuditTrailID AutoNumber (Primary Key)

DateTime Date/Time

UserName Text

FormName Text

RecordID Text

Action Text

FieldName Text

OldValue Text

NewValue Text

This allows the data to clearly indicate what sort of action prompted the Audit Trail entry:

Write the Code that Records Additions, Edits and Deletes

As with the previous version this code uses ADO so the database needs a reference to the appropriate object

library (see the instructions above for how to do this). In addition to the IDField parameter this version of

the AuditChanges macro has a second parameter (I have named it UserAction) that is used to define the

kind of action that prompted the Audit Trail entry. The entire code procedure is shown below (Listing 4):

Listing 4. A procedure to record additions, edits and deletes in the Audit Trail table

Sub AuditChanges(IDField As String, UserAction As String)


On Error GoTo AuditChanges_Err
Dim cnn As ADODB.Connection
Dim rst As ADODB.Recordset
Dim ctl As Control
Dim datTimeCheck As Date
Dim strUserID As String
Set cnn = CurrentProject.Connection
Set rst = New ADODB.Recordset
rst.Open "SELECT * FROM tblAuditTrail", cnn, adOpenDynamic,
adLockOptimistic
datTimeCheck = Now()
strUserID = Environ("USERNAME")
Select Case UserAction
Case "EDIT"
For Each ctl In Screen.ActiveForm.Controls
If ctl.Tag = "Audit" Then
If Nz(ctl.Value) <> Nz(ctl.OldValue) Then
With rst
.AddNew
![DateTime] = datTimeCheck
![UserName] = strUserID
![FormName] = Screen.ActiveForm.Name
![Action] = UserAction
![RecordID] =
Screen.ActiveForm.Controls(IDField).Value
![FieldName] = ctl.ControlSource
![OldValue] = ctl.OldValue
![NewValue] = ctl.Value
.Update
End With
End If
End If
Next ctl
Case Else
With rst
.AddNew
![DateTime] = datTimeCheck
![UserName] = strUserID
![FormName] = Screen.ActiveForm.Name
![Action] = UserAction
![RecordID] = Screen.ActiveForm.Controls(IDField).Value
.Update
End With
End Select
AuditChanges_Exit:
On Error Resume Next
rst.Close
cnn.Close
Set rst = Nothing
Set cnn = Nothing
Exit Sub
AuditChanges_Err:
MsgBox Err.Description, vbCritical, "ERROR!"
Resume AuditChanges_Exit
End Sub

Write or copy and paste the code into a standard module so that it can be called from any form.

HOW THE CODE WORKS

The code differs from that described earlier after the tblAuditTrail recordset has been opened but

before a new record is created. A Case Statement examines the second parameter (UserAction)

and enters data into a new record accordingly. If the parameter indicates that the action is an

EDIT all the appropriate information is written into the new record with the addition of the word

"EDIT" (the value of the UserAction parameter) in the Action field. The only additional cases to

consider are NEW and DELETE and since these require the same data entry requirements they

are dealt with together in the Case Else part of the Case Statement. For new records and

deletions there is no Value or OldValue data to be entered so there is no need to loop through

the controls. the procedure simply records the timestamp, user ID and form name together with

the ID of the record concerned and type of action read from the UserAction parameter. The error

handler and exit routine are the same as before.

Prepare the Form and Add its Code

The Tag property of each control to be audited must be set to Audit as described in the previous example.

As this example requires more actions to be audited the code that calls the AuditChanges routine is a little

more complex. Again, the BeforeUpdate event is used to call the AuditChanges macro when a record is

saved, which happens when a new record is added or an existing record is edited (Listing 5):

Listing 5. A Procedure to call the AuditChanges routine (new and existing records)

Private Sub Form_BeforeUpdate(Cancel As Integer)


If Me.NewRecord Then
Call AuditChanges("EmployeeID", "NEW")
Else
Call AuditChanges("EmployeeID", "EDIT")
End If
End Sub

HOW THE CODE WORKS

The BeforeUpdate event fires when data is saved to the underlying recordset. Access is able to
determine whether this data applies to an existing record or if it constitutes a new record. The If

Statement checks whether or not a new record is being created and calls

the AuditChanges macro specifying the record's ID field and either "NEW" or "EDIT" as

appropriate.

Deleting a record does not cause the BeforeUpdate event to fire. Instead, the AfterDelConfirm event is used.

You might notice that there is an OnDelete event but this is not appropriate because it fires regardless of

whether or not the user confirms the deletion when Access displays the usual warning. So if OnDelete is

used the code would run even if the user cancelled the deletion when asked. The AfterDelConfirm event fires

only when the user confirms the deletion, so this is used to call the AuditChanges macro (Listing 6).

Listing 6. A Procedure to call the AuditChanges routine (deleted records)

Private Sub Form_AfterDelConfirm(Status As Integer)


If Status = acDeleteOK Then Call AuditChanges("EmployeeID", "DELETE")
End Sub

HOW THE CODE WORKS

The AfterDelConfirm event fires when the user dismisses the warning message by choosing one

of the options (if they press the [ESC] key or use the message's Close button it assumes No). If

the user chooses Yes the message returns the value acDeleteOK to the event

procedure's Status parameter. A simple If Statement checks to see if this is the case and, if so,

calls the AuditChanges macro specifying the record's ID field and "DELETE".

Download Example Databases


You can download samples of databases equipped with the databases described in this tutorial. The "Simple"

database includes the first example in the tutorial in which the Audit Trail records edits to existing data with

the option to exclude the recording of new records. The "Detailed" database includes the second example in
the tutorial in which the Audit Trail records edits to existing data as well as the addition and deletion of

records.

To download the file right-click the icon or text link below then choose Save target as... and follow the

instructions. Both databases are supplied in *.zip folders. After downloading you should extract the database

files from the zip folders before attempting to use the databases. To do this right-click the zip file icon then

choose Extract All... and follow the instructions.

You can also download a printable PDF version of this tutorial.

AccessAuditTrail_Simple.zip [88KB]

AccessAuditTrail_Detailed.zip [88KB]

AccessAuditTrail.pdf [226KB]

Handling Errors in Your Access Database #1


Errors 101
Published: 20 September 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

This is not intended to be a complete guide to error handling in VBA but more of an “Errors 101”. In it

I explain why your code needs error handlers, what they do, and how to use them. There is quite a lot

more you can know about error handling and, whilst the same principles apply throughout VBA, the
examples here are tailored to Microsoft Access. Most of what you read here you also applies to Excel,

Word and in fact any application that can be programmed using VBA, but where appropriate the

examples use Access.

Why Worry About Errors?


This may come as a surprise to some of you, but nobody writes perfect code. The aim of any database

developer is to create code that is “bulletproof”, code that always works as it should and never

crashes. We do our best, but we can never anticipate every circumstance or prepare for every crazy

thing a user might do. Sometimes we just make a mistake or leave something out. That’s where Error
Handling comes to the rescue.
An error happens when, for whatever reason, Access is unable to execute a code statement. Nothing

dramatic happens. The macro just stops. Actually, it pauses, which is part of the problem. It goes into

“break mode” and waits for further instructions. If those instructions haven’t already been given by

the developer, then the user is in trouble. This is why every procedure, from a simple one-line macro

on a button click to a complex macro with hundreds of lines of code, needs an error handler.

A macro’s error handler tells Access what to do in the event of something going wrong. At the very

least, that simple one-line macro that couldn’t possibly fail, and anyway it wouldn’t matter if it did,

should start with the most basic of error handlers:

On Error Resume Next

This code statement simply tells Access that, if it can’t execute a code statement, it should ignore it

and move on to the next one. Use it with caution! If a command that would have caused an error is

ignored, something later in the same macro might not work correctly. The user will not be notified

that there has been a problem and will probably assume that the macro has completed all its tasks

successfully.

What Happens if there is No Error Handler?


Consider a simple navigation button on a form, like the Previous and Next buttons illustrated here

(Fig. 1).

Fig. 1 A “back” button on a form.

Each needs just one code statement to do its job, for example (Listing 1):

Listing 1:

Private Sub cmdPrevious_Click()


DoCmd.GoToRecord , , acPrevious
End Sub
What could possibly go wrong? Answer: the user might click the Previous button when the form is

displaying the first record. Crash! Access can’t go to the previous record because there isn’t one, so it

can’t execute the code statement and the macro fails. If you haven’t anticipated this eventuality and

included an error handler, the user sees a standard error message (Fig. 2).

Fig. 2 The built-in error message.

That error message is for you, the developer, to help you test and debug your code, not for the poor

user who is wondering what to do next. The Debug button is highlighted which we all know means

“Click Me!” so that’s what the user does and suddenly they find themselves in the Visual Basic Editor

looking at your code with a macro in break mode (Fig. 3).

Fig. 3 The Visual Basic Editor showing a macro in Break mode.

They should have clicked the End button to terminate the macro but, of course, they didn’t know that.

Why should they? What happens next is that your phone rings and a voice says: “the database is

broken”.

Using On Error Resume Next


Would it really matter if the user pressed the Previous button on the first record and nothing

happened? In this case no, so a simple addition to the code will prevent any problems (Listing 2).

Listing 2:

Private Sub cmdPrevious_Click()


On Error Resume Next
DoCmd.GoToRecord , , acPrevious
End Sub

The same applies for the Next button. If clicked on the last record Access will move to a new record

but if clicked whilst the form is displaying a new record the code will crash.

You should only use this sort of error handler if you are certain that there will be no adverse

consequences of ignoring an error. If you aren’t sure, then at least remember to password protect

your code.

TIP: Always password-protect your code (in the Visual Basic Editor go to Tools > Database Properties

> Protection). Just one of the reasons for doing this is that should it ever display the standard error

message its Debug button will be disabled.

I have used a very simple example to illustrate when On Error Resume Next can safely be used but

you can also make use of it within a more complex macro that would normally need a more

sophisticated error handler. In this case you can give, and later cancel, the On Error Resume

Next command by pairing it with the statement On Error GoTo 0 (Listing 3).

Listing 3:

' more macro code here


On Error Resume Next
DoCmd.GoToRecord , , acPrevious
On Error GoTo 0
' the macro continues here

In the event of such an error, this gives the instruction that, having ignored an error, if another error

should occur it now has to refer to the command in Line Zero (although we don’t see line numbers in

VBA the code engine understands them). In Line Zero, the first line of your macro, it should find your
error handler command and therefore proceed to your regular error handler. If you haven’t included

one the built-in error reporting system comes into effect.


Using a Full Error Handler
When you create any new code, you should try to anticipate what might go wrong. Don’t fall into the

trap of thinking “why would anyone do that?”. You know in what circumstances and how your macros

should be used but your database users might not. When testing your code, you should run it in

different ways and in different circumstances and note anything that might cause it to fail. If you can’t

modify your code to avoid such problems arising, then a well-written error handler can help to rescue

the situation if the code fails.

Here’s a simple example. You have a button on a form that runs a saved query. Simple enough, but

what if someone has deleted that query or changed its name (which is why my databases rarely

contain saved queries). Without an error handler here’s what happens (Fig. 4).

Fig. 4 The macro tries to open a non-existent query.

Again, a simple one-liner and you might be tempted to add just a basic error handler (Listing 4):

Listing 4:

Private Sub cmdWageCostSummary_Click()


On Error Resume Next
DoCmd.OpenQuery "qryWageCostSummary"
End Sub

If you do this the query simply won’t open. Nothing will happen, and the user will not know why. But

unlike in my earlier example, more than one thing could go wrong. Perhaps a table or field was

deleted, and you forgot to modify a query that referred to it. A different error would occur with a less

helpful error message (Fig. 5):


Fig. 5 Sometimes error messages are less helpful.

This simple macro needs a full error handler that lets the user know what the problem is and doesn’t

leave them in an embarrassing situation. Different developers have their own way of doing things.

Here’s how I write a full error handler (Listing 5):

Listing 5:

Private Sub cmdStaffingLevels_Click()


On Error GoTo cmdStaffingLevels_Click_Err
' More code might go here
DoCmd.OpenQuery "qryStaffingLevels"
' More code might go here
cmdStaffingLevels_Click_Exit:
' Any tidying-up code goes here
Exit Sub
cmdStaffingLevels_Click_Err:
Select Case Err.Number
Case 3270
MsgBox "The database is unable to open the query." &
vbCrLf & _
"Please ask the database manager to investigate the
problem.", _
vbCritical, "Error!"
Case 7874
MsgBox "The query you requested cannot be found.",
vbCritical, "Error!"
Case Else
MsgBox "An unexpected error has occurred. If you wish to "
& _
"report the error please note the following:" & vbCrLf
& _
"Error Number: " & Err.Number & vbCrLf & _
"Description: " & Err.Description, vbCritical,
"Error!"
End Select
Resume cmdStaffingLevels_Click_Exit
End Sub

How the Error Handler Works

The code contains two bookmarks. One marks the start of my Error Handler, the other the start of the

Exit Routine. Some developers simply name their bookmarks “Error Handler” and “Exit Routine” but as

you can see (Listing 5) I prefer to include the name of the macro with the suffix “_Err” or “_Exit” as

appropriate.

NOTE: A bookmark in code is a word followed by a colon (:). The bookmark is not an executable code

statement. It merely marks a specific place in the macro. Usual naming rules apply. When you type

the name of your bookmark and enter the colon the text becomes a bookmark and moves to the left

margin automatically.

The instruction indicating what to do in the case of an error should always be the first line of the

macro. It takes the form of a GoTo statement to send the code engine to the error handler bookmark

and instructs it to proceed from there. The error handler should be located at the end of your macro.

I use a Case Statement to deal with any known errors that my testing has highlighted and that I have

not been able to prevent within the macro itself. Sometimes you might need to include more code

here to deal with a particular circumstance but often all you can do is show an explanatory message

as in these examples (Fig. 6).


Fig. 6 The Error handler displays explanatory messages.

Each Case refers to a known error by its number. If you discover any further possible errors, you can

return and simply add additional Cases to the Case Statement. The Case Else part of the Case

Statement, meaning “for anything else…”, is used to deal with any error that you had not anticipated.

It makes use of the Error Number and Error Description that are available to you when an error occurs

(Fig. 7). If the user makes a note of this information, or if you have included an error log in your

database, it will help you diagnose and deal with the problem.

Fig. 7 A general error message.

When you handle an error yourself Access will not ever display the standard error message so there is

no danger that the user will find themselves in the Visual Basic Editor. Written properly, it also

ensures that the macro is safely terminated. This is very important because when code is in break

mode the user is often unable to work in the host program.

Using an Exit Routine

When adding an error handler to your code you must make sure that, before the error handler’s

bookmark, you add the statement:

Exit Sub
This works like End Sub. Its purpose is to make sure that if an error has not occurred, the macro

terminates at this point and does not continue into the error handler. This alone satisfies some

developers, but I prefer to include it in an Exit Routine where any additional “tidying-up” tasks can be

included.

The last statement of the error handler e.g. Resume <Macroname>_Exit tells the code engine that,

after carrying out the necessary tasks in the Error Handler, it must move up to the Exit

Routine bookmark and continue from there.

The “tidying-up” tasks could include several things, depending on what your macro was doing. Here

are some examples:

 Any object variables such as a database or recordset that were declared using the Set keyword
will need to be set to Nothing to release them from memory, for example:
Dim db As DAO.Database
Dim rst As DAO.Recordset
Object variables need to be “unset” using statements like:
Set db = Nothing
Set rst = Nothing
 If you have disabled user confirmation messages for a specific action such as deleting a record or
otherwise manipulating a recordset you should reinstate them. The command…
DoCmd.SetWarnings False
is cancelled by the statement…
DoCmd.SetWarnings True
 It is particularly important to remember to switch on screen updating if at any point you turned it
off for speed or to prevent the user seeing the macro’s workings on screen by setting:
DoCmd.Echo False
Switch screen updating on again using…
DoCmd.Echo True
If you fail to do this and your macro crashes with screen updating switched off your user will be
left with a blank screen so this one is really important!

Of course, you aren’t going to know at which point your macro failed so would it cause a problem if

your Exit Routine tries to “unset” an object variable that had not yet been “set”? Switching-on screen

updating that was already on, or reinstating warnings that had not been disabled (in fact anything that

is set/unset using True/False) will not cause a problem but trying to “unset” an object variable that

had not yet been “set” probably will.

For this reason, it is safest to start your Exit Routine with the error command On Error Resume Next.

A comprehensive Exit Routine for Access would like something like this (Listing 6):

Listing 6:
' Your macro code finishes here
cmdExportData_Click_Exit:
On Error Resume Next
Set rst = Nothing
Set db = Nothing
DoCmd.SetWarnings = True
DoCmd.Echo True
Exit Sub
cmdExportData_Click_Err:
' Your Error Handler begins here

Summary
Try as we might, we can’t anticipate every circumstance in which our code will be used, nor can we

foresee everything a user might do no matter how irrational or unexpected. Errors happen. Your task

as a developer is to make sure that the unexpected doesn’t turn into a disaster. Follow these rules to

make your code safe and reliable:

 Add an Error Handler to every macro.

 Only use On Error Resume Next if you are certain that it is safe to ignore an error.

 Use On Error GoTo 0 to cancel On Error Resume Next when used in a larger macro.

 Test your code and use a Case Statement to help you handle different kinds of errors.

 Make use of an Exit Routine to safely clean-up after an error.

 Password Protect your code to prevent problems in the event of unhandled errors.

Download PDF
You can download a printable PDF of this tutorial. Right-click the icon or text link below and

choose Save target as... and follow the instructions, or simply click the link to read the file online and

save via your browser.

HandlingErrorsInYourAccessDatabase1_Errors101.pdf [274KB]

andling Errors in Your Access Database #2


Add an Error Log to Your Database
Published: 25 September 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

Despite all our best efforts there is always the possibility of an error happening when our code is run.

A competent developer will always ensure that their code is properly protected by including an Error

Handler in each macro so that, in the event of the code crashing, the Error Handler takes control and

deals with the situation safely, with the minimum of disruption to the user.

In the first part of this tutorial Handling Errors in Your Access Database #1 Errors 101 I showed how I

deal with errors in my databases. With a known error (i.e. one that is anticipated but not preventable)

my error handler displays a tailored message to the user explaining what has happened. When the

error is not anticipated the error handler displays a general error message showing the Error

Number and Error Description (Fig. 1) both of which are available through VBA. Where possible, the

Error Handler will also attempt to rescue the situation to avoid damage to the database or its data.

Fig. 1 A general error message.

Hopefully, should an error occur, whether anticipated or not, the user will note the details and report it

so that action can be taken to rectify the problem. Unfortunately, this seldom happens. Usually there

is a message like “the database keeps crashing” or “I keep getting an error message”. More savvy

users will note the details or even take a screenshot of the error message, which is very helpful, but

diagnosing the problem will often require more information.

Additional information would be helpful, such as: Does this only happen to a particular user? Has it

happened before and if so how often? Does it always happen on the same computer? What was the

user doing at the time? To answer some of these questions and to make the reporting of errors easier,

I always include an Error Log in my databases.


I have even discovered that users have failed to report an error, later saying something like “Oh yes,

it does that…” as if they expected things to go wrong occasionally when, had I known about the

problem, I could have fixed it.

What the Error Log Does


The Error Log has three main components:

 A Table in which to record details of an error.

 A Macro to gather information about the error and write it into the table.

 A Call in each error handler to activate the macro when an error occurs.

In addition to installing an error log in my databases I also incorporate a simple tool by which a copy

of the log can be exported to, for example, Excel or can be sent to me or some other nominated

person by email.

I create error handlers in my usual way, with the addition of a code statement that calls (activates)

the error reporting macro, at the same time passing to it any relevant information such as the name

of the macro that failed and, if appropriate, the form in which it was located. Other important details

such as the Error number, Error Description, Time and Date, User Name etc. can be collected from the

system.

Having collected the required information, the error reporting macro writes it into the table. Instead of

relying on the users to remember the details, I can examine the error log and get the information

from there.

NOTE: Don’t rely entirely on the Error Log to help you diagnose a problem. A user might have a

valuable comment or observation that couldn’t be picked up by the system, so it’s important to listen

to what they have to say!

There are four steps to build the Error Log and its reporting system.

Step 1: Build the Error Log Table


Create a new table and add as many fields as you think you will need. I usually use those shown

below (Table 1, Fig. 2). You may wish to add more information, or collect less. If the database

requires its users to Log-on I will add that information too if I can. I usually name my

table tblErrorLog.

Table 1: Fields in the Error Log Table


Field Name Data Type

ErrorLogID AutoNumber (Primary Key)

DateTime Date/Time

UserName Short Text

ComputerName Short Text

MacroName ShortText

ErrorNumber Number (Long Integer)

ErrorDescription Short Text

Fig. 2 The Design View of the Error Log table.

Step 2: Write the Error Reporting Macro


The error reporting macro takes the form of a procedure that requires a single parameter, the name of

the macro in which the error occurred. You will see how that information is supplied in the next

section.

If you don’t already have something suitable you will have to create a standard module in which to

place the macro. I am offering two options for the code, one using ADO to write to the Error Log table,
the other using SQL. I have no particular preference. Here’s the ADO version (Listing 1):
Listing 1:

Sub ReportError(MacroName As String)


' Error handling and logging routine
Dim cnn As New ADODB.Connection
Dim rst As ADODB.Recordset
MsgBox "ERROR!" & vbCrLf _
& vbCrLf & "An unexpected error has occurred." _
& vbCrLf & "Details of the error have been logged. " _
& "If you wish to report the error please quote the following
information:" _
& vbCrLf & "Procedure: " & MacroName _
& vbCrLf & "Error Number: " & Err.Number _
& vbCrLf & "Description: " & Err.Description _
, vbCritical, "ERROR!"
Set cnn = CurrentProject.Connection
Set rst = New ADODB.Recordset
rst.Open "SELECT * FROM tblErrorLog", cnn, adOpenKeyset,
adLockOptimistic
With rst
.AddNew
![DateTime] = Now
![UserName] = Environ("USERNAME")
![ComputerName] = Environ("COMPUTERNAME")
![MacroName] = MacroName
![ErrorNumber] = Err.Number
![ErrorDescription] = Err.Description
.Update
.Close
End With
Set rst = Nothing
Set cnn = Nothing
End Sub
How the ADO Code Works

When using ADO (ActiveX Data Objects) code, depending which version of Access you are using, you

might need to set a reference to the ActiveX object library. If you aren’t sure whether or not you need

to do this try compiling the code (Debug > Compile…). If the compiler displays an error on the

ADODB variable declaration then you need to set the reference and compile again.

TIP: To set a reference to ADO, in the Visual Basic Editor go to Tools > References. In the

References dialog find and check Microsoft ActiveX Data Objects 2.8 Library then click OK.

Between the parentheses that follow the macro’s name (I’ve named the macro ReportError) is the

name and data type of the required parameter which I’ve named MacroName. This information is

supplied when the macro is “called” by the error handler.

This example uses ADO code to add a new record to the tblErrorLog table. Alternatively, I could have

constructed a SQL statement and used SQL to add the record. SQL is often faster than ADO but when

dealing with a single record it makes no difference.

The first two statements declare the variables that represent the ADO connection and the ADO

recordset that will be opened to receive the new data. These are followed by the standard error

message I show the user. You can add whatever wording you like here.

Two statements then add values to the previously declared variables, citing the current database and

the recordset which opens the Error Log table into memory.

The With Statement contains a series of commands which add a new record to the table, populate its

various fields, then save the record and close the recordset. A final pair or commands clear the two
object variables.

The SQL method has the advantage that it does not require a reference to ADO. Otherwise both

methods work equally well. Here’s how I do it using SQL (Listing 2):

Listing 2:

Sub ReportError(MacroName As String)


' Error handling and logging routine
Dim strSQL As String
MsgBox "ERROR!" & vbCrLf _
& vbCrLf & "An unexpected error has occurred." _
& vbCrLf & "Details of the error have been logged. " _
& "If you wish to report the error please quote the following
information:" _
& vbCrLf & "Procedure: " & MacroName _
& vbCrLf & "Error Number: " & Err.Number _
& vbCrLf & "Description: " & Err.Description _
, vbCritical, "ERROR!"
strSQL = "INSERT INTO tblErrorLog ([DateTime], [UserName],
[ComputerName], " & _
"[MacroName], [ErrorNumber], [ErrorDescription]) VALUES
(" & _
Chr(35) & Format(Now, "MM/DD/YYYY hh:nn") & Chr(35) & ",
" & _
Chr(34) & Environ("USERNAME") & Chr(34) & ", " & _
Chr(34) & Environ("COMPUTERNAME") & Chr(34) & ", " & _
Chr(34) & MacroName & Chr(34) & ", " & _
Err.Number & ", " & _
Chr(34) & Err.Description & Chr(34) & ");"
DoCmd.SetWarnings False
DoCmd.RunSQL strSQL
DoCmd.SetWarnings True
End Sub

How the SQL Code Works

My code creates a SQL statement and stores it in a variable before executing it so the code starts with

a variable declaration for the string that will represent the SQL. The usual message box follows, then

the building of an Insert Into SQL statement. Access creates a SLQ statement like this when you build

an Append Query in the Access Query Builder.

SQL requires the use of Data Qualifiers when a SQL statement refers to data values. A text value must

be enclosed in quotes (‘ or “), a date/time value in hash marks (#), whilst numerical values don’t

require anything. Since I am writing my SQL in VBA (which also requires the use of data qualifiers) the

SQL statement, itself being a text string, needs to be enclosed in quotes too. Trying to put quotes

inside quotes can lead to problems so to avoid this I use the Chr() function to generate the characters
I need. A quote mark is created by Chr(34) whilst Chr(35) generates a hash mark.
Having created a suitable SQL statement the code then uses the DoCmd.RunSQL command to execute

the SQL. Since all “action” queries cause Access to prompt the user for permission, I have enclosed

the DoCmd.RunSQL command in a pair of DoCmd.SetWarnings commands to deactivate then reinstate

the prompts. We don’t need to ask the user’s permission to report the error, nor do we want them to

say “No” if asked.

Step 3: Add a Call Statement to Your Error Handlers


One of the pieces of information that the error reporting macro notes is the name of the macro in

which the error occurred. This is supplied as a parameter when the error reporting macro is “called”.

Although not absolutely necessary I use the keyword Call when calling one macro from another since

it makes it clear to anyone reading the code exactly what the code is doing e.g.:

Call ReportError(<Macro Name Goes Here Enclosed in Quotes>)

The parameter takes the form of a text string so it must be enclosed in quotes. It can be anything you

like as long as it accurately defines the macro you want to report. If the reported macro is a Private

Sub on a form, such as a button click, I also add the name of the form to the parameter since

procedures on different forms might have the same name. For example a macro

named cmdWageCostSummary_Click on a form named frmHome would be reported with:

Call ReportError("frmHome_cmdWageCostSummary_Click")

In the first part of this tutorial I gave an example of errors that could arise from a macro that opened

a stored query, together with a sample of code showing how I create an error handler that dealt with

the various types of error that could occur. Here’s how I would amend that error handler when using

the Error Log (Listing 3):

Listing 3:

Private Sub cmdWageCostSummary_Click()


On Error GoTo cmdWageCostSummary_Click_Err
DoCmd.OpenQuery "qryWageCostSummary"
cmdWageCostSummary_Click_Exit:
Exit Sub
cmdWageCostSummary_Click_Err:
Call ReportError("frmHome_cmdWageCostSummary_Click")
Resume cmdWageCostSummary_Click_Exit
End Sub

Note that I have not included a Case Statement for dealing individually with different kinds of error.

You might choose to do this or not, depending on the circumstances. You may decide that some types

of error simply need to be explained to the user by means of a message, whilst others need to be

logged as well.

When called the Error Log macro writes the information it has gathered into the Error Log table

(Fig. 3).

Fig. 3 An Error recorded on the Error Log table.

Step 4: Viewing the Error Log


I like to offer a few choices for gaining access to the Error Log and usually present these on

a Database Maintenance screen along with other tools useful to the users. In this example I give three

different ways to view or share the log (Fig. 4).

Fig. 4 Offering the facility to view the Error Log.


View the Error Log Table

This option example simply opens the table. I don’t normally allow users direct access to tables so

when opening the Error Log table I also make it read-only (Listing 4).

Listing 4:

Private Sub cmdViewErrorLog_Click()


On Error Resume Next
' Open Error Log table read-only
DoCmd.OpenTable "tblErrorLog", , acReadOnly
End Sub

Export the Error Log

Access VBA allows data to be exported from a table or query in various different formats. I prefer the

flexibility of a .csv file which can easily be read by Excel and other programs.

My code (Listing 5) constructs a filename which it stores temporarily in a string variable. The filename

includes a timestamp accurate to the second. This avoids an error in the event of the user running the

macro a second time when, otherwise, it would attempt to create multiple files with the same name.

The variable string also includes the full path to the user’s desktop, obtained with the help of

the Environ() function. You could export elsewhere, for example to the user’s Documents folder, or

even ask the user to choose a location. I decided to keep it simple.

Private Sub cmdExportErrorLog_Click()


On Error GoTo cmdExportErrorLog_Click_Err
' Export Error Log as *.csv file to user's desktop
Dim strFilename As String
strFilename = Environ("UserProfile") & "\Desktop\" & _
"ErrorLog_" & Format(Now, "yymmddhhnnss") & ".csv"
DoCmd.TransferText acExportDelim, , "tblErrorLog", strFilename,
True
MsgBox "The Error log file has been exported to:" & vbCrLf & _
strFilename, vbInformation, "Export complete"
cmdExportErrorLog_Click_Exit:
Exit Sub
cmdExportErrorLog_Click_Err:
Call ReportError("frmDatabaseMaintenance_cmdExportErrorLog_Click")
Resume cmdExportErrorLog_Click_Exit
End Sub

The string variable holding the export filename and path is used both to perform the export and to

subsequently notify the user via a message box (Fig. 5). I have use

the DoCmd.TransferText command to export the entire table. You could add the facility to export a

specific date range, and create a query to export instead of the whole table.

Fig. 5 A message confirms that the Log has been exported.

Email the Error Log

Most useful for the developer is the ability for the user to email the error log to them. My code

(Listing 6) uses the DoCmd.SendObject command which employs the user’s default email client and

doesn’t require any additional programming (e.g. for Microsoft Outlook). In this example the code

sends the entire table but, as I suggested earlier, you could send a query specifying a chosen date

range.

The DoCmd.SendObject command includes several options. I have hard-coded a recipient email

address. If this argument is left blank Access prompts the user for a recipient when the macro is run.
The code also allows for Cc and Bcc to be specified. The parameter True in this example gives the user

the opportunity to edit the email message before sending by opening it in the default email program

(False would send the message straight away).

NOTE: If you offer the user the opportunity to edit the message by setting the EditMessage parameter

to True, an error would occur if the user subsequently cancelled the message. You can see in my

example (Listing 6) that this eventuality is accounted for in the macro’s Error Handler.

Listing 6:

Private Sub cmdEmailErrorLog_Click()


On Error GoTo cmdEmailErrorLog_Click_Err
' Email a copy of the Error Log file
DoCmd.SendObject acSendTable, "tblErrorLog", acFormatXLS, & _
"martin@fontstuff.com", , , "Acme Database Error Log", & _
"See Error Log attached.", True
cmdEmailErrorLog_Click_Exit:
Exit Sub
cmdEmailErrorLog_Click_Err:
Select Case Err.Number
Case 2501
MsgBox "You cancelled the email message.", vbInformation,
"Cancelled"
Case Else

Call ReportError("frmDatabaseMaintenance_cmdEmailErrorLog_Clic
k")
End Select
Resume cmdEmailErrorLog_Click_Exit
End Sub

Appendix: The Environ() Function


You will have seen that, on several occasions, I have made use of the VBA Environ() function to

gather information about the user and the environment in which they were working. This function

requires a single argument which can be a text string or a number. I recommend that you use the

Text string rather than its index number because anyone later reading your code will understand how

it is being used. Some common examples are shown here (Table 2):

Table 2: Examples of the Environ() function.

Environ(Argument) Returns

The current user’s Windows login name. e.g.


Environ(“USERNAME”)
JohnDoe

The path to the current user’s User folder e.g.


Environ(“USERPROFILE”)
C:\Users\JohnDoe
The name of the host computer e.g.
Environ(“COMPUTERNAME”)
ADMIN-PC4

The path to the current user’s Temp folder e.g.


Environ(“TEMP”)
C:\Users\JohnDoe\AppData\Local\Temp

In addition to saving information into the Error Log you can use the Environ() function to specify the

path to the user’s Desktop or Documents folders, for example:

Environ("UserProfile") & "\Desktop\"

Would construct a path like:

C:\Users\JohnDoe\Desktop\

To allow you to perhaps save a file to the current user’s, desktop as I did in an earlier example

(Listing 5).

The Environ() function is available to all your VBA programs. Use this simple macro (Listing 7, Fig. 6)

in any of your programs to create a list of its various arguments. The macro uses a loop to send a list

of examples of the 37 available arguments to the Visual Basic Editor’s Immediate Window (open it

via View>Immediate Window or Keys: [Ctrl]+G).

Listing 7:

Sub ListEnviron()
Dim i As Integer
For i = 1 To 37
Debug.Print Environ(i)
Next i
End Sub
Fig. 6 Environ() arguments sent to the Immediate Window.

Alternatively, use this macro (Listing 8, Fig. 7) to write the list to an empty worksheet in Excel:

Listing 8:

Sub ListEnviron()
Dim i As Integer
For i = 1 To 37
Range("A" & i).Value = Environ(i)
Next i
End Sub

Fig. 7 Environ() arguments written to an Excel worksheet.

Download PDF
You can download a printable PDF of this tutorial. Right-click the icon or text link below and

choose Save target as... and follow the instructions, or simply click the link to read the file online and
save via your browser.
HandlingErrorsInYourAccessDatabase2_ErrorLog.pdf [321KB]

Build a Back-End Link Checker for Your


Access Database
Published: 30 September 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

Working with Split Databases


When I started building databases I soon discovered the benefits of splitting a database. A split

database consists of two files. One comprises the “front-end” containing forms, queries and reports,

and all necessary code or stored macros. The other file comprises the “back-end” and contains only

tables. Essentially, the front-end is the user interface and the back-end is the data. The back-end data

appears in the front-end in the form of linked tables (Fig. 1).

Fig. 1 Database objects before (left) and after (right) splitting the database.

Splitting an Existing Database

When building a new database, you could of course, start with two files and link each table as you

build it. But it is so easy to split a database that I usually start construction as a single file then split

the database after I have built all the tables. To split a database, click the Access Database button in
the Move Data group of the Database Tools tab of the ribbon (Fig. 2).
Fig. 2 Using the Database Splitter.

This opens the Database Splitter which takes care of everything. After asking you for a name and

location for the back-end file it creates the back-end file, moves the tables into it, and creates links to

them in what is now the front-end file. The front-end database retains its filename (subsequently

changing it would make no difference). Unless you specify otherwise the back-end database is given

the same name as the front-end file with the added suffix “_be”.

The Benefits of a Split Database

The benefits of a split database are many. Here are just a few of them:

 Multiple Users Data can be easily supplied to multiple users, whether working locally or
remotely, lessening the risk of conflicts and offering the ability to give different users their own
tailored copies of the front-end.

 Improved Performance Only the data needs to be sent across the network. A user opens their
copy of the front-end at the beginning of a session and subsequently only data needs to travel
across the network.

 Enhanced Security Only authorised users need to be given access to the location of the back-
end file preventing unauthorized access to the data, even if an unauthorised person gains access
to a copy of the front-end file.

 Improved Reliability If a user encounters a problem causing the database to crash any
damage will usually be limited to that user’s front-end file, the back-end database being less
likely to become corrupted.
 Data Safety A user cannot accidentally (or deliberately) delete a table. Deleting a table from a
front-end file merely breaks the link and does not delete the table from the back-end database.

Splitting a database has many more advantages but the main benefit for the developer is that updates

can be made without any disruption to the user. Often, updates can be made remotely without

requiring a site-visit. The developer can simply send a copy of the updated front-end file which can

contain any new or changed features or bug fixes. I have even included update macros that remotely

make changes to the back-end file such as building new tables and adding new fields to existing ones.

All the user must do is discard the old front-end file and, after restoring the links to the new one,

continue working as before. If the location of the back-end database file changes, if its filename is

changed, or if the path from the front-end to the back-end changes, the front-end file will not be able

to find its back-end tables and the linking process will have to be repeated.

Hopefully, this should not happen very often. It is most likely that the users will have to perform the

linking process only when you, the developer, provide them with an updated copy of the front-end.

Like me, you will probably be working on a development copy of the database on your own computer.

When you supply the users with copies of the new, updated front-end file its links will refer to tables in

your development version of the back-end, and will need to be updated to refer to the tables in the

working copy of the back-end database.

Manually Restoring Broken Links

Users will not notice that links have been broken until they try to access data. They might run query

or report, or open a form based on a particular table. When that object attempts to access its data

Access will inform them that the data source can’t be found (Fig. 3).

Fig. 3 Access displays a message when the links are broken.

Manually restoring the links is a fairly simple process. Open the Linked Table Manager by clicking its

button (Fig. 4) on the Import & Link group of the External Data tab of the ribbon.
Fig. 4 The Linked Table Manager button.

In Access 2016 the Linked Table Manager dialog is a little different from earlier versions and has

additional features, but the linking process is essentially the same in all current versions. To re-link all

the back-end tables start by clicking Select All, which puts a check mark against all the listed tables

(you could do this manually if you need to link only to certain ones) then click Relink (Fig. 5). This

opens a file chooser asking you to navigate to and select the back-end database file. On

clicking OK you are asked to confirm the operation. Doing so then requires you to confirm each table.

As each one is linked “Success” appears in the dialog’s Refresh Status column. When all the links have

been refreshed you can close the dialog.

Fig. 5 Re-linking tables in Access 2016.


In Access 2013 and earlier versions the process is a little simpler. Open the Linked Table Manager

(Fig. 6) and click Select All (or choose individual tables to be linked) then click OK to open the file

chooser. Navigate to and select the back-end file and click OK to refresh the links. When all the links

have been refreshed Access displays a confirmation message.

Fig. 6 Re-linking tables an Access 2013 and earlier.

Why Build a Custom Link Checker?


For an experienced user manually refreshing the links to the back-end database is an easy enough

process but I prefer not to give users this responsibility and simplify the process as much as possible.

As with other Microsoft Office applications, the ribbon can be hidden or customized and it may be that

your database does not give the users access to the necessary tools to re-link the tables themselves.

Also, a user will not realise that there is a link problem until they attempt to access the data.

For these reasons I build a custom tool into my split databases that detects a broken back-end link as

soon as the database opens and offers to fix the problem with only minimum effort on the part of the

user.

How the Link Checker Works

I create a table in the back-end database that exists solely for the checking of links. Like all the other

tables, this table is linked to the front-end database. Most databases include some form of “Home” or
“Welcome” screen, in the form of an unbound form or dialog, that opens automatically when the

database opens (if your database doesn’t have one you will have to create one). When this screen
opens a macro runs automatically that attempts to open into memory the table I mentioned earlier. If

this operation fails it means that the back-end file is not in the expected location. This triggers another

macro which asks the user for the current location of the back-end database. It then relinks all the

tables.

The process performs the check seamlessly every time a front-end file is opened and, if the links need

to be refreshed, the only input needed from the user is to locate and select the back-end file.

Step 1: Build the Link Test Table


Build the Table

Create a new table in your back-end database. Name it tblLinkTest_DO_NOT_DELETE. You can, of

course, name it anything you like but I highly recommend the DO_NOT_DELETE bit because I have

found that some people are in the habit of removing things when they don’t know what they are or

don’t think they have a use for! Add the following fields:

Table 1: Fields for the Link Test table.

Field Name Data Type

LinkTestID AutoNumber (Primary Key)

LinkTest Short Text

I usually create a single record by entering some text such as “Test” in the LinkText field so that the

table isn’t empty.

Link the Table to the Front End

If you are in process of building a new database, you can link this table along with all the others when

you split the database as I described earlier. If you are adding it to an existing split database, you will

have to manually link this table to your front-end database. To do this open the front-end database

and, in Access 2016, click the New Data Source button in the Import & Link group of the External

Data tab of the ribbon (Fig. 7) then choose From Database then Access.
Fig. 7 Choose to link to an Access data source (Access 2016).

In Access 2013 and earlier click the Access button in the Import & Link group of the External

Data tab of the ribbon (Fig. 8).

Fig. 8 Choose to link to Access (Access 2013).

This opens the Get External Data dialog (the same in all versions). Browse to the location of the

back-end database and select it then click Open to return to the dialog. Select the option to Link to
the data source by creating a linked table then click OK. In the Link Tables dialog select your Link

Test table (Fig. 9) and click OK. A link to the table will now appear in the front-end database.

Fig 9 Create a link to the Link Test table.

Step 2: Write the VBA Code


The tool requires two macros, a function and an event procedure that runs automatically when a

specific form is opened. One macro tests the link to the back-end table. Another macro is used to

restore the links, if necessary, to the back-end tables. The function is used to generate a file-chooser

to allow the user to locate and select the back-end database (unlike Excel, Access does not have this

feature built into its VBA object model).

After each stage of code writing remember to compile (Debug>Compile…) and test your code to

check for any errors before implementing it on a live database.

If you don’t already have a suitable module in which to write your code, create a new standard

module.

The TestLinks Macro

The following code (Listing 1) tests the links to the back-end database by attempting to open the Link

Test table into memory.

The code makes use of ADO (ActiveX Data Objects) programming. Depending on which version of

Access you are using, you might need to set a reference to the ActiveX object library. If you aren’t

sure whether or not you need to do this try compiling the code (Debug > Compile…). If the compiler

displays an error on the ADODB variable declaration then you need to set the reference and compile

again.

TIP: To set a reference to ADO, in the Visual Basic Editor go to Tools > References. In

the References dialog find and check Microsoft ActiveX Data Objects 2.8 Library then click OK.

Listing 1:

Sub TestLinks()
On Error GoTo TestLink_Err
Dim cnn As New ADODB.Connection
Dim rst As New ADODB.Recordset
Set cnn = CurrentProject.Connection
rst.Open "SELECT * FROM tblLinkTest_DO_NOT_DELETE", cnn,
adOpenStatic, adLockReadOnly
TestLink_Exit:
On Error Resume Next
rst.Close
cnn.Close
Set rst = Nothing
Set rst = Nothing
Exit Sub
TestLink_Err:
Select Case Err.Number
Case -2147467259
MsgBox "The links to the back-end database are broken." &
vbCrLf & _
"Please specify the location of the back-end database
so that " & _
"the links can be restored.", vbExclamation, "Action
Required"
Call RestoreLinks
Case Else
MsgBox "An unexpected error has occurred. " & _
vbCrLf & "Error Number: " & Err.Number & _
vbCrLf & "Description: " & Err.Description, _
vbCritical, "ERROR!"
End Select
Resume TestLink_Exit
End Sub

As you can see, the error handler is an important part of this macro. If the link to the back-end table

is broken the code fails to open the Link Test table and a code error occurs. In handling the error, the
code displays a message to the user informing them that the links to the back-end are broken

(Fig. 10). When the user acknowledges the message the error handler calls the RestoreLinks macro

before safely terminating the TestLinks macro.

Fig. 10 A message informing the user the links are broken.


The RestoreLinks Macro

In this macro’s code (Listing 2) I have used DAO (Data Access Objects) programming because the

coding is simpler than in ADO.

Listing 2:

Sub RestoreLinks()
On Error GoTo RestoreLinks_Err
Dim strInputFileName As String
Dim db As DAO.Database
Dim tdf As DAO.TableDef
strInputFileName = GetOpenFileName
If Len(strInputFileName) = 0 Then
MsgBox "You did not choose a back-end database." & vbCrLf & _
"Links will not be restored.", vbExclamation, "Connection
Failed"
Exit Sub
End If
Set db = CurrentDb
DoCmd.Hourglass True
For Each tdf In db.TableDefs
If Left(tdf.Name, 4) <> "MSys" Then
If Len(tdf.Connect) > 0 Then
tdf.Connect = ";DATABASE=" & strInputFileName
tdf.RefreshLink
End If
End If
Next tdf
MsgBox "Links to the back-end database have been restored
successfully.", vbInformation, "Success!"
RestoreLinks_Exit:
On Error Resume Next
Set db = Nothing
Set tdf = Nothing
DoCmd.Hourglass False
Exit Sub
RestoreLinks_Err:
MsgBox "An unexpected error has occurred. " & _
vbCrLf & "Error Number: " & Err.Number & _
vbCrLf & "Description: " & Err.Description, _
vbCritical, "ERROR!"
Resume RestoreLinks_Exit
End Sub

After the necessary variable declarations, the macro asks the user for the location and name of the

back-end database by means of the GetOpenFileName macro which is described in the next section. If

the user doesn’t choose a file (the test being that the length of the string variable it would have been

written to is zero) a message is displayed (Fig. 11) and the macro terminated.

Fig. 11 The user did not choose a back-end file.

If a file is nominated, the macro then uses a loop to work through all of the tables in the specified

database ignoring the hidden system tables (those whose names start with MSys) and creating a link

to each one. When all the tables have been linked the macro displays a confirmation message

(Fig. 12) and safely terminates.

Fig. 12 The links have been successfully restored.


The GetOpenFileName Function

This function (Listing 3) displays a simple file chooser (Fig. 13) that returns as a string the name and

path of the chosen file. The RestoreLinks macro uses this function to get the name and location of the

back-end database file from the user. If the user does not choose a file the string returned is empty

(has a length of zero) and the RestoreLinks macro is cancelled.

Function GetOpenFileName() As String


Dim FileDialog As Object
Dim varFile As Variant
Set FileDialog = Application.FileDialog(3)
With FileDialog
.AllowMultiSelect = False
.Title = "Please select the database back-end file"
.Filters.Clear
.Filters.Add "Access Databases", "*.accdb"
If .Show = True Then
For Each varFile In .SelectedItems
GetOpenFileName = varFile
Next
End If
End With
End Function
Fig. 13 GetOpenFilename function displays a file chooser.

The Form_Open Macro

If you don’t already have a form that opens automatically when the database opens you need to build

one. By attaching a macro to the Form_Open event of this form the process of checking the links will

be the first thing database does when it opens. The form can have any other purpose you want. I

usually make it into a “Welcome” screen or a “Home” screen in the form of a switchboard.

It is VERY IMPORTANT, however, that this form is not bound to an underlying table in the back-end
database. If it is then it will attempt to connect to this table before running our Form_Open macro

and, if it can’t find it because the links are broken, it will display its own error message (Fig. 14) and

our custom link checker will not run.

Fig. 14 The Home screen can’t find its bound table.


To ensure the form opens automatically when the database opens go to File > Options > Current

Database and set the Display Form option to the name of your chosen form (Fig. 15).

Fig. 15 Set the Home screen to open automatically.

The next task is to create the macro that causes the Custom Link Checker to run automatically when

the form opens. The code must be attached to the forms Form_Open event procedure (an Event

Procedure is a macro that runs automatically when a particular event happens).

With the form in Design View select the form itself by clicking the small rectangle in the upper left

corner of the design window where the rulers meet (Fig. 16) so that a black dot appears.

Fig. 16 Select the form itself.

Go to the form’s Property Sheet (open it with [Alt]+[Enter] if necessary) and on its Event tab click

in the On Open text box. Click the build button ([…]). When the Choose Builder dialog opens

select Code Builder and click OK. This takes you to the Visual Basic Editor with an

empty Form_Open event procedure ready to receive its code. Enter the code shown here (Listing 4):

Listing 4:

Private Sub Form_Open(Cancel As Integer)


On Error GoTo Form_Open_Err
' Check links to back end
Call TestLinks
Form_Open_Exit:
Exit Sub
Form_Open_Err:
MsgBox "An unexpected error has occurred. " & _
vbCrLf & "Error Number: " & Err.Number & _
vbCrLf & "Description: " & Err.Description, _
vbCritical, "ERROR!"
Resume Form_Open_Exit
End Sub

As you can see, the code that initiates the whole process is a simple “call” to the TestLinks macro.

NOTE: The first time the front-end database file is opened from a new location the usual security

message will be displayed. The user must click the Enable Content button (Fig. 17) to allow any code

within the database to be executed, including the Link Check macro. The individual user's security

setting will dictate whether this message appears every time the database is opened or the first time

only. Once enabled, the Link Checker will proceed as normal.

Fig. 17 Click Enable Content to allow the macros to run.

Summary
I have used this system on most of the databases I have built and found it to be reliable and a great

time-saver, removing the responsibility from the user and making updates simple and quick to

implement. It is not intended to replace the built-in Linked Table manager in its entirety but instead to

simplify the process of installing updates and managing the installation of the database on new

machines, for new users, or when file paths change. If linked data comes from different sources,

additional work will be needed to re-connect all the links but there is scope within the code to handle

most such tasks.

Download Example Database


You can download a sample database containing the completed exercise used in this tutorial. You can

also download a printable PDF of this tutorial.


To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

acctut24_BackEndLinkChecker.zip [150KB]

acctut24_BackEndLinkChecker.pdf [460KB]

Access Form Design


Build Better Access Forms:
Mastering the Combo Box NotInList Event
It's All About Usability
One of the most important tasks for the database designer is to prevent "bad" data getting into the

database. Bad data can happen because the user might not know what they should enter into a field or what

restrictions might apply, or perhaps they just make a typo.

One way to help prevent bad data is to make the job of entering data as easy as possible for the user. Make

it obvious what the user has to do and they don't have to guess and risk getting it wrong. Make it easy to do

the job and the user is less likely to make a mistake and enter something they didn't mean.

In short, the database designer's job is to make their database usable.

The Benefits of Combo Boxes


A favourite tool for helping the user in their task is the combo box. This familiar tool consists of a text box to

which is attached a list of possible entries, and is recognised by the button showing a downward pointing

arrow. They are often referred to as drop-down or pull-down lists but we propellorheads call them combo

boxes (the name indicating that they are a combination of a text box and a list box).

The combo box can be used anywhere you would use a text box where it is possible to provide a list of

acceptable entries. They are most commonly used for text entries but can equally be used for numbers or

dates.
Combo boxes help the user because they can offer a number of possible entries, but their functionality goes

further than this. The database designer can specify whether or not the the user can make an entry that is

not contained in the list. If the Limit To List property of the combo box is set to No the user is permitted to

make any entry they choose (unless the Validation Rule property, or the properties of the underlying field,

impose any additional restriction). But if the Limit To List property is set to Yes the form will not accept

any entry for that field other than those included on the list. This lets you be absolutely certain that nothing

will get entered into the field unless it is an acceptable entry.

Suitable Sources for a Combo Box List


The combo box gets its list from the definition of its Row Source and Row Source Type properties.

The Row Source Type can be Table/Query (the name of a table, a stored query or an SQL statement)

or Value List (a text string listing each item as it should appear in the list).

A Fixed List Built From a Table

Having tried the various options for supplying the Row Source of a combo box I have come to the

conclusion that, in most instances, a table is best. If it is possible that the list might need to change by the

addition or removal of items I usually build a tool to allow the user to do this (I prefer not to allow users

direct access to the tables themselves!). The table can be simple with a single field (more for a multi-

column list) and setting the table as the Row Source just requires the Row Source Type to be set

to Table/Query and the table's name specified as the Row Source...

A Dynamic List Built From a Query

In some instances it might be appropriate to have a dynamic list - one whose contents change to reflect

data already in the database. The list is dynamic because it is created from a query. Each time the form is

opened the query is run and generates the list using the most up-to-date data as its source. The query could

be based on the current table or a different one.


In the same way as you might specify the name of a table as the Row Source you can name a stored

query. This is why, when you click the down-arrow next to the Row Source property text box a list of

available tables and queries is displayed...

Rather than use a stored query, I prefer instead to use an SQL statement. You can type the SQL statement

direct into the Row Source property text box or click the build button ( ) next to it to open the query-

builder and have Access write the SQL for you...

A typical SQL statement for creating a list of unique entries will look something like this:

SELECT DISTINCT tblStaff.[JobTitle]


FROM tblStaff
WHERE tblStaff.[JobTitle] Is Not Null
ORDER BY tblStaff.[JobTitle];

Note that the SQL statement uses SELECT DISTINCT to ensure that only unique values appear in the list

(i.e. that there are no duplicate entries); the WHERE clause specifies Is Not Null so that blank entries in

the source table are excluded (so there is not a blank entry on the list); an ORDER BY clause is included to

sort sort the list in ascending alphabetical order.

Choose a method to suit the individual needs of each combo box. Each method will produce a list but the

way the form behaves when the user makes an entry that doesn't match a list item depends upon you.

Using a Value List

If the Row Source Type is Value List the Row Source specifies the actual list items themselves (separated

by semicolons). This is best used when the list items are unlikely to change, and the list contains just a few

items. I often use a value list for a "Gender" field where there are only two possible entries

(Male and Female) because whilst most people understand that they can only be one or the other (I'm not
getting into discussions about hermaphrodites or transgender issues here!) a surprising number of people

are confused about how they should specify it.

For Gender I usually create a text field and use "M" to denote Male and "F" to denote Female. To make it

crystal clear to the user exactly what the letters mean I use a combo box with a value list row source. The

combo box has 2 columns. The first column is the "bound" column (i.e. the one whose data gets put into the

field) and contains the letters "M" and "F". The second column (just there for information) contains the text

"Male" and "Female".

When specifying the value list row source for a single column simply supply a list of items separated by

semicolons (e.g. Red;Yellow;Green;Blue). If you require a multi-column list you should supply all the items

for each row in turn (e.g. M;Male;F;Female) then use the Column Count, Bound Column and Column

Widths properties to define how the list appears...

These property settings result in a combo box list looking like this...

I could have chosen to hide the first (bound) column by setting its width to zero. This would remove it from

the list but would not affect the entry of its data into the field.

Because the Limit To List property of the combo box is set to Yes the combo box refuses to accept any

entry that isn't on its list (when there is more than one column, the bound column is the one that has to be

matched). The form does not check the user's entry until they attempt to leave the combo box (for example

by clicking on another field, pressing their TAB key, moving to another record, or trying to save the current

record). At this point, if the entry is refused, the NotInList event fires and Access displays its standard

message:
When the user clicks the OK button Access takes them back to the combo box and opens its list so they can

choose an acceptable entry. The only way the user can move on is to either make a choice from the list or

remove their original entry from the combo box (e.g. by pressing their ESCAPE key).

Although the standard message is quite friendly you might like to give the user a bit more help by displaying

a custom message.

Responding to User Input


Regardless of how you build the combo box list, if the Limit To List property of the combo box is set

to No the form will accept whatever the user types into the box. The only restrictions that might apply are

ones you might have created in the Validation Rule property or those inherited from the form's underlying

table (such as data type - the form won't accept text in a date or number field for example).

If you set the Limit To List property of a combo box to Yes any input from the user that does not match an

item on the list prompts the NotInList event to fire. What happens next is up to you. You can leave it at

that. Access will display its standard message (see example above) and the form absolutely refuses to

accept any input into the combo box other than one which matches an item on the list. But you still have a

number of options...

Programming the NotInList Event

With a little VBA code you can take charge of proceedings and decide for yourself how to handle things.

Start by creating a code procedure. Here's a quick way to get started:

1. In form design view open the properties window of the combo box (by right-clicking on it and
choosing Properties).

2. Click the Event tab and double-click the On Not in List property text box. You will see the
text [Event Procedure] appear in the text box.

3. Click the build button ( ) to open the form's code module in the Visual Basic Editor with an empty
code procedure...
The first line of the procedure declares two parameters (shown between the brackets) that you can make

use of in your code:

Private Sub cboGender_NotInList(NewData As String,


Response As Integer)

NewData holds the text that the user entered into the combo box and that was rejected, causing the event

to fire. You can make use of this, for example, to read back the unacceptable entry to the user in a custom

message. Alternatively, you could offer to add it to the list by writing it into the list's source table. This

parameter is treated as a String (a piece of text) regardless of the field's data type.

Response represents an instruction to Access on how to proceed. There are three possible values

represented by constants (all VBA constants can be referred to by name or by their numerical value which is

why this parameter is an Integer): acDataErrContinue (return to the combo box and open the

list), acDataErrDisplay (show the standard message), acDataErrAdded (the item has been added to the

list).

Here are some examples of how you might program the NotInList event procedure...

Refuse the Entry with a Custom Message

The standard message is self-explanatory but you might want to add a personal touch...

Private Sub cboGender_NotInList(NewData As String,


Response As Integer)
MsgBox "Sorry, I can not accept that entry." & vbCrLf & _
"Please choose an item from the list." _
, vbExclamation, "Acme Oil and Gas"
Response = acDataErrContinue
End Sub
This code results in the following message...

When the user clicks the OK button they are returned to the combo box and Access opens the list

automatically so they can see the list of options. The form refuses to accept the entry. Note the use

of acDataErrContinue to instruct Access not to display the standard message, and the use of vbCrLf to

force a line break in the message text.

Refuse the Entry and Include It in the Message

When the NotInList event fires the offending entry is passed to the NewData parameter and is available

for you to use in your code. In this example it is simply read back to the user as part of the message...

Private Sub cboGender_NotInList(NewData As String,


Response As Integer)
MsgBox "You can not enter " & Chr(34) & NewData & Chr(34) & _
" here. Enter M for Male or F for Female." & vbCrLf & _
"If you do not know the person's gender please leave " & _
"the box empty.", vbExclamation, "Acme Oil and Gas"
Response = acDataErrContinue
End Sub

Note that the code makes use of the character code Chr(34) to insert quote marks into the message...
Again, acDataErrContinue is used to return the user to the combo box without displaying the standard

message.

Offer to Accept the Entry By Adding It to the List

It is possible to accept an invalid entry providing that it is added to the combo box list. In many cases it is

inadvisable to allow this because the purpose of having a limited list of entries is to control user choice.

Allowing the user the option to add a new item to the list effectively removes that control (although you

could build a degree of control into the code). But it is sometimes appropriate and at least it makes the user

think about the entry they have made.

I occasionally use this feature at the testing stage of a database when its features are still being explored by

the users and the composition of its lists has not been finalised.

The following example illustrates how this can be done. It is important to note that, if you are going to allow

entries to be added to the Row Source of the combo box, the Row Source should ideally be a table designed

specifically for that purpose, or a query based on that table.

I'll show the code a piece at a time to begin with. You can find the entire procedure at the end of this

section.

On detecting an invalid entry, my code displays a message box asking the user if they want to add the new

item to the list...

intAnswer = MsgBox("The job title " & Chr(34) & NewData & _
Chr(34) & " is not currently listed." & vbCrLf & _
"Would you like to add it to the list now?" _
, vbQuestion + vbYesNo, "Acme Oil and Gas")
This is normal message box code displaying a message with "Yes" and "No" buttons and storing the user's

response in a variable that I have called intAnswer...

An If Statement then handles the user's response. If the user clicks the "Yes" button the code must add

their entry to the Row Source table...

If intAnswer = vbYes Then


strSQL = "INSERT INTO tblJobTitles([JobTitle]) " & _
"VALUES ('" & NewData & "');"
DoCmd.SetWarnings False
DoCmd.RunSQL strSQL
DoCmd.SetWarnings True
MsgBox "The new job title has been added to the list." _
, vbInformation, "Acme Oil and Gas"
Response = acDataErrAdded

I have used an SQL statement to add the new value to the table because the procedure is very simple and is

the same regardless of which version of Access is being used. This one is similar to an Access "Append"

query and since it is an "action" query it can be implemented in VBA using DoCmd.RunSQL. Note that I

have also used DoCmd.SetWarnings to suppress (then enable again) warning messages because

otherwise Access would ask the user's permission to add a record to a table.

I have added a message box at this point to confirm to the user that the new item has been successfully

added to the list...


Finally, I used acDataErrAdded to tell Access that I have added the item to the combo box's Row Source.

When it receives this information it requeries the combo box to refresh the list then compares the entry with

it again. If everything is OK the form accepts the entry and allows the user to move on.

The next time the user opens the combo box list they will see that the new item has been added. In my

example I used an SQL statement (like the one described earlier) based on a purpose-built table for the Row

Source of the combo box. Using an SQL statement rather than the table itself allows me to specify that the

list is sorted in alphabetical order so that the new item appears in the appropriate position in the list. If I

had used the table itself as the Row Source, each new item would be added to the end of the list. Here's the

finished result...

If the user chooses not to add the new item to the list (perhaps they just made a typo) the second part of

the If Statement is implemented...

Else
MsgBox "Please choose a job title from the list." _
, vbInformation, "Acme Oil and Gas"
Response = acDataErrContinue
End If

It displays a custom message reminding the user to choose a valid entry from the list...
Then acDataErrContinue instructs Access to return to the combo box, without accepting the entry, and

open the list...

NOTE: Instead of displaying a custom message I could have used acDataErrDisplay or, since this is the

default value of the Response parameter, left out the Else part of the If Statement altogether and allowed

Access to display its default message.

Here is the full code procedure:

Private Sub cboJobTitle_NotInList(NewData As String,


Response As Integer)
On Error GoTo cboJobTitle_NotInList_Err
Dim intAnswer As Integer
Dim strSQL As String
intAnswer = MsgBox("The job title " & Chr(34) & NewData & _
Chr(34) & " is not currently listed." & vbCrLf & _
"Would you like to add it to the list now?" _
, vbQuestion + vbYesNo, "Acme Oil and Gas")
If intAnswer = vbYes Then
strSQL = "INSERT INTO tblJobTitles([JobTitle]) " & _
"VALUES ('" & NewData & "');"
DoCmd.SetWarnings False
DoCmd.RunSQL strSQL
DoCmd.SetWarnings True
MsgBox "The new job title has been added to the list." _
, vbInformation, "Acme Oil and Gas"
Response = acDataErrAdded
Else
MsgBox "Please choose a job title from the list." _
, vbInformation, "Acme Oil and Gas"
Response = acDataErrContinue
End If
cboJobTitle_NotInList_Exit:
Exit Sub
cboJobTitle_NotInList_Err:
MsgBox Err.Description, vbCritical, "Error"
Resume cboJobTitle_NotInList_Exit
End Sub

Note that I have added a simple error handler to the code. It is always advisable to include error handling in

your Access VBA code, especially when it manipulates a recordset as this does, in case something

unforeseen happens (e.g. the new entry might be of the wrong data type or it might exceed the specified

field size).

You could easily write additional code to examine the entry before adding it to the list to see that it satisfies

any requirements you might have, and thereby exercise some control over what gets added.

Adding an Item to a Value List

In the previous example items are added to the combo box list by adding them to the table on which the list

is based. It is also possible to add an item to a value list. The technique is very simple - no SQL statement is

required so it (and its associated DoCmd statements) can be omitted. Instead a single line of code

something like this can be substituted:

Me.cboJobTitle.RowSource = Me.cboJobTitle.RowSource & ";" & NewData

This line takes the current Row Source value list then adds a semicolon followed by the user's entry. It has

some disadvantages...

The new item appears at the end of the list (I could write some code to sort the list into alphabetical order

each time an item is added but I'm not sure it's worth the bother!).

When the form is closed the new item is lost and will not appear when the form is opened again (I could

write some code to switch the form into design view and make the change, save it, and switch back into

form view... not forgetting to have remembered the user's entries on the form up to that point... but again it

isn't worth the bother!).


If it is a multi-column list you must remember to fill each column so that if several items are added each

appears on a new line.

In short, if you want the user to be able to add new items to the list you're probably going to need a Row

Source in the form of a purpose-built table.

Summary

 When the user makes an entry in a combo box on an Access form, Access knows whether or not that
entry matches an item on the combo box's list.

 To make use of this facility you must set the Limit To List property of the combo box to Yes. This
causes the form to reject any non-matching entry.

 You can customise the form's response to an non-matching entry by writing a VBA code procedure for
the NotInList event of the combo box.

 You can display a custom message and optionally offer to add the new item to the list.

 The form can only accept the non-matching entry if the new item is first added to the list.

 If you want to offer the facility to add items to the combo box list the most suitable Row Source for
the list is a purpose-built table.

Build a Combo Box Date Chooser


Part 1: Design the Form
Why Not Use a Calendar?
People often have a problem entering dates, so the more help you can give your users the better. One

solution is to provide them with a built-in calendar as described in my tutorial: A Pop-up Calendar for your

Access Forms. Whilst a calendar is my preferred option it isn't always suitable. In order for it to work the

users computer has to have a copy of the appropriate ActiveX component. But most often my reason for not

using a calendar is that it doesn't fit in with the design of my form... it doesn't look right. So sometimes I

use an alternative solution, a set of combo boxes.

Making a date by picking day, month and year from separate combo boxes is a familiar method to anyone

who has had to enter a date into an online form on a web page because, until recently, the technology didn't

exist to display an interactive calendar on a web page.


This tutorial shows you how to create a combo box based date chooser on an Access form, how to combine

the chosen day, month and year into a date, and link that date to a field. Importantly, it also shows you how

to make the date chooser "intelligent" so that the user can't make up an impossible date.

The tutorial is in two parts. The first part (this one) deals with building and configuring the combo boxes and

is written in some detail. If you are experienced in building forms you may prefer to skip this section and go

straight to the second part of the tutorial which deals with writing the VBA code that powers the

form. Follow this link to go to Part 2 of the tutorial.

If you want to try out a demo of the completed project before working through the tutorial follow this link to

download a copy of the database file.

Setting-up the Combo Boxes


Add the "Day" Combo Box

Open your form in design view and display the Toolbox (View > Toolbox) if it isn't already on view. Make

sure that the Control Wizards button on the Toolbox is not selected. You do not need the help of the

combo box wizard (if you forget to do this and the wizard starts, just click its Cancel button to close it).

Control Wizards button Combo Box tool

Click the Combo Box tool on the Toolbox then click on an empty space on your form to create an unbound

combo box ("unbound" means that it is not currently linked to a table field).

To the left of the combo box is a label that was created automatically. You don't need it so click the label to

select is and press the Delete key on your keyboard to remove it. Access has assigned the combo box a

name (in this example Combo7) but you will change this to something more meaningful later.

Add the List of Days

Right-click the combo box and choose Properties from the context menu. Click the Data tab of

the Properties Window and change the Row Source Type to Value List. For the Row Source enter the

values 1 to 31 (representing the days of the month) separated by semicolons, like


this: 1;2;3;4;5;6;7;8;9;10;11;12;13;14 etc. Finally, set the Limit To List property to Yes to prevent the

user from entering a number outside the specified range.

Resize the Combo Box

Switch your form into Form View and test your combo box. The numbers you entered for the Row

Source property now make up the combo box's list. If you enter a number not on the list a warning

message will be displayed. The combo box is far wider than it needs to be so switch back to design view and

open the combo box's properties window. Click the Format tab and change the Width property

to 1cm (there's no need to type the "cm") or 0.4in. If your form has sufficient space, you can also change

the List Rows property so that the displayed list is longer, reducing the user's need to scroll. In the

example illustrated the List Rows property has been set to 15.

Name the Combo Box

It is a good idea to give your form controls meaningful names to avoid confusion when referring to them in

your VBA code. Most developers also use a short prefix (in this case "cbo") to identify the type of control.

Find the Name property on the Other tab of the properties window and change it to cboDay.
Add the "Month" and "Year" Combo Boxes
Build two more combo boxes using the same method, setting their properties as follows:

The "Month" Combo Box

Tab Property Value

Data Row Source Type Value List

Data Row Source 1;January;2;February;3;March;4;April;5;May;6;June;

7;July;8;August;9;September;10;October;

11;November;12;December

Data Limit To List Yes

Format Width 2cm (0.8in)

Format Column Count 2

Format Column Widths 0cm;2cm (0in;0.8in) see note below

Format List Rows 12

Other Name cboMonth

Note that for the Row Source I have entered both the month numbers and their names, alternating the

data. When this is combined with a Column Count of 2 (in this case), Access divides the data logically

between the columns. The Bound Column is 1 (i.e. the first column - the month numbers) but as this is the

default I didn't need to change it. You can decide whether or not the user can see all the columns. Do this

by adjusting the Column Widths property, setting a column to a width of 0 (zero) to hide it. The bound

column passes its value to the combo box regardless of whether or not it is visible.
Width: 2cm Width: 2.5cm

Column Widths: 0cm;2cm Column Widths: 0.5cm; 2cm

The "Year" Combo Box

Tab Property Value

Data Row Source Type Value List

Data Row Source 1996;1997;1998;1999;2000;2001;2002;2003;2004;2005

see note below

Data Limit To List No

Data Validation Rule Between 1900 and 2020

Data Validation Text Please enter a 4-digit year between 1900 and 2020

Format Width 1.5cm (0.6in)

Other Name cboMonth

It may not be practicable to list all the years in the range that you users might need (although it is

possible). If the range is great it might be easier for your users to type the year directly into the box. Here I

have given an example of what you could do in such circumstances. I have listed the most likely range of

years in the Row Source property but set Limit To List to No. This allows the user to enter a year that is

not contained in the list, but I still want to prevent them from making an error, so I have set a Validation
Rule. Any entry the user makes must satisfy this rule. If the entry violates the rule Access rejects it and

displays a message, which is specified in the Validation Text property.

Align the Combo Boxes


Some people are content to place their form controls by dragging them into position and aligning them

visually. Having created a lot of forms (and also being rather picky about such things!) I have come to the

conclusion that the best way to arrange objects on a form is accurately, using measurements in the

properties window. I hate to see an untidy form! So here's how I do it...

Drag the three combo boxes to approximately their correct positions. Don't try to align them neatly yet. In

my example, I want to align the combo boxes with their corresponding date field control (here

called HireDate).

Open the Properties Window for the control to which you want to align the combo boxes and note the

value Top property (you'll find it on the Format tab). My HireDate text box has a Top value of 1.711cm.

Now Use the mouse pointer to draw a rectangle around the three combo boxes (you don't need to select a

tool to do this). This creates a multiple selection of the combo boxes.

Use the properties window to assign a Top property the same as the object to which you want to align

them. Because multiple objects are selected the the value you enter will be assigned to all the selected

objects.
NOTE: If you have room on your screen, you can leave the properties window open. When you

select different objects on your form the properties window changes to show information about

the selected object.

You might also have noticed that the height of a standard combo box (0.423cm) is less than that of a

standard text box (0.45cm). This sounds a tiny amount but it is enough to make them look different, so I

have also chosen to change the Height property of the multiple selection to match the text box.

Now the objects are all correctly aligned vertically, but they probably need aligning horizontally. This is

something that can normally be done by eye (although I don't trust myself to drag them into place). Select

each combo box in turn and use the left or right arrow keys on you keyboard to nudge them into position so

that their horizontal alignment is equal.

Additional Enhancements
I believe that interface design is one of the most important aspects of building a database. This means

making forms as user-friendly and intuitive as possible. What seems obvious to you as the designer isn't

necessarily so for the user. Of course you know what the combo boxes are for... you built them. But does

the user know? Here are some ideas for enhancing the date chooser:

The Option Group tool is also handy for quickly creating a

ready-labelled frame.

Add a few more labels of start from scratch by drawing a

rectangle, giving it an etched effect then adding the

labels.

Use your imagination. Access has the tools... you just

need to figure out how to use them!

How Will the Date Chooser Be Used?


In this example the date specified by the combo boxes will be entered into another control on the form,

the HireDate text box, which itself is bound to a table field. You need to decide whether or not you are going

to allow the user to type directly into the text box. My decision is almost always a firm NO!

To ensure that this is the case, go to the Data tab of the text box's properties and set the Locked property

to Yes. It is also helpful to set the Tab Stop property of the text box to No so that it is skipped when the

user moves through the form using their Tab key.

Finally, switch the form into form view and test the tab order (the order in which the controls are visited

when the user moves through the for using their Tab key. Check that the order is logical and, if not, return

to design view and adjust it by going to View > Tab Order.

Powering the Combo Boxes


The design stage of the form is now complete. Now would be a good time to save the form. The next step is

to add the VBA code that will translate the user's selections into a date and place it in the date field text

box. Follow the instructions for this in the Part 2 of the tutorial.

Coloured Tabs for Your Access Forms


The Tab Control is a great way to get a lot of stuff into a small space on an Access form. It looks good too,

as long as you are satisfied with the "Standard" colour scheme. But if you want to give your forms a custom

colour scheme you will find that some things can't be given custom colours, and the Tab Control is one of

them.

I was asked recently if this could be done, and my first reaction was to say "No", but in this business you

soon learn that you should never say "never". So I set about figuring out a way to do it. Here's what I

wanted to end up with...


>>>

In this tutorial you can follow the steps I took to achieve something that, whilst not being a real tab control,

works just like one. And has the added benefit of blending in with any colour scheme you choose. Along the

way you will learn some form design tricks, and how to manipulate form control properties with VBA. You'll

also see an example of form validation - making sure the user supplies all the data required.

The Task at Hand


I have an Access table containing information about people. It contains 17 fields, divided into three topics:

Personal information, Business information and Contact details. I'd like to see each group of fields on a

separate page of a tab control, and I want each page and its corresponding tab to be a different colour. I

also want to make sure that the user fills in all of the fields before proceeding to the next page.

Here's an interactive demo of what it is going to look like. Click the coloured tabs to move from page to

page:

If your browser does not have JavaScript enabled my click effects for the image above won't work. You can

see a composite image of the different pages by clicking the thumbnail below (click the Back button on your

browser to return to the tutorial):


A Tab Control on an Access form is like a set of filing card dividers. They sit in a stack on the form and you

can see only what is on the one that is on the top of the stack. But you can see all the tabs. Clicking a tab

brings that card to the top of the stack so you can see what's on it.

The Tab Control has a has a different surface (called a "page") for each tab, so when you are building your

form you can place what you want on the appropriate page. As you will see, we have to take a different

approach here. We are going to build something that looks like a tab control but it's all "smoke and mirrors".

There will be only one "page" and clicking a tab will change both its colour and the fields it displays.

Prepare the Data


You can use your own data for this tutorial if you wish, adapting the instructions to your own needs. If you

want to follow each step exactly, you need to create a table upon which to base your form. You can work in

an existing Access database or create a new one.

1. Create a new table containing the following fields:


ID, FirstName, LastName, Address, City, PostCode, Country,
EmployeeID, JobTitle, CompanyName, Department, LineManager,
HomePhone, WorkPhone, Mobile, Fax, eMail, WebSite
They can all be Text fields except ID which is an AutoNumber and also the Primary Key.

2. Close and save the table. Call it tblData.

Build the Form


In this step we build a form based on this data. Click the thumbnail images to display full-sized illustrations

of the various steps (click the Back button on your browser to return to the tutorial).

3. From the database window go to Insert > Form to display the New Form dialog and

choose Design View. Choose tblData from the drop-down list at the bottom and

click OK. Access creates a new, empty form.

4. Drag the edges of the detail section (the form background) to width of 9cm and height of

6cm.

5. Use the Rectangle Tool to draw a rectangle, a few centimetres square (the

size doesn't matter now), on the form. After releasing the mouse don't click

anywhere else. Leave the rectangle selected.

6. With the rectangle selected press the F4 key on your keyboard to display

the Properties window for your rectangle (alternatively go to View > Properties or

right-click the edge of the rectangle and choose Properties). The reason I didn't suggest
an exact size for the rectangle is that we are going to do that accurately by typing

dimensions into the Properties window. Click the Format tab of the Properties window

and enter the following values:

Left: 1cm, Top: 1cm, Width: 7cm, Height: 4cm, Special Effect: Flat, Border Width:

Hairline

Now use the Fill/Back Color Tool to select a colour for the rectangle. I

chose Pale Green. If you don't see a suitable colour on the palette, go back to

the Properties window and look at the Back Color property. This is shown as a long

number. Click on the Back Color text box to display the build button ( ). Clicking the

build button displays a full colour chart from which you can make your choice.

Use the Name property to give the rectangle a sensible name. In this example I am using

the name boxMain.

7. Use the Label Tool to draw a label about 2cm wide and 1cm high anywhere on

the form. Without deselecting the label, type the heading for the first page. In

this example I'm using the heading "Personal". Press the Enter key to accept your text

(at the same time selecting the label) then open the Properties window as before and

enter the following values:

Left: 1cm, Top: 0.5cm, Width: 2cm, Height: 0.5cm, Border Style: Solid, Border Width:

Hairline

Fill the label with the same colour that you used for the rectangle in the previous step.

You might notice that the text is a little too close to the upper left corner of the label.

Adjust the properties as follows to make it look better:

Left Margin: 0.1cm, Top Margin: 0.05cm

Small adjustments like this make all the difference when you want to create professional

looking forms...

NOTE: If your headings are longer than will fit, adjust the text box size (and other

dimensions quoted here) to suit your own requirements.


Use the Name property to give the label a sensible name. In this example I am using the

name lblTab1.

8. Now we have to get rid of the line along the bottom edge of the label, so that it looks as

if the rectangle and label are a single item. We can't just rub out the line, so instead we'll

cover it up. Draw another rectangle, fill it with the same colour as before, and set the

following property values:

Left: 1.025cm, Top: 0.95cm, Width: 1.95cm, Height: 1cm, Special Effect:

Flat, Border Style: Transparent

If you take a look at the form in Form View you will see that the dividing line between the

label and the rectangle has disappeared (if you can still see any of it, try adjusting some

of the dimensions of the new rectangle)...

Use the Name property to give the new rectangle a sensible name. In this example I am

using the name boxPatch1.

9. We don't need any more pages (we are going to use the same page all the time, using

code to change its appearance) but we need some more tabs. The easiest way to do this

is to make copies of the original label (the one with the text Personal in my example).

Click on the label to select it and make a copy. I use the keyboard

shortcut Ctrl+C followed by Ctrl+V. Alternatively go to Edit > Copy, then Edit >

Paste. This creates an exact copy of the first label. Now fill it with a different colour (I

used Pale Yellow in this example) and set its properties as follows:

Left: 3cm, Top: 0.5cm, Caption: Business, Name: lblTab2

(You can use your own Caption and Name as you choose).

Repeat the process to create a third tab, filling it with a different colour (I used Pale Blue)

and set its properties as follows:

Left: 5cm, Top: 0.5cm, Caption: Contact, Name: lblTab3


10. We also need two more of the small rectangle colour patches. Create them by making

copies of the original and filling them with the correct colours. Here are the property

values you need:

Left: 3.025cm, Top: 0.95cm, Visible: No, Name: boxPatch2

Left: 5.025cm, Top: 0.95cm, Visible: No, Name: boxPatch3

The reason for setting the Visible property of these two rectangles to No can be seen in

the illustration below. When you look at the form in Form View they disappear (we will

use VBA code to make the rectangles appear when we need them).

11. Finally, we add the data controls to the form. In this case they are all text boxes. I have

organized my data so that the Personal category requires 6 text boxes,

the Business category requires 5 text boxes, and the Contact category requires 6 text

boxes.

The plan is to create the maximum number of controls that will be required at one time

(i.e. 6). As different pages require different numbers of controls, we will use code to hide

and unhide controls as necessary. The controls will initially be unbound (i.e. they will not

be connected to specific fields in the underlying table). This will be done later with code,

when a Control Source for each text box will be defined as well as an

appropriate Caption for its label.

Use the Text Box tool to place a text box on the centre of the form. Click the

Text Box tool then click somewhere near the centre of the main coloured

rectangle. Select the text box and set the following properties:

Left: 4cm, Top: 1.5cm, Width: 3.5cm, Name: txtField1

Then select the label and set the following properties:

Left: 1.5cm, Top: 1.5cm, Width: 2.4cm, Name: lblField1, Caption: Field 1

The remaining text boxes can be added by making copies of the first one. To do this,
select the first text box and Copy then Paste (keys: Ctrl+C then Ctrl+V). This places

an exact copy of the first text box just below it. I want to move the second text box a

little closer to the first. Do this by clicking the Up Arrow on the keyboard a few times (I

thought 4 clicks did the job).

We need four more text boxes. It isn't necessary to copy again, just paste four times.

Not only does Access remember the correct dimensions of the objects being pasted, but

also the adjusted spacing!

To make sure that all the controls are perfectly aligned, select all of the textboxes by

holding down the Shift key and clicking on each one until all are selected. Then go

to Format > Align > Left. Do the same to align all the labels.

Finally, change the Properties to give each text box a suitable name (I

used txtField1 to txtField6), and each label a suitable name and caption (I

used lblField1 to lblField6 and Field 1 to Field 6).

12. The last form design task is to remove any features that aren't needed and add a suitable

caption. Open the Properties window for the form itself. To do this double-click the form

selector in the upper left corner of the form design window...

On the Format tab set the following properties:

Caption: Coloured Tabs Demo; Scrollbars: Neither; Record Selectors: No; Dividing Lines:

No

Write the Code


The building of the form is now complete. The next task is to add the VBA code to make everything work.

The code has to do do several things:


 Change the colour of the main box to match the tab that the user clicked, and at the same time hide
or unhide the appropriate colour patches so that the box and its matching tab appear to be connected.

 Change the control source of each text box so that any data entered goes into the correct field of the
underlying table.

 Change the text of the label for each text box to describe its purpose correctly.

 An optional feature: check that all the fields have been filled-in before allowing the user to move to the
next tab.

These actions have to be performed each time the user moves from one tab to another, and also when the

form opens.

TIP: Now is a good time to Save the form in case anything goes wrong whilst you are testing

your code. I normally also save a backup copy of a form, especially if I have spent a long time

working on its design!

Before starting to write the code, make a note of the colour codes you chose for the different tabs. Look at

the BackColor property of each label. You will see that your colour choice is represented as a long number.

You will need this when setting the colour in the code.

The various procedures are described individually below, and a summary of the entire code module is given

at the end.

The Form_Open Event Procedure

When a form opens, the Form_Open event occurs. We will use this event to set up the form so that the

first tab is visible, and the text boxes and their labels refer to the first set of fields.

Open the Properties window for the form itself and on the Event tab locate the On Open event. Click on

its text box then the build button ( ). Choose Code Builder from the dialog then click OK to open the

code window.

Private Sub Form_Open(Cancel As Integer)

End Sub

Place your cursor between the two lines of code and press TAB to indent your code and type:

Me.boxPatch1.Visible = True

Me.boxPatch2.Visible = False

Me.boxPatch3.Visible = False
These lines set the visible property of each of the coloured patches so that only the first can be seen. The

keyword Me refers to the form itself. This is shorthand for referring to the current form from its own code

module. Note that when you type the dot after the word Me a list of all the acceptable entries appears. You

can save typing and also avoid making typos by scrolling to the item you need then double-clicking it to add

it to your code...

The next line sets the background colour of the main rectangle:

Me.boxMain.BackColor = 13434828

The number corresponds to the colour chosen for the Back Color property of the box. You can copy this

number from the property sheet or, if you know the Red/Green/Blue components of your chosen colour you

can use the VBA RGB function to generate the number for you. Your line of code would look something like:

Me.boxMain.BackColor = RGB(204, 255, 204)

Add the following lines to assign the correct caption for each the text box labels:

Me.lblField1.Caption = "First Name:"

Me.lblField2.Caption = "Last Name:"

Me.lblField3.Caption = "Address:"

Me.lblField4.Caption = "City:"

Me.lblField5.Caption = "Postcode:"

Me.lblField6.Caption = "Country:"

Finally, add the following lines to assign the correct control source to each text box. This command dictates

which field in the underlying table each text box is bound to:

Me.txtField1.ControlSource = "[FirstName]"

Me.txtField2.ControlSource = "[LastName]"

Me.txtField3.ControlSource = "[Address]"
Me.txtField4.ControlSource = "[City]"

Me.txtField5.ControlSource = "[Postcode]"

Me.txtField6.ControlSource = "[Country]"

You might think that this procedure is not necessary, and logically you'd be correct. All these properties are

already set and saved in the design of the form itself, so why set them again when the form opens? Well, I

believe in belt and braces (suspenders if you speak American). If your braces snap, with any luck your belt

will stop your pants falling down.

The lblTab1_Click Event Procedure

This event fires when the user clicks the first coloured tab (the one labelled Personal in my example). It

does exactly the same as Form_Open event procedure but in different circumstances. It is necessary

because the user may want to return to this tab after visiting one of the others.

You can create the procedure as before but, since you are already in the Visual Basic Editor window, you can

do it from here using the two combo boxes at the top of the Visual Basic Editor code window. Open the left-

hand (General) list and click on the control's name (lblTab1).

As the default event for this control is the Click event, that is chosen automatically from the right-hand

(Declarations) list. The Visual Basic Editor creates the lines:

Private Sub lblTab1_Click()

End Sub

You can copy all the lines you typed into the Form_Open procedure and paste them into this one. In

addition, add the following lines:

Me.txtField1.SetFocus
Me.lblField6.Visible = True

Me.txtField6.Visible = True

These SetFocus statement is added for the user's convenience. The statement in this procedure occur when

the user clicks the first tab (i.e. when they are moving to this tab from one of the others). If

the SetFocus statement was omitted, the focus would remain with the control that had it on the previous

tab. By setting the focus to the first control whenever the user switches from tab to tab, the illusion of

separate tabs is reinforced.

The statements making the sixth text box and its label visible are necessary because, as you will see later,

the second tab requires only five fields and the sixth text box and its label are hidden when the user moves

there.

The lblTab2_Click Event Procedure

Create an event procedure for the Click event of the second tab and enter the following lines of code:

Private Sub lblTab2_Click()

Me.boxPatch1.Visible = False

Me.boxPatch2.Visible = True

Me.boxPatch3.Visible = False

Me.boxMain.BackColor = 10092543

Me.txtField1.SetFocus

Me.lblField6.Visible = False

Me.txtField6.Visible = False

Me.lblField1.Caption = "Employee ID:"

Me.lblField2.Caption = "Job Title:"

Me.lblField3.Caption = "Company Name:"

Me.lblField4.Caption = "Department:"

Me.lblField5.Caption = "Line Manager:"

Me.txtField1.ControlSource = "[EmployeeID]"

Me.txtField2.ControlSource = "[JobTitle]"

Me.txtField3.ControlSource = "[CompanyName]"
Me.txtField4.ControlSource = "[Department]"

Me.txtField5.ControlSource = "[LineManager]"

End Sub

This procedure is similar to the previous one but assigns a different group of fields to the text boxes, and

labels them accordingly. Because this tab requires only five fields, the sixth text box and its label are hidden

by setting their Visible property to False (which explains why that property was set to True in the previous

procedure - so that it reappears when needed).

The colour of the main box is changed to match the colour of the tab that the user clicked, and the various

coloured "patches" are hidden or unhidden as necessary to maintain the illusion of separate coloured tabs.

The lblTab3_Click Event Procedure

Like the others, this procedure assigns the correct fields to the text boxes and labels them accordingly. And

effects the formatting changes to give the appearance that the third tab has been brought to the front:

Private Sub lblTab3_Click()

Me.boxPatch1.Visible = False

Me.boxPatch2.Visible = False

Me.boxPatch3.Visible = True

Me.boxMain.BackColor = 16777164

Me.txtField1.SetFocus

Me.lblField6.Visible = True

Me.txtField6.Visible = True

Me.lblField1.Caption = "Home Phone:"

Me.lblField2.Caption = "Work Phone:"

Me.lblField3.Caption = "Mobile:"

Me.lblField4.Caption = "Fax:"

Me.lblField5.Caption = "eMail:"

Me.lblField6.Caption = "Website:"

Me.txtField1.ControlSource = "[HomePhone]"

Me.txtField2.ControlSource = "[WorkPhone]"
Me.txtField3.ControlSource = "[Mobile]"

Me.txtField4.ControlSource = "[Fax]"

Me.txtField5.ControlSource = "[eMail]"

Me.txtField6.ControlSource = "[Website]"

End Sub

That's it! You can save the form and try out the code. As you move from tab to tab the colour of the

background changes and the text box labels change to represent different fields.

Adding Simple Validation...


Making Sure the User Fills In All the Fields
A Function to Pinpoint Missing Data

Here is an example of how you can write a piece of "generic" code to check that the form has been filled in.

You could, of course, set the Required property of each field to Yes but this method involves less work and

gives you the opportunity to display a friendly message and take the user straight to the appropriate field.

In the same code module as the form's event procedures, create the following custom function. You can

enter it after the last of the event procedures:

Private Function CheckData() As Boolean

CheckData = True

Dim ctl As Control

Dim strName As String

For Each ctl In Me.Controls

If IsNull(ctl) Then

strName = ctl.Controls(0).Caption

CheckData = False

MsgBox "Please fill in the " & Chr(34) & strName & Chr(34) & " field."

ctl.SetFocus

Exit Function

End If

Next ctl
End Function

How the Function Works

The CheckData function returns False if it finds an empty text box or True if it does not (hence its

declaration as Boolean). The first line sets the value of the function to True. It uses a For... Next loop to

look in turn at each control on the form. If no empty controls are found the function returns the value True.

If it comes across an empty control an If Statement is invoked in which it notes the control's Caption (i.e.

the Caption property of the label of a text box) and sets the value of the function to false. It then displays a

message box incorporating the control's caption telling the user which control is missing data. Finally it sets

the focus of the form to that control by placing the user's cursor into the control. At this point the function

exits so that the user can correct their input.

Applying the Function

To make use of the function, add the line:

If CheckData = False Then Exit Sub

as the first line of each of the Click event procedures for the three tabs. When the user clicks one of the

tabs, before anything else happens, the function is evaluated. If it returns True (meaning that there are no

empty fields) the rest of the Click event procedure runs. If it returns False the Click event procedure exits

(i.e. is cancelled) and the user is shown a message and taken to the appropriate field.

Follow this link to see the code for the completed procedure [new window].

Download the File


You can download a fully working demo database illustrating the techniques covered in this tutorial. The files

are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a

copy of WinZip or a similar program to extract the zipped files).

ColoredTabsDemo97.mdb ColoredTabsDemo97.zip Access 97 format.

ColoredTabsDemo2000.mdb ColoredTabsDemo2000.zip Access 2000 format.


Making Sense of List Boxes
When I first started building Access forms I couldn't understand what was the point of a list box. To me it

was just like a combo box, but it stayed open all the time and there was no place for the user to type. Why

not just use a combo box? Then I discovered that by altering its properties I could allow the user to select

more than one item at a time - you can't do that with a combo box! But I couldn't figure out what to do

next... how do I get Access to understand what the user chose, and what would I want to use it for anyway?

Well, I eventually found out! In this tutorial you will discover what a list box can do and how, with the help

of some VBA programming, you can add this powerful tool to your Access forms.

What is a List Box?


A List Box is a control that provides a means of displaying a list of items (text, numbers, dates or whatever)

on an Access form. Unlike the Combo Box it lacks a text box at the top in which the user can type. The list

remains on view all the time, giving the List Box the appearance of a scrolling text box.

If the list is longer than the size of the box, a scrollbar is shown (as in the picture

on the left). So why not use a combo box?

Because the list box's list is always open, the user can see the range of choices

without having to do anything, but that is not much of a benefit. If you want the

user to be able to pick a single item from a list you might as well use a combo

Point at the image box.

above to play the


The main benefit of a List Box is that it can be configured so that the user can
animation.
select more than one item.

With the Multi Select property set to None the user can not make multiple

selections. They can choose only one item at a time. Selecting a second item de-

selects the first item.


When the Multi Select property is set to Simple the user is allowed to make

multiple selections. They have to click on each item they want to chose

(alternatively they can use the Spacebar). Clicking on an already selected item

de-selects it.

If the Multi Select property is set to Extended the user has the most control.

They can select a block of items by clicking on the first one and shift+clicking on

the last one. Alternatively they can drag the mouse down the list to select a

block. Separate non-adjacent items can be selected by clicking on the first one

then control+clicking on the others. Already selected items are de-selected with

control+click.

When a list box has its Multi Select property set to "None" the list box has a value equal to the selected

item, just like a combo box does. When used like this it can feed that value directly into a table field or

query. However, when the Multi Select property is set to "Simple" or "Extended" the list box has a value

of Null regardless of how many or which items are selected. In order to make use of the user's selection of

single or multiple items it is necessary to use a VBA procedure to examine the list, determine what has been

selected, and act on it.

When Would You Use a List Box?


 I would probably not use a list box for data entry, to put a single value into a table field. If I was going

to offer the user a reference list of items to choose from, I would normally use a combo box.

 I sometimes use a list box in the same way as I use an option group (a collection of radio buttons of

which you can only select one) where my list of choices is large and I don't have room for (or don't

like the look of) a large collection of option buttons.

 I might use a list box in multi select mode to allow a user to make more than one choice for multiple

data entry, but I can't think of an example that wouldn't represent poor database design (would the

choices all go in the same field... or into different fields... how would it decide?).

 I would often use a list box when I wanted to ask the user to choose a number of items for which

separate actions would be performed (e.g. "Which reports shall I print?", "Which contacts shall I e-

mail?").
 I would definitely use a list box to offer the user multiple choices when running a query (e.g. choose

one or more cities... London OR Paris OR New York).

In the example that follows I am going to use a list box on a form to provide the user with multiple choices

of criteria when running a query. The form contains a list box with suitable criteria values. The user will

make their choice from the list and click a button which passes their choice to a query, and then opens the

query. The form looks like this...

If you want to learn how to make an Access form into a dialog box, like this one, take a look at the step-by-

step tutorial on Custom Parameter Queries to see how it's done

Setting the List Box Properties


I'll assume that you know how to put a List Box control on to a form. If you are new to form design, take a

look at the step-by-step tutorial to see how to put a combo box on a form. Do the same, but choose the List

Box tool instead.

To set the properties of the List Box right-click on it (in form design view) and choose Properties. You need

to define what will make up the list, and how the user is permitted to make their choice:

1. Fill The List

On the Data tab you can set the Row Source property to determine what goes on the list.

 If you already have a table or query that provides a list of items, set the Row Source

Type to Table/Query and either:

1. Click in the Row Source text box, click the down-arrow and pick your table or query name from the

list, or...

2. Click in the Row Source text box, click the build button ( ) and choose Query Builder to open
a query design window. Here you can build and test a query to create a list. When you close the query

builder window an SQL statement is returned to your Row Source property. It will run a query to

create the list whenever the form opens.

 If you prefer to enter your list directly, set the Row Source Type to Value List and type the list

items directly into the Row Source text box, separating the items with semicolons e.g.:

Belgium;Denmark;France;Germany;Italy etc.

2. Determine How the User Will Select Items

On the Other tab set the Multi Select property to either Simple or Extended. This will allow the user to

make multiple selections from the list (as described above).

Getting the Data from the List Box


Unless it is used in MultiSelect "None" mode the list box always returns a value of "Null". This means that

you can't feed the user's multiple selection straight into a query or table. You must use a code procedure to

extract the information from the list box and put it into a suitable state for whatever is needed.

Like most other form controls, a list box has its own set of Event Procedures and you might be tempted to

attach your code to the AfterUpdate event of the list box. Unfortunately, this would probably not be suitable.

The AfterUpdate event fires each time the list box selection changes. So, if the user selects just a single

item, or selects multiple items by dragging down the list, then it would be fine. The selection would be made

in one step and the AfterUpdate event would fire when the user finished. But, if they selected items using

the Click/Click, Click/Shift+Click or Click/Control+Click methods the AfterUpdate event would fire several

times. This is not suitable for our purposes.

In my example, I use the OnClick event of a separate command button to run the code.

What Does the Code Have to Do?

I'm going to use the items that the user selects in the list box to build the criteria of a query. In this

example, there is data from five different regions: North, South, East, West and Central. If the user

wants to see records from just one of those regions the criteria for the Region field would simply

be "East" (for example). But supposing they want to see records for the South, West and Central regions?

There are several ways to specify this when constructing a query in the Access query design window, but the

most convenient way is either:

"South" OR "West" OR "Central" or IN("South","West","Central")

Here is a list of tasks for that I want the code to do:


 Check that the user has selected something. If they have selected nothing, maybe I could cancel the
procedure - or show them everything.

 Assuming they have chosen one or more items, the code must extract the information and put it into a
format (a text string) that a query will understand.

 The text string should be incorporated into an SQL string and passed to a query. To make things
easier I'll use a stored (ready-made) query.

 Finally the code should open the query to display the results to the user.

Prepare the Query

Create and save a query. It can be anything you like, we just need a stored query with a name. The code

will determine the result that the query returns by re-writing its SQL each time it is run, so it doesn't matter

what it is now. I have called my query qryMultiSelect.

Writing the Code


In form design view open the properties window for the Command Button and find On Click on

the Events tab. Click in the On Click text box then click the build button ( ) and choose Code Builder.

The code window will open with the cursor between the start and end lines of the command

button's Click procedure:

Private Sub cmdOK_Click()

End Sub

I'm going to be using DAO code (Data Access Objects) because I prefer it. It was the default for Access 97

but not for Access 2000 and 2002, which default to ADO (ActiveX Data Objects). If you are using Access

2000 or 2002 you need to add a code reference to DAO. Open the Tools menu and choose References. In

the References dialog find the entry for Microsoft DAO 3.6 Object Library and put a tick in the box then

click OK (NOTE: you don't have to do this if you are using Access 97). This makes sure that Access

understands the DAO code vocabulary.

Now to write the code. In the code shown below you will see the names of objects in my sample database.

You should replace these with the names of the corresponding objects in your own database. My list box is

called lstRegions. My stored query is qryMultiSelect. The query will refer to a field called Region in a table

called tblData.

First of all, enter the variable declarations:

Dim db As DAO.Database
Dim qdf As DAO.QueryDef

Dim varItem As Variant

Dim strCriteria As String

Dim strSQL As String

The next two lines assign values to the DAO variables. The first line tells Access that we are referring the

current database; the second line identifies our stored query:

Set db = CurrentDb()

Set qdf = db.QueryDefs("qryMultiSelect")

Now for the code that reads the user's selection from the listbox:

For Each varItem In Me!lstRegions.ItemsSelected

strCriteria = strCriteria & ",'" & Me!lstRegions.ItemData(varItem) & "'"

Next varItem

This For ... Next loop looks at each selected item in the list box, adding each in turn to the string

variable strCriteria. As it does so it adds a comma and quote mark in front of the item, and a quote mark

after it. This is because I'm building an SQL statement containing an IN(...) expression in which the criteria

have to be in quotes and separated by commas. (I will use the OR operator in the next example, below.)

NOTE: I have used single quote marks (') to enclose the criteria. SQL is happy with that. I could not use the

more usual double quotes (") because these are used by VBA as a text qualifier (to define a text string). As

an alternative I could have used the ASCII character code Chr(34) which represents the double quote mark

without actually typing it. The code line would look like this:

=============Alternative code line=============

strCriteria = strCriteria & "," & Chr(34) &

Me!lstRegions.ItemData(varItem) & Chr(34)

================================================

At this point it's a good idea to check that that the user actually selected something. In this example I will

assume that if the user didn't select anything they must have forgotten, or accidentally clicked

the OK button. I will show them a message box reminding them to select something before they click the

button, then cancel the procedure:

If Len(strCriteria) = 0 Then
MsgBox "You did not select anything from the list" _

, vbExclamation, "Nothing to find!"

Exit Sub

End If

Alternatively, I could interpret the user's failure to select anything as their wish to see all the data (see the

code for this in the next example).

There are different ways to check for no selection. The method I used here looks at the length of the criteria

string that was made by looping through the list. If the string has a length of zero, the loop didn't find any

selections. (I'll use a different method in the next example.)

Now I have to edit the string. To simplify its construction, each component of the string was added in the

same way i.e.: comma + quote + item + quote. The result, regardless of the number of items, is that it

starts with a comma e.g.: ,'South','West','Central'.

The following line removes the first character (the leading comma) from the string:

strCriteria = Right(strCriteria, Len(strCriteria) - 1)

The criteria string is now ready to be incorporated into an SQL statement. For this example I have kept the

SQL statement simple. You can see how the criteria string variable is added between the brackets of

the IN( ---string goes here--- ) expression:

strSQL = "SELECT * FROM tblData " & _

"WHERE tblData.Region IN(" & strCriteria & ");"

Earlier, I set a reference to my stored query in the variable qdf. The next line applies the completed SQL

statement to the stored query. This replaces the query's current SQL (which is why I said earlier that it

didn't matter what the original query did).

qdf.SQL = strSQL

Having created the desired query all that remains is to open it to display the result to the user:

DoCmd.OpenQuery "qryMultiSelect"

And finally set the object variables to Nothing to ensure that these are cleared from the memory:

Set db = Nothing

Set qdf = Nothing


Follow this link to see the code for the completed procedure [new window].

Alternative Methods
In the following example, I will use a different method to check whether the user has selected anything and

if not it will be assumed that they want to see all the data (i.e. as if they had selected all the items in the list

- which, of course, they could have done). I will also construct an SQL statement using the OR operator

instead of IN().

Checking for a Null Selection

An alternative method for checking whether the user has selected anything is to count how many items have

been selected. If the answer is zero then the user has not made a selection. the code is very simple, and

would probably be included in an If Statement:

If Me!lstRegions.ItemsSelected.Count > 0 Then

=== code to handle selected items goes here ===

Else

=== code if user selected nothing goes here===

End If

Suitable code to go inside the If Statement is shown below...

Letting the User See All the Data

In the previous example, if the user failed to make a selection from the list box, I assumed they had made a

mistake and cancelled the procedure. But what if the user wants to see all the data? If I were writing a

simple SQL statement, I could simply omit the WHERE clause, and all the data would be returned by the

query. However, because my basic SQL statement already contains a WHERE clause (and in many cases the

query will be more complex than this example) I can ask it to return all the records for the Region field by

using the expression: Like "*". My line of code would look like this:

strCriteria = "tblData.Region Like '*'"

Using the "OR" Operator

Whilst Access allows you to simply type for example: "South" OR "West" OR "Central" into the Query

design grid, this is not acceptable SQL. Access understands what you have typed and converts it into an SQL

string. Here's what the SQL string should look like when using the OR operator:

tblData.Region = "South" OR tblData.Region = "West" OR tblData.Region ="Central"


Although it looks a bit more complicated than using the IN() operator, it can be done in the same way. I

have also used the ASCII code Chr(34) to represent the double-quote (") character, as I described earlier,

to avoid any conflicts with quote marks in the VBA.

For Each varItem In Me!lstRegions.ItemsSelected

strCriteria = strCriteria & "tblData.Region = " & Chr(34) &

Me!lstRegions.ItemData(varItem) & Chr(34) & "OR "

Next varItem

strCriteria = Left(strCriteria, Len(strCriteria) - 3)

Note that this time the resulting string is tidied up by removing the last three characters.

Because I have changed the way the criteria string is written, I now need to change the basic SQL

statement as follows:

strSQL = "SELECT * FROM tblData " & _

"WHERE " & strCriteria & ";"

Apart from these changes, the remainder of the code is as in the previous example.

Follow this link to see the code for the completed procedure [new window].

Download the File


You can download a fully working demo database illustrating the techniques covered in this tutorial. The files

are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a

copy of WinZip or a similar program to extract the zipped files).

MultiSelectDemo97.mdb MultiSelectDemo97.zip Access 97 format.

MultiSelectDemo2000.mdb MultiSelectDemo2000.zip Access 2000 format.

Cascading Lists for Access Forms


Combo Boxes and List Boxes are popular tools for both assisting the user and controlling data input. Their

functionality can be enhanced by changing the contents of the list depending on circumstances. For
example, the user makes a choice from one list which causes the contents of another list to change. This

concept of cascading or interdependent lists is quite easy to put into practice with a little VBA programming.

This tutorial demonstrates several different ways to create cascading lists on your Access forms. I will use

combo boxes, rather than list boxes, as the latter have a more specific purpose (to allow the user to make

multiple selections).

If you are new to form building, or have not ever put a combo box on to a form, have a look at a step-by-

step tutorial first.

What Do Cascading Lists Do?


The purpose of cascading lists is to give the database designer greater control over data input, and to make

things easier for the user. On their own, combo boxes make data input easier and more reliable. When two

or more combo boxes work in conjunction with each other they can help to reduce the user's decision

making by prompting them with only relevant data.

In this example there are two combo boxes: one displays a list of countries, the other displays a list of

cities. If they worked independently the user could choose a country and a city but might get the

combination wrong. Also, the list of cities in would have to show all the cities available. The combo boxes

can be linked in a number of ways. The illustration below shows how linked combo boxes might work. The

user chooses a country first then opens the city list. They see a list of cities relevant to the country they

selected.

To see the demo, point at the numbered items below and watch the image change (if necessary scroll your

browser window so that you can see the whole image).

1. The form has two regular combo boxes

2. The Country combo box displays a list of countries

3. Choosing "France" loads a list of French cities into the City combo box

4. Choosing "United Kingdom" loads a list of UK cities into the City combo box

5. Choosing "United States" loads a list of US cities into the City combo box
If your browser does not have JavaScript enabled my rollover effects for the image above won't work. You

can see the individual images by clicking the thumbnails below:

The following examples show several different ways of achieving this effect and are presented in an

increasing order of complexity.

Example 1: Multiple Row Source Tables


This is the simplest method to code but it requires a table for the main combo box and several tables for the

dependent combo box. This example has a table (tblCountries) that is assigned as the Row Source of the

main combo box (cboCountry). There are also three more tables (tblFrance, tblUnitedKingdom,

and tblUnitedStates) which will provide in their turn the row source for the dependent combo box

(cboCity). To begin with, no row source is specified for the dependent combo box.
The plan is to have the contents of the cboCity list change to reflect the user's choice from cboCountry.

This will be achieved by programmatically defining the Row Source property of cboCity using the After

Update event of cboCountry. Here's the code that does the job:

Private Sub cboCountry_AfterUpdate()

On Error Resume Next

Select Case cboCountry.Value

Case "France"

cboCity.RowSource = "tblFrance"

Case "United Kingdom"

cboCity.RowSource = "tblUnitedKingdom"

Case "United States"

cboCity.RowSource = "tblUnitedStates"

End Select

End Sub

How it works...

The code for this method is very simple. The AfterUpdate event fires when the user makes a choice from

the cboCountry combo box. The code uses a Case Statement to assign one of the city tables to to the Row

Source property of the cboCity combo box according to the user's choice.

Example 2: A Single Row Source Table


This example uses a single table which provides data for the Row Source of both combo boxes. The table

(tblAll) contains two fields, one for the name of the City and the other for the name of the Country to which

it belongs...

The Row Source property of the cboCountry combo box takes the form of an SQL statement which

represents a query of the tblAll table returning the unique values found in the Country field, sorted into

ascending order:

SELECT DISTINCT tblAll.Country FROM tblAll ORDER BY tblAll.Country;

As in the previous example, no row source is specified for the dependent combo box. The code will deal with

that. The following procedure runs on the After Update event of the cboCountry combo box:

Private Sub cboCountry_AfterUpdate()

On Error Resume Next

cboCity.RowSource = "Select tblAll.City " & _

"FROM tblAll " & _

"WHERE tblAll.Country = '" & cboCountry.Value & "' " & _

"ORDER BY tblAll.City;"

End Sub

How it works...

When the user makes a choice from the cboCountry combo box the AfterUpdate event fires. The attached

code defines a Row Source to the cboCity combo box in the form of an SQL statement.
NOTE: When writing SQL in VBA code I usually write my SQL statements as shown above with

each clause on a separate line. I think this makes them easier to read. If you do this remember

to enclose each line in quotes, concatenate (i.e. join) each line with an ampersand (&) and insert

the line continuation character space+underscore ( _) when you break the code line.

The SQL statement changes according to the user's choice as determined by the inclusion

of cboCountry.Value in the WHERE clause. If, for example, the user had chosen France then the line

containing the WHERE clause would read:

"WHERE tblAll.Country = 'France' " & _

Example 3: Synchronising the Lists


This example uses the same code for the After Update event of the cboCountry combo box to assign the

appropriate Row Source to the cboCity combo box as in the previous example. This time an additional

procedure has been added to synchronise the cboCity Row Source with any existing value that might

already be in the City field.

When a Row Source is assigned to the cboCity combo box, it remains assigned until it is reassigned by a

change in the cboCountry combo box. So, when navigating through existing set of records, the list of cities

may not be appropriate for the city shown in the City field.

For example, you enter a record for an address in the United Kingdom so cboCity displays a list of UK cities.

You then move back through the records and stop on an address in France. You might wish to edit that

address so you open the cboCity combo and instead of seeing a list of French cities you see a list of UK

cities! This happened because that was the last Row Source assigned to the combo box and nothing has

happened since to change it.

This example corrects that using the following code on the form's On Current event:

Private Sub Form_Current()

On Error Resume Next

' Synchronise country combo with existing city

cboCountry = DLookup("[Country]", "tblAll",

"[City]='" & cboCity.Value & "'")

' Synchronise city combo with existing city

cboCity.RowSource = "Select tblAll.City " & _


"FROM tblAll " & _

"WHERE tblAll.Country = '" & cboCountry.Value & "' " & _

"ORDER BY tblAll.City;"

End Sub

How it works...

The OnCurrent event fires when a form moves from one record to another (or when the form is refreshed or

requeried).

The first line uses the DLookup function (this works like Excel's VLOOKUP function). This gets the value

(i.e. the existing city name) from the City field, looks for it in tblAll table, and returns the

corresponding Country name which it places in cboCountry combo box. (NOTE: If, as in my example there

is a Country field in the form's underlying table then this data should be in place anyway - but I'm just

making sure! In fact, you might want to omit or modify this line if you were allowing users to enter city

names that were not on the list.)

The next line is the same as the one that runs on the AfterUpdate event of the cboCountry combo box but

we need it here because the AfterUpdate event has not been fired.

Example 4: Mixing Controls - Using an Option Group


The method works just as well with different combinations of controls. Here I have replaced

the cboCountry combo box with an Option Group called grpCountry with option buttons for the various

countries.

The illustration below shows how it works. To see the demo, point at the numbered items below and watch

the image change (if necessary scroll your browser window so that you can see the whole image).

1. If there is no city chosen, the Country group displays a "Null" value

2. Choosing "France" loads a list of French cities into the City combo box

3. Choosing "United Kingdom" loads a list of UK cities into the City combo box

4. Choosing "United States" loads a list of US cities into the City combo box
If your browser does not have JavaScript enabled my rollover effects for the image above won't work. You

can see the individual images by clicking the thumbnails below:

Writing the cascade code...

This form incorporates all the features of the previous example (e.g. synchronising the form with the data)

but the code differs because I have used an Option Group (named grpCountry) instead of a combo box for

the user to choose a country. Of course, an option group does not require a Row Source. Instead, all the

options are on view as labelled option buttons (sometimes called "radio buttons"). Each Option Button has

an Option Value property. This value must be a whole number but it can be whatever you want (within limits

- its data type is Long) but it is logical to give them sequential numbers so mine are France(1), United

Kingdom(2) and United States(3). When the user chooses one of the options by clicking it a black dot

appears in the button and the Option Group has a value corresponding to the Option Value of the selected

button. So, if the user selects the United States button the Option Group has a value of 3.

NOTE: When you draw a group of Option Buttons you should draw the Option Group frame first,

then draw the Option Buttons inside. When you do this all the buttons inside the frame become

part of the same group and function properly. Only one can be selected at a time, and the

selected button passes its value to the group.

The code used to assign a Row Source to the cboCity combo box runs on the After Update event of

the grpCountry Option Group (NOT the event of the individual button). The event fires when the user

selects one of the buttons:


Private Sub grpCountry_AfterUpdate()

On Error Resume Next

Dim strCountry As String

Select Case grpCountry.Value

Case 1

strCountry = "France"

Case 2

strCountry = "United Kingdom"

Case 3

strCountry = "United States"

End Select

cboCity.RowSource = "Select tblAll.City " & _

"FROM tblAll " & _

"WHERE tblAll.Country = '" & strCountry & "' " & _

"ORDER BY tblAll.City;"

End Sub

How it works...

First of all, a Case Statement checks the value of the group and puts the corresponding country name a

string variable called strCountry. Then an SQL statement, incorporating the variable and querying

the tblAll table, is assigned as the row source of the cboCity combo box.

It works in much the same way as previously but the code gets the name of the country from an Option

Group instead of a Combo Box.

Writing the synchronise code...

Similarly the forms On Current event is used to synchronise the Option Group with the City field, and again

the code is a bit more complex:

Private Sub Form_Current()

On Error Resume Next

Dim strCountry As String


If IsNull(cboCity.Value) Then

grpCountry.Value = Null

End If

' Synchronise country combo with existing city

strCountry = DLookup("[Country]", "tblAll",

"[City]='" & cboCity.Value & "'")

Select Case strCountry

Case "France"

grpCountry.Value = 1

Case "United Kingdom"

grpCountry.Value = 2

Case "United States"

grpCountry.Value = 3

End Select

' Synchronise city combo with existing city

cboCity.RowSource = "Select tblAll.City " & _

"FROM tblAll " & _

"WHERE tblAll.Country = '" & strCountry & "' " & _

"ORDER BY tblAll.City;"

End Sub

How it works...

You might have noticed that in the first illustration none of the option buttons are selected, all are greyed

out. Form designers often like to set one of the buttons of an option group as the "default" button so that it

is already selected when the form opens or when the user moves to a new record. It's easy to do, just find

the option group's Default Value property and set it to whatever value represents the button you want

selected. But I didn't want to do that. Since I was going to synchronise the option group country with any

existing city, I decided to give the group a Null value if no city name was present. This is achieved by the If

Statement at the start of the code procedure.


Next comes the code to determine the correct country for the existing city (if present). Like the previous

example it uses the DLookup function, but unlike the previous example it can't feed the country name

straight into the option group. It has to convert the name to a number (the value of the appropriate option

button) so it uses a Case Statement to do this. Finally, the same code as before synchronises the combo box

list with the existing city.

Download the File


You can download a fully working demo database illustrating the techniques covered in this tutorial. The files

are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a

copy of WinZip or a similar program to extract the zipped files).

CascadingListsDemo97.mdb CascadingListsDemo97.zip Access 97 format.

CascadingListsDemo2000.mdb CascadingListsDemo2000.zip Access 2000 format.

A Pop-up Calendar for your Access Forms


Why Use a Calendar?

Getting dates entered accurately always manages to be a problem. Everyone seems to have their own way

of writing a date. In the USA the standard format is month/day/year whereas most of Europe uses

day/month/year. What about date separators... should you use a slash, a dash or a dot, or is it OK to use

nothing at all? Or can you type something like 27-Sep-02? In fact, most of these options are acceptable date

formats and can be catered for with Windows settings, and even field property settings in Access tables and

forms.

Difficulties arise when you create an unbound text box to accept a date on a form that is designed to, for

example, pass criteria to a query. Because there is no underlying form field to reject incorrect dates, you

need some other way to make sure that the user has entered good data. Sometimes you just don't know

what date to enter... "the 3rd Thursday in April next year".


What you really need is a calendar, and thankfully Microsoft has provided us with one that can be placed on

a form. This tutorial shows you how to find the calendar tool and use it to include pop-up calendars on your

Access forms, and explains how to add the simple VBA programming instructions necessary to make the

calendar work.

How to Add a Calendar to an Access Form


There are three steps to creating a pop-up calendar. First you must prepare the form, then draw and

customize the calendar to your requirements, and finally add the VBA code which will drive the process.

The calendar will be hidden until the user clicks the form's date field, when the calendar will appear. When a

date is chosen the calendar will pass the it to the date field before being hidden again. If the user uses their

[TAB] key to enter the date field, the calendar will not appear, and they will be able to type a date directly

into the field if they wish.

Step 1: Prepare the Form

Date fields on forms are usually represented by simple Text Box controls. The first step is to convert your

date field text box into a Combo Box. The reason for this is to prompt the user to click it! When a user sees

the arrow button on a combo box they know that clicking it will reveal a list. In this case, instead of a list,

they will see a calendar.

Select the date field text box on your form and go to Format > Change To > Combo Box...

This will convert your existing text box to a combo box. Alternatively you could draw a new combo box on

your form with the Combo Box tool in the form design toolbox.
>>>

Step 2: Draw the Calendar

You won't find the a calendar tool in the form design toolbox but one is provided as an "optional extra". Click

the More Controls button on the toolbox, and wait a moment while Access builds its list of available

controls. Then look for Calendar Control 10.0 (for Access 2002, or 9.0 in Access 2000, or 8.0 in Access

97) and select it...

>>>

The mouse pointer will turn into the Calendar Control tool and you can click anywhere on your form to insert

a calendar...

You will probably want to resize and position the calendar to suit the design of your form. Double-click the

calendar to open its own properties window (alternatively, right-click the calendar and choose Properties in

the usual way, then go to the Other tab and find Custom. Click the build button [...] to open the custom
properties window). These properties are additional to the regular set of properties that you normally see for

form controls. You can change font, colour scheme, and layout to suit your own requirements.

In this example, I want to make the pop-up calendar quite small, so I have opted not to see the Month/Year

Title and also set the font size to 8. You can resize the calendar in the normal way by dragging one of the

handles around its edge. I have also given the calendar a size 1 border using the Line/Border Width tool

on the form design toolbar. Here's how my calendar will look in Form View...

Remember that the calendar is going to be hidden until the user asks for it by clicking on the date field

combo box (you don't have to do it this way but I prefer to!). Make sure that your form is big enough to

display the calendar when it is un-hidden. If you wish, you can place the calendar over existing controls.

This does make building the form a bit difficult but it can help to economise on space.

NOTE: The Calendar Control is an ActiveX control (actually a file called mscal.ocx) supplied with

Microsoft Office. It is normally installed with a standard installation of Microsoft Office, Excel or

Access, but if you can't find it on the list you will need to get hold of a copy. If you are

distributing your file, or planning to use it on more than one computer, you will also need to

make sure that the host computer has the mscal.ocx file installed. You will find it on the CD that

your copy of Excel came on, or you can download a copy from my web site

at: http://www.fontstuff.com/mailbag/qvba01.htm.

Finally, right-click on the calendar control and choose Properties to open its properties sheet and on

the Format tab set the Visible property to No.

Step 3: Write the Code to Power the Calendar

Code to Display the Calendar

Two code procedures are required: one to un-hide the calendar when the user clicks the date field combo

box; and another to re-hide the calendar when the user chooses a date. The first procedure will be refined

to match the calendar's date with any date that is already shown in the date field (otherwise to display the
current date). The second procedure must include an instruction to transfer the chosen date to the date field

combo box.

Open the properties sheet for the date field combo box and on the Event tab click in the space next to On

Mouse Down. Then click the build button [...] to open the Choose Builder dialog, choose Code

Builder and click OK...

>>>

The code editor window opens with the first and last lines of the date field combo's mouse down event

procedure already written, and your cursor is in the space between. Enter the following two lines, the first to

unhide the calendar and the second to transfer the focus to it:

ocxCalendar.Visible = True

ocxCalendar.SetFocus

NOTE: I have named the date field combo box on my form cboStartDate and the calendar ocxCalendar.

Substitute the names of your combo and calendar for these when you write the code.

Now enter the following lines, in which an If Statement checks the date field combo box to see if it already

contains a date. If it does, then the calendar is instructed to display this date. If not, the procedure uses the

VBA Date function to instruct the calendar to display the current date:

If Not IsNull(cboStartDate) Then

ocxCalendar.Value = cboStartDate.Value

Else

ocxCalendar.Value = Date

End If

The finished procedure should look like this:


Return to your form and test the code (now is a good time to save the form in case anything goes wrong -

just click the save button on the toolbar). Switch the form to Form View and click the form field combo box.

The calendar should appear displaying either today's date (if the combo box was empty) or the same date

as shown in the combo box.

Code to Hide the Calendar and Transfer the Date

Return to the code editor window and open the left-side combo box at the top of the code window. This

displays a list of objects on the form. Choose the name of your calendar (in this example ocxCalendar),

then open the right-side combo box to display a list of procedures and choose Click. This prompts the editor

to create the first and last lines of the calendar's click event procedure...

>>>

(NOTE: if the lines of any other procedure are created when you make the first choice you can just delete

them.)

Enter the following lines of code:

cboStartDate.Value = ocxCalendar.Value

cboStartDate.SetFocus

ocxCalendar.Visible = False

The first line transfers the date chosen in the calendar to the date field combo box. The next line sets the

focus back to the date field combo box. The last line hides the calendar again. The finished code should look

like this:
Return to the form design window and save the form. Switch to Form View and test your calendar. If

anything fails to work properly, go back to the code window and check your typing!

Your pop-up calendar is finished! Unless of course...

What About Multiple Calendars?


It's quite likely that you will have more than one date field on a form. In which case, do you need a separate

calendar for each?... No!

Each date field combo box can call the same calendar control, but the calendar needs to know which combo

called it so that it can return a date to the correct one. This needs only a minor change to the code we have

already used.

Declare the Variable

First of all, declare a variable to hold the name of the combo box that called the calendar. I'll name my

variable cboOriginator and declare it with the line...

Dim cboOriginator As ComboBox

...at the very top of the form's code window after the Option Compare Database and Option

Explicit statements but before the first procedure, like this:

Declaring the variable like this makes it global so that it will hold a value after the procedure that gave it the

value has finished, and can pass a value from one procedure to another.

Pass the Originator's Name to the Variable

There will be a series of almost identical MouseDown procedures, one for each date field combo box. Add a

line to each one to pass the name of the originating combo box to the variable:

Set cboOriginator = cboStartDate


NOTE: the use of the "Set" keyword when assigning a value to the variable. The set keyword is required

when assigning a value to an object variable.

Other references to the combo box in the procedure can now be written as cboOriginator:

Pass the Date Back to the Originating Combo Box

The calendar's Click procedure is similarly modified to pass the date back to the combo box named in

the cboOriginator variable.

A final line:

Set cboOriginator = Nothing

...empties the variable, ready for the next call.

Download the File


You can download a fully working demo database illustrating the techniques covered in this tutorial. The files

are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a

copy of WinZip or a similar program to extract the zipped files).

MultiCalendarDemo97.mdb MultiCalendarDemo97.zip Access 97 format.

MultiCalendarDemo2000.mdb MultiCalendarDemo2000.zip Access 2000 format.


Access Queries
ustomizing Access Parameter Queries
Published: 8 February 2015

Revised: 16 August 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

Everyone likes parameter queries! The database developer doesn't have to anticipate the user's every

requirement, and the user can vary their enquiries without having to get involved with query design.

But there's one question I'm always asked by my Access students. Can you customize the parameter

input dialog?

The basic input dialog box that appears when you run a parameter query simply asks the user for

some text input. Here is a standard parameter input dialog (Fig. 1).

Fig. 1 A standard parameter dialog box.

It serves its purpose, but there is room for improvement. The title "Enter Parameter Value" is
guaranteed to frighten the life out of a new user! Perhaps it could say something a little more friendly.

Let's assume they've decided to continue and type something. What are they allowed to type? Perhaps

they can't remember the names of all the offices or they might make a typo. Couldn't we add a combo

box with a list of choices? The query might require several separate dialogs for different fields.

Wouldn't it be useful if they could be combined into a single friendly dialog box.

So, can you customize the parameter input dialog? No, you can't. But you can do something much

better. You can build a completely new one. In fact with this method you don't even build a parameter

query. You build a regular query that takes its criteria from a special form. You have three things to

do...

1. Build a dialog box with as many combo boxes as you need.


2. Design a query to read its criteria from the information on the dialog box.

3. Create a macro or visual basic procedure to tell them both what to do.

This tutorial explains all of these steps in detail so, if you are already an experienced user, you might

want to skip straight to the bits that interest you.

Finally, there is a section illustrating some useful variations on this technique such as allowing for Null

entries (when the user leaves the combo box empty).

I'm using a sample database containing details of the employees of a company that has offices in a

number of different cities. Each office has several different departments. I'm going to build a query

that enables the user so view an employee list selected by Office and Department. You can download

a sample copy of the completed database file here: acctut08.zip [100KB]

Step 1: Build the Dialog Box


You are going to build a dialog box to replace the one that the user would normally see when running

a parameter query. The dialog box is just a form built using the Access form design tools. It will

contain two combo boxes, one for Office and one for Department, and a couple of command buttons

to make things happen.

1.1 Create a New Form in Design View

Click the Create tab on the Access ribbon then click the Form Design button to create a new form in

Design View. A form design tab opens containing a blank form. You don't need to specify a table or

query for the form's data. This is going to be an "unbound" form, one that isn't linked to a data

source. Your design window is probably maximized at this point but later you will tell Access to display

the form as a dialog box.

Start by dragging the edges of the form to make a rectangle about 7 cm wide by 4 cm tall. It should

look something like this (Fig. 2).


Fig. 2 Drag the form's background to the desired size.

Now you are ready to put some objects on the form. Access will have added a collection of Form

Design Tools tabs to the ribbon. The tools necessary for this task are all located on the Design tab.

1.2 Prepare the List Data

I'm going to put two combo boxes on the form, one listing the Offices and another listing the

Departments. But first, I need something that Access can use to build the lists. If you have ever used

the wizard to create a combo box you will know that you have several choices. You can create a table,

listing each item you want to appear on the list; you can build a query that makes the list from a data

source such as one of the tables already in the database; or you can simply type your list on to the

combo's properties sheet. Each method has its own advantages. A query, for example could be self-

updating. A typed list would be easiest but fiddly to change later. I've chosen to create a couple of

tables. Their data won't change often, but when it does it will be easy to update them. I've called the
tables tblDepartmentList (Fig. 3) and tblOfficeList and each has just one

field, Department and Office respectively.


Fig. 3 A table is used to populate the combo box list.

TIP: Why the strange names for the tables? Access developers follow certain conventions when

naming database objects. This becomes important when you get involved with VBA code where spaces

in object names aren't allowed (and it's a pain typing all those underscores), and where prefixes such

as "tbl" help to identify what sort of object is being referred to (e.g. tbl = table, qry = query, frm =

form).

Once you have created your tables, you are ready to add the combo boxes to the form.

1.3 Add the Combo Boxes

First, switch off the Use Control Wizards option on the Design tab of the ribbon (Fig. 4). This will

prevent the wizard from running when you select the Combo Box tool. You are going to do it the

"hard" way! If you forget to disable the wizard, don't panic, just click the Cancel button on the first
window of the wizard when it appears.

Fig. 4 Switch off the Control Wizards option.

To add a combo box click the Combo Box button (Fig. 5) on the toolbox then click on the form about
1 cm down and 3 cm in from the left.
Fig. 5 The Combo Box tool.

Access creates an unbound combo box on the form, and a text label to go with it (Fig. 6).

Fig. 6 Use the Combo Box tool to position and add a new combo box.

Click the label to select it. You can press the [Delete] key on your keyboard to delete the label, or

click the label a second time to insert your cursor and edit the text to something meaningful. You can

resize or move the control (an object on a form such as a combo box is called a "control") or its label

by dragging the dots that appear around the edge of the object when it is selected. Watch the cursor

to see what will happen when you drag it (Fig. 7).


Fig. 7 The cursor indicates its function when dragging.

TIP: Here's an easy way to re-size a label to fit its text. Just double-click one of the small dots and the

label will snap to the correct size.

TIP: It can be really fiddly moving and resizing those labels and controls with the mouse. I prefer to

use the keyboard... Select the object then, on the keyboard, use [Arrow] to move

or [Shift]+[Arrow] to re-size. Hold the [Control] key down at the same time to fine tune the

movements. The up, down, left and right arrow keys all do different things so experiment!

Now you have an "unbound" combo box on your form. The term unbound indicates that it is not

connected to any field. It will just display a value that we can make use of later. In this example I

need two of these so I'll repeat the process (Fig. 8).

Fig. 8 A second combo box is added to the form.

TIP: If you have an object like a control or command button on your form and you want another
exactly the same, select the object then Copy and Paste (don't bother to click anywhere in between)

and you've got an exact copy of the original. A quick way to do this is to select the control you want to
duplicate then hold down the [Control] key and press C then V. An exact copy of the control will

appear directly below it.

1.4 Set the Combo Box Properties

You may be tempted to switch to Form View and check out your combo boxes, but they won't work

yet. We have to give them their instructions by setting their properties. Select the combo box whose

properties you want to set and click the Properties button (Fig. 9) on the Design tab of the ribbon (or

right-click the combo box and choose Properties from the context menu).

Fig. 9 The Properties button.

This opens the Properties pane at the right-hand side of the Form Design window.

The first thing to do is give the combo box a meaningful name (an object's "name" is a different thing

from its "label"). Click the "Other" tab on the properties pane. You'll see the name that Access gave it,

something like Combo1, but it makes sense to change it to something meaningful so you don't get

confused when you have to refer to it in your VBA code, or in a macro or query. I've called

mine cboOffice (note the propellerhead naming convention again).

Next we have to tell the combo box where to get its list. Switch to the "Data" tab of the Properties

pane. If you used a table or query to make your list (I used tables) leave Row Source Type set

to Table/Query. Then click in the box next to Row Source, click the down-arrow and choose your

table or query from the list (Fig. 10).

If you chose to type out your list instead, change the Row Source Type to Value List then, in Row

Source, type the items you want to see in your list, separated by semicolons (;). Like this...

"Birmingham"; "London"; "Manchester"; "New York"; "Paris"

The are several more properties that you can change if you wish. I've chosen to set

the Limit To List property to Yes - this prevents the user from typing something that isn't on the list.

I've also left the Auto Expand setting as Yes - this means that the user can type the first one or two

letters of an entry for Access to fill out the full name automatically. Auto Expand is either really useful

or profoundly irritating depending on what's in your list, so you can turn it off if you want.
Fig. 10 Set the properties of the combo box.

I haven't bothered to set a Default Value, because I want the user to have the option of leaving the

entry blank. I could have set a Default Value of "London" for example (remember to put quote marks

around text).

TIP: To make things easy for your users the items in your combo box list should be in alphabetical

order. If your list is based directly on a table the order can be disrupted if new items are added. For
this reason I usually use a query as the row source, even when working with a simple one-column

table, because this allows me to sort the list items so that they always appear in the correct order.

There is no need to create a separate stored query for this. Instead of selecting a table as the Row

Source click in the Row Source textbox then click the Build button [...] to open the query builder

where you can create an embedded query that sorts the table data for you. When you close the query

builder it generates a SQL statement for the Row Source property.

Other helpful properties are Status Bar Text and Control Tip Text which you'll find on the "Other"

tab. They both offer help for the user and both can accept up to 255 characters of text (make sure

your status bar is long enough for what you type!). Status Bar Text appears on the status bar at the

bottom of the Access window when the user enters the combo box. Control Tip Text appears in a pop-
up box next to the mouse pointer when the user points at the combo box. If you need specific help on

any of the properties just click on the appropriate property and press the [F1] key on your keyboard.

Set the properties for each of the combo boxes on your form.

1.5 Draw the Command Buttons

I'm using two command buttons. One for [OK] that will run the query and close the form when the

user clicks it, and one for [Cancel] that will just close the form if the user changes their mind.

Click the Command Button button on the Design tab of the ribbon, then click on the form where you

want your button to appear. You may want to resize the button. Do it the same way as for the combo

boxes. Create a second button (copy/paste is quick and easy) and arrange them to suit yourself (Fig.

11).
Fig. 11 Use the Command Button tool to add two command buttons to the form.

1.6 Set the Command Button Properties

Give your buttons sensible captions like OK and Cancel. Use the properties window to set their

properties as follows...

OK Button: Caption: OK, Name: cmdOK, Default: Yes

Cancel Button: Caption: Cancel, Name: cmdCancel, Cancel: Yes

Setting the Default property to Yes for the OK button makes it the form's default button. As long as

the user hasn't selected another button on the form (by tabbing to it for example), pressing

the [Enter] key on the keyboard will have the effect of pressing the button. You may not want to set

this property if you think the user might press enter by mistake (users do that sort of thing!).

Similarly, setting the Cancel property Yes for the Cancel button means that button will be pressed if

the user presses the [Esc] key on the keyboard.

We're going to use the OK button to run the query (so pressing [Enter] after selecting from the

combos makes sense), and the Cancel button will close the form without running the query (users

expect things to go away if they press the [Esc] key). These properties simply assign actions to the

key presses - we still have to create procedures to tell the buttons what to do.

At this point you might like to decorate the background of the form to suit the colour scheme of your

database (Fig. 12). To change the background colour, click on the Detail area of the form and change

its BackColor property.


Fig. 12 Make the form’s background a suitable colour.

One property we haven't set yet is the On Click event property. I'll return to that later. First we have

to tidy up the form and make it look like a proper dialog box...

1.7 Turning the Form into a Dialog Box

To make our form look more professional we need to change some of its properties so that it looks like

a proper dialog box. An Access form has several features that a dialog box does not need, like record

selectors and navigation buttons. Double-click the Form Selector button (Fig. 13), it's the small

rectangle located where the two rulers meet, to select the form itself and display the form properties

window. When the form is selected a black dot appears in the centre of the Form Selector.

Fig. 13 Select the form itself.

Go to the Format tab of the Form Properties pane and set the properties as follows:

Caption: [Choose a suitable title], Scrollbars: Neither, Record Selectors: No, Navigation Buttons: No,

Dividing Lines: No, Auto Center: Yes [Optional], Border Style: Dialog, Control Box: No [Optional].

Setting the Border Style to Dialog automatically removes the Min/Max buttons from the form. You can

also choose to set the Close Button property to No (which disables it rather than removing it), or to

set the Control Box property to No (which removes both the Control Box* and the Close Button). I

have chosen the latter option in this example but sometimes I prefer not to do this. You must give

your user some sort of 'escape route'. They might have arrived at this dialog box by mistake and just

want to get rid of it. Here, our custom Close button will let them do that. Users will often close a form

or dialog using its own close button (the one in the upper right corner marked with a cross [X]). You

may not want them to do this if, for example, the dialog is shown mid-way through a procedure.

Unexpectedly closing a dialog might crash your code! So when I want complete control over the user's

actions I provide my own close button that I can program events the way I want.

*NOTE: What! You never heard of the Control Box? It's the tiny icon in the upper left corner of almost

every window. Click on it with your mouse to get a menu of window options.
Go to the Other tab of the Form Properties pane and set the following property to make sure that the

form appears as a dialog and not as a tabbed form or window:

Pop Up: Yes

Take a look at your form dialog box in From View and if necessary resize the detail area so that it is a

suitable size for what it shows. You may want to re-arrange the contents so that everything looks

good. Here is our (almost) finished dialog box (Fig. 14):

Fig. 14 The form’s design is complete.

It's time to close and save the form as we are about to turn our attention to the query. I have called

this form frmEmployeeLocator. You can, of course, call it anything you like.

The next step is to design the query that will make use of the choices that the user makes from the

form's combo boxes...

^ top

Step 2: Design the Query


2.1 Create the Query

Select the Create tab of the Access ribbon and click the Query Design button to create a new query in

Design View. Build your query as you would normally, choosing the table whose data you want to

query and adding the fields you want to see. Don't add any criteria yet. In my example, I'm going to

set the criteria for the Office field of my Staff table to the value that the user chose on

the cboOffice combo box on my new custom dialog box. I'll do the same for the Department field.
2.2 Set the Field Criteria

We need to tell the query to use as its criteria the values currently showing in the appropriate combo

box of the custom dialog box. If you know what to type you can enter the instructions directly into the

criteria cell. I find it easier to use the Build tool. Right-click in the first criteria cell of your chosen field

(here I'm using the Office field) and choose Build from the context menu (Fig. 15).

Fig. 15 Select the Build tool.

This opens the Expression Builder window. In the lower part of the window there are three columns.

In the left column click the [+] next to the name of your database to expand its contents. Then

expand Forms and All Forms until you can see the name of the dialog box you just created (mine was

named frmEmployeeLocator) and click it.

This displays in the centre column a list of all the objects (controls, labels etc.) on this form. Find the

name of the combo box that contains the list appropriate to the field whose criteria you are setting

(mine is cboOffice) and double-click it. Its full designation appears in the upper section of the

Expression Builder (Fig. 16).


Fig. 16 Select the new dialog in the Expression Buider.

Click the [OK] button to close the Expression Builder and return to the query design window, where

you will see that the correct information has been placed into the criteria cell. Repeat this for each of

the combo boxes (taking care to apply the correct combo box criteria to each field). The result will

look something like this (Fig. 17).

Fig. 17 The query contains a reference to each combo box.

We haven't finished yet, but you might like to try out the query at this point...

2.3 Test the Query

Leave the query window open but go to the Navigation Pane and open your custom dialog box. Make
selections from the combo boxes but don't close the dialog box. Now switch back to the query design

window and run the query by pressing the Run button on the Query Design tab of the ribbon. If
everything has been done correctly, your query will proceed using the criteria you selected in the

dialog box and you will see the appropriate result (Fig. 18).

Fig. 18 Test the new dialog box.

If you need to modify the query in any way do it now, then close and save it, giving it a suitable name

(mine is called qryEmployeeLocator). We are now ready to create the instructions that will link the
query with the dialog box...

Step 3: Create the Code


The final task is to create the instructions that operate when the user clicks the OK or Cancel buttons

on the dialog box. You could do this using an Access Macro but I have chosen to use VBA code

because this gives me much greater control. It is also an easy introduction to writing VBA code if you

haven't done it before. Open your custom dialog box in Design View if it is already open right-click on

it and choose Design View from the menu.

3.1 Programming the Cancel Button

The easy one first! The Cancel button needs to be programmed to close the form without running the

query when it is clicked by the user. Right-click the Cancel button and choose Properties from the
context menu to open the Properties Window. Switch to the Event tab. Here you will see a list of

events associated with the Cancel button. We are going to attach a VBA procedure to one of these.

Fig. 19 Create an Event Procedure for the Cancel button.

Click in the box next to On Click, then click the Build button (the one with the [...] symbol) (Fig. 19).

Choose Code Builder from the dialog box that appears then click [OK] to open the Visual Basic

Editor. The first parts of the code are already in place (Listing 1):

Listing 1:

Private Sub cmdCancel_Click()

End Sub

Place your cursor in the empty line between the Sub and End Sub statements and press [Tab] to

indent your code (this makes it easier to read). Type DoCmd followed by a dot. When you type the dot

a list of possible entries appears...

Fig. 20 Start writing the code to close the dialog box.


Choose Close from the list (Fig. 20) and type a space. Typing the space both enters the text Close into

your code and also enters the space. This prompts the next list of possible entries to open...

Fig. 21 Continue writing the code to close the dialog box.

Choose acForm (Fig. 21) and type a comma (,). Typing the comma enters the text acForm into your

code and also enters the comma. Now you can see a panel indicating the various pieces of information

(called arguments) that this statement needs (Fig.22). The current one is in bold, telling us that we

should enter the form's name next. The remaining arguments are in italic, indicating that they are

optional. Type, in quotes, the name that you gave your custom dialog box (mine is

called frmStaffLocator).

Fig. 22 The Visual Basic Editor tells you what it requires.

Your finished code module looks like this (Listing 2):

Listing 2:
Private Sub CmdCancel_Click()
On Error Resume Next
DoCmd.Close acForm, "frmStaffLocator"
End Sub

NOTE: You might have noticed that, although not on the screenshots, I have added a basic error

handler "On Error Resume Next" to each of my completed code listings. Even the simplest of macros

might fail if the unexpected happens, so it is good practice always to add an error handler of some

kind. This won't prevent your code from crashing but it will at least prevent a catastrophe from

happening if it does.

Close the code module window. The On Click property of the Cancel button now

shows [Event Procedure] indicating that there is some code associated with that event. You can edit or

view the code at any time simply by clicking the Build button.

TIP: If you want to remove the code associated with a particular event, just delete the entry

[Event Procedure] from that event in the Properties Window.

Your code will be saved when you save the changes to the form so do that now. Switch the form to

Form View and test your Cancel button. When you click the button the dialog box closes. Because you

set the Cancel property of the Cancel button to Yes, pressing the [Esc] on your keyboard also closes

the dialog box.

3.2 Programming the OK Button

The OK button needs to be programmed to run the query and then close the dialog box. The query
already knows that it must get its criteria from the dialog box. I am getting the procedure to close the

dialog box to deter the user from trying to run the query again whilst it is already open.

Use the same method as for the Cancel button to add an event procedure to the On Click event of the

OK button. Your code should look something like this (Listing 3).

Listing 3:

Private Sub cmdOK_Click()


On Error Resume Next
DoCmd.OpenQuery "qryStaffLocator", acViewNormal, acEdit
DoCmd.Close acForm, "frmStaffLocator"
End Sub
It is important that the code runs the query before it closes the dialog box. If the instructions were

reversed the query would not be able to retrieve its criteria and the user would see an error message.

Note also that when opening the query you have a number of choices. You could, for example, choose

to open the query Read Only (preventing the user from making changes to the data) or Add (allowing

the user to add new entries but not edit or delete existing ones).

Save and test your custom dialog box. Remember that this kind of query has to be run from the dialog

box. If you try to open the query by itself there will be an error (unless the dialog box is already open

and showing choices). To do this simply place a command button on a form and add the appropriate

code to its On Click event. For example (Listing 4):

Listing 4:

Private Sub cmdEmployeeLocator_Click()


On Error Resume Next
DoCmd.OpenForm "frmEmployeeLocator"
End Sub

4. Variations on a Theme
In the tutorial I built a dialog box with multiple combo boxes. If you need only one you can save the

user the effort of clicking buttons. See the example below...

If the user leaves either combo box blank, the query will return no data. There are two ways to handle

this.

1. You can insist that the user makes choices from all the combo boxes, by changing the code to

check that both combo boxes contain a value (i.e. the user has chosen something). How...

2. You can modify the query to interpret an empty combo box as the user's wish to see all results for

that particular field (i.e. leaving the Department combo box blank means you want to see records for

all departments). How...

4.1 A Simple Single-Combo Dialog

This demonstrates the technique at its simplest and most elegant. It looks just like a parameter query

dialog but it has a combo box (Fig. 23).


Fig. 23 A single-parameter dialog box.

If you are querying on a single variable field, then you need only one combo box. You don't need any

buttons because you can run the query code when the user chooses an item from the list. You do this

by attaching the event procedure to the After Update event of the combo box itself (Fig. 24).

Fig. 24 Create an After Update event procedure.

The code looks the same (Listing 5), it's just in a different place...

Listing 5:

Private Sub cboOffice_AfterUpdate()


DoCmd.OpenQuery "qryEmployeeLocator", acViewNormal, acEdit
DoCmd.Close acForm, "frmEmployeeLocator"
End Sub

I have not allowed for the user wanting to close the dialog box without running the query, and if I was

feeling benevolent I might change the form properties to reinstate the control box/close button.

If you decide that the user must make a choice (i.e. they are not allowed to leave one of the combo

boxes blank) you need to be able to check their entries before running the query. To do this, modify
the code that runs when the user clicks the OK button to look something like this (Listing 6):
Listing 6:

Private Sub cmdOK_Click()


On Error Resume Next
If IsNull(cboOffice) Or IsNull(cboDepartment) Then
MsgBox "You must choose both Office and Department."
Exit Sub
End If
DoCmd.OpenQuery "qryEmployeeLocator", acViewNormal, acEdit
DoCmd.Close acForm, "frmEmployeeLocator"
End Sub

Or for a more professional looking message box (Listing 7)...

Listing 7:

Private Sub cmdOK_Click()


On Error Resume Next
If IsNull(cboOffice) Or IsNull(cboDepartment) Then
MsgBox "You must choose both Office and Department." _
& vbCrLf & "Please try again.", vbExclamation, _
"More information required."
Exit Sub
End If
DoCmd.OpenQuery "qryEmployeeLocator", acViewNormal, acEdit
DoCmd.Close acForm, "frmEmployeeLocator"
End Sub

The code has been modified to include an IF statement that checks to see if either combo box is

empty. If so, a message is displayed and the procedure terminated. If not, the query runs as normal.

TIP: When you modify existing code, Access sometimes displays an annoying habit of ignoring your

changes! The solution is simple. In the code editing window select all your new code, Cut it (so that it

disappears) then immediately Paste the code back again. It sounds crazy but it works!

4.3 Allowing Null Entries

Most people would assume that a blank combo box would mean they wanted to see everything. A

query assumes the opposite so, if you leave any combo box empty the query returns no records. If
you want to allow the user to leave any or all of the combo boxes empty you have to modify the query

criteria. (See also the tutorial Parameter Queries: Handling Null Responses).

For each query field that you want to allow a null entry modify your criteria from:

[Forms]![frmStaffLocator]![cboOffice]

to:

[Forms]![frmStaffLocator]![cboOffice] Or Like [Forms]![frmStaffLocator]![cboOffice] Is Null

When you do this and run the query for the first time you will find that Access has changed the way

your criteria were written from the text above to something more complex. The criteria grid now

contains several rows of entries. Leave it alone! Access has just broken down

your ... Or Like ... Is Null statement into its component parts, listing all possible combinations of null

and not-null entries on separate lines. The query needs this but, thankfully, you can type it out the

quick way!

TIP: Sometimes you have a lot to type into a query criteria cell. It's important that everything is

correct so make life easier for yourself! Right-click the cell and choose Zoom to open a large editing

window that lets you see what you are typing.

Final Thoughts
Running your queries from a form or switchboard offers a high degree of user-friendliness and allows

you to help the user make their choices by providing them with lists of options.

This means that the query has to be run from the form rather than from the query itself so you will

probably want to build a switchboard listing available queries for the user to run.

Remember that, if you choose to allow the user to leave a combo box blank, the query definition

starts to get complex. Limit your dialog boxes to two or three combo boxes, unless you are prepared

to wait a long time for Access to execute a highly complex query!

Download Example Database


You can download a sample database containing the completed exercise used in this tutorial. You can

also download a printable PDF of this tutorial.


To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

acctut08_CustomizingAccessParameterQueries.zip [118 KB]

acctut08_CustomizingAccessParameterQueries.pdf [385 KB]

Access Query and Filter Criteria


When constructing a query or a filter, you need to tell Access what to look for in each field. You do this by

defining criteria - typing something (an "expression") into the Criteria cell of the query or filter grid. If you

do not define any criteria for a particular field, Access assumes that you are applying no constraints and will

display everything it has. This means that you only have to define criteria for those fields you are interested

in.

Here are some examples of the more common types of criteria. Often Access will complete the expression so

that you need only type the text you want to match. However, sometimes Access has a choice so you should

always check that what Access has written is the same as you intended. If what you type doesn't make

sense to Access, you will see an error message.

The list of examples below is not exhaustive. Try using combinations of different expressions and see what

you get. Also, don't immediately assume that you have made a mistake if you get no records when you run

the query or filter. It means that Access can't find anything to match your criteria. That may be because

you've asked for something impossible, but it could equally mean that your criteria were perfectly OK but

there simply aren't any matching records.

This tutorial is arranged in the following sections:

 Matching Text

 Using Wildcards

 Working with Numbers

 Working with Dates

 Excluding Things

 Finding Empty Fields


Matching Text
When you enter text into the criteria cell your text should be enclosed in quotes ("") to distinguish it from

other expressions and operators that you may need to add.

"Text"

To match a word or phrase simply type the text you want to match. The

query will find all the records that match the text exactly. Access will add the

quote marks at each end. It is only necessary to enter the quotes yourself if

you type text that might confuse the query. For example you may want to

type a phrase that contains the words "and" or "or". Access would normally interpret these words as

instructions. You can manually insert the quote marks at each end of the phrase to make sure the criterion

means what you intend it to. This example will display all the records that contain the entry London in

the Town field.

"Text" Or "Text"

To match one of two or more words or phrases, type the text you want to

match separated by the word "or". The query will find all the records that

match any of the words or phrases. Enter quote marks yourself if you think

the text might confuse the query. This example will display all the records

that contain either London or Paris in the Town field.

"Text"

"Text"

To match one of several words or phrases, you can type each word or phrase

in a new row moving down the column. This gives the same result as using

"or" but has the advantage that your criteria might be easier to read. This

example will display all the records that contain the

entry London, Paris or Amsterdam in the Town field. Note: If this method is combined with criteria for other

fields those criteria must be repeated for each row.

In ("Text", "Text", "Text"…)

To match a word or phrase from a list, type the list items separated by

commas, and enclose the list in round brackets (parentheses). Access will

add the expression "In" and place quote marks where needed - you can

do this manually if you wish. This example will display all the records that

contain UK or USA or France in the Country field.


Not "Text"

To exclude a word or phrase, use the expression "Not" followed by the word

of phrase you want to exclude (enclosed in quotes). This example will display

records that contain anything other than London in the Town field.

Not In ("Text", "Text", "Text"…)

To exclude a list of words or phrases from the search use the same

method as for matching from a list but add the expression "Not" at

the beginning. This example will display all records that contain

anything other than UK or USA or France in the Country field.

^ top

Using Wildcards
A wildcard is a special character that can stand for either a single character or a string of text. Wildcards

are useful when you want the query to look for a range of different possible values, and also when you are

not certain exactly what you are looking for but can give the query some clues to work with.

The two wildcards we commonly use are the asterisk or star (*) and the question mark (?).The asterisk (*)

represents any string of text from nothing up to an entire paragraph or more. The question mark (?)

represents a single character only (although you could use, for example, two question marks to represent

two unknown characters).

For example:

 Yor* would find York, Yorkshire and Yorktown but not New York.

 Mar? would find Mark but not Mario, Martin or Omar.

 F*d would find Fred and Ferdinand but not Frederick.

Like "Text*"

To match text starting with a particular letter or string type the letter or

string of text followed by an asterisk. Access will add the expression "Like"

and place quotes around your typing. This example will display all records
that have an entry starting with S in the Company field.

Like "*Text"

To match text ending with a particular letter or string type an asterisk

followed by a letter or string of text. This example will display all records

that have an entry ending with Plc in the Company field.

Like "[Letter-Letter]*"

To match text starting with letters within a certain range you must type the

entire expression as shown (this one is too complicated for Access to work

out what you want. This example will display all the records with entries

starting with the letters A - D in the Company field.

You can often get the same results by using mathematical operators such as greater than (>) and less than

(<). These are normally used for specifying numbers and dates but can also be used for text.

For example:

 <"N" would find all entries beginning with a letter lower than the letter N in the alphabet. In other
words, all entries starting with the letters A - M.

 >"F" And <"H" would find all entries beginning with the letters F and G.

^ top

Working with Numbers


When working with numbers we normally use the mathematical operators to define the range of numbers

from which we want to select.

For example, where X represents a number:

 <X finds values less than X.

 >X finds vales greater than X

 >=X finds values greater than or equal to X

 <>X finds vales not equal to X


It is important that your field type is correctly defined as a Number field for numerical queries to work

properly. Here are some examples…

To match a number simply type the number that you want the query to find.

This example will display the record(s) with the entry 385 in

the CustomerNumber field.

<X

To find values less than a certain number type a less than sign (<) followed

by the number. This example will display all records with an entry less

than 1000 in the CustomerNumber field.

Between X And Y

To find values in a range of numbers type the expression shown where X and

Y represent the numbers at opposite ends of the range. This example will

display all records with entries falling within the range 500-700 in

the CustomerNumber field.

^ top

Working with Dates


Dates behave the same way as numbers, so you can use some of the same techniques when constructing

your date query or filter. Remember, for dates to be treated properly by Access it is important that your

field type has been correctly defined as a Date/Time field. It doesn't matter how you enter the date, as long

as you use a recognised format. The date will be displayed in the resulting dynaset in whatever format you

chose when you created the table.

When you enter a date in the criteria cell you can use any standard date format, but each date must be

enclosed by hash marks (#).

For example:
 <#1/1/98# finds dates earlier than 1 January 1998

 =#27-Sep-50# finds dates equal to 27 September 1950

 Between #5/7/98# And #10/7/98# finds dates no earlier than 5 July 1998 and no later than 10
July 1998

Here are some more examples…

=#Date#

To match a particular date type the date enclosed by hash marks (#). This

example will display all the records with entries for 27 September 1998 in

the Invoice Date field.

=Date()

To match today's date type the expression shown. Date() means "today".

This example will display all the records with entries for the current date in

the Invoice Date field.

Year([Fieldname])=Year(Now())

To match the current year type the expression shown, entering the

name of the current field in square brackets where indicated. This

example will display all the records with entries for the current year

in the Invoice Date field.

Year([Fieldname])=Year

To match a particular year type the expression shown, entering the name

of the current field in square brackets where indicated and the required

year in place of Year. This example will display all the records with a date

in 1998 in the Invoice Date field.


<Date()-30

To match a particular calculated date range you will need to use a

combination of expressions. This expression employs a calculation that

subtracts 30 from the current date and also includes the less than operator.

This example will display all the records with a date more than 30 days old in

the Invoice Date field.

^ top

Excluding Things
Sometimes you want to specifically exclude criteria from your search. This is done with the expression Not.

This expression can be used on its own or in combination with other expressions.

For example:

 Not "text" finds all records except those matching the specified text.

 Not Like "X*" finds all records except those starting with the specified letter (or string of text).

Here are some more examples:

Not "Text"

To exclude specific records from the search use the expression Not followed

by the text which matches those records you want left out. The text needs to

be between quotes as shown here - Access will normally do that for you. This

example will find all records for contacts in towns other than London.

Not Like "Text*"

You can use wildcards with the Not expression, which then becomes Not

Like followed by your wildcard criteria. Here is just one example. This

example will find all records for contacts in towns starting will letters other

than L.

And Not "Text"

The Not expression can be used in combination with other expressions,


when it becomes And Not followed by the text you want to exclude from your search. This example will find

all records for contacts in towns starting with the letter L but will exclude those in London.

^ top

Finding Empty Fields


A query can be used to find records where specific fields are empty. To do this you use the expression Is

Null. Conversely, to find records for which specific fields are not empty you use the expression Is Not Null.

The expression Null simply means "nothing".

If you have made use of the "allow zero length" field property you can search for zero length entries.

Sometimes you want to distinguish between, for example, records for which you don't happen to have the

particular piece of information for a certain field and those for which you know there definitely isn't any

information available. Is the Fax Number field empty because you don't know the person's fax number or is

it because they don't have a fax? Either way you can't type a fax number into the field. It has to be left

empty. Well, not exactly…

You can make a "zero length entry" (providing this feature has been enabled in the properties of the field -

in the table's design view). To do this when entering data type two double-quote marks together without a

space between, like this… "". When you leave the field the quote marks disappear and the field looks just

like any other empty field - except Access knows it contains a zero length entry. You can search for zero

length entries with a query. It is important to remember that if you make use of zero length entries, Is

Null will not find them. It regards them as a piece of text and therefore a field containing a zero length

entry is not empty, it just doesn't contain any data. Confused? Read it again then try it out - it does make

sense eventually!

Here are some examples:

Is Null

To find empty fields use the Is Null expression. This looks for fields that

contain no data. This example will find all records for contacts whose fax

number has not been recorded.


Is Not Null

To find fields that are not empty use the Is Not Null expression. This looks

for fields that contain data. If there is something in the field the record will

be shown. Note that Is Not Null will find fields containing zero length

entries. (If you want to leave them out try excluding them with the And

Not expression.) This example finds all records for contacts whose fax number has been recorded.

""

To find zero length entries use "" expression. This looks for zero length

entries in the specified field. This example would find, depending on why you

had made use of the zero length entry feature, all records for contacts who

did not have a fax.

^ top

As I said at the beginning, this is not an exhaustive list of query criteria. Many of these expressions can be

combined to create more complex criteria. You can use calculations to construct criteria. The scope is almost

limitless. Use your imagination and see what you get! Above all, remember that Access is logical. If you

don't get the result you were expecting, read the grid a line at a time (which is what Access does) and see if

it makes sense. Sometimes it helps to go and check out the SQL (the language Access uses to write the

query - SQL stands for Structured Query Language). You can view the SQL by clicking the SQL View button

on the

Using Parameter Queries


Published: 1 November 1998

Revised: 21 August 2018

Author: Martin Green

Screenshots: Access 2016, Windows 10

For Access Versions: 2007, 2010, 2013, 2016

A useful feature of the query is that it can be saved and used again and again, whenever we want to

ask the same question. The result we see (the recordset) always reflects the most up-to-date
information in the database because what you save is the question, not the answer. You just ask the

question again by running the query.


Sometimes we want to ask a question time and time again, but the details (the query's criteria) may

vary. It would be handy to have a way to run a query and make changes to its criteria without having

to design a completely new one each time. Access has a tool to solve that problem, the Parameter

Query. In fact, the parameter query can be any sort of query. You just employ the methods described

here to specify the criteria.

When you run a parameter query Access presents you with a dialog box prompting you for the

parameter value, which it enters into the appropriate criteria cell. You can have as many parameters

as you like in a single query. Here's how it's done.

Entering a Parameter
Instead of typing a value or expression into the criteria cell, type some text enclosed in square

brackets ([ ]). The text you type will appear as a prompt on a dialog box, so you might want it to be

in the form of a question to the user. In this example (Fig. 1) the user will be prompted to type the

name of the Office when they run the query. The text that the user types will be used as the criteria

for that particular field. The dialog box looks like this…

Fig. 1 When you enter a parameter into a query it asks for the criteria when the query is run.

If the user were to type London then this query would display all the records with the entry London in

the Office field. The query simply substitutes the text the user enters into the parameter dialog for the

parameter you entered into that field's criteria row. Parameters can be used for text, numbers or

dates and can be used alone or combined with normally entered criteria.

Using Multiple Parameters


You can enter a parameter almost anywhere you would place a piece of text, number or date in a

regular criteria expression. For example, supposing you wanted the query to prompt the user for two

dates to define a date range. Instead of typing the actual beginning and end dates into the criteria

cell, type a prompt in square brackets. The user will see two separate dialog boxes, each asking for a
date (Fig. 2). After receiving the dates the query proceeds, inserting the dates into the appropriate

places in the criteria expression.


In this example the query would display all the records which contained dates in the range 1 July

2005 to 31 December 2005 in the HireDate field...

Fig. 2 Access displays a separate dialog box for each parameter used.

You can use as many parameters as you want, in as many fields as necessary. The dialog boxes

appear in the same order as they do on the QBE grid.

Combining Parameters with Wildcards


A useful feature of the query is its ability to accept wildcards (i.e. an asterisk "*" representing any

string of characters; one or more question marks "?", each representing a single character). Wildcards

allow you a degree of flexibility when specifying criteria. When you don't know exactly what you are

looking for you can use wildcards to give the query a "clue". This method can also be applied to

parameter queries, but you need to do a bit more than just add an asterisk or question mark. The

correct syntax is as follows…

For a single wildcard:

Like [type prompt here] & "*"

For two wildcards:

Like "*" & [type prompt here] & "*"

When using a single wildcard it can be placed before or after the prompt. You can use asterisks or

question marks, or a combination of both.

Using a Single Wildcard

In this example a single wildcard has been used, an asterisk (Fig. 3).
Fig. 3 A parameter employing a single wildcard.

The parameter...

Like [Which Last Name] & "*"

... creates a prompt in which the user can enter the first letter or string of letters of the names they

want to see. The user has entered the text "gr" causing the query to select records with entries in

the LastName field of any length starting with the letters "gr".

Using Two Wildcards

In this example (Fig. 4) two wildcards have been used, both asterisks.

Fig. 4 A parameter employing two wildcards.

The parameter...

Like "*" & [Which Last Name] & "*"


...creates a prompt in which the user can enter a letter or string of letter that should occur anywhere

in the names they want to see. The user has entered the text "en", causing the query to select records

with entries in the LastName field of any length containing with the letters "en" together.

Being Creative with Parameters


You can enter a parameter almost anywhere you would normally enter a specific piece of data in your

query criteria. Sometimes the syntax (how you write it out) can be a bit tricky, but persevere until you

get the result you need.

Here are a few examples...

Finding records for a specific year (or month) from a collection of dates

Supposing you have a field called BirthDate containing a range of dates covering several years. Use

the following criteria...

Year([BirthDate])=[Enter a year]

...will create a prompt in which the user can type a year number (e.g. 2005) to see all the records for

people born in that year. I you would rather see records for specific months use...

Month([BirthDate])=[Enter a month from 1-12])

Note that the prompt tells the user to enter a number for the month. Access doesn't understand

month names.

Finding records for a specific month and year from a collection of dates

If you want to be more specific and call for a particular month and year, the criteria...

Month([BirthDate])=[Enter a month from 1-12] And Year([BirthDate])=[Enter a year]

...will present the user with two dialog boxes, the first asking for a month and the second asking for a

year.

Creating a list of records with dates in the last so many days

You may want to view all the invoices generated in a recent period, such as the last 30 days. The

criteria...

>Date()-[The last how many days?]


...means "today minus how many days". The user enters a number (e.g. 30) to see a list of dates

since that many days before today. The Date() part creates the current date so this query is always

up-to-date.

What about variable calculations?

You can even include parameters as part of the definition of a new calculated field. (If you want to

learn about calculating in Access check out the tutorial Calculating in Access Queries)

For example, you have a list of invoices in which there is a field called TotalGoods and you need to

calculate the discount (or tax or whatever!), but this changes from time to time. You need to create a

new calculated field to work out the new figures. Instead of...

Discount: [TotalGoods]*25/100

...which would always calculate a discount of 25% (note: unlike Excel, Access doesn't understand the

% sign as a mathematical operator). You could substitute the fixed figure with a parameter...

Discount: [TotalGoods]*[What discount rate - percent]/100

...which would prompt the user to enter a figure representing the required discount.

NOTE: When you run a query that uses a combination of parameters and calculations or functions for

the first time, Access will often re-arrange what you have written in the QBE (Query By Example) grid

of the query design window. This is so that it can compose the query's SQL correctly. The QBE grid is

merely a graphic representation of the SQL language that actually powers the query (check it out in

the query's SQL view). You can, of course, compose your criteria in this fashion yourself if you wish.
The examples I have given here are simply an easier way for you to enter the parameter criteria.

Asking the Questions in the Right Order


When you create a query using more than one parameter, the user sees the prompts in the order that

the fields are arranged in the design view of the query, reading from left to right. You normally

arrange the fields in the way in which you want to see the results displayed. But what if you want the

prompts to appear in a different order? Get to know the Query Parameters Window.

Using the Query Parameters Window

To control the order in which the prompts appear when running a parameter query containing more
than one parameter, you can specify the desired order in the Query Parameters window. Here's

how...
1. In the query design view choose right-click on the upper part of the query design screen and

choose Parameters... from the context menu to open the Query Parameters window.

2. In the Parameter column, type the prompt for each parameter exactly as it was typed in the

QBE grid.

3. In the Data Type column specify the kind of data (as defined in the table properties). Pick a

type from the list. The default type is Short Text.

4. List the parameters in the order in which you want the dialog boxes to appear when the user

runs the query.

Click OK to accept your entries and close the window.

Here is an example of a query containing two parameters (Fig. 5).


Fig. 5 Defining the order in which the parameter prompts appear.

If you didn't specify otherwise, the prompts would appear in the order that the parameters are
arranged in the QBE grid reading from left to right. The user would be asked for an Office first and

then a Department. If, however, you make use of the Query Parameters window you can choose the
order of the prompts. In this example (Fig. 5) the parameters have been arranged in a different order

so that the user is asked for a Department first and then an Office.

There is no need to make entries in the Query Parameters window if you are happy with the way the

query runs, unless you are creating a Crosstab Query containing parameters, in which case

you must enter the details of the parameters to make the query run correctly.

What if the User Doesn't Enter Anything?


There is always the possibility that the user will dismiss the parameter dialog without making an

entry, either because they don't know what they can type or because they want to see everything.

Take a look at my tutorial Parameter Queries - Handling Null Responses to find out how to prepare for

this.

What if the user Doesn't Know What to Type?


It's possible that your users won't know all the entries they can make. I'm often asked how to make

things easier for them by offering a list from which they can simply choose an item. Take a look at my

tutorial on Customizing Access Parameter Queries to find out how to do this. It shows you how to build

your own custom parameter dialog boxes powered by VBA code. You don't need any prior knowledge

of programming. The tutorial explains everything step-by-step and it's a great introduction to

programming your databases with VBA.

Dos and Don'ts for Parameter Queries


Prompts must not match field names

When you are designing a parameter query, make sure that the prompt is not exactly the same as one

of the field names. You might be tempted to enter the parameter [LastName] to prompt the user to

enter a name into the dialog box for the LastName field. This won't do! Access uses field names in

square brackets for calculations in queries so your entry will not be recognised as a parameter and will

not appear as a prompt. If you really want to use the name of the field as a prompt, a simple solution

is to turn it into a question by adding a question mark e.g. [LastName?].

Don't use illegal characters

You can type just about anything for the prompt, but you mustn't use the period (.) exclamation mark

(!) square brackets ([]) or the ampersand (&). Everything else is OK.

Don't write too much!


You are only allowed one line of text in the prompt dialog box, which amounts to about 40 to 50

characters depending on what you say. Anything extra just gets cut off. Check your work!

Download Example Database


You can download a sample database containing the completed exercise used in this tutorial. You can

also download a printable PDF of this tutorial.

To download the file right-click the icon or text link below and choose Save target as... and follow

the instructions. The database is supplied in a *.zip folder. After downloading you should extract the

database file from the zip folder before attempting to use the database. To do this right-click the zip

file icon then choose Extract All... and follow the instructions.

acctut01_UsingParameterQueries.zip [93 KB]

acctut01_UsingParameterQueries.pdf [223 KB]

Parameter Queries - What If the User Ignores


the Prompt?
The idea of a parameter query is that it offers the user some choice when they run the query (see the

tutorial on Using Parameter Queries). Instead of you having to anticipate the various combinations of criteria

that you are going to need and creating a separate query for each, you use parameters to prompt for

information when the query is run. Access does this by presenting the user with an input box into which

they type what they want to see. When they click the [OK] button the query places what the user typed

into the appropriate place in the query definition and runs the query.

But what if the user leaves the input box empty? You might expect that if the query receives no input it

would return all the records, but that isn't what happens. It returns nothing at all - an empty recordset.

It's really easy to adjust your criteria so that the query will return all the records when the user ignores the

prompt and doesn't type anything. They just click [OK] and if they want to see all the records. Here's how...

A Regular Parameter Query

Here's a regular parameter query with a prompt for the user to enter the name of the Office whose records

they want to see in the query's result...


In this example the user typed Cardiff and got the following result...

If they had ignored the prompt and left the input box empty they would have seen no results at all.

Giving the Option to Return All Records

Suppose the user doesn't know what to type, or perhaps this time would like to see all the records? All we

have to do is adjust the criteria to accept whatever the user types, or to return all the records if they type

nothing (i.e. if the input is "null").

Here the criteria have been modified to accept a null entry...

...and here's what the user saw when they left the prompt box empty...
Ignoring the prompt returns all the records thanks to the modification to the criteria. To summarise...

A parameter that requires an input from the user, otherwise no records are returned is written...

[type prompt here]

A parameter that can accept an input from the user, but that will return all records if no input is made is

written...

[type prompt here] Or Like [repeat prompt here] Is Null

As you can see, the prompt is entered twice although the user sees only one input box. It is important that

the prompt is exactly the same in both cases, otherwise Access will treat them as separate parameters and

the user will see two input boxes (although the query will probably still work!).

Variations on a Theme

If you prefer you can put the two parts of the parameter on separate lines in the query grid, like this...

Going down the column is equivalent to typing "Or" in the criteria. But I prefer to do it the other way. Doing

it this way is fine if you aren't combining the parameter with criteria on other fields. If you are, then you

have to be careful that your criteria read the way you intended. I find it's easier to put the whole thing on

one line.

TIP: Sometimes you have a lot to type in the cell of the QBE grid. You can stretch the column to

fit what you type - point to the tops of the columns where they meet. When your cursor changes

from an arrow to a black cross with horizontal arrows you can drag to the desired width or

double-click to snap to fit. An easier way is to right-click on the cell and choose Zoom... from

the shortcut menu. A large text box opens into which you can type your entry. Close the text

box to put your typing into the cell. This is great for doing those fiddly corrections to existing

entries. It works in tables too!


Just like regular parameters, these ones can be combined into multiple parameters. For example, I could

have had an additional prompt for Department which could also accept a null entry (click the thumbnail to

see the full-size image)...

Now the user can specify an Office or a Department or neither or both. Now don't tell me you don't think

that's clever!

But what would be really useful, of course, would be a custom prompt with combo boxes for users who can't

remember the names of all the offices and departments. As the man said... "You ain't seen nuthin' yet..."

Calculating in Access Queries


Sooner or later you will want to perform calculations with your data. It is not good practice to store these

calculations in tables, as part of the data itself, but to create them when needed.

The reason for this is quite logical. Providing the data itself contains everything necessary to perform the

calculation, to store the calculated result would be a waste of space. Also, should the data change, the result

would have to be recalculated and edited to remain correct.

You can create calculations in queries, reports and forms (but not in tables). Instead of making a table

containing fields for, for example, Quantity, Unit Price, and Total, we build a table containing the data

(Quantity and Unit Price) and a query showing these fields plus and additional calculated field which

multiplies the two together. No calculations are stored, saving space in the database, but the results are

instantly accessible by running the query which calculates them as they are needed.

This tutorial shows you how to perform simple calculations in a query. For more examples of calculations

look at Working with Dates in Access Queries and Calculating Totals in Access Queries.

When you perform a calculation you create and name a new field that contains the calculation you need. To

begin, open a new query in design view and specify whatever criteria you need (if any). Then…

Name the New Field


In the Field row of a new column type the name of your new field followed by a colon. For

example: New Field:

Enter the Calculation


Type the name(s) of the field(s) to be calculated using the appropriate mathematical operators (+, -, *, /

etc.). For example if you wanted to multiply two fields together: [Field 1]*[Field 2]

Here are some examples…

Calculating with Different Fields


Total Cost: [Quantity]*[Unit Price]

In this example the query creates a new field called Total

Cost and displays in it the value in the Quantity field multiplied

by the value in the Unit Price field.

Calculating with Fields and Constants


Discount Price: [Unit Price]*0.9

In this example the query creates a new field called Discount

Price and displays in it the value in the Unit Price field multiplied

by 0.9.

Note: This is the same as multiplying by 90% or subtracting 10% but the query does not recognise the %

symbol and we have to devise an alternative expression.

Formatting the New Field


When you are performing calculations, you may wish to see your results formatted in a particular way -

currency for example. Normally field formatting is specified in a the design of a table, but as the field is

being created by the query there is another way…

In the query's design view, right-click anywhere in the new field's column and choose Properties… from

the shortcut menu (or click somewhere in the new field and choose View > Properties from the menu).

This opens the Field Properties dialog box...


Click the format box, and then click the down-arrow to display a choice of formats. Choose from the list, in

this example Currency is selected. Finally, click [X] to close the dialog box.

When you run the query your new field will appear, containing the calculated values and formatted as you

specified.

Dos and Don'ts for Calculated Fields

 In new field definition what goes to the left of the colon becomes the new field's name and what goes
to the right becomes the calculation.

 Remember to follow the usual rules for naming fields (no illegal characters) and choose a name that
does not already exist amongst those in the table(s) being calculated.

 Make sure you spell the names of the calculated fields correctly (a good reason for keeping field names
short) and remember to enclose field names in square brackets.

Calculations are simple and straightforward. You just substitute field names for numbers. If you use a

calculation in a Make-Table query the new table will include a new field containing the new calculated data.

For more examples of calculations look at Working with Dates in Access Queries and Calculating Totals in

Access Queries.

Working with Dates in Access Queries


Access has a number of powerful tools to enable specific dates and date ranges to be specified in criteria.

Many tasks can be achieved with simple calculations, and there are a number of date functions to help in

performing more complex jobs.

Make sure that any fields you have that contain dates know that their data type is Date/Time. This is vital

when it comes to sorting, filtering and calculating dates. If you enter a date into a Text or Memo field it will

still look like a date to you, but Access will treat it as a string of text. You won't be able to sort into correct
date order, nor will you be able to calculate. If you have any problems with dates, check out the design

view of the table in which the dates are stored.

Simple Date Calculations


When you enter a date into an Access table, Access recognises it as a date, and checks it against the

calendar to make sure it is a possible date. You'll get an error message if you try to enter an impossible date

such 31st September or 29th February in a non leap year. Then it stores the date as a number known as

the date serial. PCs use the 1900 System to store dates. 1st January 1900 was day 1, 2nd January 1900

was day 2 etc. You don't need to know the serial number of your dates, but you can make use of it in

mathematical calculations.

Here are a few examples…

To add or subtract days from a date...

Create a new field with an expression that adds

(+) or subtracts (-) the required number from the

field containing the original date. For example…

Due Date: [Invoice Date]+60

Due Date: creates a new field called Due Date

[Invoice Date]+60 takes the date it finds in the Invoice Date field

and adds 60 to its serial number.

The result is automatically displayed as a date.*[note]

To calculate the number of days between two dates

Create a new field with an expression that subtracts the field containing the earlier date from the field

containing the later date. For example…

Stay: [Departure Date]-[Arrival Date]


Stay: creates a new field called Stay

[Departure Date]-[Arrival Date] subtracts the

date found in the field Arrival Date from the date

found in the field Departure Date.

The result is automatically displayed as a

number.*[note]

Note: If the result fails to display as a date, or displays a date in the wrong format, switch to design view

and click in the new field column.

On the menu choose View > Properties (or right-

click the field and choose Properties from the

shortcut menu). Click in the Format section on

the General tab of the Field Properties dialog box.

Click the down-arrow and choose an appropriate

format from the list. Close the dialog box and run the

query again to see your dates displayed

correctly. [back]

^ top

Using Date Functions


There are four basic date functions which extract part of a date so that it may be displayed on its own, in a

new field (further refined with criteria id required), or with additional information as part of the criteria of

the field in which the date itself occurs. These functions are…

 Year([Fieldname]) returns the year from a date e.g. 20/8/99 would return 1999

 Month([Fieldname]) returns the month from a date e.g. 20/8/99 would return 8
 Day([Fieldname]) returns the day of the month from a date e.g. 20/8/99 would return 20

 Weekday([Fieldname]) returns the day of the week form a date e.g. 20/8/99 would
return 6 representing Friday. The weekdays numbering from 1 to 7 starting with Sunday.

Use these functions in a new field if you want to display the extracted data in addition to the original date.

Remember to type the name of the new field first followed by a colon (:). When you do this you can further

refine the query by entering criteria in the new field's criteria row…

Year( )

This function is used to extract the year from

particular date.

In this example, the function is used simply to display

the year in a new field…

Year : Year([Date]) creates a new field

called Year containing data calculated from the

field [Date].

If no criteria are defined then all the records are displayed. The usual criteria for defining numbers can be

used to display specific years or ranges. For example…

1996 displays records for dates in the year 1996

only.

1996 Or 1997 displays records for dates in 1996 or

1997.

>1997 displays records for dates from 1998 onwards.

Between 1996 and 1999 displays records for dates

in the years 1996, 1997, 1998 and 1999.

If you don't need to see the extracted data separately, you can enter the function as part of the criteria of

the original date field...


The Year( ) function does not have to be used in a

separate field. It can form part of the criteria

definition. For example…

Year([Date])=1997 displays records for dates in

1997

Year([Date])>1995 displays records for dates from

1995 onwards.

Remember that you still have to include the name of

the field (in this case the field is called [Date]) within

the function, even though the criteria are typed in

that field's column.

The same applies to the Month( ), Day( ) and Weekday( ) functions. For example…

Month([Date])=9 displays records for

dates in September

Month([Date]) Between 4 and

8 displays records for dates in April, May,

June, July and August

Day([Date])=15 displays dates which are

the 15th day of the month.

Weekday([Date])=4 displays dates

which are a Wednesday.

^ top

Advanced Date Functions


Access contains a number of more sophisticated date functions for when you can't get the result you need

using a simple calculation or one of the basic date functions. Here's an example...
DateAdd(interval, number, [Fieldname])

Use this function to add (or subtract) a specific amount of time to a date. The interval part of the function

refers to the type of time unit you want to add and requires you to enter a code…

 yyyy for year

 q for quarter (i.e. 3 months)

 m for month

 d for day

 ww for week

The number part of the function refers to how many of those units you want to add. You can use a minus

number if you want to subtract from the date. Finally you need to supply the name of the field containing

the original date.

Adding or subtracting days or weeks can be performed with simple mathematical calculations as

demonstrated above. The DateAdd function makes it easy to add years, months or quarters to a date. For

example…

Date Due: DateAdd("m",2,[Date])

This expression creates a new field called Date Due into which it

enters dates from the Date field to which it adds 2 calendar months.

This query could be further refined to display records whose calculated dates fall in a specific range by

entering criteria in the new field's criteria row. For example, if these were due dates for invoices where the
payment terms were one calendar month, entering <#15/8/99# in the criteria would display only those

invoices for which payment was due

Working out Someone's Age


When storing personal data, it is normal practice to record a person's date of birth rather than their age.

This is for the simple reason that the date of birth never changes, but the age will be different next year. If

you need to show someone's age you should ask Access to calculate it for you.

Calculating someone's age from their birthday is not quite as simple as it might at first seem! Of course, you

take the current year and subtract it from the year in which they were born. This will give you an accurate

age in years - providing they have had their birthday this year. So, to be absolutely certain you need to

decide if the day and month of their birthday has passed yet.

If it has you subtract their birth year from the current year. If not, then you subtract their birth year from

the current year, and then subtract 1 from the answer. A simple calculation we do in our heads without even

thinking about it, but quite a task to explain to Access. To get Access to do it this way requires a number

of conditional statements, resulting in a very complicated formula! (Conditional statements are easier in

Excel that in Access. Take a look at the example in the tutorial Working Out a Person's Age in Excel).

Fortunately, there are a couple of easier ways. Providing you understand the results they provide and use

them accordingly, they should suit most purposes. Both examples below take birth dates from a field

called DoB and calculate an age, which is displayed in a new field called Age.

To Calculate Approximate Age


Use this expression... Age: Year(Now( ))-Year([DoB])

Age: creates a new field called Age

Year(Now( )) calculates the year number from

the current year

Year([DoB]) calculates the year number from

the date it finds in the [DoB] field

The expression subtracts the person's birth year

from the current year to give an approximate age,

but does not take account of whether or not thy


have had their birthday. It will tell you how old

they will be this year.

To Calculate Accurate Age


Use this expression... Age: (Date()-([DoB])/365.25

It makes use of the fact that Access uses serial numbers to store dates

Date( ) calculates today's date

The expression subtracts the person's birth date serial number

(which it finds in the field [DoB]) from today's serial number,

leaving the number of days in between. It then divides that

number by 365.25 (being the average number of days in a year)

to give their age in years.

In this example the field has been formatted to show two decimal

places.

Applying the Technique


You can use these expressions to calculate someone's age on a particular date, and refine the query by

adding criteria to display only the records for people whose ages fall within a particular range
This example creates a new field called Age and calculates the

person's age on a particular date, which it finds in a field

called Chosen Date. It then uses the expression <35 in the criteria

row to display only the results that calculate to less than 35.

The result is a list of people who were under 35 years of age on the

date in question.

If you want to calculate people's ages on a particular date, providing that it is the same date for everyone,

you don't need to retrieve that date from a field. Simply include it in the expression…

Age: (#01/11/65#-[DoB])/365.25 would create a new field called Age and calculate how old the people

were on 1st November 1965. Note: If the person's birth date is later than the date used in the expression

(i.e. they had not been born by that date) their age would show as a minus number!

Access and SQL


Access and SQL
Part 1: Setting the SQL Scene
This is the first in a series of tutorials demonstrating how you can use SQL to build a better database. The

tutorials are not intended to be a course on SQL. There are many excellent books and online tutorials on the

subject, some of which are listed below.

In this tutorial I review the various tasks that SQL is used for in Access and answer some of the questions

that I asked when I started to explore the language.

 What is SQL?

 What is SQL for?

 Can SQL do things that the Query Design tool can't?

 Do I need to know SQL?

 Online resources for SQL.

 Books on SQL.

What is SQL?
SQL is Structured Query Language. Some people say "Ess Queue Ell" and others prefer "Sequel". SQL has

been around for a while. It started life in the late 1970s when computer database designers needed a

language they could use to talk to the first relational databases (ones in which the data was stored in
multiple, linked tables - Access is a relational database). The first SQL standard was published by ANSI in

1986 and it has been updated several times since then. Although it is intended to be a standard language,

many "dialects" exist. Microsoft Access uses a dialect called Jet SQL whereas its cousin Microsoft SQL Server

uses T-SQL. But the core language is supported by many programs and is essentially simple and easy to

learn.

An SQL statement might look something like this:

SELECT tblStaff.* FROM tblStaff WHERE tblStaff.Gender="M";

This statement says "show me all the fields from the tblStaff table for record where the Gender field is 'M'."

This is a simple example but it illustrates how easy the SQL language is to understand.

^ top

What is SQL for?


SQL is used to interact with your database's data. Access uses SQL for many tasks: whenever a query is run

it uses SQL to filter the data; whenever a report is displayed SQL is used to gather the data to be displayed;

SQL provides the data displayed on an Access form.

Running Queries

Access provides the user with a friendly interface, the query Design View, for building queries. This view

offers a graphical representation of the query and the user requires no knowledge of SQL to use it. When

the user "runs" the query Access translates the graphical representation into an SQL statement which it

passes to its Jet Database Engine. Jet returns the appropriate data which Access displays in the query

Datasheet View.

Point at the icons below to display the Query view...

Design View SQL View Datasheet View


In this series of tutorials you will find out how to build new queries and change existing queries using VBA

and SQL, and find out why many professional database developers prefer not to use stored queries at all.

Filtering and Sorting Forms and Reports

Access forms offer the user the opportunity to filter and sort the recordset being displayed. This is done by

choosing an option from the Records menu or from the form's own right-click context menu. Several

options are available.

When a filter is chosen Access applies an SQL WHERE clause to the form's Filter property. The illustration

below shows a form being filtered and the resulting property value being shown in the form's property

sheet:

>>>

Similarly, when a sort order is chosen Access applies an ORDER BY clause to the form's Order By property:

>>>
Both of these properties can be set using VBA in both forms and reports. If fact, a report's Filter and Order

By properties can only be changed by the user in the report's design view. This series of tutorials will show

you how to write VBA and SQL code to filter and sort forms and reports at run-time and create powerful

tools for the user.

Specifying the Recordset Displayed by a Form or Report

When you build a form or report, the purpose of which is to display data, you have to specify the recordset

on which it is based. This information is stored as the object's Recordset property. Once set, this property

can not be changed by the user without going into the object's design view, which requires knowledge of

form or report design and is time-consuming.

Although you are unlikely to want to change the recordset property of a form it is common for database

users to want a standardised report design. Many people deal with this by creating multiple copies of the

report, but with VBA you can use just one report and change its recordset as many times as you want,

simply by creating an SQL statement for each report you want to display.

These are just some of the things that SQL is be used for in an Access database.

^ top

Can SQL do things that the Query Design tool can't?


Access uses SQL to perform all the data functions of its Query Design tool, including the various "action"

queries (Make Table, Append, Update and Delete). All these functions can be achieved using very simple

VBA statements combined with SQL without the need to use the query design tool or to maintain any stored

queries.

There a number of functions that can be performed using SQL that can not be represented by the QBE grid

(QBE = Query By Example) and can only be executed by creating the SQL manually, either by entering it

into the SQL view window or by using VBA. Access refers to these functions as SQL Specific Queries.

Access lists these on the Query menu:


There are three kinds of SQL Specific queries: the Union Query, the Pass-Through Query and the Data

Definition Query. Here's what they do:

The Union Query

Union queries simultaneously query separate datasets (either tables or other queries) and combine the

result into a single recordset. This is not the same as querying linked tables. The datasets must have a

similar structure (the data types must match but field names need not be the same).

For example, you might have a Suppliers table and a Customers table and you want to create a phone list

from both sets of data. If you were restricted to using the QBE grid, you would have to create two queries,

one for each table, and then combine the results. A Union query can interrogate both tables at the same

time and present the results as a single set of data.

The Pass-Through Query

Pass-Through queries are useful when you want to query data in another server-type database (such as

Microsoft SQL Server or Oracle) that has its own more powerful database engine running on a network

server. The query is designed in the SQL view of Access (or with VBA) but when run it is sent direct to the

server where it is executed, bypassing the Access Jet engine. The result is returned to Access and displayed

in the normal way, and the need to have a linked table in the Access database is eliminated.

The Data Definition Query

Data Definition queries open up a completely different range of possibilities. They are used to create and

modify tables. Fields can be defined and their properties specified. Fields and tables can be deleted.

Relationships and indexes can be created and defined.

Data Definition queries are a very powerful tool in the SQL language and can perform many tasks on the

structure of a database without recourse to VBA programming.

SQL Can Make Query Design Easier!

The QBE grid of the Query design window is a very useful tool, but sometimes it is easier to write SQL

directly into the SQL View than to try to represent it on the grid. An example of this is specifying the sort

order of a query. Here is a (very) simple example to illustrate the point:

Suppose you want to sort your query by two columns. In the QBE grid you specify the sort direction of each

column:
>>>

But Access determines the sort order by the position of the columns in the QBE and

sorts left-to-right. Here the data is sorted first by Firstname then by Lastname. But

what if you wanted to sort the other way? Rearranging the columns would mean that

the Lastname field would appear before the firstname field and you might not want

that!

The answer is to specify the correct sort order in the SQL:

>>>

The SQL SELECT clause determines the order in which the data is displayed and the ORDER BY clause

determines the sort order. Getting the desired sort order is simply a matter of editing the ORDER BY clause.

Here's how you would have to represent that in the QBE grid:
To get this result in using the QBE grid you have to enter additional columns

(remembering to uncheck their "show" option). This is a very simple example. With

multiple columns and a complex sort order there is a lot more to do.

Whatever way you do it the result is the same. The data is displayed in the

order Firstname, Lastname but the sort order is Lastname, Firstname.

NOTE: When you make changes the query's SQL view Access modifies the QBE grid to

represent the new SQL statement graphically.

^ top

Do I need to know SQL?


Most database users don't need to know SQL. Anyone who has built a anything but the most basic of queries

using the Access query design window has already been using SQL without knowing it. Criteria like: Between

#01/01/2003# AND #01/07/2003# or Not("London") or In("London", "Paris", "NewYork") are all phrases

from the SQL language.

Access contains many tools and wizards that hep the user create a powerful database without having to

learn SQL. But if you are at all serious about database building (and the fact that you are reading this

suggests that you are!) a knowledge of SQL will help you build better, more powerful databases. So my

answer is an emphatic "YES!".

But don't panic... this doesn't mean that you have to go on an SQL course right away, or even buy a book

(although there are plenty of excellent books and online resources on the subject). I started learning SQL by

building queries in the Access query design window, then looking at the SQL that was Access generated in

the SQL View. It's an excellent way to learn the basics. And if you follow this series of tutorials you should

learn enough to help you figure out the rest yourself.

^ top

Online Resources for SQL


There is plenty of SQL reference material available free on the web. Just go to your favourite search engine

and type in "SQL tutorial" for a (large) selection. I had planned to include a selection here but most of them

are pretty turgid and don't make easy reading. Whilst I suppose it isn't fair to complain when it's free, it is
often difficult to find the SQL amongst the plethora of adverts and pop-ups. Anyway, you've found me so

you're in the best place!

For an overview of SQL try W3 Schools - Learn SQL which has introductory and advanced SQL tutorials

at http://www.w3schools.com/sql/

^ top

Books on SQL
Most good books on Microsoft Access and Access development will have a section on SQL so take a look at

the Access page of my bookshelf section.

I have only one book on SQL. I bought it when I was considering a project working with both Microsoft

Access and Microsoft SQL Server, but it is an excellent and very readable introduction for the newcomer to

SQL as well as a vital reference work for anyone considering working with both programs:

SQL: Access to SQL Server

Susan Sales Harkins, Martin Reid

Apress ISBN: 1893115305

Find this book at Amazon.com (USA)

Find this book at Amazon.co.uk (UK)

^ top

What's Next?
The next tutorial in this series will explain how to incorporate SQL into VBA code:

Access and SQL Part 2: Putting VBA and SQL Together

Access and SQL


Part 2: Putting VBA and SQL Together
In the first tutorial in this series on Access and SQL I explained where SQL fitted into the overall Access

picture. This second tutorial introduces some SQL basics, those essential rules you really need to know

when working with SQL in your Access databases. This series of tutorials concentrates on combining SQL

with VBA to help build more powerful, flexible and user-friendly databases. Here's what this tutorial

contains:
 A 5-minute Course in SQL

 Getting VBA to Speak SQL

 How to Write SQL in VBA

 Working with Variables

 Debugging Your SQL Code

A 5-minute Course in SQL


Some Technical Terms

SQL contains a number of keywords such as SELECT, DELETE, UPDATE, FROM, WHERE, OR, AND,

DISTINCT and many others.

SQL keywords are usually combined with arguments in the form of table names, field names, criteria etc.

to form an SQL statement.

An SQL statement may contain one or more clauses such as a WHERE clause (containing the criteria of a

query) or an ORDER BY clause (determining the order in which a query's data is displayed).

The Structure of an SQL Statement

This illustration shows the principal parts of a typical SQL query statement:

For clarity, the different clauses are shown on separate lines. This is how the SQL view of the Access query

design window displays its SQL.

What About the Brackets?

Access uses parentheses (round brackets) to enclose the various parts of the WHERE clause but these can

be left out if you find this easier (I do!).

WHERE (((tblStaff.Office)="London”))
is the same as:

WHERE tblStaff.Office="London”

Don't Skimp on Information

Whenever a field is specified you have the option to append the table name, separating the two with a dot.

SELECT Firstname, Lastname FROM tblStaff ORDER BY Lastname

is the same as:

SELECT tblStaff.Firstname, tblStaff.Lastname FROM tblStaff ORDER BY tblStaff.Lastname

providing that the fields belong to the data source specified in the FROM clause. If your query refers to more

than one table you must include the table name along with the field name.

Data Type Qualifiers

When supplying values to an SQL statement, for example as query criteria, their data type must be correctly

defined by a "qualifier". This is done by enclosing the value between a pair of appropriate characters.

Text must be enclosed in either single quotes (') or double quotes ("), for example:

WHERE tblStaff.Department = "Marketing”

or

WHERE tblStaff.Department = 'Marketing’

A Date should be enclosed in hash marks (#) also called pound or number signs, for example:

WHERE tblStaff.BirthDate = #09/27/1950#

A number, of any sort, needs no qualifier and can be entered as it is, for example:

WHERE tblInvoices.InvoiceNumber > 1500

Get the Date Format Right

Dates in SQL must be written in the US date format (month/day/year). This is imperative regardless of

the default date format settings on your computer. The Access query design window accepts dates in your
local default format but it converts the date you type to the correct format when it builds the SQL

statement.

Remember the Semicolon!

An SQL statement must finish with a semicolon (;). If you omit the semicolon when writing an SQL

statement in the SQL view of the Access query design window your query will still work because Access

corrects the error for you! You must remember to include it when writing SQL in VBA and elsewhere.

^ top

Getting VBA to Speak SQL


VBA and SQL are different things. VBA is a full-blown programming language that you can use to get Access

(and other Microsoft Office programs) to do just about anything you want. SQL is a language used

exclusively to manipulate the data and structure of a database.

VBA calls on a vast range of objects, properties, methods, functions and constants to construct the

sometimes complex statements used to control the program. SQL uses a limited range of "keywords"

combined with information you supply, such as table names, field names and criteria, to construct

essentially simple (although sometimes long) statements detailing what you want the program to do with

your data.

SQL can often be used alone, for example when setting the RecordSource property of a form or report in

design view, or when working in the SQL View of the Access query design window. But this tutorial is about

how to combine SQL with VBA. Its aim is explain the rules of SQL and to encourage you to use good code-

writing practice and to avoid the pitfalls and problems that can occur. The way you write your code is a very

personal thing so I'm going to show you how I do things, using some techniques I have learnt from others

and some I've figured out myself. You don't have to do it my way, but it works for me so it's what I teach!

VBA is a very flexible language and there are often many different ways in which VBA can achieve the same

task. SQL on the other hand is a very precise and inflexible language. Everything has to be just so, but it

has the advantage of also being very simple. As I explained in Part 1: Setting the SQL Scene there are

several dialects of SQL. Here I will be using the Jet SQL that drives the Access database engine.

^ top

How to Write SQL in VBA


Different developers have their own ways of doing things and this is reflected in their coding style. What you

will see here is the way I like to write my code.

Use a String Variable

Whenever you write SQL into your VBA code it is important to remember that the SQL is always in the form

of a text string. SQL statements can also be quite long, and for that reason they are usually assigned to text

variables so that they are easier to handle.

When working with SQL in a VBA procedure I usually assign the SQL to a text variable and I usually name

my variable strSQL. Something like this...

Dim strSQL As String

strSQL = "... THE SQL STATEMENT GOES HERE ...”

DoCmd.RunSQL strSQL

Of course you could do away with the variable and apply the SQL directly, like this:

DoCmd.RunSQL "... THE SQL STATEMENT GOES HERE ...”

But, as you will see in the later tutorials building an SQL statement might involve several stages and many

lines of code so I usually opt to store it in a variable.

Write SQL Keywords in Upper Case

In case you haven't already noticed, I always write the SQL keywords in capitals (upper case). Access

doesn't care if you do this or not but you will find your code much easier to read and understand if you do.

Compare these two statements:

Select tblStaff.Firstname, tblStaff.Lastname from tblStaff where tblStaff.Office="Paris”;

SELECT tblStaff.Firstname, tblStaff.Lastname FROM tblStaff WHERE tblStaff.Office="Paris”;

I am using simple examples here but imagine trying to read an SQL statement that was ten times as long as

these (NOTE: the maximum length of an SQL statement in VBA is 32,768 characters!).

Enclose Field Names in Square Brackets

Okay, I'm getting picky here, but I always put square brackets around field names. Access only demands

that you do this when your field names contain spaces. For example, FirstName is OK but in your code First

Name must be written [First Name]. The brackets tell Access that all the words in the field name belong
together. They also tell Access that "this is a field name" and so allows you to use otherwise reserved words

for the names of fields (such as [Date] which is also the name of a function) without causing conflicts.

But I do this for another reason too. I know that if I see some text in square brackets I know it's a field

name, whether it has spaces in it or not...

SELECT tblStaff.[Firstname], tblStaff.[Lastname] FROM tblStaff WHERE tblStaff.[Office]="Paris”;

Write Each Clause on a Separate Line

Unless my SQL statement is very short, for example:

SELECT tblStaff.* FROM tblStaff;

I like to write each clause of the SQL statement on a separate line. This makes long statements much easier

to read and edit...

SELECT tblStaff.[Firstname], tblStaff.[Lastname]

FROM tblStaff

WHERE tblStaff.[Office]="Paris”;

When you do this in a VBA procedure you must remember that you are dealing with a single string of text,

so you must use the VBA line break character (a space followed by an underscore) and concatenate the lines

using an ampersand (&). Don't forget that each line must have both opening and closing quotes.

Alternate Single and Double Quotes

It's easy to get your quotes confused when you are constructing an SQL statement in VBA. The statement

itself needs to be enclosed in quotes because it is a VBA text string. But you might also have some text in

the SQL as criteria in the WHERE clause. If you use the same type of quote mark for each Access will get

confused. Look at this example...


There is a problem with nested quotes in the WHERE clause. Compare the two examples below. In the first

example the VBA sees two text strings enclosed by double quote marks, and between them a word it doesn't

know (Paris) so it generates an error.

But when the quote marks are alternated as shown in the second example, the problem doesn't arise. The

VBA sees a text string enclosed by double quotes, inside which is some more text enclosed in single quotes.

I working with multiple sets of quotes gets confusing, you can always use the ASCII character code for the

double quote mark - Chr(34) - instead. There is an example of this in the next section.

Putting It All Together

Some of these rules are essential, others are just my way of doing things (and that of many other database

developers). Follow them and you will write good code that is easy to read and to debug. The illustration

below shows a completed SQL statement written the way I suggest [click the thumbnail to see a full-sized

image]:

^ top

Working with Variables


In the examples I have shown so far, the criteria in the WHERE clause have been "hard-coded", written

directly into the SQL statement. But this won't often be the case. One of the main reasons for working with

SQL in your VBA procedures is that you can make changes to things.

You might be changing the criteria, fields or even data sources specified in your SQL statements each time

the code is run. The information that the SQL statement needs is often obtained from the user through their

choices in a dialog box or from the values in fields on a form. Forthcoming tutorials in this series will show

how this can be done.

Consider this simple example where the criteria value is "hard-coded" into the WHERE clause of the SQL

statement:

You want to allow the user to choose a value for the Office criteria each time the query is run, so you build a

dialog box in which there is a combo box containing a list of Offices. The combo box is named cboOffice. You

can insert a reference to the value of the combo box directly into the SQL statement:

Alternatively, you can place the value of the combo box into a variable and then insert the variable into the

SQL statement:

Using a variable can make the SQL statement code easier to read and understand, especially when there are

several variable criteria to consider. It is sometimes essential to use this method when the value has to be

examined or manipulated in some way before it is passed to the SQL.


Whatever method you choose, you must remember to include any necessary data type qualifiers in the SQL

string. In the illustration below, a single quote mark is included in the SQL string either side of the text

variable (marked with red arrows):

Alternatively, the ASCII character code for the double quote mark (Chr(34)) can be inserted either side of

the text variable, but outside the SQL string. This method requires more typing but avoids conflicts and

confusion arising from nesting quotes.

Remember that as with "hard-coded" criteria, variables require the correct qualifiers for their data type:

quotes for text, hash-marks (pound signs) for dates, and nothing for numbers.

^ top

Debugging Your SQL Code


As with any other sort of programming, you SQL code can fail to work properly. It might be because you

made a logic error, or got the SQL syntax wrong, or perhaps you just made a typo. If this results in

workable code it might not produce the result you were expecting. Most often though the result is code that

won't run and Access throws up an error. Your job is to figure out what went wrong and put it right.

If you are running an SQL statement from within a VBA procedure you will see the regular Visual Basic error

message. Sometimes these are quite difficult to interpret.

A trick used by many Access developers is to test their SQL in the SQL View of the Access query design

window. If there is a problem Access will display an error message. The error messages you get from the

SQL View of the Access query tool are often far more helpful and descriptive that those you get from VBA!

SQL Error Messages

It pays to familiarise yourself with the different sorts of message so that you can quickly trace the source of

code errors. Most SQL errors are syntax errors. The Jet database engine can't interpret the SQL statement

because it doesn't make sense. These often arise from simple typographical errors or omissions so it is

important, as with most computer programming, to take care when typing! Here are some examples (NOTE:

the red arrows are mine - the messages aren't that helpful!):
The most common type of error is the "missing operator". You would normally understand the term

"operator" to mean a mathematical symbol such as =,> or < but when an Access SQL error message uses

the expression "missing operator" you should look for something left out of the statement. It could be a

mathematical operator but it might also be a word like AND or OR, or perhaps a quote mark. Here the quote

mark before the word Brussels is missing:

Sometimes error messages are more specific and point you directly to the clause that is causing the

problem. Here the word "BY" has been omitted from the ORDER BY clause of the SQL statement:

If you misspell the name of a database object such as a table, or refer to one that doesn't exist, the Jet

database engine will not recognise it. These errors are usually quite easy to trace:
Not really an error message, but a response to an error in the SQL statement. If you type the name of a

field incorrectly, the Jet database engine sometimes interprets your SQL as a parameter query and displays

the familiar parameter dialog. Although this might seem confusing at first, it is usually quite easy to

diagnose and correct because the offending name is displayed on the dialog. You just need to find it in your

code and correct it:

VBA Error Messages

When you try to run a faulty SQL statement from VBA it will usually generate a run-time error resulting in a

error VBA message like this one:

The error messages are delivered in the usual way using the Visual Basic error message dialog and don't

explicitly state that the problem with your code lies in your SQL statement, although this is usually evident

from the message itself. Pressing the Debug button will take you to the offending line of code but this may

not be the SQL statement itself. Here are some examples:


In the example above the code crashed when it tried to apply the SQL statement to a stored query. Access

checked the SQL statement before applying it to the query and found a syntax error ("ORDER" should be

"ORDER BY") and highlighted the code line that it could not execute, rather than the one that contained the

error.

In the next example, it is clear that there is a spelling error somewhere but it might take a while to find:

Here, the error lies in the misspelling of a table name in the SELECT

clause of the SQL statement but the error did not arise until the code

tried to run the query and the specified table could not be found.

The next example must get the prize for the most confusing error message that Access has to offer!

I have seen it in two different circumstances. It is displayed if an error occurs because, from a VBA

procedure you ran a parameter query - or a query that thought it was a parameter query and so displayed

the parameter dialog (see the example above where the field name was spelled incorrectly) - and you

clicked the parameter dialog's Cancel button. In normal circumstances that would be OK but if as a result

your code crashed, the "You canceled the previous operation." message is displayed and, I suppose, it

makes sense.
But this message also appears when there is a different kind of error, when you attempt to use an SQL

statement from VBA in which the data type of a WHERE clause's criteria does not match the data type of the

corresponding field. For example you might have a date field and supply a text data type: WHERE

tblStaff.BirthDate='Tuesday’ instead of matching the data types correctly: WHERE

tblStaff.BirthDate=#09/27/1950#.

Include an Error Handling Routine

Finally, remember that a faulty SQL statement can crash your VBA code so remember to test your

procedures thoroughly and always include an error handling routine.

^ top

What's Next?
Enough theory! Now it's time to see some SQL code in action. The next tutorial is a practical session

showing you how SQL can build and modify tables and records:

Access and SQL


Part 3: Some Practical Examples
Here are a few examples of SQL being implemented from VBA. Future tutorials in this series will expand on

these techniques, and show you more things you can do with SQL in your Access databases.

Using DoCmd.RunSQL to Run Action Queries


The DoCmd object in VBA can be used to perform a wealth of different actions including one called RunSQL.

You can't run any sort of SQL statement using the RunSQL method, it is specifically for running the type of

queries that Access calls "action queries". These include Delete Queries (used to delete records from a

table), Append Queries (used to add records to a table), Update Queries (used to edit records in a table)

and Make Table Queries (used to create a new table).

Working from the Access query design window you are limited to the four query types described above, but

using SQL in conjunction with VBA (or by entering SQL directly into the SQL view of the Access query design

window) you can accomplish a lot more, including the use of "data-definition queries" which are used to

build and modify the structure of the database itself.

The RunSQL method prompts you for two arguments, the SQL Statement itself which must be supplied as

a string (i.e. it should be enclosed in quotes) and Use Transaction which is optional:
The second argument concerns Transaction Processing and assumes True if you omit it. When transaction

processing is applied to a query, Access first performs a "dry run" of the query during which it writes all the

changes to a temporary log file but does not make any permanent changes to the database. If the query

finishes its task without any problems, the changes noted in the log file are applied and the job

(the transaction) is completed. If, however, Access encounters problems whilst executing the query, the

transaction terminates and the log file is discarded without any changes being made. Transaction processing

is a very useful safeguard for your data and should always be applied, unless you have a particular reason

not to do so.

The following exercises demonstrate some of the things you can do. You can work in an existing database or

create a new one.

Build a New Table

1. Open a VBA code window (Access 2000/2002: use Alt+F11, Access 97: go to the Modules tab and
click New).

For the purpose of this exercise, you will run the code directly from the Immediate Window. This
window allows you to implement a code statement directly by typing in into the window (it has to be
typed as a single line) and pressing Enter.

2. Open the Immediate Window by pressing Ctrl+G. In Access 2000/2002 the Immediate Window will
appear, usually docked to the lower edge of the Visual Basic Editor window. In Access 97 a new
window (the Debug Window) will appear, divided horizontally into two sections - use the lower
section of this window.

3. Enter the following line of code as a single line, then press Enter:

DoCmd.RunSQL "CREATE TABLE tblTest ([StaffID] COUNTER


CONSTRAINT ndxStaffID PRIMARY KEY, [FirstName] TEXT(25),
[LastName] TEXT(30), [BirthDate] DATETIME);"

4. Switch to the Access database window (press F11) and move to the Tables tab. (NOTE: if the Tables
tab was already displayed, refresh the view by switching to a different tab then back to the Tables
tab again). You should see that a new table (tblTest) has been created.

Take a look at the table. You will see that it contains the four fields specified in the SQL statement:
Switch it into design view and see that the data types are as specified, with the text field of a specified size,

and that the StaffID field is an autonumber field and also the primary key field:

If you try to run the same line of code again an error occurs because a table with the specified name already

exists:

A method for dealing with this eventuality will feature in a later tutorial in this series.

Add Records to a Table

Lets add some data to the table. Close the table if it is open, and return to the Immediate Window.

1. Enter the following line of code as a single line, then press Enter (feel free to insert your own
details!):

DoCmd.RunSQL "INSERT INTO tblTest ([FirstName], [LastName], [BirthDate])


VALUES ('Martin', 'Green', #09/27/1950#);"

Note the single quote marks around the text values Martin and Green, and remember
that single quotes are used here so as not to conflict with the double quotes that enclose the
complete SQL statement. Note also that the date value is enclosed by hash marks (#) and that the
date is supplied in US (m/d/y) format.

2. Switch to the Access database window and open the table. You will see your new record. Because
the StaffID field is an autonumber field its value is assigned automatically. It does not need to be
specified in the SQL.
Before the SQL is executed, Access displays a message asking permission to add a record to the table. This

is usual when Access performs any action query, and can be suppressed with VBA code if you don't want

your users to see it:

If the user decides not to append the record they can click the No button and the action is cancelled without

any further consequences, but when this happens when the SQL statement is being run from VBA an error

occurs:

So, if you want to give the user the option to cancel the record, your code will have to handle the error

when it arises.

Add a Field to a Table

SQL can be used to make changes to the structure of an existing table. Fields can be added, removed or

changed. Here's how to add a new field. Close the table if it is open, and return to the Immediate Window.

1. Enter the following line of code as a single line, then press Enter:

DoCmd.RunSQL "ALTER TABLE tblTest ADD COLUMN [Age] BYTE;"


2. Switch to the Access database window and open the table. You will see a new field has been added.

If you take a look at the table's design view you will see that the SQL has also assigned the byte data type

to new field.

Modify Existing Records

In addition to working with the structure of a table, SQL can be used to modify existing data. You may have

used an Access Update Query. This example uses the same method from VBA. We have added a new field to

the table, now we can enter some data.

First, an example of updating specific records by adding a WHERE clause to the SQL statement:

1. Enter the following line of code as a single line into the Immediate Window, then

press Enter (substituting the appropriate criteria to your FirstName and LastName fields):

DoCmd.RunSQL "UPDATE tblTest SET [Age]=52 WHERE [FirstName]='Martin'

AND [LastName]='Green';"

As when you added records to the table, Access displays a confirmation message when records are about to

be updated. Remember that cancelling the update will raise an error in VBA.

If you do not include a where clause the SQL UPDATE statement will modify all the records in the table. This

may be appropriate if you want to apply the same value to all the records, or if you want to calculate a value

making use of existing data:

2. Enter the following line of code as a single line into the Immediate Window, then press Enter:
DoCmd.RunSQL "UPDATE tblTest SET [Age]=Int((Date()-[BirthDate])/365.25);"

Instead of applying a specific value, this SQL statement performs a calculation on each record making use of

the value already present in the BirthDate field and the Access Date() function to calculate each person's

age:

NOTE: It isn't good database practice to store calculated data in a table that also contains the data from

which it was calculated. Why? Mainly because it wastes space. If you know a person's birth date you can

calculate their age at any time using a query. Also, if you store their age as a number it will not update itself

as time passes, so eventually it will become incorrect. But I was stuck for an idea so I allowed myself an

exception to the rule!

Delete a Table

It is just as easy to delete things with SQL as it is to create them. Records can be deleted, as can fields and

even entire tables:

1. Enter the following line of code as a single line into the Immediate Window, then press Enter:

DoCmd.RunSQL "DROP TABLE tblTest;"

You won't see any warning message, but you will find that the table has gone (a bit too easy for comfort!).

You might need to refresh the database window as described earlier before the table's entry is removed.

Summary
These practical examples have demonstrated how you can manipulate a database's structure and its data by

implementing SQL statements with VBA, working independently of the Access query tool. They show the

potential of working directly with SQL from your VBA procedures to build, modify and populate tables with

ease.

I have shown only a few examples of what can be done. SQL is capable of a great deal more. Future

tutorials in this series will explore practical uses for these techniques, as well as the more familiar uses of

SQL to interrogate data and supply the user with information.


What's Next?
The next tutorial in this series illustrates how you can build and modify queries "on the fly" by working

directly with the query's SQL, and includes a practical project to build a multi-purpose query:

Access and SQL Part 4: Building Queries "On the Fly"

Access and SQL


Part 4: Building Queries "On the Fly"
This tutorial is about using SQL to build queries "on the fly" (or as we propellorheads say: "at run-time").

This means that you build the queries when you need them, rather than trying to anticipate the user's needs

and preparing them in advance.

The tutorial will deal with regular "select" queries, ones which filter the source data and show the result to

the user (as opposed to "action" queries which manipulate the data in some way).

You can download a copy of the database used in this tutorial. It contains completed examples of the forms,

queries and code described in the tutorial. Follow the link at the bottom of this page. The database contains

a table listing the details of the staff of a fictional multinational company. It contains the sort of personal

details you might expect such as FirstName, LastName, BirthDate and Gender together with business details

such as Office, Department, JobTitle and Email.

This tutorial is in two parts. This first part will show you how to create a fully working multi-purpose query.

The second part will explain how to add some refinements to create a really professional query tool. You will

find a link to the second part of the tutorial at the bottom of this page.

Why Build Queries on the Fly?


Like many Access developers, I tend to create very few stored queries. Instead I try to create a small

number of general purpose queries that can be changed on demand to suit the user's requirements. This

allows my databases to be much more flexible and I don't have to guess what the users might want to

know. I can then use switchboards and dialog boxes to gather the information from the user which is used

to create the query's SQL statement. The user is really building a query themselves but they don't need to

know anything about Access to do it.

Another important reason for working this way is that inquisitive (or careless!) users might delete or change

stored queries, and not know how to replace them.

Building a Multi-Purpose Query


The Plan...

The aim of this project is to create a single stored query whose criteria can be changed to suit the user's

requirements. The query will be opened when the user clicks a button on a dialog box. The dialog box will

also be used to gather the criteria from the user.

The Stored Query

You need a query that can be used as the basis of our multi-purpose query. Its design is completely

irrelevant because it is going to be changed each time it is used, but Access doesn't let you create an

"empty" query so you'll have to make something up (anything will do).

Ask Access for a new query in design view, add a table, put a field into the grid and then close and save the

query. Give it a sensible name - in this example I am calling the query qryStaffListQuery.

The Dialog Box

I have decided that my users will probably want to query on three different

criteria: Office, Department and Gender. So, the first step is to build a dialog box to help the users specify

their criteria. It's up to you what your dialog box looks like and what it contains. I have chosen to use

combo boxes showing lists of all the possible criteria choices. Future tutorials in this series will show

examples of other design methods.

If you are not experienced in building dialog forms like this one, take a look at my tutorial Customizing

Access Parameter Queries which contains detailed step-by-step instructions.

Point at the labels next to the combo boxes in the illustration below to see what is contained on their lists...
So, now we have the required components of the multi-purpose query. The next step is to write the code to

make them work together.

Writing the VBA and SQL Code


When the user clicks the dialog's OK button several things must happen:

 Gather the user's choices from the combo boxes and write them into an SQL statement.

 Apply the SQL statement to the stored query.

 Open the stored query.

 Close the dialog box.

The code to carry out these operations will run on the On Click event of the OK button.

In form design view right-click the OK button and choose Properties to open its properties window and

locate On Click on the Events tab. Click in the white bar then click the Build button: . In the Choose

Builder dialog select Code Builder and click OK. You are now ready to write the code...

Declare and Fill the Variables

The first few lines of code establish contact with the database, telling Access that we are referring to the

current database (i.e. the one containing the code) and identifying the query that we are going to work on.

In addition, a string (i.e. text) variable is declared, which I have called strSQL. It will hold the SQL

statement that will be applied to the query:

Dim db As DAO.Database

Dim qdf As DAO.QueryDef

Dim strSQL As String

Set db = CurrentDb

Set qdf = db.QueryDefs("qryStaffListQuery”)

NOTE: I am using DAO language here because I think it is simpler for this sort of work. DAO is

the default for Access 97, but the default for Access 2000/2002 is ADO. Access 97 users need do

nothing (you can omit the "DAO." bits if you want but it doesn't really matter) but Access

2000/2002 users need to set a reference to DAO so that their database understands the code. In

the Visual Basic Editor go to Tools > References. In the dialog box scroll down to "Microsoft

DAO 3.x Object Library" (where x is the highest number if you have more than one) and put a

tick in the box. Click OK to set the reference. You only need to do this once for the database and

any code you put in it will be able to use the reference.


Build the SQL Statement

Next comes a VBA statement which places a text string into the strSQL variable. There was a detailed

explanation of how I like to write my VBA/SQL and the rules you need to know in the second tutorial in this

series: Access and SQL Part 2: Putting VBA and SQL Together.

This code combines SQL keywords and clauses into which have been placed references to the combo boxes

that contain the user's criteria choices:

strSQL = "SELECT tblStaff.* ” & _

"FROM tblStaff ” & _

"WHERE tblStaff.Office='" & Me.cboOffice.Value & "’ ” & _

"AND tblStaff.Department='" & Me.cboDepartment.Value & "’ ” & _

"AND tblStaff.Gender='" & Me.cboGender.Value & "’ ” & _

"ORDER BY tblStaff.LastName,tblStaff.FirstName;”

Although it doesn't seem to make a readable SQL statement as it is, when Access reads the code and

substitutes, for example, Me.cboOffice.Value with London a sensible SQL statement results.

Here is what your code should look like so far [click the thumbnail to see a full-sized image]:

Test the Code

Now is a good time to test the code you have written so far and there are a couple of ways you can do this.

You can "print" the SQL to the Immediate Window or you can display it in a message box (or you can do

both!). Here's what to do:

Using the Immediate Window:

In Access 97 the Immediate Window is called the Immediate (lower) pane of the Debug Window. In all

versions the window can be displayed by pressing Ctrl+G from the VBA code window. First, add the

following line of code before the End Sub line of your cmdOK_Click procedure:

Debug.Print strSQL

Using a Message Box:

Add the following line of code before the End Sub line of your cmdOK_Click procedure:
MsgBox strSQL

Now you are ready to run a test. Switch to Access and open your dialog box in Form View (now is a good

time to save the form!). Make some choices from the combo boxes and click the OK button...

If you chose to use a message box it will open displaying the SQL string that your code created from the

choices in the dialog box. Read the SQL statement to check that it makes sense:

If you chose to use the Immediate Window, switch to the VBA code window and press Ctrl+G to open the

Immediate Window where the SQL string will be displayed (it is written in a single line):

The message box method is quick and, because I am familiar with SQL, it is my preferred method. Using the

Immediate Window has the advantage that you can select and copy from it the SQL statement that your

code generated and paste it into a query to test it. To do this first select and copy the SQL statement then

return to the Access database window and choose Create query in design view. Close the Show

Table box and open the SQL window by choosing View > SQL View. Delete any entry that is already there

(it usually shows SELECT;) and paste in your SQL statement. Run the query and check the result.
If you see an error message when you try to run your SQL you can trace the error and correct it. Read the

section on "Debugging Your SQL Code" in Part 2 of this series. When you are satisfied that this part of your

code is working properly, you can remove the line(s) Debug.Print strSQL or MsgBox strSQL.

Update and Open the Query

All that remains is to apply the SQL statement to the stored query that you saved earlier, and to close the

dialog box.

Enter the line:

qdf.SQL = strSQL

to apply your new SQL statement to the stored query. There is no need to give a command to save this

change to the query because this happens automatically. Next add the lines:

DoCmd.OpenQuery "qryStaffListQuery"

DoCmd.Close acForm, Me.Name

to open the query displaying the results of the user's criteria choices, and to close the dialog. Finally add the

lines:

Set qdf = Nothing

Set db = Nothing

which empty the variables used to identify the query and the database. All variables normally lose their

values when a procedure finishes, and the memory that was allocated to them is freed. But sometimes

Access forgets to clear "object" variables (those with which you have to use the "Set" keyword) so it is good

coding practice to clear them manually like this, just to make sure. Your finished code should look like this

[click the thumbnail to see a full-sized image]:

Return to the Access database window and save the dialog box form (to save your code changes).

Job Done!
Your Multi-Purpose Query is now ready to run. Remember that the query should be run from the dialog box.

If you open the stored query it will display records using the same criteria as the last time it was run.
The Multi-Purpose Query will work fine as it is, but you can make it even more user-friendly with the

addition of a few refinements. The second part of this tutorial shows you how to turn your multi-purpose

query into a really professional tool.

Go to the next part of the tutorial >>>

Access and SQL


Part 5: More Instant Queries
In the last part of this tutorial I showed how you could build an all-purpose query by supplying the user with

a dialog box from which they could choose their query criteria. The dialog box had combo boxes displaying

criteria choices and when the user clicked a command button their choices were used to construct an SQL

statement. The SQL statement was then applied to a stored query and the user saw the result of their

choices without having to get involved with query design. This tutorial takes the idea a stage further.

Offering More Choices


Instead of combo boxes I am going to use list boxes. List boxes can offer the user the ability to choose more

than one item, a property known as Multi Select. I am also going to allow the user to specify either "And" or

"Or" conditions. Here's how my dialog box looks...

The user makes their choices from the list boxes and option buttons and clicks the OK button to see the

result. In the example below the user has asked for a list of all the Female staff working in

the Design departments of the Nice and Paris offices [click the thumbnail to see a full-sized image]:

Building the Dialog Box


The first job is to build the dialog box. I have decided to allow the user to query three

fields: Office, Department and Gender. You will see that the code that powers the dialog box is easily

modified to suit any number of fields.

I am not going to describe the form building process in detail so, if you are not experienced in building

dialog forms like this one, take a look at my tutorial Customizing Access Parameter Queries which contains

detailed step-by-step instructions.

This illustration shows the names I have given to the various objects on the dialog box...

There are three list boxes, one for each field, and two pairs of option buttons. The option buttons allow the

user to choose whether the AND operator or the OR operator is used when combining the criteria.

Setting Up the List Boxes

Building the Lists

The Row Source property of a list box (likewise a combo box) defines what makes up the list of items

displayed. You can specify the name of a table containing the list of items, or a query which creates a

suitable list, or you can simply type the list directly into the properties sheet of the list box (this is called

a Value List).

For the first two fields I decided to use a query. You can create a stored query for this and enter its name

into the Row Source property textbox but, unless you plan to use the query for something else, it is a better

idea to enter an SQL statement directly into the Row Source. (Why? There's always a chance that the stored

query will get changed or deleted by someone, then your dialog won't work. It's neater too, we access-

heads like neat-and-tidy!).


Since this tutorial is about SQL we might as well take a look at the SQL statement I used.

SELECT DISTINCT tblStaff.Office FROM tblStaff ORDER BY


tblStaff.Office;

The SQL statement queries the same table that contains the data I am querying with my dialog box. The

"SELECT DISTINCT" clause instructs Access to return only one of each of the items it finds. Without this a

simple "SELECT" clause would return an Office name for each record in the table. The "ORDER BY" clause

tells Access to return the values sorted into alphabetical order.

This method gives me the advantage that should any new Offices or Departments be created as data is

added to the table, the SQL statement (which is run each time the dialog opens) will ensure that the list box

always displays an up-to-date list. There is a possible disadvantage to using this method. If the table on

which the SQL statement is based contains a very large number of rows, or if you are using a large number

of list boxes, this could seriously slow down the opening of the dialog box as it waits for the queries to be

processed. In this case I would opt for using a purpose made table for the Row Source of each list box.

TIP: You don't need to write the SQL statement into the Row Source yourself. Click on the Row

Source textbox and the properties window will display a build button to the right of the

textbox. Click the build button to open the Query Builder tool. This works just like the familiar

Access query tool. Use it to construct and test your Row Source query. When you close the

Query Builder it returns a ready-made SQL statement to the Row Source textbox.

Since the Gender list box will contain only two items ("Male" and "Female") there is little point in using an

SQL statement so instead I have used a Value List as the Row Source. To do this type the values directly

into the textbox, separating the items with a semicolon (;). The actual values in the Gender field are "M"

and "F" so, to help the user, I have created a list containing two columns. When creating a multi-list column

using this method enter the values for each row of the list together, e.g. M;Male;F;Female. Here's how it

looks on the properties sheet...


You must also specify which column of a multi-column list is the Bound Column, meaning which is to be

taken as the "value" of the list box. Here it is the first column, containing the letters "M" and "F". The words

"Male" and "Female" are just there to help the user decode the letters (and don't think I'm joking here! More

than once I've had to explain to a user what they meant... "I can type M for Man but it won't accept W for

Woman". No, not dumb - just not thinking the same way as me. Okay, I'm being kind, I mean dumb!)

It is also necessary, with a multi-column list, to specify how many columns are to be displayed and how

wide the columns should be. If you don't do this you will only see one column.

I have specified a Column Count of 2 and Column Widths of 0.5cm and 1.5cm. Note that when using metric

measurements Access sometimes rounds the values to the nearest imperial equivalent, hence 0.503cm and

1.501cm.

TIP: You can create a multi-column list using the Query Builder too. Just design a query that

returns more than one column, then specify Bound Column, Column Count and Column

Widths as described above.

Enabling Multi Select

Unlike combo boxes, list boxes can offer the facility to select more that one item from the list. But you must

enable the Multi Select feature for this to be possible. The Multi Select property is located on the Other tab

of the list box property sheet.


I have chosen to enable "Simple" multi select. In this mode the user clicks an item to select it, whilst

clicking a selected item deselects it. They can select as many items as they want by clicking on each in turn.

You can also choose "Extended" multi select, which allows you to drag down the list to select a group of

items, or use click and shift-click to select a continuous group, or click and control-click to select a non-

continuous group.

Preparing the Option Buttons

I have used option buttons to offer the user the choice of "AND" or "OR" when composing the query. I want

each pair of option buttons to work as an option group, so that only one member of the group is selected at

a time. When one button is selected the other gets deselected.

Normally this is achieved on an Access form by placing the option buttons inside a frame. Doing this

automatically forces them to work as an option group. But I didn't want to draw frames on my dialog so I'm

relying on code to take care of the "group" activity. I did consider using a frame formatted in such a way as

not to be visible but sometimes I just like doing things the hard way!

If you don't specify default values for option buttons they start with a "Null" value (neither True nor False).

This can confuse the user, and will confound my code if they don't change the situation by making a choice.

For this reason I have set the Default Value property of each "And" button to True and that of each "Or"

button to False.

The Stored Query


Create and save a query. Anything will do because the dialog box code is going to change it anyway. I have

called my query qryStaffListQuery.

Writing the VBA and SQL Code


The following section explains step-by-step what code is needed and how it works. You can write the code

yourself, or copy it from the code listing below and paste it directly into the Access code window.

Coding the Option Buttons

An option button can have the value True or False. Each option button needs some VBA attached to

its OnClick event that says: "If my value is True make my partner's value False, but if my value
is False make my partner's value True." A simple VBA If Statement will do it. A procedure like this is needed

for each option button:

Private Sub optAndDepartment_Click()


If Me.optAndDepartment.Value = True Then
Me.optOrDepartment.Value = False
Else
Me.optOrDepartment.Value = True
End If
End Sub

TIP: When you have to repeat blocks of code like this, it makes sense to copy and paste then

change the appropriate bits, but sometimes this can be quite fiddly. Instead, make use of the

Visual Basic Editor's Replace tool (Edit > Replace or Keys: Ctrl+H). Paste a copy of the original

procedure then select it and open the Replace tool. Make sure that Selected Text is chosen and

enter the original text item into the Find What textbox and the new item in the Replace

With textbox. Click the Replace All button and your new procedure is written for you...

Coding the OK Button

Everything else runs on the OnClick event of the OK button. When the user clicks the OK button the code

must assemble an SQL statement based on the user's choices from the list boxes and option buttons. It

must then apply the SQL statement to the stored query and open the query so that the user can see the

result.
Start by declaring the string variables that are going to hold the information collected from the dialog box.

In my example they are as follows:

This variable will be used to collect the selections from the list boxes:

Dim varItem As Variant

These variables will hold the collected choices from each list box:

Dim strOffice As String


Dim strDepartment As String
Dim strGender As String

These variables will hold the And/Or choices from the option groups:

Dim strDepartmentCondition As String


Dim strGenderCondition As String

This variable will hold the completed SQL statement:

Dim strSQL As String

Getting the Choices from the List Boxes

The code must look at each list box in turn and find out which items have been selected by the user. It's

possible that the user might not select anything at all. If this happens I will assume that they want to see

everything (i.e. selecting nothing will have the same effect as selecting everything).

NOTE: I'm assuming that the data being queried is text. If the data is non-text such as numbers

or dates, some small but important modifications have to be made. Read the note about data

types near the bottom of the page.

The following code uses a For...Next loop to gather all the selections from the lstOffice list box and join them

together, separated by commas, into a text string. The text string is stored in a variable called strOffice...

For Each varItem In Me.lstOffice.ItemsSelected


strOffice = strOffice & ",'" & Me.lstOffice.ItemData(varItem) _
& "'"
Next varItem

The next step is to check the length of the resulting string. If its length is zero, it means that the user didn't

select anything. So the next section of code takes the form of an If Statement which builds a suitable

criteria string representing the users choice...


If Len(strOffice) = 0 Then
strOffice = "Like '*'"
Else
strOffice = Right(strOffice, Len(strOffice) - 1)
strOffice = "IN(" & strOffice & ")"
End If

If the user selects nothing the code inserts a wildcard into the criteria clause, resulting in something like

this:

WHERE tblStaff.[Office] Like '*’

But if the user selects one or more items the code creates an IN clause (after removing the leading comma

from the string) like this:

WHERE tblStaff.[Office] IN('Amsterdam’,'London’,'Paris’)

This complete process is repeated for each list box and the resulting criteria are stored in separate variables.

I called my variables strOffice, strDepartment and strGender.

Getting the Choices from the Option Buttons

The code needs to find out whether the user chose And or Or from each pair of option buttons. It is only

necessary to look at the value of one button from each pair since I know that if one's value is True then the

other must be False, and vice versa. Again an If Statement does the job...

If Me.optAndDepartment.Value = True Then


strDepartmentCondition = " AND "
Else
strDepartmentCondition = " OR "
End If

Each set of option buttons requires an if statement like this. My dialog has two sets. The If Statements

create suitable strings representing the specified conditions and stores them in separate variables. Mine are

called strDepartmentCondition and strGenderCondition.

NOTE: Notice that there are spaces each side of the text in the condition strings " AND " and "

OR ". The following section of code also includes spaces as part of the string. It is very important

when combining hard-coded text and variables that you remember to include spaces where they

will be needed in your finished SQL string.


Building the Query's SQL Statement

Having gathered all the information from the dialog box, it's now time to build the SQL statement. I have

defined a string variable called strSQL to hold it. Here's how the code builds the SQL statement

incorporating hard-coded SQL with the variables created in the previous section...

strSQL = "SELECT tblStaff.* FROM tblStaff " & _


"WHERE tblStaff.[Office] " & strOffice & _
strDepartmentCondition & "tblStaff.[Department] " &
strDepartment & _
strGenderCondition & "tblStaff.[Gender] " &
strGender & ";"

Test the Code

Now is a good time to test the code you have written so far. An easy way to do this is to display the SQL

statement in a message box. Do this by adding the line:

MsgBox strSQL

Alternatively you can "print" the SQL statement to the Immediate Window of the Visual Basic Editor by

adding the line:

Debug.Print strSQL

Before you run your code check that you have everything you need by comparing it with Code Listing 1 then

compile it with Debug > Compile...

If the dialog box is in Design View switch it into Form View and click Save, then make some choices from

the list boxes and option buttons and click the OK button. If you asked for a message box it will be

displayed now, and you will be able to read the SQL statement. Or you can return to the Visual Basic Editor

to view the resulting SQL statement in the Immediate Window (choose View > Immediate Window or

Keys: Ctrl+G).
This gives you the opportunity to make sure that your code is working correctly before proceeding to the

next step.

Apply the SQL Statement to the Stored Query

All that remains is to apply the SQL statement to the stored query and open the query for the user to view

the results. There are two ways to write the code for this, depending on whether you want to

use DAO or ADO. If you use Access 97 you must use the DAO method. If you use Access 2000 or later you

can use either.

Using DAO (Access 97):

If you are using Access 97 you have to write the code this way, but if you use Access 2000 or later you can

use DAO instead of the default ADO if you prefer (many Access developers prefer DAO because of its simpler

coding style). If you are not using Access 97 you need to set a reference to DAO. To do this, in the Visual

Basic Editor go to Tools > References and scroll down the list until you find Microsoft DAO 3.6 Object

Library (if you can't find 3.6 use the highest number available). Place a tick in the check box and click OK.

Two additional variables must be declared (Note: Access 97 users can omit the DAO. prefixes, but the code

will work fine if they are left in place.):

Dim db As DAO.Database
Dim qdf As DAO.QueryDef

Here is the code which will apply the SQL statement to the query:

Set db = CurrentDb
Set qdf = db.QueryDefs("qryStaffListQuery")
qdf.SQL = strSQL
Set qdf = Nothing
Set db = Nothing

The first two lines tell Access that we are talking about the current database, and name the query we are

concerned with. The third line applies the new SQL statement to the query. The last two lines empty

the db and qdf variables to clear the memory.

Using ADO (Access 2000 onwards):

Because this code deals with database structure, you need to set a reference to ADOX. To do this, in the

Visual Basic Editor go to Tools > References and scroll down the list until you find Microsoft ADO Ext.

2.x for DDL and Security (where x is the highest number you have). Place a tick in the check box and

click OK.

Two additional variables must be declared:

Dim cat As New ADOX.Catalog


Dim cmd As ADODB.Command

In ADO the information about the structure of a database is stored in its Catalog. The Command object in

ADO is similar to the QueryDef object in DAO. Here is the code which will apply the SQL statement to the

query:

cat.ActiveConnection = CurrentProject.Connection
Set cmd = cat.Views("qryStaffListQuery").Command
cmd.CommandText = strSQL
Set cat.Views("qryStaffListQuery").Command = cmd
Set cat = Nothing

The first line tells Access that we are talking about the current database, and the second line identifies our

stored query (in ADO a query is a View or a Procedure depending on its type). The third line applies the new

SQL statement and the fourth line saves the changes back to the catalog. Finally, the catalog variable is set

to Nothing to clear the item from the memory.

Open the Query and Close the Dialog Box

The code to open the query and close the dialog box is the same for DAO ad ADO:

DoCmd.OpenQuery "qryStaffListQuery"
DoCmd.Close acForm, Me.Name

You can consider the project finished at this point, but it can be made a little more user-friendly with the

addition of a few refinements...


Refinements: Leave the Form and Query Open

Now that you have made it really easy for the user to query their data, they are sure to want to run lots of

queries! You can save them the trouble of reopening the dialog box each time by omitting the

line DoCmd.Close acForm, Me.Name and adding some extra code to allow them to leave the query

window open too. They can arrange the query window and dialog box so that they can see both on the

screen, then as they make their choices in the dialog they will see the results in the query window as soon

as they click the OK button.

In fact the query must be closed before the new SQL statement can be implemented (the SQL statement will

be applied to the query even if it is open but the user will not see the changes until the query is run again).

The following code should be placed at the beginning of the cmdOK_Click procedure, immediately following

the variable declarations:

If SysCmd(acSysCmdGetObjectState, acQuery, "qryStaffListQuery")


= acObjStateOpen Then
DoCmd.Close acQuery, "qryStaffListQuery"
End If

It simply checks to see if the query is already open and, if it is, closes it. The code will reopen it after the

new SQL statement has been applied. If the query takes more than a moment to run, the user will see

query close then open. To make this process invisible turn off screen updating by placing this line before the

code above:

DoCmd.Echo False

...and add the following line to the very end of the procedure before the End Sub line to restore screen

updating:

DoCmd.Echo True

TIP: When testing your code it's a good idea to temporarily remove or disable the line that turns

off screen updating (DoCmd.Echo False) in case anything goes wrong and you're left with a

frozen window. The line which restores screen updating (DoCmd.Echo True) usually comes

near the end of the procedure. If there is an error somewhere which prevents the procedure

reaching this line then screen updating will not be restored and you will be left with a blank or

frozen screen. Even after several years as a VBA developer I still forget to do this, and

sometimes I find myself staring at a blank screen wondering what the heck has happened! If it

happens to you, don't panic - instead switch to the Visual Basic Editor and open the Immediate
Window (Ctrl+G). Type DoCmd.Echo True into the Immediate Window and press Enter and

when you return to the Access window you will find that Screen Updating has been restored. If

you have a procedure that switches off screen updating, remember always to include a line in

your error handler to restore screen updating in the event of an error.

Refinements: Restore a Missing Query

Users (that includes you and me!) have the annoying habit of deleting things if they don't know what they

are. If the code can't find the query because someone has deleted or renamed it there will be an error. In

the last tutorial in this series I explained how to create a function to test for the existence of a query. This

time, I'll explain how to include the code in the cmdOK_Click procedure. The method is slightly different for

DAO and ADO so I'll outline both.

Using DAO (Access 97)

If you haven't already added the db and qdf variables (as described above) you should add them now,

together with a third variable blnQueryExists.

Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim blnQueryExists As Boolean

The code is in two parts, and should be placed right at the beginning of the cmdOK_Click procedure, after

the variable declarations. The first part uses a For...Next loop to search through the database's queries

looking for one with the name specified. If it finds the named query it sets the value of

the blnQueryExists variable to True and exits the loop (it must exit the loop now because any other query it

finds will set the variable's value back to False)...

Set db = CurrentDb
blnQueryExists = False
For Each qdf In db.QueryDefs
If qdf.Name = "qryStaffListQuery" Then
blnQueryExists = True
Exit For
End If
Next qdf

The second part looks at the value of the blnQueryExists variable and if it is False it creates a new copy of

the query...

If blnQueryExists = False Then


Set qdf = db.CreateQueryDef("qryStaffListQuery")
End If
Application.RefreshDatabaseWindow

The last line is not usually necessary because the new query will be opened later in the procedure. But, in

case something unforeseen happens, this makes sure that the new query appears without the user having to

refresh the window themselves.

Using ADO (Access 2000 onwards)

If you haven't already added the cat and cmd variables (as described above) you should add them now,

together with the variables qry and blnQueryExists.

Dim cat As New ADOX.Catalog


Dim cmd As New ADODB.Command
Dim qry As ADOX.View
Dim blnQueryExists As Boolean

The code is in two parts, and should be placed right at the beginning of the cmdOK_Click procedure, after

the variable declarations. The first part uses a For...Next loop to search through the database's queries (a

query in ADO is called a View) looking for one with the name specified. If it finds the named query it sets

the value of the blnQueryExists variable to True and exits the loop (it must exit the loop now because any

other query it finds will set the variable's value back to False)...

Set cat.ActiveConnection = CurrentProject.Connection


blnQueryExists = False
For Each qry In cat.Views
If qry.Name = "qryStaffListQuery" Then
blnQueryExists = True
Exit For
End If
Next qry

The second part looks at the value of the blnQueryExists variable and if it is False it creates a new copy of

the query...

If blnQueryExists = False Then


cmd.CommandText = "SELECT * FROM tblStaff"
cat.Views.Append "qryStaffListQuery", cmd
End If
Application.RefreshDatabaseWindow
Note that when using VBA to create a new query, ADO insists that I provide the query with an SQL

statement. Anything will do, but it won't accept an empty string (""). With DAO I can create an "empty"

query definition (the procedure supplies the SQL later anyway).

The Completed Code Procedures

Here is the completed code listing for the dialog box. It is in a format that you can copy and paste directly

into the Access code window.

Code Listing 2 (DAO) for Access 97

Code Listing 2 (ADO) for Access 2000 (and later)

Note About Data Types

When composing SQL statements it is important to get everything right. SQL is a simple language but it is

very precise and does not tolerate errors. In this tutorial I have made use of the IN() clause which is very

useful for presenting a list of items when specifying your criteria:

WHERE tblStaff.[Office] IN('Amsterdam','London',Paris')

...is the same as:

WHERE tblStaff.[Office] = 'Amsterdam' OR tblStaff.[Office] = 'London' OR


tblStaff.[Office] = 'Paris'

The IN() clause can be used for any data type, not just text as I have illustrated here, but you must

remember to use the appropriate qualifier when composing the SQL. The qualifier is a symbol enclosing the

data which tells Access what the data type is and, of course, the data type must match the data type of the

field that the criteria applies to (my Office field is a text field).

Text criteria must be enclosed in quote marks. SQL will accept single quotes (') or double quotes (") but

since here I am creating the SQL statement as a VBA text string the whole thing is itself enclosed in double

quotes, so I use single quotes for the individual criteria items.

Date criteria must be enclosed in hash marks (#) (you might call them number signs). Remember also

that when hard-coding dates into SQL they must always be written in the US format of month/day/year.

[Note: If your Windows regional settings dictate that you use the European day/month/year date format it is

you should use that format when entering criteria into the grid of the Access query design window. Access

converts it to the US format when it constructs the SQL statement. Take a look at the query in SQL View

and you will see it.]


Number criteria should be entered without any qualifier.

So, to modify the code I used to gather data from the list boxes:

If the list box contains a list of dates:

For Each varItem In Me.lstBirthDate.ItemsSelected


strBirthDate = strBirthdate & ",#" &
Me.lstBirthDate.ItemData(varItem) & "#"
Next varItem

Note that the single quotes have been replaces by hash marks (marked in red). The resulting IN() clause

looking something like this:

WHERE tblStaff.[BirthDate] IN(#9/27/1950#,#2/7/1968#,#6/19/1977#)

If the list box contains a list of numbers:

For Each varItem In Me.lstID.ItemsSelected


strID = strID & "," & Me.lstID.ItemData(varItem)
Next varItem

Note that the single quotes have been removed leaving just the comma and, since no qualifier is required

for numerical data, the closing qualifier is removed completely. The resulting IN() clause looking something

like this:

WHERE tblStaff.[ID] IN(25,50,75,100)

Some Additional Features


If you want to add extra functionality to your dialog box take a look at part two of this tutorial. It shows you

how to build list box lists with code, how to let the user choose how the data is sorted by add an ORDER BY

clause to the query, and how to add a Clear All button to reset the list boxes...

Go to the next part of the tutorial >>>

Download the File


CORRECTION: The sample file accompanying this tutorial originally contained a typo in the error-handling

code. The last line of the cmdOK_Click procedure of each of the sample forms read Resume
cmdOK_Click_Err when it should have read Resume cmdOK_Click_Exit. This error was corrected on 12 April

2004.

AccessAndSQL5_97.zip [82KB] Access 97 format.

AccessAndSQL5_2000.zip [122KB] Access 2000 format.

Access and SQL


Part 6: Dynamic Reports
An Access Report is similar to an Access Form. It displays data from a record source you specify (a table or

query) and you can customize the way the data is displayed through its design. In fact designing a report

uses the many of the same techniques as designing a form. Forms are chiefly used for entering and editing

data whereas reports are used for viewing and printing data.

Reports are often seen as inflexible, with the decisions about which data is to be displayed being made at

design time. Access users often think that if they want to see a different set of data they need to build a

new report, or change the query on which the report is based. Not true!

When you look at data in a table or form Access provides you with tools for filtering and sorting...

But when you view a report in Print Preview these tools are absent, so you could be forgiven for thinking

that it's too late to change what's on display.

This tutorial will show you how to make use of a report's Filter and OrderBy properties to create dynamic

reports which the user can filter and sort using a simple dialog, and see the result appear immediately in the

report's Print Preview window. To see a different set of data all they have to do is specify a different filter,

click the button, and the report will change to display the new data.

In the example below the user filtered the report for a list of all the Female staff working in

the Production and Shipping departments of the Birmingham and Cardiff offices [click the thumbnail to see a

demonstration in a new window]:


You can download a fully working sample file containing all the examples described here from the link at the

bottom of this page.

Building the Report


Nothing special here, and no code required! You can use an existing report or create a new one for this

project. You should bear in mind that the tool you are going to build will filter the report's data so it might

be appropriate for you to base your report on an unfiltered table rather than on a query where the data is

already filtered by the time it reaches the report. It's up to you. Familiarize yourself with the names of the

fields displayed in the report and their data types.

Building the Dialog Box


The job of the dialog box is to allow the user to specify criteria for filtering and, in the more sophisticated

examples sorting, the records displayed by the report. This means that you must choose which fields you

want to offer the user and, in most of these examples, be able to provide a list of suitable search criteria to

populate a combo box or list box for each field.

To turn an Access form into a dialog box only requires you to change a few settings on the form's properties

sheet. If you haven't built an Access form as a dialog box before click here to find out what you need to do.

I have created several different dialog boxes, offering different levels of complexity. You can choose from

simple examples using combo boxes, or more complex ones using multi-select list boxes:

A simple dialog using combo


Combo boxes and an option
boxes.
group. Multi-select list boxes.

List boxes and sorting options.

Or free-text filtering where the user can search on any text string:
In my combo box and list box examples, the fields that I have chosen for filtering contain a known range of

entries. One is a list of company Offices and another is a list of Departments. My database has tables

containing lists of these entries and I am using these tables for the Row Source of the combo boxes and list

boxes. Later in this tutorial I will show how to use wildcards to allow users to filter records using string

fragments (such as records starting with a particular letter, or containing a particular group of letters).

The important parts of the VBA code for each dialog box are described and explained below. To

see the full code module for each dialog box follow the links marked "Code Listing 1" etc.

These will display all the code used in the dialog box in a form that you can copy and paste into

your own code window if you wish.

A Simple Dialog Using Combo Boxes

This is the simplest of the designs. The dialog has two combo boxes, one for each of the fields that I am

allowing the user to filter. You can add as many combo boxes as you like providing you modify the code

accordingly. There are also two command buttons: one for applying the filter and another for removing it.

The illustration below shows the layout of the dialog and the names I used for the various objects...

The aim of the code behind the Apply Filter button is to construct an SQL WHERE clause using the user's

combo box choices. The SQL will then be applied to the report's Filter property and the FilterOn property of

the report will be set to True so that it displays the filtered recordset.

If the user makes a choice from a combo box the report should display only the records which have that

value in the field. So if, for example, they choose New York from the Office combo box the code will have to

construct SQL like this:


[Office] = 'New York'

But if the user leaves a combo box empty, I want the report to display all the records for that field, which

would require SQL like this:

[Office] Like '*'

Coding the "Apply Filter" Button

My code starts by declaring three string variables, one each to hold the SQL from the combo boxes and one

to hold the combined criteria for the filter:

Private Sub cmdApplyFilter_Click()


Dim strOffice As String
Dim strDepartment As String
Dim strFilter As String

If a combo box is empty its value is Null so I can use an If Statement to check whether or not the user

made a choice and then construct the appropriate SQL:

If IsNull(Me.cboOffice.Value) Then
strOffice = "Like '*'"
Else
strOffice = "='" & Me.cboOffice.Value & "'"
End If

There is a similar If Statement for each combo box:

If IsNull(Me.cboDepartment.Value) Then
strDepartment = "Like '*'"
Else
strDepartment = "='" & Me.cboDepartment.Value & "'"
End If

Next comes a line which combines the criteria to form a WHERE clause for the filter:

strFilter = "[Office] " & strOffice & " AND [Department] "
& strDepartment

And finally the filter is applied to the report and switched on:

With Reports![rptStaff]
.Filter = strFilter
.FilterOn = True
End With
End Sub

Anticipating Errors

Before completing any project like this you should consider what might go wrong. All but the most basic of

your VBA procedures should contain a simple error handler, but you can help things a great deal by

anticipating what problems might arise. Changes in the design of the report or the fields included might

result in an error, but if you are fairly confident that this won't happen you can leave it for the error handler

to take care of. This project requires the report to be open before the filter is applied. If it isn't then you get

an error:

Notice that the error doesn't distinguish between the report not being open and not existing at all. It can

only "see" an object if it is open. It may be that the user simply forgot to open the report, or perhaps its

name has been changed or someone might have deleted it.

You could have the report open automatically when the dialog opens (or vice versa) by including the

procedure:

Private Sub Form_Load()


DoCmd.OpenReport "rptStaff", acViewPreview
End Sub

But this would still need to take account of a missing or renamed report. My preferred method of dealing

with this possibility is to check whether or not the report is open and advise the user accordingly. This code

goes at the beginning of the cmdApplyFilter_Click procedure after the variable declarations:

If SysCmd(acSysCmdGetObjectState, acReport, "rptStaff") <>


acObjStateOpen Then
MsgBox "You must open the report first."
Exit Sub
End If

If a report with the name specified is not found in an "Open" state the If Statement displays a message to

the user then terminates the procedure to prevent an error occurring.

Coding the "Remove Filter" Button

All that is required to remove the filter is a line which sets the FilterOn property of the report to False:

Private Sub cmdRemoveFilter_Click()


On Error Resume Next
Reports![rptStaff].FilterOn = False
End Sub

The line On Error Resume Next tells Access to ignore any error which might occur if, for example, the report

has already been closed by the user.

You can review the complete code for the dialog box in Code Listing 1.

^ top

Adding an Option Group

Combo boxes are not the only way to offer the user a choice of predefined options. This dialog box,

otherwise exactly the same as the previous example, makes use of an option group to allow the user to filter

the records by gender.

Option groups are useful when there are just a few items to choose from. Here there are three

choices: Female, Male or Both...


When you design your dialog box you can create an option group manually or with the help of the wizard.

An Option Group consists of a Frame containing a collection of Option Buttons (sometimes called Radio

Buttons). Alternatively you can use check boxes or toggle buttons.

If you use the wizard to create the option group all the work is done for you. If you prefer to create the

option group manually, first draw a frame on the form then name it and add a suitable caption to the label.

Then add the individual buttons inside the frame. As you do this each button is automatically assigned a

value. The first button you add has the value 1, the next has the value 2 and so on. In my example the

values are: Female (1), Male (2), Both (3). You can assign any value you like to each button by changing

its Option Value property. You must use whole numbers (data type Long Integer) and normally they should

all be different.

When option buttons are used as a group like this, only one member of the group can be selected at a time.

The selected button passes its value to the group itself. It is a good idea to set a starting value for the group

by specifying the frame's Default Value property. In my example the default value is 3, so that when the

dialog box opens the Both button is already selected.

Detecting and programming the user's choice is simple and is best done with a Case Statement. First it is

necessary to declare an additional variable to hold the Gender criteria:

Dim strGender As String

Then the Case Statement can place the appropriate criteria string into the variable depending on the value

of the option group:

SelectCase Me.fraGender.Value
Case 1
strGender = "='F'"
Case 2
strGender = "='M'"
Case 3
strGender = "Like '*'"
End Select

The only other change to the code is the addition of the criteria for the Gender field to the filter string:

strFilter = "[Office] " & strOffice & " AND [Department] "
& strDepartment & " AND [Gender] " & strGender

You can review the complete code for the dialog box in Code Listing 2.
^ top

Using Multi-Select List Boxes

The combo boxes used in the previous examples have the disadvantage that the user can select only one

item from each. This dialog box uses list boxes. These offer a multi-select facility so that the user can make

several choices from each if they wish.

Designing a list box is similar to designing a combo box in that is needs a Row Source from which to make

its list. As before I have used tables for this. You must enable the multi-select feature by choosing a suitable

mode for the Multi Select property of the list box. Choose Simple or Extended depending on how you want

the user to make their choices.

If you want to know more about list box basics read my tutorial Making Sense of List Boxes.

In this example the code has the job of creating a criteria string from the user's choices in each of the list

boxes. If, for example, the user chooses London, Brussels and Paris from the Office list box it means that

they want to see records for the London OR Brussels OR Paris offices.

There are different ways this can be represented in SQL. One way is to use the keyword "OR" like this:

[Office] = 'London' OR [Office] = 'Brussels' OR [Office] = 'Paris'

but this can result in very long criteria strings, so I prefer to use an SQL "IN" clause like this:

[Office] IN('London','Brussels','Paris')

To do this I use a code statement that loops through the selected items in the list box and strings them

together, separated by commas:


Dim varItem As Variant
For Each varItem In Me.lstOffice.ItemsSelected
strOffice = strOffice & ",'" & Me.lstOffice.ItemData(varItem) _
& "'"
Next varItem

If the resulting string has no length (i.e. it is empty because the user didn't select anything) the code

creates criteria which shows all records. But if the user did make a selection my code creates an IN clause

listing the selections, after first removing the leading comma from the front of the string:

If Len(strOffice) = 0 Then
strOffice = "Like '*'"
Else
strOffice = Right(strOffice, Len(strOffice) - 1)
strOffice = "IN(" & strOffice & ")"
End If

Each list box requires a similar set of code statements. The code for the Gender option group and the code

that creates the filter string are exactly the same as in the previous example.

You can review the complete code for the dialog box in Code Listing 3.

^ top

Offering the Option to Sort the Records

In many cases sorting the records in a report is just as important as filtering them. If you don't want to offer

a sorting option to the user, you can predefine the sort order by basing the report on a query or use an SQL

statement incorporating an ORDER BY clause as the report's Record Source. For example:

SELECT * FROM tblStaff ORDER BY [Office], [Department];

will display the records sorted first by the Office field then by the Department field, both in ascending order.

Adding sorting options to the dialog increases its power a great deal but to make it a really useful and user-

friendly tool requires a bit of thought and quite a lot of code (although much of it is quite simple and

repeated several times). Here's how the dialog box looks:


I have put the sorting tools (three combo boxes and three command buttons) inside a frame purely for

design effect. It has no other practical implications. The features of the sorting tools are as follows:

 There are three combo boxes, allowing the user to sort by up to three fields at a time.

 The combo boxes have a hierarchy (1, 2, 3) by which the sort is prioritised. The records are
sorted first by the choice in combo box 1, then by the choice in combo box 2, then by the choice in
combo box 3.

 Each combo box contains a list of fields from which the user can choose to sort by, plus the option
"Not Sorted".

 If the "Not Sorted" option is chosen any combo boxes lower in the hierarchy are automatically disabled
and their values set to "Not Sorted" too.

 If the user chooses a sort field, and the value in the next combo box is disabled, then the next combo
box becomes enabled with its value remaining as "Not Sorted".

 The user can not choose the same field in two different combo boxes.

 There are three command buttons, corresponding to the three combo boxes, by which the user can
specify the sort direction for each field.

 When a field is chosen the corresponding command button is enabled, but when a combo box has the
value "Not Sorted" the corresponding command button is disabled.

 Clicking the command button changes its caption from Ascending to Descending. Clicking it again
changes it back again. The default caption is Ascending.

As you can see, there is quite a lot of functionality here and that requires quite a lot of code, but it is fairly

simple and much of it is repeated for each combo box so the task of creating an ORDER BY clause isn't as

daunting as it might seem.


Filling The Combo Box Lists

To create the list, each combo box has its Row Source Type property set to Value List. This allows a list of

items to be typed directly into the Row Source property textbox:

Separated by semicolons, each item becomes an option on the combo box list:

Coding the Combo Boxes

Each combo box has three jobs to do when the user makes a choice:

1. It has to check that the choice is not the same as a choice that has already been made.

2. It has to disable the combo boxes below it in the hierarchy if the user chose "Not Sorted" and set
their values to "Not Sorted".

3. If the user chose "Not Sorted" it has to disable its own sort direction command button and any below
it in the hierarchy and set their captions to "Ascending".

The first job is best handled by the combo box's Before Update event. This is because, if the user's choice is

not acceptable, the update can be cancelled easily. The After Update event doesn't offer this facility. Here's

the code for the first combo box:

Private Sub cboSortOrder1_BeforeUpdate(Cancel As Integer)


If Me.cboSortOrder1.Value <> "Not Sorted" Then
If Me.cboSortOrder1.Value = Me.cboSortOrder2.Value _
Or Me.cboSortOrder1.Value = Me.cboSortOrder3.Value Then
MsgBox "You already chose that item."
Cancel = True
Me.cboSortOrder1.Dropdown
End If
End If
End Sub
There are two If Statements, one inside the other (this is referred to as "nested"). The first If Statement

checks to see if the user chose "Not Sorted". If they did, nothing happens because it's OK if more than one

combo box has this value. But if any other value has been chosen it checks to see if the chosen value

matches any of the other combo boxes. If it does a message is shown to the user, their action is undone (by

cancelling the event) and the combo box list is dropped to prompt the user to make a different choice.

The Before Update event procedure of each combo box has similar code with the numbers changed so that

each combo box checks the other two.

The remaining jobs are handled by the combo boxes' Change event:

Private Sub cboSortOrder1_Change()


Dim i As Integer
If Me.cboSortOrder1.Value = "Not Sorted" Then
For i = 2 To 3
With Me.Controls("cboSortOrder" & i)
.Enabled = False
.Value = "Not Sorted"
End With
Next i
For i = 1 To 3
With Me.Controls("cmdSortDirection" & i)
.Enabled = False
.Caption = "Ascending"
End With
Next i
Else
Me.cboSortOrder2.Enabled = True
Me.cmdSortDirection1.Enabled = True
End If
End Sub

When the user changes the value of the combo box an If Statement checks the new value to see if it is "Not

Sorted". If it is, a For...Next loop disables the other combo boxes and sets their values to "Not Sorted". Then

another For...Next loop disables the command buttons and sets their captions to "Ascending".

If the user made a choice other than "Not Sorted" then its own sort direction command button is enabled

and the combo box below is enabled.

As before, the code is similar for the remaining combo boxes. Each one referring to the appropriate controls.
NOTE: One of the aims of writing good code is to make it as brief as possible, and in the above

example I have demonstrated this with the use of loops and With Statements. Using loops

avoids having to repeat chunks of code. The variable "i" not only tells Access how many times to

run the loop but also serves to identify which control is being manipulated each time. I can do

this because I gave the controls names which included numbers. In the first loop, the first time it

runs the value of "i" is 2 so Me.Controls("cboSortOrder” & i) means the same

as Me.cboSortOrder2 and so on. Although there are only three combo boxes in this example the

loop would require no additional code for any number of combo boxes. I would just have to

change the upper limit of the value of "i". With Statements also help by removing the need to

repeat the first part of a long code line when writing several consecutive statements all referring

to the same thing.

Coding the Sort Direction Command Buttons

I want to make the selection of a sort direction (i.e. ascending or descending) as easy as possible. Since

there are only two choices it isn't worth using combo boxes, and an option buttons would take up too much

room. So I borrowed Microsoft's idea (take a look at the Query Wizard!) and used command buttons.

Clicking one of the command buttons doesn't do anything other than change it's caption. The code which

later builds the SQL statement for the report filter will read the button's caption to determine which way to

sort the records. The code is simple and the same for each button (with the appropriate control names

inserted):

Private Sub cmdSortDirection1_Click()


If Me.cmdSortDirection1.Caption = "Ascending" Then
Me.cmdSortDirection1.Caption = "Descending"
Else
Me.cmdSortDirection1.Caption = "Ascending"
End If
End Sub

Coding the Apply Filter Command Button

The code used to create the filter string is exactly the same as in the previous example, but this time there

is an additional task: to create an Order By string that will be applied to the OrderBy property of the report.

In the same way that the code generates a criteria string, applies it to the Filter property of the report and

then activates it by setting the FilterOn property to True, it now has to generate a sort string, apply it to

the OrderBy property of the report and activate it by setting the OrderByOn property to True.
If the user chose to sort the data by LastName then by FirstName, here's what a completed OrderBy string

should look like:

[LastName], [FirstName]

Fields are simply listed in the desired order, separated by commas. I am in the habit of enclosing field

names in square brackets as shown here but this is only absolutely necessary when the names contain

spaces.

Unless specified otherwise, each field is sorted in ascending order (A-Z, 1-9) but if a field is to be sorted in

descending order (Z-A, 9-1) the keyword DESC is used. The sort direction must be specified for each field. if

it is omitted it is assumed that the field is to be sorted in ascending order. So, if the user chose to sort the

data by LastName then by FirstName both in descending order the string should look like:

[LastName] DESC, [FirstName] DESC

Here is the code used to build the sort string:

If Me.cboSortOrder1.Value <> "Not Sorted" Then


strSortOrder = "[" & Me.cboSortOrder1.Value & "]"
If Me.cmdSortDirection1.Caption = "Descending" Then
strSortOrder = strSortOrder & " DESC"
End If
If Me.cboSortOrder2.Value <> "Not Sorted" Then
strSortOrder = strSortOrder & ",[" &
Me.cboSortOrder2.Value & "]"
If Me.cmdSortDirection2.Caption = "Descending" Then
strSortOrder = strSortOrder & " DESC"
End If
If Me.cboSortOrder3.Value <> "Not Sorted" Then
strSortOrder = strSortOrder & ",[" &
Me.cboSortOrder3.Value & "]"
If Me.cmdSortDirection3.Caption = "Descending" Then
strSortOrder = strSortOrder & " DESC"
End If
End If
End If
End If

The code consists of a series of nested If Statements, each one depending on the result of the previous one.

First of all, an If Statement checks to see if the value of the first combo box is something other than "Not

Sorted". If the value is "Not Sorted" the If Statement finishes and the OrderBy string remains empty. But if
the first combo box contains a field name then it is placed, surrounded by square brackets, into a variable

named strSortOrder. A second If Statement then looks at the caption of the first command button and if it

reads "Descending" the keyword DESC is added to the string in the strSortOrder variable.

The process is repeated for each combo box and finishes if the value "Not Sorted" is found, but if a field

name is found its name and the sort direction is noted and the code moves to the next combo box.

The last section of the code applies and activates both the filter string and the sort string:

With Reports![rptStaff]
.Filter = strFilter
.FilterOn = True
.OrderBy = strSortOrder
.OrderByOn = True
End With

Coding the Remove Filter Command Button

In the earlier examples the this button has simply set the FilterOn property of the report to False. Now it has

to do the same for the OrderByOn property:

With Reports![rptStaff]
.FilterOn = False
.OrderByOn = False
End With

But this time I have added some extra functionality. The following statement removes the selections from

the Office list box (there is a similar one for the Department list box):

For Each varItem In Me.lstOffice.ItemsSelected


Me.lstOffice.Selected(varItem) = False
Next varItem

The option group is reset to its original value (Gender = Both):

Me.fraGender.Value = 3

Finally the following loop sets the value of each combo box to "Not Sorted" and disables it, then sets the

caption of each sort direction command button to "Ascending" and disables it. The last line enables the first

combo box ready for the user to make their next choice.

For i = 1 To 3
Me.Controls("cboSortOrder" & i).Value = "Not Sorted"
Me.Controls("cboSortOrder" & i).Enabled = False
Me.Controls("cmdSortDirection" & i).Enabled = False
Me.Controls("cmdSortDirection" & i).Caption = "Ascending"
Next i
Me.cboSortOrder1.Enabled = True

You can review the complete code for the dialog box in Code Listing 4.

^ top

Filtering with Free Text

Each example so far has offered the user a fixed range of choices for filtering the report's records. That is

fine if the fields that are being filtered contain a known range of items. But you might need to offer the

facility to filter using free text, i.e. allowing the user to enter anything they want.

This final example shows how you can allow the user to enter any string of text into a text box and choose

how that string is used to filter the records...

For each available field the dialog box has a text box and a set of option buttons. The user can enter a letter

or a string of text in the text box and make a choice from the option group to specify how the string is used

in the filter. If the user leaves a text box empty then all records are returned for that field.

Supposing the user might enter the string mar into the FirstName textbox. The results they get will depend

upon the choices they make from the option buttons:

Choosing Starts with... would return: Martin, Mark, Margaret, Mariana etc.

Choosing Contains... would return: Martin, Anne-Marie, Marlon, Omar etc.


Choosing Ends with... would return: Omar, Dagmar, Valdemar etc.

Choosing Equals... would return only: Mar

The method used by the filter is to combine the string with one or more asterisk wildcards. In SQL the

asterisk (*) combined with a string and the keyword LIKE represents any string of text:

Like "g*" represents the letter g followed by any string of text.

Like "*g*" represents any string of text containing the letter g.


Like "*g" represents any string of text ending with the letter g.

The code used to create the filter string is quite simple:

If IsNull(Me.txtFirstName.Value) Then
strFirstName = "Like '*'"
Else
SelectCase Me.fraFirstName.Value
Case 1
strFirstName = "Like '" & Me.txtFirstName.Value & "*'"
Case 2
strFirstName = "Like '*" & Me.txtFirstName.Value & "*'"
Case 3
strFirstName = "Like '*" & Me.txtFirstName.Value & "'"
Case 4
strFirstName = "= '" & Me.txtFirstName.Value & "'"
End Select
End If

An If Statement looks for an entry in the text box. If the text box is empty (i.e. its value is Null) a filter

expression is created that will return all the records. If there is an entry in the text box a Case Statement

creates the required filter expression depending on the value of the option group.

This is repeated for each field and the filter expressions are combined to create a filter string:

strFilter = "[FirstName] " & strFirstName & _


" AND [LastName] " & strLastName

The filter string is applied to the report and activated in the same way as in the previous examples.

You can review the complete code for the dialog box in Code Listing 5.

^ top
Ideas for Further Improvements
The examples I have shown are each designed to illustrate particular techniques. Remember you can mix

and match as many of the different tools as you need but remember to always think about the user. These

tools are meant to make their lives easier - so keep it simple!

Opening the Report Automatically

The above examples include a few lines of code that look for the open report and warn the user if it isn't

found:

If SysCmd(acSysCmdGetObjectState, acReport, "rptStaff") <>


acObjStateOpen Then
MsgBox "You must open the report first."
Exit Sub
End If

But you could save them the trouble by opening the report automatically as soon as they press the Apply

Filter button:

If SysCmd(acSysCmdGetObjectState, acReport, "rptStaff") <>


acObjStateOpen Then
DoCmd.OpenReport "rptStaff", acViewPreview
End If

Or perhaps you could ask the user what they want to do:

Dim Response As VbMsgBoxResult 'For Access 97 use: As Variant


If SysCmd(acSysCmdGetObjectState, acReport, "rptStaff") <>
acObjStateOpen Then
Response = MsgBox("The report is not open." _
& vbCrLf & "Do you want to open it now?" _
, vbQuestion + vbYesNoCancel)
SelectCase Response
Case vbYes
DoCmd.OpenReport "rptStaff", acViewPreview
Case vbNo
Exit Sub
Case vbCancel
DoCmd.Close acForm, Me.Name
Exit Sub
End Select
End If
Automate the Report's Title

Most reports need some sort of descriptive title and you might like the title to change to reflect the records

displayed. You can use the user's choices in the combo boxes, list boxes and other controls to construct a

variety of labels to place in the header or footer section of the report.

For each title you need to place an empty control in the report's Report Header or Report Footer section. If

these sections are not already shown on your report when you view it in Design View, go to View > Report

Header/Footer to switch them on.

You can either add a Label control (and use the code to specify its Caption property) or an

unbound TextBox control (and use the code to specify its Value). My preference is to use a TextBox because

it has a Can Grow property which, when set to Yes, allows the control to increase in height to accommodate

the amount of text it contains. Also, its value can be set when the report is in Preview view (unlike a label

caption which requires the report to be switched into Design view for editing). This is useful if you aren't

sure how long the title is going to be. Make sure you draw the control as wide as the report permits because

the width doesn't change, only the height:

The text of your title can be made up of plain text combined with values extracted from the dialog box

controls. It can be applied to the report at the same time as the filter. Here's an example of the sort of code

you might use (the textbox control is named txtReportTitle):

With Reports![rptStaff]
.Filter = strFilter
.FilterOn = True
.txtReportTitle.Value = _
"Staffing Report - " & Format(Date, "dd mmm yyyy") _
& vbCrLf & "Office: " & Me.cboOffice.Value _
& vbCrLf & "Department: " & Me.cboDepartment.Value
End With
Note the use of vbCrLf in the code to add line breaks to the finished text. Here's the result:

Download the File


You can download a fully-working database file containing this tutorial's completed project in either Access

97 or Access 2000 format. The database contains all five dialog boxes described in the tutorial and a sample

report and tables. The files are provided in Access 97 and Access 2000 format.

AccessAndSQL6_97.zip [107KB] Access 97 format.

AccessAndSQL6_2000.zip [115KB] Access 2000 format.

You might also like