Access VBA Manual

download Access VBA Manual

of 321

description

MS Access VBA tips and tricks for A Level ICT Projects

Transcript of Access VBA Manual

CONTENT

Useful Web Resources2What are Database objects?3The Query Lost My Records!5Common Errors with Null6Calculated Fields10Relationships between Tables13Validation Rules16When to use validation rules19Don't use Yes/No fields to store preferences21Using a Combo Box to Find Records23Referring to Controls on a Subform29Enter value as a percent30Assigning Auto Keys for Aligning Controls32Why does my form go completely blank?34Scroll records with the mouse wheel36Avoid #Error in form/report with no records39Limiting a Report to a Date Range41Print the record in the form45Bring the total from a subreport back onto the main report47Numbering Entries in a Report or Form49Getting a value from a table: DLookup()54Locking bound controls56Nulls: Do I need them?61Common Errors with Null63Problem properties67Default forms, reports and databases72Calculating elapsed time76A More Complete DateDiff Function77Constructing Modern Time Elapsed Strings in Access86Quotation marks within quotes94Why can't I append some records?96Rounding in Access98Assign default values from the last record105Managing Multiple Instances of a Form113Rolling dates by pressing "+" or "-"117Return to the same record next time form is opened118Unbound text box: limiting entry length121Properties at Runtime: Forms125Highlight the required fields, or the control that has focus127Combos with Tens of Thousands of Records130Adding values to lookup tables134Use a multi-select list box to filter a report141Print a Quantity of a Label145Has the record been printed?147Code accompanying article:Has the record been printed?148Cascade to Null Relations153List Box of Available Reports159Format check boxes in reports163Sorting report records at runtime165Reports: Page Totals168Reports: a blank line every fifth record169Reports: Snaking Column Headers171Duplex reports: start groups on an odd page173Lookup a value in a range175Action queries: suppressing dialogs, while knowing results178Truncation of Memo fields180Crosstab query techniques182Subquery basics186Ranking or numbering records192Common query hurdles196Reconnect Attached tables on Start-up201Self Joins203Field type reference - names and values for DDL, DAO, and ADOX205Set AutoNumbers to start from ...207Custom Database Properties210Error Handling in VBA215Extended DLookup()219Extended DCount()224Extended DAvg()228Archive: Move Records to Another Table232List files recursively235Enabling/Disabling controls, based on User Security240Concatenate values from related records245MinOfList() and MaxOfList() functions250Age() Function253TableInfo() function261DirListBox() function265PlaySound() function267ParseWord() function269FileExists() and FolderExists() functions275ClearList() and SelectAll() functions278Count lines (VBA code)281Insert characters at the cursor287Hyperlinks: warnings, special characters, errors292Intelligent handling of dates at the start of a calendar year300Splash screen with version information309Printer Selection Utility316

Useful Web Resourceswww.allenbrowne.com

What are Database objects?

When you create a database, Access offers youTables, Queries, Forms, Reports, Macros,andModules. Here's a quick overview of what these are and when to use them.TablesAll data is stored in tables. When you create a new table, Access asks you definefields(column headings), giving each a unique name, and telling Access thedata type. You can use the "Text" type for most data, including numbers that don't need to be added e.g. phone numbers or postal codes. Once you have defined a table's structure, you can enter data. Each new row that you add to the table is called arecord.

QueriesUse a query to find or operate on the data in your tables. With a query, you can display the records that match certaincriteria(e.g. all the members called "Barry"),sort the data as you please (e.g. by Surname), and evencombine datafrom different tables.You caneditthe data displayed in a query (in most cases), and the data in the underlying table will change. Special queries can also be defined to makewholesale changesto your data, e.g. delete all members whose subscriptions are 2 years overdue, or set a "State" field to "WA" wherever postcode begins with 6.

FormsThese are screens fordisplayingdata from andinputtingdata into your tables. The basic form has an appearance similar to an index card: it shows only one record at a time, with a different field on each line. If you want to control how the records aresorted,define a query first, and then create a form based on the query. If you have defined a one-to-many relationship between two tables, use the "Subform" Wizard to create a form which contains another form. The subform will then display only the records matching the one on the main form.

ReportsIf forms are for input, then reports are for output.Anything you plan to printdeserves a report, whether it is a list of names and addresses, a financial summary for a period, or a set of mailing labels.

MacrosAn Access Macro is ascriptfor doing a job. For example, to create a button which opens a report, you could use a macro which fires off the "OpenReport" action. Macros can also be used toset one fieldbased on the value of another (the "SetValue" action), tovalidatethat certain conditions are met before a record saved (the "CancelEvent" action) etc.

ModulesThis is where youwrite your own functionsand programs if you want to. Everything that can be done in a macro can also be done in a module.Modules are far more powerful, and are essential if you plan to write code for amulti-userenvironment.

Calculated FieldsHow do you get Access to store the result of a calculation? For example, if you have fields namedQuantityandUnitPrice, how do you get Access to writeQuantity * UnitPriceto another field calledAmount?

Calculations in QueriesCalculated columns are part of life on a spreadsheet, but do not belong in a database table. Never store a value that is dependent on other fields - it's a basic rule of normalization.So, how do you get the calculated field if you do not store it in a table? Use a queryCreate a query based on your table.Type your expression into the Field row of the query design grid:Amount: [Quantity] * [UnitPrice]This creates a field namedAmount. Any form or report based on this query treats the calculated field like any other, so you can easily sum the results. It is simple, efficient, and fool-proof.

You want to store a calculated result anyway?There are circumstances where storing a calculated result makes sense. Say you charge a construction fee that is normally an additional 10%, but to win some quotes you may want to waive the fee. The calculated field will not work. In this case it makes perfect sense to have a record where the fee is 0 instead of 10%, so you must store this as a field in the table.To achieve this, use the After Update event of the controls on your form to automatically calculate the fee.Set theAfter Updateproperty of theQuantitytext box to[Event Procedure].Click the Build button (...) beside this. Access opens the Code window. Enter this line between thePrivate Sub...andEnd Sublines:Private Sub Quantity_AfterUpdate() Me.Fee = Round(Me.Quantity * Me.UnitPrice * 0.1, 2)End Sub

Set theAfter Updateproperty of theUnitPricetext box to[Event Procedure], and click the Build button. Enter this line.Private Sub UnitPrice_AfterUpdate() Call Quantity_AfterUpdateEnd Sub

Now whenever theQuantityorUnitPricechanges, Access automatically calculates the new fee, but the user can override the calculation and enter a different fee when necessary.

What about Calculated fields in Access 2010?Access 2010 allows you to put a calculated field into a table, like this:

Just choose Calculated in the data type, and Expression appears below it. Type the expression. Access will then calculate it each time you enter your record.This may seem simple, but it creates more problems than it solves. You will quickly find that the expressions are limited. You will also find it makes your database useless for anyone using older versions of Access - they will get a message like this:

Relationships between TablesDatabase beginners sometimes struggle with what tables are needed, and how to relate one table to another. It's probably easiest to follow withan example.As a school teacher, Margaret needs to track eachstudent's name and home details, along with thesubjectsthey have taken, and thegradesachieved. To do all this in a single table, she could try making fields for:NameAddressHome PhoneSubjectGrade

But this structure requires her to enter the student'sname and address againfor every new subject! Apart from the time required for entry, can you imagine what happens when a student changes address and Margaret has to locate and update all the previous entries? She tries a different structure with onlyone recordfor each student. This requiresmany additional fields- something like:NameAddressHome PhoneName of Subject 1

Grade for Subject 1Name of Subject 2Grade for Subject 2Name of Subject 3

Buthow manysubjects must she allow for? How muchspacewill this waste? How does she knowwhich columnto look in to find "History 104"? How can sheaveragegrades that could be in any old column? Whenever you see thisrepetition of fields, the data needs to be broken down intoseparate tables.The solution to her problem involves makingthree tables: one forstudents, one forsubjects, and one forgrades. The Students table must have aunique code for each student, so the computer doesn't get confused about two students with the same names. Margaret calls this field StudentID, so theStudentstable contains fields:StudentID - a unique code for each student.Surname - split Surname and First Name to make searches easierFirstNameAddress - split Street Address, Suburb, and Postcode for the same reason

SuburbPostcodePhone

