Professional Documents
Culture Documents
located in the lower left corner of the form (Fig. 1) so why bother creating your own custom
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
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
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
section. The footer is always visible in the form's window so, if the form is too big for the window, the
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
A Form Footer section appears at the bottom of the form (Fig. 4). Notice that a Form Header section is
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
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
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.
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.
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
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
Width: Format 3 cm
Height: Format 1 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).
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
Left: Format 4 cm
Select the second of the new buttons and go to the Property Sheet and enter the following property
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
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
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
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
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
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
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.
I haven't included First and Last record buttons because I seldom use them myself, but it is a simple
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.
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
^ top
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.
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
This takes you into the Visual Basic Editor where Access has created an empty event procedure for
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
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.
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.
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:
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
Listing 3:
^ top
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
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
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).
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
Listing 5:
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
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
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 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:
As usual the Visual Basic Editor helpfully supplies a list of options to choose from.
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
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,
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:
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:
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,
All the techniques described here have been tested successfully in Microsoft Access versions 2007,
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
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
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
code to turn it into a custom Record Counter which you can position and format to suit your own
requirements.
^ top
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 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:
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).
The mouse pointer changes to show that it is ready to add a Label. Click on the form approximately
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
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
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
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).
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
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.
Check the label's finished appearance by switching to Form View (Fig. 10).
^ 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
*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:
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
I want the label to read Record x of y where x is the index number of the current record and y is the
1. Place the cursor between the lines beginning Private Sub… and End Sub and press [Tab] to
indent your typing by one tab-space.
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
The completed event procedure looks like this (Listing 3) (in addition to any existing code you might
have there):
Listing 3:
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
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.
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
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
count when a new, unsaved record is displayed, edit the code as follows (Listing 4):
Listing 4:
This changes the label's behaviour so that the record index number and total record count are the
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
Listing 5:
This changes the label's behaviour so that the text New Record is displayed (Fig. 15).
^ top
Buttons), now that you also have your own Custom Record Counter you might like to remove the
built-in one.
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
All the techniques described here have been tested successfully in Microsoft Access versions 2007,
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]
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).
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
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
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.
Click on the form approximately where you want the combo box to appear (Fig. 3). You can accurately
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
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
The combo box does not have a list yet. That is the next task.
^ top
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
Select the name of the table that the form's recordset is based upon (Fig. 7), click the Add button
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
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
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
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.
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 SQL statement that the Query Builder generated is now written into the Row Source property of
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
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
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
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
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
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
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
^ top
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
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
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:
End Sub
To start, we need an error handler just in case something goes wrong. A simple one will do.
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 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:
Note: I have used EmployeeID which is the name of the Primary Key field in this example. Make sure
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
Me.Bookmark = rst.Bookmark
Listing 2:
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
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
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
All the techniques described here have been tested successfully in Microsoft Access versions 2007,
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]
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
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
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
^top
not linked to an underlying recordset and that has certain settings applied so that, usually, it floats in
Click the Form Design button (Fig. 2) on the Create tab of the Access ribbon to create a new, empty
form.
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.
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
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.
In this example the user will be searching through a list of names. They will be offered two sets of
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.
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
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
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.
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
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
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
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
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
With the List Box selected, go to the Format tab of the Property Sheet and make the following
The exact settings will depend upon your data and the width of your List Box. Check the result in Form
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.
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
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
will make it work. Each of the command buttons requires a macro (called an Event Procedure) that will
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
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
Listing 1:
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.
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-
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
Listing 2:
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
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
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
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
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.
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
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 =
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 =
Listing 3:
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 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)
Which is then added to the WHERE clause of the SQL statement that forms the Row Source of the List
Box as:
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
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.
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:
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
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.
^top
corner of the design window where the rulers meet (Fig. 21) so that a black dot appears, and go to
*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
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
will display the relevant records and perhaps another on the database’s Switchboard or Home screen.
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:
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
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:
it reads:
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.
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
Listing 7:
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
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:
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:
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
Listing 9:
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
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
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
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
Listing 10:
Listing 11:
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:
I usually write:
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!
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
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]
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
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.
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
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
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.
^top
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:
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.
Click in the Text Box and enter a suitable expression such as:
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.
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).
Tool Value
Font Size: 24 pt
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
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.
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
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
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.
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).
except the text does not change as you move through the records and, importantly, it can’t be
selected.
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
^top
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
With the form in Design View click the View Code button (Fig. 13) on the Design tab of the ribbon.
macro that, when run, will update the Label’s caption. Type the following code statements (Listing 1):
Listing 1:
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
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
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:
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
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
Assuming everything is OK (if not then check your code and correct any mistakes) click the Visual
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.
^top
Uses the Firstname and Lastname fields separated by a space and concatenated with the ampersand
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 & ")"
You can even include a calculation such as the next example which makes use of the Birthdate field to
Me.Firstname.Value & " " & Me.Lastname.Value & " - Age: " & Int((Date
- Me.BirthDate.Value) / 365.25)
Here is another example, this time using four of the form’s fields
Me.Lastname.Value & ", " & Me.Firstname.Value & " : " &
Me.Department.Value & " Dept. " & Me.Office.Value
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
Me.Lastname.Value & ", " & Me.Firstname.Value & " : " &
Me.Department.Value _
& " Dept. " & Me.Office.Value
^top
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]
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
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.
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
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.
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).
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
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
Width: Format 1 cm
Height: Format 1 cm
Caption: Format A
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
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
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
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
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.
For accurate placement use the [Arrow] keys to move the selected items up, down, left or right. Hold
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
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.
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.
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.
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:
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
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
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
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:
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.
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
Listing 4:
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.
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
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
As with the Filter by Letter tool the code runs on the AfterUpdate event of the Option Group
(Listing 5):
Listing 5:
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
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
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
Listing 6:
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.
Masterclass. The database contains two copies of the form: one equipped with the Filter by Letter tool
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]
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.
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
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
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
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.
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
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.
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.
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
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.
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
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.
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
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
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).
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.
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
checked must be designated and the code that will call the AuditChanges procedure added.
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
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
Check, compile and save your code as described earlier, and you are ready to test your new Audit Trail.
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.
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
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:
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
Write or copy and paste the code into a standard module so that it can be called from any form.
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
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)
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).
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".
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
AccessAuditTrail_Simple.zip [88KB]
AccessAuditTrail_Detailed.zip [88KB]
AccessAuditTrail.pdf [226KB]
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
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,
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.
(Fig. 1).
Each needs just one code statement to do its job, for example (Listing 1):
Listing 1:
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).
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
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”.
happened? In this case no, so a simple addition to the code will prevent any problems (Listing 2).
Listing 2:
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
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:
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
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
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).
Again, a simple one-liner and you might be tempted to add just a basic error handler (Listing 4):
Listing 4:
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
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.
Listing 5:
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
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.
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
When adding an error handler to your code you must make sure that, before the error handler’s
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
The “tidying-up” tasks could include several things, depending on what your macro was doing. Here
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
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
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.
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
HandlingErrorsInYourAccessDatabase1_Errors101.pdf [274KB]
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.
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
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,
it does that…” as if they expected things to go wrong occasionally when, had I known about the
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
There are four steps to build the Error Log and its reporting system.
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.
DateTime Date/Time
MacroName ShortText
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:
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
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
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:
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
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 prompts. We don’t need to ask the user’s permission to report the error, nor do we want them to
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.:
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
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
Listing 3:
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).
a Database Maintenance screen along with other tools useful to the users. In this example I give three
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:
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
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.
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
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:
Call ReportError("frmDatabaseMaintenance_cmdEmailErrorLog_Clic
k")
End Select
Resume cmdEmailErrorLog_Click_Exit
End Sub
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):
Environ(Argument) Returns
In addition to saving information into the Error Log you can use the Environ() function to specify the
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
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
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]
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
Fig. 1 Database objects before (left) and after (right) splitting the 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 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
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).
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
(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
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.
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.
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:
I usually create a single record by entering some text such as “Test” in the LinkText field so that the
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
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.
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
After each stage of code writing remember to compile (Debug>Compile…) and test your code to
If you don’t already have a suitable module in which to write your code, create a new standard
module.
The following code (Listing 1) tests the links to the back-end database by attempting to open the Link
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
In this macro’s code (Listing 2) I have used DAO (Data Access Objects) programming because the
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.
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
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
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
Database and set the Display Form option to the name of your chosen form (Fig. 15).
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
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.
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:
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
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
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]
database. Bad data can happen because the user might not know what they should enter into a field or what
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.
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
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).
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
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
query. This is why, when you click the down-arrow next to the Row Source property text box a list of
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-
A typical SQL statement for creating a list of unique entries will look something like this:
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
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.
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
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
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
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.
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...
With a little VBA code you can take charge of proceedings and decide for yourself how to handle things.
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
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...
The standard message is self-explanatory but you might want to add a personal touch...
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
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...
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.
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
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
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
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
An If Statement then handles the user's response. If the user clicks the "Yes" button the code must add
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
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
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
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
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.
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
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
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
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.
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
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
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
If you want to try out a demo of the completed project before working through the tutorial follow this link to
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).
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.
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
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.
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:
7;July;8;August;9;September;10;October;
11;November;12;December
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
Data Validation Text Please enter a 4-digit year between 1900 and 2020
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
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
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
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
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
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:
ready-labelled frame.
labels.
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
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 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.
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
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.
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
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.
want to follow each step exactly, you need to create a table upon which to base your form. You can work in
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
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
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
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
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
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.
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
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
Left: 1.025cm, Top: 0.95cm, Width: 1.95cm, Height: 1cm, Special Effect:
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
Use the Name property to give the new rectangle a sensible name. In this example I am
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:
(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)
copies of the original and filling them with the correct colours. Here are the property
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
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
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: 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
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
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
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
Caption: Coloured Tabs Demo; Scrollbars: Neither; Record Selectors: No; Dividing Lines:
No
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
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.
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.
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:
Add the following lines to assign the correct caption for each the text box labels:
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
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-
As the default event for this control is the Click event, that is chosen automatically from the right-hand
End Sub
You can copy all the lines you typed into the Form_Open procedure and paste them into this one. In
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
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.
Create an event procedure for the Click event of the second tab and enter the following lines of code:
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.lblField4.Caption = "Department:"
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
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.
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:
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.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.
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
CheckData = True
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
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
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].
are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a
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.
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
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
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-
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
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
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
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
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-
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
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:
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
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
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.
On the Other tab set the Multi Select property to either Simple or Extended. This will allow the user to
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
In my example, I use the OnClick event of a separate command button to run the code.
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
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.
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
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
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
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.
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
The next two lines assign values to the DAO variables. The first line tells Access that we are referring the
Set db = CurrentDb()
Now for the code that reads the user's selection from the listbox:
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:
================================================
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
If Len(strCriteria) = 0 Then
MsgBox "You did not select anything from the list" _
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
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
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
The following line removes the first character (the leading comma) from the string:
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
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
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
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().
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
Else
End If
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:
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:
have also used the ASCII code Chr(34) to represent the double-quote (") character, as I described earlier,
Next varItem
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:
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].
are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a
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-
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
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
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
The following examples show several different ways of achieving this effect and are presented in an
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:
Case "France"
cboCity.RowSource = "tblFrance"
cboCity.RowSource = "tblUnitedKingdom"
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.
(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:
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:
"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
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
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
This example corrects that using the following code on the form's On Current event:
"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
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.
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).
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
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
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
Case 1
strCountry = "France"
Case 2
Case 3
End Select
"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
Similarly the forms On Current event is used to synchronise the Option Group with the City field, and again
grpCountry.Value = Null
End If
Case "France"
grpCountry.Value = 1
grpCountry.Value = 2
grpCountry.Value = 3
End Select
"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
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
are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a
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
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.
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
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,
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.
>>>
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
>>>
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
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
>>>
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:
ocxCalendar.Value = cboStartDate.Value
Else
ocxCalendar.Value = Date
End If
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
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.)
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!
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.
First of all, declare a variable to hold the name of the combo box that called the calendar. I'll name my
...at the very top of the form's code window after the Option Compare Database and Option
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.
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:
Other references to the combo box in the procedure can now be written as cboOriginator:
The calendar's Click procedure is similarly modified to pass the date back to the combo box named in
A final line:
are provided in Access 97 and Access 2000 format, and also as Zip files for faster download (you will need a
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).
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...
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
Finally, there is a section illustrating some useful variations on this technique such as allowing for Null
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 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
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
Start by dragging the edges of the form to make a rectangle about 7 cm wide by 4 cm tall. It should
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.
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
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.
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.
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
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
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
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
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).
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
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
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...
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
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.
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.
Give your buttons sensible captions like OK and Cancel. Use the properties window to set their
properties as follows...
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
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
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...
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.
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
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
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
^ top
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).
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
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
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
We haven't finished yet, but you might like to try out the query at this point...
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).
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...
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
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.
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:
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
your code and also enters the space. This prompts the next list of possible entries to open...
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).
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
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 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:
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
Listing 4:
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
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
This demonstrates the technique at its simplest and most elegant. It looks just like a parameter query
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).
The code looks the same (Listing 5), it's just in a different place...
Listing 5:
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:
Listing 7:
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!
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:
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
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
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.
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
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
Matching Text
Using Wildcards
Excluding Things
"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
"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
"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
entry London, Paris or Amsterdam in the Town field. Note: If this method is combined with criteria for other
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
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.
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
^ 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
For example:
Yor* would find York, Yorkshire and Yorktown but not New York.
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"
followed by a letter or string of text. This example will display all records
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
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
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
<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
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
^ top
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
When you enter a date in the criteria cell you can use any standard date format, but each date must be
For example:
<#1/1/98# finds dates earlier than 1 January 1998
Between #5/7/98# And #10/7/98# finds dates no earlier than 5 July 1998 and no later than 10
July 1998
=#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
=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
Year([Fieldname])=Year(Now())
To match the current year type the expression shown, entering the
example will display all the records with entries for the current year
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
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
^ 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).
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.
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.
all records for contacts in towns starting with the letter L but will exclude those in London.
^ top
Null. Conversely, to find records for which specific fields are not empty you use the expression Is Not Null.
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
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!
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
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
^ 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
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
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
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
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.
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
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
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
When using a single wildcard it can be placed before or after the prompt. You can use asterisks or
In this example a single wildcard has been used, an asterisk (Fig. 3).
Fig. 3 A parameter employing a single wildcard.
The parameter...
... 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".
In this example (Fig. 4) two wildcards have been used, both asterisks.
The parameter...
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.
query criteria. Sometimes the syntax (how you write it out) can be a bit tricky, but persevere until you
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
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...
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...
...will present the user with two dialog boxes, the first asking for a month and the second asking for a
year.
You may want to view all the invoices generated in a recent period, such as the last 30 days. The
criteria...
since that many days before today. The Date() part creates the current date so this query is always
up-to-date.
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...
...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.
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.
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
4. List the parameters in the order in which you want the dialog boxes to appear when the user
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.
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.
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
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
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.
characters depending on what you say. Anything extra just gets cut off. Check your work!
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.
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...
Here's a regular parameter query with a prompt for the user to enter the name of the Office whose records
If they had ignored the prompt and left the input box empty they would have seen no results at all.
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
...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...
A parameter that can accept an input from the user, but that will return all records if no input is made is
written...
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
have had an additional prompt for Department which could also accept a null entry (click the thumbnail to
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..."
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
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…
etc.). For example if you wanted to multiply two fields together: [Field 1]*[Field 2]
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 %
currency for example. Normally field formatting is specified in a the design of a table, but as the field is
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 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.
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.
Many tasks can be achieved with simple calculations, and there are a number of date functions to help in
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
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.
[Invoice Date]+60 takes the date it finds in the Invoice Date field
Create a new field with an expression that subtracts the field containing the earlier date from the field
number.*[note]
Note: If the result fails to display as a date, or displays a date in the wrong format, switch to design view
format from the list. Close the dialog box and run the
correctly. [back]
^ top
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( )
particular date.
field [Date].
If no criteria are defined then all the records are displayed. The usual criteria for defining numbers can be
only.
1997.
If you don't need to see the extracted data separately, you can enter the function as part of the criteria of
1997
1995 onwards.
the field (in this case the field is called [Date]) within
The same applies to the Month( ), Day( ) and Weekday( ) functions. For example…
dates in September
^ top
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…
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
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…
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
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.
It makes use of the fact that Access uses serial numbers to store dates
In this example the field has been formatted to show two decimal
places.
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
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!
tutorials are not intended to be a course on SQL. There are many excellent books and online tutorials on the
In this tutorial I review the various tasks that SQL is used for in Access and answer some of the questions
What is 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.
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
it uses SQL to filter the data; whenever a report is displayed SQL is used to gather the data to be displayed;
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.
and SQL, and find out why many professional database developers prefer not to use stored queries at all.
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
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
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
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
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.
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
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.
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.
Data Definition queries are a very powerful tool in the SQL language and can perform many tasks on the
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
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 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
NOTE: When you make changes the query's SQL view Access modifies the QBE grid to
^ top
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
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
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
^ top
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
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
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:
^ top
What's Next?
The next tutorial in this series will explain how to incorporate SQL into VBA code:
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
SQL contains a number of keywords such as SELECT, DELETE, UPDATE, FROM, WHERE, OR, AND,
SQL keywords are usually combined with arguments in the form of table names, field names, criteria etc.
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).
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
Access uses parentheses (round brackets) to enclose the various parts of the WHERE clause but these can
WHERE (((tblStaff.Office)="London”))
is the same as:
WHERE tblStaff.Office="London”
Whenever a field is specified you have the option to append the table name, separating the two with a dot.
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.
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:
or
A Date should be enclosed in hash marks (#) also called pound or number signs, for example:
A number, of any sort, needs no qualifier and can be entered as it is, for example:
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.
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
(and other Microsoft Office programs) to do just about anything you want. SQL is a language used
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
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
When working with SQL in a VBA procedure I usually assign the SQL to a text variable and I usually name
DoCmd.RunSQL strSQL
Of course you could do away with the variable and apply the SQL directly, like this:
But, as you will see in the later tutorials building an SQL statement might involve several stages and many
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.
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!).
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
I like to write each clause of the SQL statement on a separate line. This makes long statements much easier
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.
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
example the VBA sees two text strings enclosed by double quote marks, and between them a word it doesn't
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.
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
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
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
string. In the illustration below, a single quote mark is included in the SQL string either side of the text
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
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
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
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!
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
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
When you try to run a faulty SQL statement from VBA it will usually generate a run-time error resulting in a
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
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=#09/27/1950#.
Finally, remember that a faulty SQL statement can crash your VBA code so remember to test your
^ 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:
these techniques, and show you more things you can do with SQL in your Access databases.
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)
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
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
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:
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.
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!):
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
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.
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:
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.
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
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):
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
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
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
1. Enter the following line of code as a single line into the Immediate Window, then press Enter:
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
directly with the query's SQL, and includes a practical project to build a multi-purpose query:
This means that you build the queries when you need them, rather than trying to anticipate the user's needs
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
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.
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
Another important reason for working this way is that inquisitive (or careless!) users might delete or change
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
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
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.
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
If you are not experienced in building dialog forms like this one, take a look at my tutorial Customizing
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
Gather the user's choices from the combo boxes and write them into an SQL statement.
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...
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
Dim db As DAO.Database
Set db = CurrentDb
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
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
"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]:
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
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
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.
All that remains is to apply the SQL statement to the stored query that you saved earlier, and to close the
dialog box.
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"
to open the query displaying the results of the user's criteria choices, and to close the dialog. Finally add the
lines:
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
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
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.
than one item, a property known as Multi Select. I am also going to allow the user to specify either "And" or
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]:
fields: Office, Department and Gender. You will see that the code that powers the dialog box is easily
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
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.
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-
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
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
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
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
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.
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
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.
yourself, or copy it from the code listing below and paste it directly into the Access code window.
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
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...
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.
This variable will be used to collect the selections from the list boxes:
These variables will hold the collected choices from each list box:
These variables will hold the And/Or choices from the option groups:
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
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...
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
If the user selects nothing the code inserts a wildcard into the criteria clause, resulting in something like
this:
But if the user selects one or more items the code creates an IN clause (after removing the leading comma
This complete process is repeated for each list box and the resulting criteria are stored in separate variables.
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...
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
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
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...
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
MsgBox strSQL
Alternatively you can "print" the SQL statement to the Immediate Window of the Visual Basic Editor by
Debug.Print strSQL
Before you run your code check that you have everything you need by comparing it with Code Listing 1 then
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.
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
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
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
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.
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
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
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
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
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
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
If you haven't already added the db and qdf variables (as described above) you should add them now,
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
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...
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
If you haven't already added the cat and cmd variables (as described above) you should add them now,
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)...
The second part looks at the value of the blnQueryExists variable and if it is False it creates a new copy of
the query...
statement. Anything will do, but it won't accept an empty string (""). With DAO I can create an "empty"
Here is the completed code listing for the dialog box. It is in a format that you can copy and paste directly
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
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
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
So, to modify the code I used to gather data from the list boxes:
Note that the single quotes have been replaces by hash marks (marked in red). The resulting IN() clause
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:
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...
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.
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
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
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
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
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:
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
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
But if the user leaves a combo box empty, I want the report to display all the records for that field, which
My code starts by declaring three string variables, one each to hold the SQL from the combo boxes and one
If a combo box is empty its value is Null so I can use an If Statement to check whether or not the user
If IsNull(Me.cboOffice.Value) Then
strOffice = "Like '*'"
Else
strOffice = "='" & Me.cboOffice.Value & "'"
End If
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
You could have the report open automatically when the dialog opens (or vice versa) by including the
procedure:
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 a report with the name specified is not found in an "Open" state the If Statement displays a message to
All that is required to remove the filter is a line which sets the FilterOn property of the report to False:
The line On Error Resume Next tells Access to ignore any error which might occur if, for example, the report
You can review the complete code for the dialog box in Code Listing 1.
^ top
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
Option groups are useful when there are just a few items to choose from. Here there are three
An Option Group consists of a Frame containing a collection of Option Buttons (sometimes called Radio
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
Detecting and programming the user's choice is simple and is best done with a Case Statement. First it is
Then the Case Statement can place the appropriate criteria string into the variable depending on the value
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
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
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
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:
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
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
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:
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
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
To create the list, each combo box has its Row Source Type property set to Value List. This allows a list of
Separated by semicolons, each item becomes an option on the combo box list:
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
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
The remaining jobs are handled by the combo boxes' Change event:
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
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
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
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):
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
[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:
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
In the earlier examples the this button has simply set the FilterOn property of the report to False. Now it has
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):
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
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
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
Choosing Starts with... would return: Martin, Mark, Margaret, Mariana etc.
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:
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:
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
The above examples include a few lines of code that look for the open report and warn the user if it isn't
found:
But you could save them the trouble by opening the report automatically as soon as they press the Apply
Filter button:
Or perhaps you could ask the user what they want to do:
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
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
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 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
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:
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.