Table Topics | Queries and Record Sets
| Pausing Code to Wait Until a Shelled Process Is Finished in Access 95-2002 | |
| Here's how to shell to another program from
Access, stop your code while the shelled process operates and then resume your code once the process is finished.
To do this you use the api functions "WaitforSingleObject", and "OpenProcess"
to launch a shelled process and wait for it to complete. Listed below is the code
to use.
1. On the declarations page of your module, add the following functions: Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal _
dwAccess As Long, ByVal fInherit As Integer, ByVal hObject _
As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal _
hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal _
hObject As Long) As Long
2. Try out this test function, which launches any app you want to and waits until it is finished to display a message box (Note an " _ " underscore means line continuation): Function LaunchApp32 (MYAppname As String) As Integer
On Error Resume Next
Const SYNCHRONIZE = 1048576
Const INFINITE = -1&
Dim ProcessID&
Dim ProcessHandle&
Dim Ret&
LaunchApp32=-1
ProcessID = Shell(MyAppName, vbNormalFocus)
If ProcessID<>0 then
ProcessHandle = OpenProcess(SYNCHRONIZE, True, ProcessID&)
Ret = WaitForSingleObject(ProcessHandle, INFINITE)
Ret = CloseHandle(ProcessHandle)
MsgBox "This code waited to execute until " _
& MyAppName & " Finished",64
Else
MsgBox "ERROR : Unable to start " & MyAppname
LaunchApp32=0
End If
End Function
3. It is important to note that your function must include the code to close the process handle after the shelled application is complete, otherwise you will have a memory leak until you shut down Windows. | |
| Presenting a List of Directories to a User using the Windows Shell Browse for Folder Dialog | |
| You can provide Users with a simple
Directory dialog rather than using the standard File Open or File Save As
dialogs from the common dialog suite which shows both files and directories.
To do this you use the Directory dialog built into the Shell OLE container.
Here's the code to do it:
In the declarations page of a module, add the following declares (an "_" means line continuation): Type shellBrowseInfo
hWndOwner As Long
pIDLRoot As Long
pszDisplayName As Long
lpszTitle As String
ulFlags As Long
lpfnCallback As Long
lParam As Long
iImage As Long
End Type
Const BIF_RETURNONLYFSDIRS = 1
Const MAX_PATH = 260
Declare Sub CoTaskMemFree Lib "ole32.dll" (ByVal hMem As Long)
Declare Function SHBrowseForFolder Lib "shell32" (lpbi As shellBrowseInfo) As Long
Declare Function SHGetPathFromIDList Lib "shell32" (ByVal pidList As Long, _
ByVal lpBuffer As String) As Long
Then use the following function, supplying it the title you want to use for the dialog, and the handle of the calling form. (use the Me.hwnd property of the form): Public Function GetFolder(dlgTitle As String, Frmhwnd as Long) As String
Dim intNullChr As Integer
Dim lngIDList As Long
Dim lngResult As Long
Dim strFolder As String
Dim BI As shellBrowseInfo
With BI
.hWndOwner = Frmhwnd
.lpszTitle = dlgTitle
.ulFlags = BIF_RETURNONLYFSDIRS
End With
lngIDList = SHBrowseForFolder(BI)
If lngIDList Then
strFolder = String$(MAX_PATH, 0)
lngResult = SHGetPathFromIDList(lngIDList, strFolder)
Call CoTaskMemFree(lngIDList) 'this frees the ole pointer to lngIDlist
intNullchr = InStr(strFolder, vbNullChar)
If intNullchr Then
strFolder = Left$(strFolder, intNullChr - 1)
End If
End If
GetFolder = strFolder
End Function
This function will return the path to the folder selected, so long as it is not a system folder such as the printers folder.
| |
| How to Get the Name of the Currently Open Database. | |
To get the directory path of the currently open database in
Access 2-97, retrieve the Name property of the database in VBA. Here's the simple code to do it:
Function GetDbPath() As String
Dim MyDb as Database
Set MyDb = CurrentDB()
GetDbPath = MyDb.Name
End Function
In Access 2000 or 2002 (Xp), use the CurrentProject.Name and CurrentProject.Path properties to get the same information.
| |
| How to Get the Network Log In Name and Computer Name of the Currently Logged In User | |
Two easy API calls can provide you with network log in name of the current user of the workstation and the network name of the computer itself for use in your Access application. Declare the following functions in your module and add the following function:
Private Declare Function api_GetUserName Lib "advapi32.dll" Alias _
"GetUserNameA" (ByVal lpBuffer As String, nSize As Long) As Long
Private Declare Function api_GetComputerName Lib "Kernel32" Alias _
"GetComputerNameA" (ByVal lpBuffer As String, nSize As Long) As Long
Public Function CNames(UserOrComputer As Byte) As String
'UserorComputer; 1=User, anything else = computer
Dim NBuffer As String
Dim Buffsize As Long
Dim wOK As Long
Buffsize = 256
NBuffer = Space$(Buffsize)
If UserOrComputer = 1 Then
wOK = api_GetUserName(NBuffer, Buffsize)
CNames = Trim$(NBuffer)
Else
Wok = api_GetComputerName(NBuffer, Buffsize)
CNames = Trim$(NBuffer)
End If
End Function
Our System Information sample mdb in the Free file library contains this an other useful system api functions.
| |
| How to Create your own Input Box forms and pause code for User input. | |
| In your application you may need to get information or a selection from a user that an a standard Input Box isn't designed to handle.
As an example, you may want to offer the user only a selection of one of two choices. Since there's no way to restrict what the user can enter into an input box, an input box won't work well. To get around this limitation you can design a form that acts as psudo input box, opening the form to get input, and then when closed, resuming your code.
Here's how:
DoCmd OpenForm "MyForm", acNormal
While SysCmd(acSysCmdGetObjectState, acForm, _
"My Form Name") = acObjStateOpen
DoEvents 'Do Nothing Wait for Closing
Wend
[Resume Code here]
| |
| Code to Return the Last Day of the Month, the Beginning of a Quarter or End of a Quarter Based on a Specific Date | |
Access' Date Add and DateDiff functions generally fill most needs for date manipulation. However, there are a few instances where you need to return a date for comparison purposes which are not simple additive date result from a current date. Two examples are specifying the date for the end of the current month (e.g. for billing purposes), or gathering data which occurs, or is scheduled to occur based on whether it is after the beginning of a quarter, or before the end of a quarter. The following two functions provide the code to determine the end of the month from a specific date and the first or last day of any yearly quarter based on a specific date. (The second function below, providing the quarterly dates relys upon the end of the month function so both must be included in your module.) Here's the end of the month function:
Function EOMonth (Anydate)
'-------------------------------------------------------------------------------
'Purpose: Returns the last day of the month for the date specified
'Accespts: A Date or Date Variable.
'Returns: VarType 7 Date
'------------------------------------------------------------------------------
On Error GoTo Err_EOM
Dim NextMonth, EndofMonth
NextMonth = DateAdd("m", 1, Anydate)
EndofMonth = NextMonth - DatePart("d", NextMonth)
EOMonth = EndofMonth
Exit_EOM:
Exit Function
Err_EOM:
MsgBox "Error" & " " & Err & " " & Error$
Resume Exit_EOM
End Function
Here's the Function to determine the beginning date or ending date of any quarter base on a date supplied:
Function BEQuarter (ByVal Anydate, BeginOrEnd As Boolean) As Variant
'-------------------------------------------------------------------
'Purpose: Returns the beginning or the end of a quarter
'Uses: EOMonth() Function
'Input: AnyDate: A date value, use of #'s to signify a date when variable
' from a query or the immediate window.
'' BeginOrEnd: 0 Finds Beginning of Quarter, -1 Finds End of Quarter
''Returns: VarType 7 date
'---------------------------------------------------------------------
On Error GoTo Err_EOQ
If BeginOrEnd <> 0 And BeginOrEnd <> -1 Then
MsgBox "Error: BeginOrEnd must be 0 or -1"
GoTo Exit_EOQ
End If
Dim EndofQuarter, BeginofQuarter, PrevQuarter
Static MonthVar(12) As Integer
If MonthVar(12) = 0 Then
MonthVar(1) = 2
MonthVar(2) = 1
MonthVar(3) = 3
MonthVar(4) = 2
MonthVar(5) = 1
MonthVar(6) = 3
MonthVar(7) = 2
MonthVar(8) = 1
MonthVar(9) = 3
MonthVar(10) = 2
MonthVar(11) = 1
MonthVar(12) = 3
End If
Anydate = Anydate - DatePart("d", Anydate)
EndofQuarter = DateAdd("M", MonthVar(DatePart("M", Anydate)), Anydate)
EndofQuarter = EOMonth(EndofQuarter)
If DatePart("m", EndofQuarter) = 6 Then
BeginofQuarter = DateAdd("q", -1, EndofQuarter) + 2
Else
BeginofQuarter = DateAdd("q", -1, EndofQuarter) + 1
End If
If BeginOrEnd = -1 Then
BEQuarter = EndofQuarter
Else
BEQuarter = BeginofQuarter
End If
Exit_EOQ:
Exit Function
Err_EOQ:
MsgBox "Error" & " " & Err & " " & Error$
Resume Exit_EOQ
End Function
| |
| How to Test Whether an Instance of the Application is Already Running when the Application is Launched | |
| To prevent a second instance from
loading if a user mistakenly attempts to launch it twice, you can run code from your autoexec macro to test whether the app is already running and terminate the launch if a copy of it is already open.
To do this, incorporate the two following simple functions in your database and call the Function IsRunning below: Function IsRunning() as integer
Dim db As Database
Set db = CurrentDB()
If TestDDELink(db.Name) Then
IsRunning = -1
Else
IsRunning =0
End If
End Function
' Helper Function
Function TestDDELink (ByVal strAppName$) As Integer
Dim varDDEChannel
On Error Resume Next
Application.SetOption ("Ignore DDE Requests"), True
varDDEChannel = DDEInitiate("MSAccess", strAppName)
' When the app isn't already running this will error
If Err Then
TestDDELink = False
Else
TestDDELink = True
DDETerminate varDDEChannel
DDETerminateAll
End If
Application.SetOption ("Ignore DDE Requests"), False
End Function
This code works in all versions of Access. | |
| Add Pizzazz to your Message Boxes by varying the font weight. | |
|
Access 97 allowed you to break your message box into paragraphs, and to bold the first paragraph in the Message Box, just like Access itself does with standard messages. However, creating this functionality in Access 2000 or 2002 is a bit more limited and tricky as noted below. The "@" symbol added to your message text will break the message into paragraphs, with text before the first @ shown in bold. You are limited to three paragraphs with the "@" symbol following each paragraph. If you only want to break for two paragraphs, you must use two @@ symbols at the end of the second paragraph. The following code shows a formatted message box for Access 95-97: If MsgBox("You have just deleted the current record.@ _
Click ""OK"" to confirm your delete or ""Cancel"" to undo your deletion.@@ ", _
vbOKCancel, "My AppName") = vbOK Then
'Do somthing here
End IfIn Access 2000 and 2002
(Xp),
this functionality is not directly available because the VBA environment is
now separate from Access. You can however replicate it (with certain
limitations) by using the EVAL() function as a wrapper around the message box
code. So it would look like:
If Eval("MsgBox('You have just deleted the current record.@ _
Click ""OK"" to confirm your delete or ""Cancel"" to undo your deletion.@@', _
1, 'My AppName')") = vbOK Then
'Do somthing here
End If
Note: You can not use variables in your message boxes using this method and you also can't
use VB intrinsic constants such as vbOKCancel, the latter must be given as specific numbers
which you can obtain using the object browser.
| |
| How to Hide the Main Access Window when Running a Form Shortcut from the Desktop | |
If you have placed a short cut to an Access form on the Windows desktop, you may not want the main Access window to be displayed when your form is opened. There is no way to hide the window entirely if a form is launched from the desktop, Access will flash on the screen momentarily; but you can quickly hide
the main Access window using the following code and steps:
Dim lngReturn as Long lngReturn = ShowWindow(Application.hwndAccessApp, SW_HIDE) This last piece of code, to quit the application, is required to be added to what ever form will last be visible, be it a switch board form or single form. If you don't add this code, the form will be closed, but Access will still be running in the background, hidden from view. Note: This tip only works with Pop Up or modal forms and will not work with reports. This tip submitted by Larry Christopher | |
| How Do I Open My Application's Help File to the Contents or to a Specific Item in the Index/Search Section | |
| Often you may want to take specific actions when opening the help file for your application. Access provides the ability to open the help file to a specific topic. But if you want to make sure that when you open the file it opens the search index on a specific topic or
that it opens to the Contents (under Win95/NT) then you need to use the WinHelp api function. Here's how to do it:
To open to a specific help search/index topic call your help file as follows, where "strHelpVal" is the name of an index topic: Private Declare Function WinHelp Lib "user32" Alias _
"WinHelpA" (ByVal hWnd As Long, ByVal lpHelpFile As String, _
ByVal wCommand As Long, ByVal dwData As Any) As Long
Const HELP_PARTIALKEY = &H105&
Public Function WHPartialKey()
Dim dwReturn%
Dim strHelpVal$
strHelpVal = "Active Window" & Chr$(0) 'vbnullstring in A 95 and 97
dwreturn = WinHelp(Application.hWndAccessApp, "acmain80.hlp", _
HELP_PARTIALKEY, strHelpVal)
End Function
To open the help file to the general contents use the following (note if the user last had the index showing this will open the file on the index tab, if the
contents were showing it will open the contents):
Private Sub cmdHelp_Click()
On Error Resume Next
Dim dwReturn&
dwReturn= WinHelp(Me.hwnd, "acmain80.hlp", &HB, 0&)
End Sub
| |
| How Can I Check If a Network Drive Is Available? | |
Your application may need to access a
file on a network drive or attach to tables for a backend database on a
network drive. Prior to actually performing that action you may want to check
to see if the network connection is available. Here's a function to do just
that:
Private Declare Function prn_WNetGetConnection Lib "mpr.dll" _
Alias "WNetGetConnectionA" (ByVal LocalName$, ByVal RemoteName$, _
cbRemoteName&) As Long
Private Declare Function prn_WNetAddConnection Lib "mpr.dll" Alias _
"WNetAddConnectionA" (ByVal NetPath$, Password, LocalName&) As Long
Const ERROR_NO_ERROR = 0
Const ERROR_ACCESS_DENIED = 5
Const ERROR_BAD_NETPATH =53
Const ERROR_BAD_NET_NAME = 67
Const ERROR_ALREADY_ASSIGNED = 85
Const ERROR_INVALID_PASSWORD = 86
Const ERROR_MORE_DATA = 234
Const ERROR_INVALID_ADDRESS = 487
Const ERROR_BAD_DEVICE = 1200
Const ERROR_CONNECTION_UNAVAIL = 1201
Const ERROR_DEVICE_ALREADY_REMEMBERED = 1202
Const ERROR_NO_NET_OR_BAD_PATH = 1203
Const ERROR_NO_NETWORK = 1222
Const ERROR_NOT_CONNECTED = 2250
Public Function at_CheckNet%(DriveOrPrinter$)
'------------------------------------------------------------
'Purpose: To check to see if a drive or printer on the network is available
'Accepts: Drive as "C:","LPT3:" or "\\network_server\drive"
'Returns: True (-1) on Success, False (0) on Failure
'-------------------------------------------------------------
On Error GoTo Err_CN
Dim dwError&
Dim RemoteNamesz&
Dim RemoteName$
at_CheckNet = True
If Instr(DriveOrPrinter, "\\") < 1 Then 'local named resource
RemoteName = String(255, 0)
RemoteNamesz = Len(RemoteName)
dwError = prn_WNetGetConnection(DriveorPrinter, RemoteName, RemoteNamesz)
If dwError = ERROR_CONNECTION_UNAVAIL or _
dwError = ERROR_NOT_CONNECTED Or _
dwError = ERROR_NO_NETWORK Then
at_CheckNet = 0
GoTo NetMsg
End If
Else 'a network address is supplied to the function
'we supply a null password, which may be required & a null connection name
'since we're not actually connecting, just checking the connection
'will return ERROR_DEVICE_AREADY_REMEMBERED if available
dwError = prn_WNetAddConnection(DriveOrPrinter, Null, 0&)
If dwError = ERROR_NO_NETWORK Or _
dwError = ERROR_NOT_CONNECTED Or _
dwError = ERROR_CONNECTION_UNAVAIL Or _
dwError = ERROR_NO_NET_OR_BAD_PATH Or _
dwError = ERROR_ACCESS_DENIED Or _
dwError = ERROR_BAD_NETPATH Or _
dwError = ERROR_BAD_NET_NAME Then
at_CheckNet = 0
GoTo NetMsg
End If
End If
GoTo Exit_CN
NetMsg:
MsgBox "The required network device is not currently available.", 16, "Check Net"
Exit_CN:
Exit Function
Err_CN:
MsgBox "Error: " & Err & " " & Error$, 16, "Check Net"
Resume Exit_CN
End Function
Note: If you want to actually create a network connection using WNetAddConnection, change the last parameter call in the declarations to ByVal LocalName as String, rather than a long or integer, and pass your local drive or port connection like "c:". | |
| How to Determine If a File Exists and if it is Locked by Another Process. | |
| Access provides the built in Dir() command that can determine if a file
or directory exists but that
function can not tell you if the file is locked or otherwise useable. (e.g. if you need to move the file, write to it
or for an Access jet db backend, whether it may be compacted.) To gain that
information you can use the following API call to the CreateFile function.
1. On the declarations page of your module, add the following functions Private Declare Function CreateFile& Lib "kernel32" Alias "CreateFileA" (ByVal _
lpFileName as String, ByVal dwDesiredAccess as Long, ByVal dwShareMode as Long, _
lpSecurityAttributes As Any, ByVal dwCreationDisposition as Long, _
ByVal dwFlagsAndAttributes as Long, ByVal hTemplateFile as Long)
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject as Long) as Long
2. The following function can be called to check for file existence and whether the file is unlocked and writeable or moveable. (Note an " _ " underscore means line continuation): Public Function FileExists(strFileName As String, ChkType As Byte) As Long
'--------------------------------------------------------------------------- _
Purpose: Checks for file existence and read/write capability. _
Returns: File handle number (>0) if file exists and is useable based on the ChkType _
a negative number if there is an error (e.g. the file is not found or is locked.) _
ChkType: 1=Check for existence, 2= Check for file lock, 3 = Check if file is share writeable. _
----------------------------------------------------------------------------
Dim lngFileHandle As Long
Dim lngLastError As Long
Const FILE_SHARE_READ = &H1: Const FILE_SHARE_WRITE = &H2
Const GENERIC_READ = &H80000000
Const GENERIC_WRITE = &H40000000
Const OPEN_EXISTING = 3&
'Possible return Error values (multiplied by -1)
Const INVALID_FILE_HANDLE = -1
Const ERROR_FILE_NOT_FOUND = 2: Const ERROR_PATH_NOT_FOUND = 3
Const ERROR_ACCESS_DENIED = 5: Const ERROR_INVALID_DRIVE = 15
Const ERROR_DEVICE_NOT_READY = 21: Const ERROR_SHARING_VIOLATION = 32
Const ERROR_LOCK_VIOLATION = 33: Const ERROR_NETWORK_UNREACHABLE = 1231
Select Case ChkType
Case 1
'Simple check for existence, no read-write desired
lngFileHandle = CreateFile(strFileName, 0, 0, ByVal 0&, OPEN_EXISTING, _
0, ByVal 0&)
Case 2
'Check whether file is locked if write access is desired
lngFileHandle = CreateFile(strFileName, GENERIC_READ Or GENERIC_WRITE, _
0, ByVal 0&, OPEN_EXISTING, 0, ByVal 0&)
Case 3
'Read-write desired, share allowed
lngFileHandle = CreateFile(strFileName, GENERIC_READ Or GENERIC_WRITE, _
FILE_SHARE_READ Or FILE_SHARE_WRITE, ByVal 0&, OPEN_EXISTING, 0, ByVal 0&)
End Select
CloseHandle (lngFileHandle)
lngLastError = Err.LastDllError
If lngLastError <> 0 Then
FileExists = lngLastError * -1
Else
FileExists = lngFileHandle
End If
End Function
3. It is important to note that your function must include the code to close the process handle after the shelled application is complete, otherwise you will have a memory leak until you shut down Windows. | |
| How to Save Memory When Writing VBA Code | |
Access and VBA generally do a good job
of "cleaning up" memory when they are done running your code. But there are a
number of common coding methods used by developers which hinder Access'
release of memory. Here's some steps to take in writing functions to save
memory and speed your application:
| |
| How to Compile VBA code by Command from with a VBA Module. | |
| If your application exports forms, reports or modules to another database (e.g. an update
or repair database sent to a client site,) you can have the application call an undocumented sys command function in Access to
compile the VBA modules in the target database once the export is completed. The Sys Command function to call is SysCmd 504, 16483; as example, the following code creates a new module in a target database and then compiles and saves the code in that database. Function CreateMod(strTargetDbName as String)
Dim objAccess as Object
Dim objModule as Object
Set objAccess = CreateObject("Access.Application")
objAccess.OpenCurrentDatabase (strTargetDbName)
'Create a module
objAccess.DoCmd.RunCommand acCmdNewObjectModule
Set objModule = objAccess.Modules(objAccess.Application.CurrentObjectName)
'Now add some code
objModule.InsertText "Global Const Is_Admin as Boolean = True"
objModule.InsertText "Global Const APP_Name = 'SuperApp'"
objModule.InsertText "Function MyFunction(ByVal lngValue1&, ByVal strValue2$) As String"
objModule.InsertText "'Code is intentionally not present"
objModule.InsertText "End Function"
objAccess.DoCmd.Close acModule, objModule, acSaveYes
objAccess.DoCmd.Rename "basSomeModuleName", acModule, objModule
'Now compile the module and quit
objAccess.SysCmd 504, 16483 'Compile and save
objAccess.CloseCurrentDatabase
objAccess.Quit
Set objAccess = Nothing
End Function
| |
| How write code that converts between Access versions (Determining whether a db is an MDE/ADE) | |
| Shown below is a function that can be run in your database to determine if
the application is an MDE or ADE database. This is useful to determine if you have design access to objects within the database. This
function works in databases using ADO or DAO data access methods
in Access 97, 2000 or 2002. This code demonstrates how to make code convertible between Access versions by using Object as an object type rather than a direct reference to an Access or Data Access object (e.g. in the example below setting App as an Object rather than as "as Application". This causes code to use late binding of object properties, thereby allowing object properties (such as in this case CurrentProject) to be specified and to compile successfully even in versions of Access where that property is not generally available. Function TestMDE() As Boolean
On Error Resume Next
Dim App As Object
Dim dbs As Object
Dim strMDE As String
Const ACCESS_ADP = 1
Set App = Application 'late bind to the Access.Application object
If CInt(Val(SysCmd(acSysCmdAccessVer))) > 8 Then
If App.CurrentProject.ProjectType = ACCESS_ADP Then
Set dbs = App.CurrentProject
Else
Set dbs = CurrentDb()
End If
Else
Set dbs = CurrentDb()
End If
strMDE = dbs.Properties("MDE")
If Err = 0 And strMDE = "T" Then
TestMDE = True
Else
TestMDE = False
End If
Set dbs = Nothing
End Function
| |
| How to Call a Function or Sub Procedure when the Procedure Name is stored in a Table. | |
| In certain applications you may need to call a
function or sub procedure from a list of functions and subs stored within a table.
To do so you can use the built in Eval() function to cause the function to be
executed and pass the function or sub parameters. As an example, the
following code shows how to execute a function who's name is stored in a table;
the function name is fetched by a combo box on a form that shows a series of specific
reports and contains a hidden column that has the name of the function that
compiles the data for the report. The function requires one
parameter, which is pulled from the form. | |
| Maintaining the Internal Integrity of Compiled VBA Code in A Database | |
| When developing Access databases that
contain VBA code, over time as you add and delete VBA code in the database,
the internal compiled VBA section of the database file can become bloated,
fragmented and from time to time, corrupt causing crashes of the application.
This is because when VBA code is deleted in the editor, the internal
compilation is simply marked as unused, but is not always overwritten or
deleted. There is a method available to eliminate this fragmentation and potential corruption called decompiling a database (MDB and ADP dbs only). This process marks all the compiled VBA in the database to be deleted and when followed by a compact and repair, "cleans out" the database file, eliminating all compiled code (but not written code) so that with the next compile of the VBA only the current code is compiled internally in the db:
This procedure although part of routine maintenance for dbs it not something
that normally needs to be done very frequently; it could be done following major
development work or prior to distribution of the application. | |