TheSubjectstable will have fields:SubjectID - a unique code for each subject. (Use the school's subject code)Subject - full title of the subjectNotes - comments or a brief description of what this subject covers.

TheGradestable will then have just three fields:StudentID - a code that ties this entry to a student in the Students tableSubjectID - a code that ties this entry to a subject in the Subjects tableGrade - the mark this student achieved in this subject

After creating the three tables, Margaret needs tocreate a linkbetween them. Now sheentersall thestudentsin the Students table, with the unique StudentID for each. Next sheentersall thesubjectsshe teaches into the Subjects table, each with a SubjectID. Then at the end of term when the marks are ready, she canenterthem in theGradestable using the appropriate StudentID from the Students table and SubjectID from the Subjects table.To help enter marks, she creates a form, using the "Form/Subform" wizard: "Subjects" is the source for the main form, and "Grades" is the source for thesubform. Now with the appropriate subject in the main form, and adds each StudentID and Grade in the subform.The grades were entered by subject, but Margaret needsto view them by student. She creates another form/subform, with themain formreading its data from theStudentstable, and thesubformfrom theGradestable. Since she used StudentID when entering grades in her previous form, Access links this code to the one in the new main form, and automatically displays all the subjects and gradesfor the student in the main form.

Validation RulesValidation rules prevent bad data being saved in your table. You can create a rule for afield(lower pane of table design), or for thetable(in the Properties box in table design.) Use the table's rule to compare fields.

Validation Rules for fieldsWhen you select a field in table design, you see itsValidation Ruleproperty in thelower pane.This rule is applied when you enter data into the field. You cannot tab to the next field until you enter something that satisfies the rule, or undo your entry.To do this ...Validation Rule for FieldsExplanation

Accept letters (a - z) onlyIs Null OR Not Like "*[!a-z]*"Any character outside the range A to Z is rejected. (Case insensitive.)

Accept digits (0 - 9) onlyIs Null OR Not Like "*[!0-9]*"Any character outside the range 0 to 9 is rejected. (Decimal point and negative sign rejected.)

Letters and spaces onlyIs Null Or Not Like "*[!a-z OR "" ""]*"Punctuation and digits rejected.

Digits and letters onlyIs Null OR Not Like "*[!((a-z) or (0-9))]*"Accepts A to Z and 0 to 9, but no punctuation or other characters

Exactly 8 charactersIs Null OR Like "????????"The question mark stands for one character.

Exactly 4 digitsIs Null OR Between 1000 And 9999

Is Null OR Like "####"For Number fields.

For Text fields.

Positive numbers onlyIs Null OR >= 0Remove the "=" if zero is not allowed either.

No more than 100%Is Null OR Between -1 And 1100% is 1. Use 0 instead of -1 if negative percentages are not allowed.

Not a future dateIs Null OR = 12#) And (frm.CurrentView = 1) And (lngCount 0&) Then 'Save any edits before moving record. RunCommand acCmdSaveRecord 'Move back a record if Count is negative, otherwise forward. RunCommand IIf(lngCount < 0&, acCmdRecordsGoToPrevious, acCmdRecordsGoToNext) DoMouseWheel = Sgn(lngCount) End If

Exit_Handler: Exit Function

Err_Handler: Select Case Err.Number Case 2046& 'Can't move before first, after last, etc. Beep Case 3314&, 2101&, 2115& 'Can't save the current record. strMsg = "Cannot scroll to another record, as this one can't be saved." MsgBox strMsg, vbInformation, "Cannot scroll" Case Else strMsg = "Error " & Err.Number & ": " & Err.Description MsgBox strMsg, vbInformation, "Cannot scroll" End Select Resume Exit_HandlerEnd Function

Savethe module, with a name such as modMouseWheel.Open your form in design view. On the Event tab of the Properties sheet, set theOn Mouse Wheelproperty to [Event Procedure]Click theBuild button(...) beside the property. Access opens the code window. Between thePrivate Sub... andEnd Sublines,enter Call DoMouseWheel(Me, Count)Repeat steps 4 and 5 for your other forms.

How it worksThe function accepts two arguments: A reference to theform(which will be the active form if the mouse is scrolling it), and The value ofCount(a positive number if scrolling forward, or negative if scrolling back.)

Firstly, the code tests the Accessversionis at least 12 (the internal version number for Access 2007), and the form is inForm view. It does nothing in a previous version or in another view where the mouse scroll still works. It also does nothing if the count is zero, i.e. neither scrolling forward nor back.Before you can move record, Access mustsavethe currentrecord. Explicitly saving is always a good idea, as this clears pending events. If the record cannot be saved (e.g. required field missing), the line generates an error and drops to the error hander which traps the common issues.The highlightedRunCommandmoves to the previous record if the Count is negative, or the next record if positive. This generates error 2046 if you try to scroll up above the first record, or down past the last one. Again the error handler traps this error.Finally we set the return value to the sign of the Count argument, so the calling procedure can tell whether we moved record.

Avoid #Error in form/report with no recordsCalculated expressions show #Error when a form or report has no records. This is known as a Hash Error.This sort-of makes sense for a developer - if the controls don't exist, you cannot sum them. But seeing this type of error can be confising for the user, so the obvious thing to do is eliminate this type of errot.

In formsThe problem does not arise in forms that are displaying a new record (in other words the form is ready to accept data for a new record).You will find it does occur if the form's Allow Additions property is Yes, or if the form is bound to a non-updatable query.To avoid the problem, test the RecordCount of the form's Recordset. In older versions of Access, that meant changing:

=Sum([Amount])

to: =IIf([Form].[Recordset].[RecordCount] > 0, Sum([Amount]), 0)

This wont work in newer versions of Access. You will need a new Function to take care of this error.

Code it YoutselfCopy this function into a standard module, and save the module with a name such as modHashError

Public Function FormHasData(frm As Form) As Boolean 'Purpose: Return True if the form has any records (other than new one). ' Return False for unbound forms, and forms with no records. 'Note: Avoids the bug in Access 2007 where text boxes cannot use: ' [Forms].[Form1].[Recordset].[RecordCount] On Error Resume Next 'To handle unbound forms. FormHasData = (frm.Recordset.RecordCount 0&)End Function

Now use this expression in the Control Source of the text box: =IIf(FormHasData([Form]), Sum([Amount]), 0)

In reportsUse the HasData property specifically for this purpose.So, instead of: =Sum([Amount])

use: =IIf([Report].[HasData], Sum([Amount]), 0)

If you have many calculated controls, you need to do this on each one. But note, if Access discovers one calculated control that it cannot resolve, it gives up on calculating the others. Thereforeone bad expression can cause other calculated controls to display#Error, even if those controls are bound to valid expressions.

Limiting a Report to a Date RangeThere are two methods to limit the records in a report to a user-specified range of dates.

Method 1: Parameter queryThe simplest approach is to base the report on a parameter query. This approachworks for all kinds of queries, but has thesedisadvantages: Inflexible: both dates must be entered Inferior interface: two separate dialog boxes pop up No way to supply defaults No way to validate the dates

To create the parameter query you need to create a new query to use as the RecordSource of your report.In query design view, in the Criteria row under your date field, enter: >= [StartDate] < [EndDate] + 1

Choose Parameters from the Query menu, and declare two parameters of typeDate/Time: StartDate Date/Time EndDate Date/Time

To display the limiting dates on the report, open your report in Design View, and add two text boxes to the Report Header section. Set their ControlSource property to=StartDateand=EndDaterespectively.

Method 2: Form for entering the datesThe alternative is to use a small unbound form where the user can enter the limiting dates. This approach may not work if the query aggregates data, but has the followingadvantages: Flexible: user does not have to limit report tofromandtodates. Better interface: allows defaults and other mechanisms for choosing dates. Validation: can verify the date entries.

Here are the steps. This example assumes a report namedrptSales, limited by values in theSaleDatefield.Create a new form that is not bound to any query or table. Save with the namefrmWhatDates.Add two text boxes, and name themtxtStartDateandtxtEndDate. Set theirFormatproperty toShort Date, so only date entries will be accepted.Add a command button, and set its Name property tocmdPreview.Set the button'sOn Clickproperty to[Event Procedure] and click the Build button (...) beside this. Access opens the code window.Between the "Private Sub..." and "End Sub" lines paste in the code below.

Private Sub cmdPreview_Click()'On Error GoTo Err_Handler 'Remove the single quote from start of this line once you have it working. 'Purpose: Filter a report to a date range. 'Documentation: http://allenbrowne.com/casu-08.html 'Note: Filter uses "less than the next day" in case the field has a time component. Dim strReport As String Dim strDateField As String Dim strWhere As String Dim lngView As Long Const strcJetDate = "\#mm\/dd\/yyyy\#" 'Do NOT change it to match your local settings. 'DO set the values in the next 3 lines. strReport = "rptSales" 'Put your report name in these quotes. strDateField = "[SaleDate]" 'Put your field name in the square brackets in these quotes. lngView = acViewPreview 'Use acViewNormal to print instead of preview. 'Build the filter string. If IsDate(Me.txtStartDate) Then strWhere = "(" & strDateField & " >= " & Format(Me.txtStartDate, strcJetDate) & ")" End If If IsDate(Me.txtEndDate) Then If strWhere vbNullString Then strWhere = strWhere & " AND " End If strWhere = strWhere & "(" & strDateField & " < " & Format(Me.txtEndDate + 1, strcJetDate) & ")" End If 'Close the report if already open: otherwise it won't filter properly. If CurrentProject.AllReports(strReport).IsLoaded Then DoCmd.Close acReport, strReport End If 'Open the report. 'Debug.Print strWhere 'Remove the single quote from the start of this line for debugging purposes. DoCmd.OpenReport strReport, lngView, , strWhere

Exit_Handler: Exit Sub

Err_Handler: If Err.Number 2501 Then MsgBox "Error " & Err.Number & ": " & Err.Description, vbExclamation, "Cannot open report" End If Resume Exit_HandlerEnd Sub

Open the report in Design View, and add two text boxes to the report header for displaying the date range. Set theControlSourcefor these text boxes to:=Forms.frmWhatDates.txtStartDate=Forms.frmWhatDates.txtEndDate

Now when you click theOkbutton, the filtering works like this: both start and end dates found: filtered between those dates; only a start date found: records from that date onwards; only an end date found: records up to that date only; neither start nor end date found: all records included.

You will end up using this form for all sorts of reports. You may add an option group or list box that selects which report you want printed, and a check box that determines whether the report should be opened in preview mode.

Print the record in the formHow do you print just the one record you are viewing in the form?Create areport, to get the layout right for printing. Use theprimary keyvalue that uniquely identifies the record in the form, and open the report with just that one record.The stepsOpen yourformin design view.Click thecommand buttonin the toolbox (Access 1 - 2003) or on the Controls group of the Design ribbon (Access 2007 and 2010), and click on your form.If the wizard starts, cancel it. It will not give you the flexibility you need.Right-click the new command button, and chooseProperties. Access opens the Properties box.On theOthertab, set theNameto something like:cmdPrintOn theFormattab, set theCaptionto the text you wish to see on the button, or thePictureif you would prefer a printer or preview icon.On theEventtab, set theOn Clickproperty to:[Event Procedure]Click theBuildbutton (...) beside this. Access opens the code window.Paste the code below into the procedure. ReplaceIDwith the name of your primary key field, andMyReportwith the name of your report.The code

Private Sub cmdPrint_Click() Dim strWhere As String

If Me.Dirty Then 'Save any edits. Me.Dirty = False End If

If Me.NewRecord Then 'Check there is a record to print MsgBox "Select a record to print" Else strWhere = "[ID] = " & Me.[ID] DoCmd.OpenReport "MyReport", acViewPreview, , strWhere End IfEnd Sub

Bring the total from a subreport back onto the main reportYour subreport has a total at the end - a text box in the Report Footer section, with a Control Source like this: =Sum([Amount])Now, how do you pass that total back to the the main report?Stage 1If the subreport is calledSub1, and the text box istxtTotal, put the text box on your main report, and start with this Control Source: =[Sub1].[Report].[txtTotal]Stage 2Check that it works. It should do if there are records in the subreport. If not, you get#Error. To avoid that, test theHasDataproperty, like this: =IIf([Sub1].[Report].[HasData], [Sub1].[Report].[txtTotal], 0)Stage 3The subreport total could be Null, so you might like to use Nz() to convert that case to zero also: =IIf([Sub1].[Report].[HasData], Nz([Sub1].[Report].[txtTotal], 0), 0)

TroubleshootingIf you are stuck at some point, these further suggestions might help.Total does not work in the subreportIf the basic=Sum([Amount])does not work in the subreport:Make sure the total text box is in the Report Footer section, not the Page Footer section.Make sure the Name of this text box is not the same as the name of a field (e.g. it cannot be called Amount.)The field you are trying to sum must be a field in the report's source table/query. If Amount is a calculated text box such as: =[Quantity]*[PriceEach]then repeat the whole expression in the total box, e.g.: =Sum([Quantity]*[PriceEach])Make sure that what you are trying to sum is a Number, not text. SeeCalculated fields misinterpreted.Stage 1 does not workIf the basic expression at Stage 1 above does not work:Open the main report in design view.Right-click the edge of the subform control, and choose Properties.Check the Name of the subreport control (on the Other tab of the Properties box.)The Name of the subreport control can be different than the name of the report it contains (its Source Object.)Uncheck the Name AutoCorrect boxes under: Tools | Options | GeneralFor details of why, seeFailures caused by Name Auto-CorrectStage 2 does not workIf Stage 2 does not work but Stage 1 does, you must provide 3 parts for IIf():an expression that can be True or False (theHasDataproperty in our case),an expression to use when the first part is True (the value from the subreport, just like Stage 1),an expression to use when the first part is False (a zero.)

Numbering Entries in a Report or FormReportThere is a very simple way to number records sequentially on a report. It always works regardless how the report is sorted or filtered.With your report open in Design View:From the Toolbox (Access 1 - 2003) or the Controls group of the Design ribbon (Access 2007 and later), add a text box for displaying the number.Select the text box, and in the Properties Window, set these properties:Control Source=1

Running SumOver Group

That's it! This text box will automatically increment with each record.FormCasual users sometimes want to number records in a form as well, e.g. to save the number of a record so as to return there later. Don't do it! Although Access does show "Recordxxofyy" in the lower left ofthe form, this number can change for any number of reasons, such as:The user clicks the "A-Z" button to change the sort order;The user applies a filter;A new record is inserted;An old record is deleted.In relational database theory, the records in a table cannot have any physical order, so record numbers represent faulty thinking. In place of record numbers, Access uses the Primary Key of the table, or the Bookmark of a recordset. If you are accustomed from another database and find it difficult to conceive of life without record numbers, check outWhat, no record numbers?You still want to refer to the number of a record in a form as currently filtered and sorted? There are ways to do so. In Access 97 or later, use the form'sCurrentRecordproperty, by adding a text box with this expression in the ControlSource property: =[Form].[CurrentRecord]InAccess 2, open your form in Design View in design view and follow these steps:From the Toolbox, add a text box for displaying the number.Select the text box, and in the Properties Window, set its Name totxtPosition. Be sure to leave theControl Sourceproperty blank.Select the form, and in the Properties Window set the On Current property to[Event Procedure].Click the "..." button beside this. Access opens the Code window.Between the linesSub Form_Current()andEnd Sub, paste these lines:

On Error GoTo Err_Form_Current Dim rst As Recordset

Set rst = Me.RecordsetClone rst.Bookmark = Me.Bookmark Me.txtPosition = rst.AbsolutePosition + 1

Exit_Form_Current: Set rst = Nothing Exit Sub

Err_Form_Current: If Err = 3021 Then 'No current record Me.txtPosition = rst.RecordCount + 1 Else MsgBox Error$, 16, "Error in Form_Current()" End If Resume Exit_Form_Current

The text box will now show a number matching the one between the NavigationButtons on your form.QueryFor details of how to rank records in a query, seeRanking in a Query

Hide duplicates selectivelyThis article explains how to use theIsVisibleproperty in conjunction withHideDuplicatesto selectively hide repeating values on a report.Relational databases are full of one-to-many relations. In Northwind, one Order can have many Order Details. So, in queries and reports, fields from the "One" side of the relation repeat on every row like this:

TheHideDuplicatesproperty (on the Format tab of the Properties sheet) helps. SettingHideDuplicatesto Yes for OrderID, OrderDate, and CompanyName, gives a more readable report, but is not quite right:

The Date and Company for Order 10617 disappeared, since they were the same the previous order. Similarly, the company name is hidden in order 10619. How can we suppress the date and company only when repeating the same order, but show them for a new order even if they are the same as the previous row?When Access hides duplicates, it sets a special property namedIsVisible. By testing the IsVisible property of the OrderID, we can hide the OrderDate and CompanyName only when the OrderID changes.Set the properties of the OrderID text box like this:Control Source . . .=IIf(OrderID.IsVisible,[OrderDate],Null)

Hide Duplicates . . .No

Name . . . . . . . . .txtOrderDate

TheControl Sourcetests the IsVisible property of the OrderID. If it is visible, then the control shows the OrderDate. If it is not visible, it shows Null. Leave theHideDuplicatesproperty turned off. We must change the name as well, because Access gets confused if a control has the same name as a field, but is bound to something else.Similarly, set the ControlSource of the CompanyName text box to: =IIf(OrderID.IsVisible,[CompanyName],Null)and change its name to (say) txtCompanyName.Now the report looks like this:

Note that theIsVisibleproperty is not the same as theVisibleproperty in the Properties box.IsVisibleis not available at design time. Access sets it for you when the report runs, for exactly the purpose explained in this article.If you are trying to create the sample report above in the Northwind sample database, here is the query it is based on:SELECT Orders.OrderID, Orders.OrderDate, Customers.CompanyName, [Order Details].ProductID, Products.ProductName, [Order Details].QuantityFROM Products INNER JOIN ((Customers INNER JOIN Orders ON Customers.CustomerID=Orders.CustomerID) INNER JOIN [Order Details] ON Orders.OrderID=[Order Details].OrderID) ON Products.ProductID=[Order Details].ProductIDWHERE Orders.OrderID > 10613ORDER BY Orders.OrderID;In summary, useHideDuplicateswhere you do want duplicates hidden, but for other controls that should hide at the same time, test theIsVisibleproperty in their ControlSource.

Getting a value from a table: DLookup()Sooner or later, you will need to retrieve a value stored in a table. If you regularly make write invoices to companies, you will have aCompanytable that contains all the company's details including aCompanyIDfield, and aContracttable that stores just theCompanyIDto look up those details. Sometimes you can base your form or report on a query that contains all the additional tables. Other times, DLookup() will be a life-saver.DLookup() expects you to give itthreethings inside the brackets. Think of them as: Look up the _____ field, from the _____ table, where the record is _____Each of these must goin quotes, separated by commas.You must also use square brackets around the table or field names if the names contain odd characters (spaces, #, etc) or start with a number.This is probably easiest to follow with some examples:you have aCompanyIDsuch as 874, and want to print the company name on a report;you haveCategorysuch as "C", and need to show what this category means.you haveStudentIDsuch as "JoneFr", and need the student?s full name on a form.Example 1:Look up theCompanyNamefield from tableCompany, whereCompanyID = 874. This translates to: =DLookup("CompanyName", "Company", "CompanyID = 874")You don't want Company 874 printed for every record! Use an ampersand (&) to concatenate thecurrent valuein theCompanyIDfield of your report to the "Company = " criteria: =DLookup("CompanyName", "Company", "CompanyID = " & [CompanyID])If the CompanyID is null (as it might be at a new record), the 3rd agumenent will be incomplete, so the entire expression yields #Error. To avoid that use Nz() to supply a value for when the field is null: =DLookup("CompanyName", "Company", "CompanyID = " & Nz([CompanyID],0))Example 2:The example above is correct ifCompanyIDis a number. Butif the field is text, Access expects quote marksaround it. In our second example, we look up theCategoryNamefield in tableCat, whereCategory= 'C'. This means the DLookup becomes: =DLookup("CategoryName", "Cat", "Category = 'C'")Single quoteswithin the double quotes is one way to do quotes within quotes. But again, we don't want Categoy 'C' for all records: we need the current value from ourCategoryfield patched into the quote. To do this, we close the quotation after the first single quote, add the contents ofCategory, and then add thetrailing single quote. This becomes: =DLookup("CategoryName", "Cat", "Category = '" & [Category] & "'")Example 3:In our third example, we need thefull namefrom aStudenttable. But the student table has the name split intoFirstNameandSurnamefields, so we need to refer to them both and add aspacebetween. To show this information on your form, add a textbox with ControlSource: =DLookup("[FirstName] & ' ' & [Surname]", "Student", "StudentID = '" & [StudentID] & "'")Quotes inside quotesNow you know how to supply the 3 parts for DLookup(), you are using quotes inside quotes. The single quote character fails if the text contains an apostrophe, so it is better to use the double-quote character. But you must double-up the double-quote character when it is inside quotes.

Locking bound controlsIt is very easy to overwrite data accidentally in Access. Setting a form'sAllowEditsproperty prevents that, but also locks any unbound controls you want to use for filtering or navigation. This solution locks only the bound controls on a form and handles its subforms as well.

First, the code saves any edits in progress, so the user is not stuck with a half-edited form. Next it loops through all controls on the form, setting theLockedproperty of each oneunlessthe control:is an unsuitable type (lines, labels, ...);has noControl Sourceproperty (buttons in an option group);is bound to an expression (Control Sourcestarts with "=");is unbound (Control Sourceis blank);is named in the exception list. (You can specify controls you do not want unlocked.)If it finds asubform, the function calls itselfrecursively. Nested subforms are therefore handled to any depth. If you do not want your subform locked, name it in the exception list.The form'sAllowDeletionsproperty is toggled as well. The code changes the text on the command button to indicate whether clicking again will lock or unlock.To help the user remember they must unlock the form to edit, add a rectangle named rctLock around the edge of your form. The code shows this rectangle when the form is locked, and hides it when unlocked.Using with your formsTo use the code:Open a new module. In Access 95 - 2003, click the Modules tab of the Database window, and click New. In Access 2007 and later, click Module (rightmost icon) on the Create ribbon. Access opens a code module.Paste in the code from the end of this article.Save the module with a name such as ajbLockBound.(Optional) Add a red rectangle to your form to indicate it is locked. Name it rctLock.To initialize the form so it comes up locked, set theOn Loadproperty of your form to: =LockBoundControls([Form],True)Add a command button to your form. Name it cmdLock.Set itsOn Clickproperty to[Event Procedure].Click the Build button (...) beside this.Set up the code like this:

Private Sub cmdLock_Click() Dim bLock As Boolean bLock = IIf(Me.cmdLock.Caption = "&Lock", True, False) Call LockBoundControls(Me, bLock) End Sub

(Optional) Add the names of any controls you do not want unlocked at steps 3 and 4. For example, to avoid unlocking controls EnteredOn and EnteredBy in the screenshot above, you would use: Call LockBoundControls(Me, bLock, "EnteredOn", "EnteredBy")Note that if your form has any disabled controls, changing theirLockedproperty affects the way they look. To avoid this, add them to the exception list.The codePublic Function LockBoundControls(frm As Form, bLock As Boolean, ParamArray avarExceptionList())On Error GoTo Err_Handler 'Purpose: Lock the bound controls and prevent deletes on the form any its subforms. 'Arguments frm = the form to be locked ' bLock = True to lock, False to unlock. ' avarExceptionList: Names of the controls NOT to lock (variant array of strings). 'Usage: Call LockBoundControls(Me. True) Dim ctl As Control 'Each control on the form Dim lngI As Long 'Loop controller. Dim bSkip As Boolean 'Save any edits. If frm.Dirty Then frm.Dirty = False End If 'Block deletions. frm.AllowDeletions = Not bLock For Each ctl In frm.Controls Select Case ctl.ControlType Case acTextBox, acComboBox, acListBox, acOptionGroup, acCheckBox, acOptionButton, acToggleButton 'Lock/unlock these controls if bound to fields. bSkip = False For lngI = LBound(avarExceptionList) To UBound(avarExceptionList) If avarExceptionList(lngI) = ctl.Name Then bSkip = True Exit For End If Next If Not bSkip Then If HasProperty(ctl, "ControlSource") Then If Len(ctl.ControlSource) > 0 And Not ctl.ControlSource Like "=*" Then If ctl.Locked bLock Then ctl.Locked = bLock End If End If End If End If Case acSubform 'Recursive call to handle all subforms. bSkip = False For lngI = LBound(avarExceptionList) To UBound(avarExceptionList) If avarExceptionList(lngI) = ctl.Name Then bSkip = True Exit For End If Next If Not bSkip Then If Len(Nz(ctl.SourceObject, vbNullString)) > 0 Then ctl.Form.AllowDeletions = Not bLock ctl.Form.AllowAdditions = Not bLock Call LockBoundControls(ctl.Form, bLock) End If End If Case acLabel, acLine, acRectangle, acCommandButton, acTabCtl, acPage, acPageBreak, acImage, acObjectFrame 'Do nothing Case Else 'Includes acBoundObjectFrame, acCustomControl Debug.Print ctl.Name & " not handled " & Now() End Select Next 'Set the visual indicators on the form. On Error Resume Next frm.cmdLock.Caption = IIf(bLock, "Un&lock", "&Lock") frm!rctLock.Visible = bLock

Exit_Handler: Set ctl = Nothing Exit Function

Err_Handler: MsgBox "Error " & Err.Number & " - " & Err.Description Resume Exit_HandlerEnd Function

Public Function HasProperty(obj As Object, strPropName As String) As Boolean 'Purpose: Return true if the object has the property. Dim varDummy As Variant On Error Resume Next varDummy = obj.Properties(strPropName) HasProperty = (Err.Number = 0)End Function

Nulls: Do I need them?Why have Nulls?Learning to handle Nulls can be frustrating. Occasionally I hear newbies ask, "How can I prevent them?" Nulls are a very important part of your database, and it is essential that you learn to handle them.A Null is"no entry"in a field.The alternativeis to require an entry ineveryfield ofeveryrecord! You turn up at a hospital too badly hurt to give your birth date, and they won't let you in because the admissions database can't leave the field null? Since some fields must be optional, so you must learn to handle nulls.Nulls are not a problem invented by Microsoft Access. They are a veryimportant part of relational database theory and practice, part of any reasonable database. Ultimately you will come to see the Null as your friend.Think of Null as meaningUnknown.

Null is not the same as zeroOpen the Immediate Window (press Ctrl+G), and enter:? Null = 0VBA responds,Null. In plain English, you asked VBA,Is an Unknown equal to Zero?, and VBA responded with,I don't know.Null is not the same as zero.If an expression contains a Null, the result is often Null. Try:? 4 + NullVBA responds withNull, i.e.The result is Unknown.The technical name for this domino effect isNull propagation.Nulls are treated differently from zeros when youcountoraveragea field. Picture a table with anAmountfield and these values in its 3 records:4, 5, NullIn the Immediate window, enter:? DCount("Amount", "MyTable")VBA responds with2. Although there are three records, there are only two known values to report. Similarly, if you ask:? DAvg("Amount", "MyTable")VBA responds with4.5, not3.Nulls are excluded from operations such as sum, count, and average.Hint: To count all records, use Count("*") rather than Count("[SomeField]"). That way Access can respond with the record count rather than wasting time checking if there are nulls to exclude.

Null is not the same as a zero-length stringVBA uses quote marks that open and immediately close again to represent a string with nothing in it. If you haveno middle name, it could be represented as a zero-length string. That is not the same as saying your middle name is unknown (Null). To demonstrate the difference, enter this into the Immediate window:? Len(""), Len(Null)VBA responds that the length of the first string is zero, but the length of the unknown is unknown (Null).Text fields in an Access table can contain a zero-length string to distinguishUnknownfromNon-existent. However, there is no difference visible to the user, so you are likely to confuse the user (as well as the typical Access developer.) Recent versions of Access default this property to Yes: we recommend you change this property for all Text and Memo fields. Details and code inProblem Properties.

Null is not the same as Nothing or MissingThese are terms that sound similar but mean do not mean the same as Null, the unknown value.VBA usesNothingto refer to an unassigned object, such as a recordset that has been declared but not set.VBA usesMissingto refer to an optional parameter of a procedure.To help you avoid common traps in handling nulls, see:Common Errors with Null

Common Errors with NullHere are some common mistakes newbies make with Nulls. Error 1: Nulls in CriteriaIf you enter criteria under a field in a query, it returns only matching records. Nulls are excluded when you enter criteria.For example, say you have a table of company names and addresses. You wanttwo queries: one that gives you thelocalcompanies, and the other that gives youall the rest. In the Criteria row under theCityfield of thefirst query, you type:"Springfield"and in thesecond query:Not "Springfield"Wrong! Neither query includes the records where City is Null.SolutionSpecifyIs Null. For the second query above to meet your design goal of "all the rest", the criteria needs to be:Is Null Or Not "Springfield"Note: Data Definition Language (DDL) queries treat nulls differently. For example, the nulls are counted in this kind of query: ALTER TABLE Table1 ADD CONSTRAINT chk1 CHECK (99 < (SELECT Count(*) FROM Table2 WHERE Table2.State 'TX'));

Error 2: Nulls in expressionsMaths involving a Null usually results in Null. For example, newbies sometimes enter an expression such as this in the ControlSource property of a text box, to display the amount still payable:=[AmountDue] - [AmountPaid]The trouble is that if nothing has been paid, AmountPaid is Null, and so this text box displays nothing at all.SolutionUse the Nz() function to specify a value for Null:= Nz([AmountDue], 0) - Nz([AmountPaid], 0)

Error 3: Nulls in Foreign KeysWhile Access blocks nulls in primary keys, it permits nulls in foreign keys. In most cases, you shouldexplicitly blockthis possibilityto prevent orphaned records.For a typical Invoice table, the line items of the invoice are stored in an InvoiceDetail table, joined to the Invoice table by an InvoiceID. You createa relationshipbetween Invoice.InvoiceID and InvoiceDetail.InvoiceID,with Referential Integrityenforced. It'snot enough!Unless you set theRequiredproperty of the InvoiceID field toYesin the InvoiceDetail table, Access permits Nulls. Most often this happens when a user begins adding line items to the subform without first creating the invoice itself in the main form. Since these records don't match any record in the main form, these orphaned records are never displayed again. The user is convinced your program lost them, though they are still there in the table.SolutionAlwaysset the Required propertyof foreign key fields to Yes in table design view, unless you expressly want Nulls in the foreign key.

Error 4: Nulls and non-VariantsIn Visual Basic, the only data type that can contain Null is the Variant. Whenever you assign the value of a field to a non-variant, you must consider the possibility that the field may be null. Can you see what could go wrong with this code in a form's module?Dim strName as StringDim lngID As LongstrName = Me.MiddleNamelngID = Me.ClientIDWhen the MiddleName field contains Null, the attempt toassign the Null to a string generates an error.Similarly the assignment of the ClientID value to a numeric variable may cause an error. Even if ClientID is the primary key, the code is not safe: the primary key contains Null at a new record.Solutions(a) Use a Variant data type if you need to work with nulls.(b) Use theNz()function to specify a value to use for Null. For example:strName = Nz(Me.MiddleName, "")lngID = Nz(Me.ClientID, 0)

Error 5: Comparing something to NullThe expression:If [Surname] = Null Thenis a nonsense that will never be True. Even if the surname is Null, VBA thinks you asked:Does Unknown equal Unknown?and always responds "How do I know whether your unknowns are equal?" This isNull propagationagain: the result is neither True nor False, but Null.SolutionUse theIsNull()function:If IsNull([Surname]) Then

Error 6: Forgetting Null is neither True nor False.Do these two constructs do the same job?(a)If [Surname] = "Smith" Then MsgBox "It's a Smith"Else MsgBox "It's not a Smith"End If

(b)If [Surname] "Smith" Then MsgBox "It's not a Smith"Else MsgBox "It's a Smith"End IfWhen the Surname is Null, these 2 pieces of codecontradicteach other. In both cases, theIffails, so theElseexecutes, resulting in contradictory messages.Solutions(a) Handle allthreeoutcomes of a comparison -True, False, and Null:If [Surname] = "Smith" Then MsgBox "It's a Smith"ElseIf [Surname] "Smith" Then MsgBox "It's not a Smith"Else MsgBox "We don't know if it's a Smith"End If(b) In some cases, the Nz() function lets you to handle two cases together. For example, to treat a Null and a zero-length string in the same way:If Len(Nz([Surname],"")) = 0 Then

Problem propertiesRecent versions of Access have introduced new properties or changed the default setting for existing properties. Accepting the new defaults causes failures, diminished integrity, performance loss, and exposes your application to tinkerers.Databases: Name AutoCorrectAny database created with Access 2000 or later, has theName AutoCorrectproperties on. You must remember to turn it off for every new database you create:In Access 2010, click File | Options | Current Database, and scroll down to Name AutoCorrect Options.In Access 2007, click the Office Button | Access Options | Current Database, and scroll down to Name AutoCorrect Options.In Access 2000 - 2003, theName AutoCorrectboxes are under Tools | Options | General.The problems associated with this property are wide-ranging. For details, see:Failures caused byName Auto-Correct.You may also wish to turn offRecord-level locking:In Access 2010: File | Options | Advanced.In Access 2007: Office Button | Access Options | Advanced.In Access 2000 - 2003: Tools | Options | Advanced.Although record-level locking may be desirable in some heavily networked applications, there is a performance hit. Even more significantly, if you have attached tables from Access 97 or earlier and record-level locking is enabled, some DAO transactions may fail. (The scenario that uncovered this bug involved de-duplicating clients - reassigning related records, and then removing the duplicate.)In Access 2007 and later, you will also want to uncheck the box labelledEnable design changes for tables in Datasheet view (for this database)under File (Office Button) | Access Options | Current Database. In Access 2007 and later you can create a template database that sets these settings for every new database. For details, seeDefault forms, reports and databases.Fields: Allow Zero LengthTable fields created in Access 97 had theirAllow Zero Lengthproperty set to No by default. In Access 2000 and later, the property defaults to Yes, and you must remember toturn it off every time you add a fieldto a table.To the end user, there is no visible difference between a zero-length string (ZLS) and a Null, and the distinction should not be forced upon them. The average Access developer has enough trouble validatingNullswithout having to handle the ZLS/Null distinction as well in every event procedure of their application. The savvy developer uses engine-level validation wherever possible, and permits a ZLS only in rare and specific circumstances.There is no justification for having this property on by default. There is no justification for the inconsistency with previous versions.EvenAccess itself gets the distinction between Null and ZLS wrong: DLookup() returns Null when it should yield a ZLS.You must therefore set this property for every field in the database where you do not wish to explicitly permit a ZLS. To save you doing so manually, this code loops through all your tables, and sets the property for each field:Function FixZLS() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim fld As DAO.Field Dim prp As DAO.Property Const conPropName = "AllowZeroLength" Const conPropValue = False Set db = CurrentDb() For Each tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) = 0 Then If tdf.Name "Switchboard Items" Then For Each fld In tdf.Fields If fld.Properties(conPropName) Then Debug.Print tdf.Name & "." & fld.Name fld.Properties(conPropName) = conPropValue End If Next End If End If Next Set prp = Nothing Set fld = Nothing Set tdf = Nothing Set db = NothingEnd Function

How crazy is this? We are now running code to get us back to the functionality we had in previous versions? And you have to keep remembering to set these properties with any structural changes? This is enhanced usability?If you create fields programmatically, be aware that these field properties are set inconsistently. The setting you get forAllow Zero Length,Unicode Compression, and other properties depends on whether you use DAO, ADOX, or DDL to create the field.Prior to Access 2007, numeric fields always defaulted to zero, so you had to manually remove theDefault Valuewhenever you created a Number type field. It was particularly important to do so for foreign key fields.Tables: SubdatasheetNameIn Access 2000, tables got a new property calledSubdatasheetName. If the property is not set, it defaults to "[Auto]". Its datasheet displays a plus sign which the user can click to display related records from some other table that Access thinks may be useful.This automatically assigned property is inherited by forms and subforms displayed in datasheet view. Clearly, this is not a good idea and may haveunintended consequencesin applications imported from earlier versions. Worse still, there areserious performance issuesassociated with loading a form that has several subforms where Access is figuring out and collecting data from multiple more related tables.Again, the solution is to turn off subdatasheets by setting the property to "[None]". Again, there is no way to do this by default, so you must remember to do so every time you create a table. This code will loop through your tables and turn the property off:Function TurnOffSubDataSh() Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const conPropName = "SubdatasheetName" Const conPropValue = "[None]" Set db = DBEngine(0)(0) For Each tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) = 0 Then If tdf.Connect = vbNullString And Asc(tdf.Name) 126 Then 'Not attached, or temp. If Not HasProperty(tdf, conPropName) Then Set prp = tdf.CreateProperty(conPropName, dbText, conPropValue) tdf.Properties.Append prp Else If tdf.Properties(conPropName) conPropValue Then tdf.Properties(conPropName) = conPropValue End If End If End If End If Next Set prp = Nothing Set tdf = Nothing Set db = NothingEnd Function

Public Function HasProperty(obj As Object, strPropName As String) As Boolean 'Purpose: Return true if the object has the property. Dim varDummy As Variant On Error Resume Next varDummy = obj.Properties(strPropName) HasProperty = (Err.Number = 0)End FunctionForms: Allow Design ChangesTheAllow Design Changesproperty for new forms defaults to True ("All Views"). This is highly undesirable for developers. It is also undesirable for tinkerers, as there is some evidence that altering the event procedures while the form is open (not design view) can contribute to corruption. (In Access 2007 and later, this property seems to be removed from the Property Sheet and ignored by the interface, though it is still present and still defaults to True.)Again, we find ourselves having to work around the new defaults. Rather than setting these properties every time you create a form, consider taking a few moments to create someDefault Forms and Reports.Find DialogYou should also be aware that theFinddialog (default form toolbar, Edit menu, or Ctrl+F) nowexposes a Replace tab. This allows users to perform bulk alterations on data without the checks normally performed by Form_BeforeUpdate or follow-ons in Form_AfterUpdate. This seems highly undesirable in a database that provides no triggers at the engine level.A workaround for this behavior is to temporarily set the AllowEdits property of the form to No before youDoCmd.RunCommand acCmdFind.

Default forms, reports and databasesAccess provides a way to set up a form and a report, and nominate them as the template for new forms and reports:in Access 2010: File | Access Options | Object Designers,in Access 2007: Office Button | Access Options | Object Designers,in Access 1 2003: Tools | Options | Forms/Reports.That's useful, as it lets you create forms and reports quickly to your own style.However, these forms/reports do not inherit all properties and code. You will get a better result if youcopy and paste your template form or reportin the database window (Access 1 - 2003) or Nav Pane (Access 2007 and later.) The form created this way inherits all properties and event procedures.It will take you 30-45 minutes to set up these default documents. They will save 5-15 minutes on every form or report you create.A default formCreate a new form, in design view. If you normally provide navigation or filtering options in the Form Header section, display it:in Access 2010: right-click the Detail section, and chooseForm Header/Footer,in Access 2007:Show/Hide(rightmost icon) on the Layout ribbon,in Access 1-2003:Form Header/FooteronViewmenu.Drag these sections to the appropriate height.In addition to your visual preferences, consider setting properties such as these:Allow Design ChangesDesign View OnlyDisallow runtime changes. (Access 2003 and earlier.)

Allow PivotTable ViewNoDisallowing these views prevents tinkerers from trying them from the toolbar or View menu.

Allow PivotChart ViewNo

Width6"Adjust for the minimum screen resolution you anticipate.

Now comes the important part: set thedefault properties for each type of control.Select theTextboxicon in the Toolbox (Access 1 - 2003) or on the Controls group of the Design ribbon (Access 2007 and later.) The title of the Properties box reads, "Default Text Box". Set the properties that new text boxes should inherit, such as:Special EffectFlatWhatever your style is.

Font NameMS Sans SerifChoose a font that will definitely be on your user's system.

Allow AutoCorrectNoGenerally you want this on for memo fields only.

Repeat the process for the defaultCombo Boxas well. Be sure to turnAuto Correctoff - it is completely inappropriate for Access to correct items you are selecting from a list. Set properties such asFont Namefor the defaultLabel,Command Button, and other controls.Add anyevent proceduresyou usually want, such as:Form_BeforeUpdate, to validate the record;Form_Error, to trap data errors;Form_Close, to ensure something (such as a Switchboard) is still open.Save the form. A name that sorts first makes it easy to copy and paste the form to create others.A default Continuous FormCopy and paste the form created above. This form will be the one you copy and paste to create continuous forms.You have already done most of the work, but the additional properties for a continuous form might include:Set the form'sDefault Viewproperty to Continuous Forms.For the default Text Box, setAdd Colonto No. This will save removing the colon from each attached label when you cut them from the Detail section and paste them into the Form Header.If your continuous forms are usually subforms, consider adding code to cancel the form's Before Insert event if there is no record in the parent form.Create other "template forms" as you have need.A default reportThe default report is designed in exactly the same way as the forms above. Create a blank report, and set its properties and the default properties for each control in the Toolbox.Suggestions:Set the defaultmarginsto 0.7" all round, as this copes with the Unprintable area of most printers:In Access 2010, clickPage Setupon thePage Setupribbon.In Access 2007, click the Extend arrow at the very bottom right of thePage Layoutgroup on thePage Setupribbon.In Access 1 - 2003, choosePage Setupfrom theFilemenu, and click theMarginstab.Set the report'sWidthto 6.85". (Handles Letter and A4 with 1.4" for margins.)Show theReport Header/Footer(View menu in Access 1 - 2003; in Access 2007, the rightmost icon in the Show/Hide group on the Layout ribbon).In Access 2010, right-click the Detail section, and chooseReport Header/Footer.In Access 2007,Show/Hide(rightmost icon) on theLayoutribbon.In Access 1 - 2003,Viewmenu.Add a text box to the Report Header section to automaticallyprint the report's captionas its title. ItsControl Sourcewill be: =[Report].[Caption]Add a text box to the Page Footer section to show the page count. Use aControl Sourceof: ="Page " & [Page] & " of " & [Pages]Set theOn No Dataproperty to: =NoData([Report])The last suggestion avoids displaying "#Error" when the report has no data. Copy the function below, and paste into a general module. Using the generic function means you automatically get this protection with each report, yet it remains lightweight (no module) which helps minimize the possibility of corruption. The code is:

Public Function NoData(rpt As Report) 'Purpose: Called by report's NoData event. 'Usage: =NoData([Report]) Dim strCaption As String 'Caption of report. strCaption = rpt.Caption If strCaption = vbNullString Then strCaption = rpt.Name End If DoCmd.CancelEvent MsgBox "There are no records to include in report """ & _ strCaption & """.", vbInformation, "No Data..."End Function

A default databaseIn Access 2007 and later, you can also create a default database, with the properties, objects, and configuration you want whenever you create a new (blank) database.Click the Office Button, and click New. Enter this file name: C:\Program Files\Microsoft Office\Templates\1033\Access\blankand click Create. The name and location of the database are important.If you installed Office to a different folder, locate the Templates on your computer.To set thedatabase properties, click the Office Button and choose Access Options.On theCurrent Databasetab of the dialog, uncheck theName AutoCorrectoptions to prevent thesebugs.On theObject Designerstab, uncheckEnable design changes for tables in Datasheet viewto prevent users modifying your schema.Set other preferences (such as tabbed documents or overlapping windows, and showing the Search box in the Nav Pane.)After setting the options, set thereferencesyou want for your new databases.Open the code window (Alt+F11) and choose References on the Tools menu.Importany objects you always want in a new database, such as:the defaultformandreportabove,modulescontaining your commonly used functions,tableswhere you store configuration data,yoursplashscreen, or other commonly used forms.To import, click theExternal Datatab on the ribbon, then theImport Access Databaseicon on theImportgroup.Now any new database you create will have these objects included, properties set, and references selected.You can create default databases for both the new file format (accdb) and the old format (mdb) by creating both a blank.accdb and a blank.mdb in the Access templates folder.

Calculating elapsed timeHow do you calculate the difference between two date/time fields, such as the hours worked between clock-on and clock-off?UseDateDiff()to calculate the elapsed time. It returnswhole numbersonly, so if you want hours and fractions of an hour, you must work in minutes. If you want minutes and seconds, you must get the difference in seconds.Let's assume a date/time field namedStartDateTimeto record when the employee clocks on, and another namedEndDateTimefor when the employee clocks off. To calculate the time worked, create a query into this table, andtype this into the Field row of the query design grid: Minutes: DateDiff("n", [StartDateTime], [EndDateTime])

Minutesis the alias for the calculated field; you could use any name you like. You must use"n"for DateDiff() to return minutes:"m"returns months.To displaythis value ashours and minuteson your report, use a text box with this Control Source: =[Minutes] \ 60 & Format([Minutes] Mod 60, "\:00")This formula uses:the integer division operator (\) rather than regular division (/), for whole hours only;the Mod operator to get the left over minutes after dividing by 60;the Format() function to display the minutes as two digits with a literal colon.Do not use the formula directly in the query if you wish to sum the time; the value it generates is just a piece of text.If you need to calculate a difference inseconds,use"s": Seconds: DateDiff("s", [StartDateTime], [EndDateTime])You can work in seconds for durations up to 67 years.If you need to calculate theamount of paydue to the employee based on anHourlyRatefield, use something like this: PayAmount: Round(CCur(Nz(DateDiff("n", [StartDateTime], [EndDateTime]) * [HourlyRate] / 60, 0)), 2)

A More Complete DateDiff FunctionThe following is a function I helped Graham Seach develop. As it states, it lets you calculate a "precise" difference between two date/time values.You specify how you want the difference between two date/times to be calculated by providing which of ymwdhns (for years, months, weeks, days, hours, minutes and seconds) you want calculated.For example:?Diff2Dates("y", #06/01/1998#, #06/26/2002#)4 years?Diff2Dates("ymd", #06/01/1998#, #06/26/2002#)4 years 25 days?Diff2Dates("ymd", #06/01/1998#, #06/26/2002#, True)4 years 0 months 25 days?Diff2Dates("ymwd", #06/01/1998#, #06/26/2002#, True)4 years 0 months 3 weeks 4 days?Diff2Dates("d", #06/01/1998#, #06/26/2002#)1486 days

?Diff2Dates("h", #01/25/2002 01:23:01#, #01/26/2002 20:10:34#)42 hours?Diff2Dates("hns", #01/25/2002 01:23:01#, #01/26/2002 20:10:34#)42 hours 47 minutes 33 seconds?Diff2Dates("dhns", #01/25/2002 01:23:01#, #01/26/2002 20:10:34#)1 day 18 hours 47 minutes 33 seconds

?Diff2Dates("ymd",#12/31/1999#,#1/1/2000#)1 day?Diff2Dates("ymd",#1/1/2000#,#12/31/1999#)-1 day?Diff2Dates("ymd",#1/1/2000#,#1/2/2000#)1 daySpecial thanks to Mike Preston for pointing out an error in how it presented values when Date1 is before Date2.Updated 2012-08-07 as the results of a request inUtterAccess. Please note that this addition has not been as thoroughly tested as usual. Please let me know if you have any problems with it!

'***************** Code Start **************Public Function Diff2Dates(Interval As String, Date1 As Variant, Date2 As Variant, _Optional ShowZero As Boolean = False) As Variant'Author: ? Copyright 2001 Pacific Database Pty Limited' Graham R Seach MCP MVP [email protected]' Phone: +61 2 9872 9594 Fax: +61 2 9872 9593' This code is freeware. Enjoy...' (*) Amendments suggested by Douglas J. Steele MVP''Description: This function calculates the number of years,' months, days, hours, minutes and seconds between' two dates, as elapsed time.''Inputs: Interval: Intervals to be displayed (a string)' Date1: The lower date (see below)' Date2: The higher date (see below)' ShowZero: Boolean to select showing zero elements''Outputs: On error: Null' On no error: Variant containing the number of years,' months, days, hours, minutes & seconds between' the two dates, depending on the display interval' selected.' If Date1 is greater than Date2, the result will' be a negative value.' The function compensates for the lack of any intervals' not listed. For example, if Interval lists "m", but' not "y", the function adds the value of the year' component to the month component.' If ShowZero is True, and an output element is zero, it' is displayed. However, if ShowZero is False or' omitted, no zero-value elements are displayed.' For example, with ShowZero = False, Interval = "ym",' elements = 0 & 1 respectively, the output string' will be "1 month" - not "0 years 1 month".

On Error GoTo Err_Diff2Dates

Dim booCalcYears As Boolean Dim booCalcMonths As Boolean Dim booCalcDays As Boolean Dim booCalcHours As Boolean Dim booCalcMinutes As Boolean Dim booCalcSeconds As Boolean Dim booCalcWeeks As Boolean Dim booSwapped As Boolean Dim dtTemp As Date Dim intCounter As Integer Dim lngDiffYears As Long Dim lngDiffMonths As Long Dim lngDiffDays As Long Dim lngDiffHours As Long Dim lngDiffMinutes As Long Dim lngDiffSeconds As Long Dim lngDiffWeeks As Long Dim varTemp As Variant

Const INTERVALS As String = "dmyhnsw"

'Check that Interval contains only valid characters Interval = LCase$(Interval) For intCounter = 1 To Len(Interval) If InStr(1, INTERVALS, Mid$(Interval, intCounter, 1)) = 0 Then Exit Function End If Next intCounter

'Check that valid dates have been entered If IsNull(Date1) Then Exit Function If IsNull(Date2) Then Exit Function If Not (IsDate(Date1)) Then Exit Function If Not (IsDate(Date2)) Then Exit Function

'If necessary, swap the dates, to ensure that'Date1 is lower than Date2 If Date1 > Date2 Then dtTemp = Date1 Date1 = Date2 Date2 = dtTemp booSwapped = True End If

Diff2Dates = Null varTemp = Null

'What intervals are supplied booCalcYears = (InStr(1, Interval, "y") > 0) booCalcMonths = (InStr(1, Interval, "m") > 0) booCalcDays = (InStr(1, Interval, "d") > 0) booCalcHours = (InStr(1, Interval, "h") > 0) booCalcMinutes = (InStr(1, Interval, "n") > 0) booCalcSeconds = (InStr(1, Interval, "s") > 0) booCalcWeeks = (InStr(1, Interval, "w") > 0)

'Get the cumulative differences If booCalcYears Then lngDiffYears = Abs(DateDiff("yyyy", Date1, Date2)) - _ IIf(Format$(Date1, "mmddhhnnss") 0 Or ShowZero) Then If booCalcMinutes Then varTemp = varTemp & IIf(IsNull(varTemp), Null, " ") & _ lngDiffMinutes & IIf(lngDiffMinutes 1, " minutes", " minute") End If End If

If booCalcSeconds And (lngDiffSeconds > 0 Or ShowZero) Then If booCalcSeconds Then varTemp = varTemp & IIf(IsNull(varTemp), Null, " ") & _ lngDiffSeconds & IIf(lngDiffSeconds 1, " seconds", " second") End If End If

If booSwapped Then varTemp = "-" & varTemp End If

Diff2Dates = Trim$(varTemp)

End_Diff2Dates: Exit Function

Err_Diff2Dates: Resume End_Diff2Dates

End Function'************** Code End *****************

Constructing Modern Time Elapsed Strings in AccessOffice 2007This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist.Summary:Learn how to use Microsoft Office Access 2007 to display the time elapsed between the current date and another date. (5 printed pages)Kerry Westphal, Microsoft CorporationMarch 2009Applies to:2007 Microsoft Office system, Microsoft Office Access 2007OverviewMany Web 2.0 applications are designed to make it easy to vizualize complex data. I found myself recently challenged with this task while working on a project where I wanted to display on a report to show the time elapsed between the current date and another date. Some example scenarios could include how much time has elapsed since a user profile was updated, the time that remains until taxes are due, or how long a library book was checked out. I did not merely want to show the hours or even days elapsed, but something more in sync with the way I want the information given to mespecifically, that when dates are closer to the current date and time that they are represented exactly, and dates and times that are farther away are shown generally. I wrote theElapsedTimeuser-defined function to perform this task. The function can be used in a query to obtain a string that represents the time elapsed. The string returned is either specific or general depending on the length of time elapsed. For example, if the date is close to the current date, it appears as "In 12 hours, 27 minutes". If the date was long ago, it appears as, "A year ago". The following screen shot shows the results of theElapsedTimefunction when it is used to track items in a calendar.Figure 1. Report showing modern elapsed time string

How It WorksTheElapsedTimefunction does the work. CallElapsedTimefrom a form, report, or query to get a string that shows the time elapsed between the date that you pass the function and the current date. PassElapsedTimea date/time value as its only argument and the rest is completed for you.

Public Function ElapsedTime(dateTimeStart As Date) As String'*************************************************************' Function ElapsedTime(dateTimeStart As Date) As String' Returns the time elapsed from today in a display string like,' "In 12 hours, 41 minutes"'************************************************************* On Error GoTo ElapsedTime_Error Dim result As String Dim years As Double Dim month As Double Dim days As Double Dim weeks As Double Dim hours As Double Dim minutes As Double

If IsNull(dateTimeStart) = True Then Exit Function

years = DateDiff("yyyy", Now(), dateTimeStart) month = DateDiff("m", Now(), dateTimeStart) days = DateDiff("d", Now(), dateTimeStart) weeks = DateDiff("ww", Now(), dateTimeStart) hours = DateDiff("h", Now(), dateTimeStart) minutes = DateDiff("n", Now(), dateTimeStart) Select Case years Case Is = 1 result = "Next year" Case Is > 1 result = "In " & years & " years" Case Is = -1 result = "Last Year" Case Is < -1 result = Abs(years) & " years ago" End Select Select Case month Case 2 To 11 result = "In " & month & " months" Case Is = 1 result = "This month" Case Is = -1 result = "Last month" Case -11 To -2 result = Abs(month) & " months ago" End Select

Select Case days Case 2 To 6 result = "In " & days & " days" Case Is = 1 result = "Tomorrow" Case Is = -1 result = "Yesterday" Case -6 To -2 result = Abs(days) & " days ago" End Select Select Case weeks Case 2 To 5 result = "In " & weeks & " weeks" Case Is = 1 result = "Next week" Case Is = -1 result = "Last week" Case -5 To -2 result = Abs(weeks) & " weeks ago" End Select

Select Case hours Case Is = 1 Select Case minutes - (Int(minutes / 60) * 60) Case Is = 0 result = "In an hour" Case Is = 1 result = "In an hour and one minute" Case Is = -1 result = "In an hour and one minute" Case 2 To 59 result = "In an hour and " & _ minutes - (Int(minutes / 60) * 60) & " minutes" Case 60 result = "In an hour" Case -59 To -2 result = "In an hour and " & _ minutes - (Int(minutes / 60) * 60) & " minutes" Case -60 result = "In an hour" End Select Case 2 To 23 Select Case minutes - (Int(minutes / 60) * 60) Case Is = 1 result = "In " & Int(minutes / 60) & _ " hours and one minute" Case Is = 0 result = "In " & Int(minutes / 60) & " hours" Case 2 To 59 result = "In " & Int(minutes / 60) & " hours, " & _ minutes - (Int(minutes / 60) * 60) & " minutes" Case Is = -1 result = "In " & Int(minutes / 60) & _ " hours and one minute" Case -59 To -2 result = "In " & Int(minutes / 60) & " hours, " & _ minutes - (Int(minutes / 60) * 60) & " minutes" Case Is = 60 result = "In " & Int(minutes / 60) & " hours" Case Is = -60 result = "In " & Int(minutes / 60) & " hours" End Select Case Is = -1 Select Case (Int(minutes / 60) * 60) - minutes + 60 Case Is = 0 result = "An hour ago" Case Is = 1 result = "An hour and 1 minute ago" Case 2 To 59 result = "An hour ago and " & _ (Int(minutes / 60) * 60) - minutes + 60 & _ " minutes ago" Case 60 result = "An hour ago" Case Is = -1 result = "An hour and 1 minute ago" Case -59 To -2 result = "An hour ago and " & _ (Int(minutes / 60) * 60) - minutes + 60 & _ " minutes ago" Case -60 result = "An hour ago" End Select Case -23 To -2 Select Case (Int(minutes / 60) * 60) - minutes + 60 Case Is = 0 result = Abs(Int(minutes / 60) + 1) & " hours ago" Case Is = 1 result = Abs(Int(minutes / 60) + 1) & _ " hours and one minute ago" Case 2 To 59 result = Abs(Int(minutes / 60) + 1) & " hours, " _ & (Int(minutes / 60) * 60) - minutes + 60 & _ " minutes ago" Case 60 result = Abs(Int(minutes / 60)) & " hours ago" Case Is = -1 result = Abs(Int(minutes / 60) + 1) & _ " hours and one minute ago" Case -59 To -2 result = Abs(Int(minutes / 60) + 1) & _ " hours, " & _ (Int(minutes / 60) * 60) - minutes + 60 & _ " minutes ago" Case -60 result = Abs(Int(minutes / 60) + 1) & " hours ago" End Select End Select Select Case minutes Case 2 To 59 result = "In " & minutes & " minutes " Case Is = 1 result = "In 1 minute" Case Is = 0 result = "Now" Case Is = -1 result = "A minute ago" Case -59 To -2 result = Abs(minutes) & " minutes ago" End Select ElapsedTime = result

ElapsedTime_Exit: Exit Function ElapsedTime_Error: MsgBox "Error " & Err.Number & ": " & Err.Description, _ vbCritical, "ElapsedTime" Resume ElapsedTime_Exit

End Function

Quotation marks within quotesIn Access, you use the double-quote character around literal text, such as the Control Source of a text box: ="This text is in quotes."Often, you need quote marks inside quotes, e.g. when working with DLookup(). This article explains how.BasicsYou cannot just put quotes inside quotes like this: ="Here is a "word" in quotes"Error!Access reads as far as the quote before word, thinks that ends the string, and has no idea what to do with the remaining characters.The convention is to double-up the quote character if it is embedded in a string: ="Here is a ""word"" in quotes"It looks a bit odd at the end of a string, as the doubled-up quote character and the closing quote appear as 3 in a row: ="Here is a ""word"""Summary:Control Source propertyResultExplanation

="This is literal text."This is literal text.Literal text goes in quotes.

="Here is a "word" in quotes"Access thinks the quote finishes beforeword, and does not know what to do with the remaining characters.

="Here is a ""word"" in quotes"Here is a "word" in quotesYou must double-up the quote character inside quotes.

="Here is a ""word"""Here is a "word"The doubled-up quotes afterwordplus the closing quote gives you 3 in a row.

ExpressionsWhere this really matters is for expressions that involve quotes.For example, in the Northwind database, you would look up theCityin theCustomerstable where theCompanyNameis "La maison d'Asie": =DLookup("City", "Customers", "CompanyName = ""La maison d'Asie""")If you wanted to look up the city for the CompanyName in your form, you need to close the quote and concatenate that name into the string: =DLookup("City", "Customers", "CompanyName = """ & [CompanyName] & """")The 3-in-a-row you already recognise. The 4-in-a-row gives you just a closing quote after the company name. As literal text, it goes in quotes, which accounts for the opening and closing text. And what is in quotes is just the quote character - which must be doubled up since it is in quotes.As explained in the article onDLookup(), the quote delimiters apply only to Text type fields.The single-quote character can be used in some contexts for quotes within quotes. However, we do not recommend that approach: it fails as soon as a name contains an apostrophe (like theCompanyNameexample above.)

Why can't I append some records?When you execute an append query, you may see a dialog giving reasons why some records were not inserted:

The dialog addresses four problem areas. This article explains each one, and how to solve them.Type conversion failureAccess is having trouble putting the data into the fields because thefield type does not match.For example, if you have a Number or Date field, and the data you are importing contains: - Unknown N/Athese are not valid numbers or dates, so produce a "type conversion" error.In practice, Access has problems with any data that is is not in pure format. If the numbers have a Dollar sign at the front or contain commas or spaces between the thousands, the import can fail. Similarly, dates that are not in the standard US format are likely to fail.Sometimes you can work around these issues by importing the data into a table that has all Text type fields, and then typecasting the fields, using Val(), CVDate(), or reconstructing the dates with Left(), Mid(), Right(), and DateSerial(). For more on typecasting, seeCalculated fields misinterpreted.Key violationsThe primary key must have a unique value. If you try to import a record where the primary key value is 9, and you already have a record where the primary key is 9, the import fails due to a violation of the primary key.You can also violate a foreign key. For example, if you have a field that indicates which category a record belongs to, you will have created a table of categories, and established a relationship so only valid categories are allowed in this field. If the record you are importing has an invalid category, you have a violation of the foreign key.You may have other unique indexes in your table as well. For example, an enrolment table might have a StudentID field (who is enrolled) and a ClassID field (what class they enrolled in), and you might create a unique index on the combination of StudentID + ClassID so you cannot have the same student enrolled twice in the one class. Now if the data you are importing has an existing combination of Student and Class, the import will fail with a violation of this unique index.Lock violationsLock violations occur when the data you are trying to import is already in use.To solve this issue, make sure no other users have this database open, and close all other tables, queries, forms, and reports.If the problem persists, Make sure you have set Default Record Locking to "No Locks" under File (Office Button) | Options | Advanced (Access 2007 or later), or in earlier versions: Tools | Options | Advanced.Validation rule violationsThere are several places to look to solve for this one:There is something in theValidation Ruleof one of thefields, and the data you are trying to add does not meet this rule. TheValidation Ruleof each field is in the lower pane of table design window.There is something in theValidation Ruleof thetable, and the data you are trying to add does not meet this rule. TheValidation Ruleof the table is in thePropertiesbox.The field has theRequiredproperty set to Yes, but the data has no value for that field.The field has theAllow Zero Lengthproperty set to No (as it should), but the data contains zero-length-strings instead of nulls.If none of these apply, double-check the key violations above.Still stuck?If the problem data is not obvious, you might consider clicking Yes in the dialog shown at the beginning of this article. Access will create a table namedPaste ErrorsorImport Errorsor similar. Examining the specific records that failed should help to identify what went wrong.After fixing the problems, you can then import the failed records, or restore a backup of the database and run the complete import again.

Rounding in AccessTo round numbers, Access 2000 and later has a Round() function built in.For earlier versions, get thiscustom rounding functionby Ken Getz.The built-in functionUse the Round() function in the Control Source of a text box, or in a calculated query field.Say you have this expression in the Field row in query design: Tax: [Amount] * [TaxRate]To round to the nearest cent, use: Tax: Round([Amount] * [TaxRate], 2)Rounding downTo round all fractional values down to the lower number, useInt(): Int([MyField])All these numbers would then be rounded down to 2: 2.1, 2.5, 2.8, and 2.99.To round down to the lower cent (e.g. $10.2199 becomes $10.21), multiply by 100, round, and then divide by 100: Int(100 * [MyField]) / 100Be aware of what happens when negative values are rounded down: Int(-2.1) yields -3, since that is the integer below. To roundtowards zero, useFix()instead of Int(): Fix(100 * [MyField]) / 100Rounding upTo roundupwardstowards the next highest number, take advantage of the way Int() rounds negative numbers downwards, like this: - Int( - [MyField])As shown above, Int(-2.1) rounds down to -3. Therefore this expression rounds 2.1 up to 3.To round up to the higher cent, multiply by -100, round, and divide by -100: Int(-100 * [MyField]) / -100Round to nearest 5 centsTo round to the nearest 5 cents, multiply the number by 20, round it, and divide by 20: Round(20 * [MyField], 0) / 20Similarly, to round to the nearest quarter, multiply by 4, round, and divide by 4: Round(4 * [MyField], 0) / 4Round to $1000The Round() function in Excel accepts negative numbers for the number of decimal places, e.g. Round(123456, -3) rounds to the nearest 1000. Unfortunately, the Access function does not support this.To round to thenearest$1000, divide by 1000, round, and multiply by 1000. Example: 1000 * Round([Amount] / 1000, 0)To round down to thelower$1000, divide by 1000, get the integer value, and multiply by 1000. Example: 1000 * Int([Amount] / 1000)To round up to thehigher$1000, divide by 1000, negate before you get the integer value. Example: -1000 * Int( [Amount] / -1000)To roundtowards zero, use Fix() instead of Int().Alternatively,Ken Getz' custom rounding functionbehaves like the Excel function.Why round?There is aDecimal Placesproperty for fields in a table/query and for text boxes on a form/report. This property only affects the way the field isdisplayed, not the way it is stored. The number will appear to be rounded, but when you sum these numbers (e.g. at the foot of a report), the total may not add up correctly.Round the field when you do the calculation, and the field will sum correctly.This applies to currency fields as well. Access displays currency fields rounded to the nearest cent, but it stores the value to the hundredth of a cent (4 decimal places.)Bankers roundingThe Round() function in Access uses a bankers rounding. When the last significant digit is a 5, it rounds to the nearest even number. So, 0.125 rounds to 0.12 (2 is even), whereas 0.135 rounds to 0.14 (4 is even.)The core idea here is fairness: 1,2,3, and 4 get rounded down. 6,7,8, and 9 get rounded up. 0 does not need rounding. So if the 5 were always rounded up, you would get biased results - 4 digits being rounded down, and 5 being rounded up. To avoid this, the odd one out (the 5) is rounded according to the previous digit, which evens things up.If you do not wish to use bankers rounding, get Ken Getz' custom function (linked above.)Floating point errorsFractional values in a computer are typically handled as floating point numbers. Access fields of type Double or Single are this type. The Double gives about 15 digits of precision, and the Single gives around 8 digits (similar to a hand-held calculator.)But these numbers are approximations. Just as 1/3 requires an infinite number of places in the decimal system, most floating point numbers cannot be represented precisely in the binary system. Wikipedia explains theaccuracy problemsyou face when computing floating point numbers.The upshot is that marginal numbers may not round the way you expect, due to the fact th