Advanced Ideas using VBA

This article, the second of two parts, continues our discussion of Visual Basic for Applications. It is excerpted from chapter seven of the Access Cookbook, Second Edition, written by Ken Getz, Paul Litwin, and Andy Baron (O'Reilly, 2005; ISBN: 0596006780). Copyright © 2005 O'Reilly Media, Inc. All rights reserved. Used with permission from the publisher. Available from booksellers or direct from O'Reilly Media.

Contributed by
Rating: 5 stars5 stars5 stars5 stars5 stars / 7
January 04, 2007
Rate this Article:
MEH MEH++


SEARCH ASP FREE
TOOLS YOU CAN USE

advertisement

7.6 Pass a Variable Number of Parameters to a Procedure

Problem

You need a procedure that will work on a list of items, and you don't know ahead of time how many there will be. You know that VBA will allow you to use optional parameters, but this requires you to know exactly how many items you might ever need to pass, and in your case, it's impossible to predict that value. How can you accomplish this?

Solution

You have two choices in solving this problem: you can pass an array as a parameter, or you can pass a comma-delimited list, which Access will convert into an array for you. An array (an ordered list of items) must contain a single data type. By using the variant data type, though, you can pass a list of varying types into your procedure. This solution demonstrates both these techniques.

From 07-06.MDB , load the module basArrays in design mode and do the following:

  1. Open the Immediate window (press Ctrl+G or choose the View -> Immediate Window menu item). In these steps, you will run code from the Immediate window.
  2. If you need a procedure that will take a list of words and convert each to uppercase, you can use the UCaseArray procedure. To test it, type the following in the Immediate window:

    TestUCase 5

    You can replace the 5 in the command line with any value between 1 and 26. The procedure will create as many strings as you request, place them into an array, and then call UCaseArray. This procedure will convert all the strings in the array to uppercase. The test procedure will display the original version, followed by the altered version of the array. As you can see, no matter how many items you specify for the UCaseArray procedure to work on, it'll convert them all to uppercase. Figure 7-9 shows this procedure in use. 


    Figure 7-9.  TestUCase with five strings converted
  3. Say you need a procedure that can accept any number of numeric arguments and perform some operation on them. The sample procedure SumThemUp accepts an array of integers, calculates their sum, and returns the total. To try it, type:

    TestSum 15

    in the Immediate window (you can use any number between 1 and 20). The sample routine, TestSum, will generate an array full of random integers between 1 and 9 and will send the array to SumThemUp for processing. Figure 7-10 shows TestSum working with 15 values. 
  4. You may need to write a function that can accept a list of values instead of an array. The ParamArray declaration modifier allows you to do this. Try the MinValue function in basArrays: pass to it a comma-delimited list of values, and the function will return the minimum numeric value from the list you entered. For example:

    varMin = MinValue(0, -10, 15)

    will return -10, which is the minimum of the three values you passed it.


    Figure 7-10.  TestSum summing 15 values

Both UCaseArray and SumThemUp accept a variant as a parameter. This variant variable can hold either a single value or an array of values. From the calling end, you can pass either a variant or an actual array of values. To send an array as a parameter, you must add the trailing (  ) characters, indicating to Access that the variable represents an array. Therefore, to pass the array named aintValues to SumThemUp, call the function like this, making sure to include the (  ) in the array name:

  varSum = SumThemUp(aintValues( ))

To receive a parameter that is an array, the procedure declaration can include the parentheses:

  Public Function SumThemUp (aintValues( ) As Integer) As Variant

in which case you can pass only an array. You can also declare it like this:

  Public Function SumThemUp (varValues As Variant) As Variant

in which case you can pass it either a single variant value or an array of values.

Once the procedure has received the array, it needs a way to loop through all the elements of the array. Access provides two methods for walking the array: looping through the items either with a For...Next loop (by index number), or with a For Each...Next loop (without using the index). UCaseArray uses the first method to loop through all the members of its array, and SumThemUp uses the second.

To loop through the elements of an array by number, you must know the bounds of the array; i.e., the lowest and highest element numbers. Access provides two functions, LBound and UBound, to retrieve the lowest and highest element numbers. UCaseArray includes code like this:

  For intI = LBound(varValues) To UBound(varValues)
    
varValues(intI) = UCase(varValues(intI))
  Next intI

This code loops through all the elements in the array, no matter what the starting and ending items are. In Basic, you can declare an array with any positive integer as its start and end points. For example, in this expression:

  Dim avarArray(13 To 97) as Integer

you'd need to loop from 13 to 97 to access each element of the array. The LBound and UBound functions make it possible for generic routines to loop through all the elements of an array, even though they don't know ahead of time how many elements there will be.

The UCaseArray procedure is quite simple: once it determines that the input value is actually an array (using the IsArray function), it loops through all the elements of the passed-in array, converting each to uppercase. The array is passed by reference, using the ByRef keyword, which means that the modified array is returned to the calling procedure. The code for UCaseArray is:

  Public Sub UCaseArray(ByRef varValues As Variant)

     ' Convert the entire passed-in array to uppercase.
    
Dim intI As Integer

     If IsArray(varValues) Then
        For intI = LBound(varValues) To UBound(varValues)
           varValues(intI) = UCase(varValues(intI))
        Next intI
     Else
        varValues = UCase(varValues)
     End If
  End Sub

The SumThemUp function is no more complex. It uses the For Each...Next syntax to walk through all the elements of the array, maintaining a running sum as it loops. In this case, the variant variablevarItem  takes on the value of each element of the array as it loops through the items, and adds its value to varSum . The source code for SumThemUp is:

  Public Function SumThemUp(varValues As Variant) As Variant

     ' Find the sum of the values passed in.
    
Dim varItem As Variant
    
Dim varSum As Variant

     varSum = 0
     If IsArray(varValues) Then
        For Each varItem In varValues
           varSum = varSum + varItem
        Next varItem
     Else
       
varSum = varValues
    
End If
    
SumThemUp = varSum
  End Function

Passing a list that Access converts to an array for you is no more difficult. To use this technique, you must declare your procedure's formal parameters so that the list of values is the last parameter the procedure expects to receive. Use the ParamArray keyword to indicate that you want to treat an incoming list as an array, and declare your array parameter as an array of variants:

  Public Function MinValue(ParamArray varValues( ) As Variant) As Variant

Once inside the procedure, you can treat the array parameter like any other array. That is, you can either loop from LBound to UBound for the array, or use a For Each...Next loop to visit each element.

Discussion

To use this method effectively, be aware that unless told otherwise, Access always creates arrays with the first element numbered 0. Some programmers insist on starting all arrays with 1 and so use the Option Base 1 statement in their modules' Declarations areas. Others are happy with 0 as their starting point, and some leave the option base setting at 0 (its default) but disregard the element numbered 0. You must never assume anything about the lower or upper bounds on arrays, or sooner or later generic routines won't work. If you're writing code that will be called by other programmers, you need to be aware of these variations on the normal usage.

If you decide to use the For Each...Next syntax to access all of the elements of an array, both the variable you use to loop through the elements and the array itself must be variants. In addition, note that you cannot set the values of items in an array using the For Each...Next syntax; it only allows you to retrieve the values from the array. If you want to loop through an array to set its values, you must use the standard For...Next syntax, using a numeric value as the loop counter.

In Access 2000 and later, you can use an array as the return value for a function. Thus, you could rewrite the UCaseArray procedure as follows:

  Public Function UCaseArrayFunc(ByVal varValues As Variant) As String()
     
' Convert the entire passed in array to upper case.
     
Dim intI As Integer
     
Dim astrWorking() As String
      If IsArray(varValues) Then
         
ReDim astrWorking(LBound(varValues) To UBound(varValues))
         
For intI = LBound(varValues) To UBound(varValues)
             
astrWorking(intI) = CStr(UCase(varValues(intI)))
         
Next intI
         
UCaseArrayFunc = astrWorking
     
End If
  End Function

The advantage of this technique is that the function returns a second array and the original array, varValues, is not modified. Unlike the first example, UCaseArray, the array is passed ByVal, which means that UCaseArrayFunc works with a copy of the original array. Any modifications occurring in UCaseArrayFunc will affect only this copy, leaving the original array in the calling procedure unchanged.

7.7 Sort an Array in VBA

Problem

Although it's a database product, Access doesn't include a way to sort an array. You need to present sorted arrays in an application, and you can't find a reasonable way to sort them without first saving them to a table. You know you've seen array-sorting methods in other languages. Can you write a sorting routine that executes quickly?

Solution

It's true that Access doesn't provide a built-in sorting mechanism for arrays. Entire volumes in libraries are devoted to the study of various sorting and searching algorithms, but its not necessary to dig too deep for array-sorting methods for Access. Because you'll probably place any large data sets into a table, most arrays in Access aren't very large. Therefore, almost any sort will do. This solution uses a variant of the standard quicksort algorithm. (For more information on various sorting and searching algorithms, consult your computer library. This is a big topic!)

To try the sorting mechanism, load the module named basSortDemo in 07-07.MDB. From the Immediate window, type:

  TestSort 6

where the 6 can be any integer between 1 and 20, indicating the number of random integers between 1 and 99 that you want the routine to sort. The sample routine, TestSort, will create the array of integers and send it off to VisSortArray, a special version of the sorting routine acbSortArray that shows what it's doing as it works. Figure 7-11 shows the output from a sample session.

                                                               

  Figure 7-11.  The output from a sample run of TestSort

To use this sorting code in your own applications, follow these steps:

  1. Import the module named basSortArray into your application.
  2. Create the array you'd like to sort. This must be an array of variants, but those variants can hold any datatype; this solution uses an array of Integers and the Solution in Recipe 7.8 uses an array of Strings.
  3. Call acbSortArray, passing to it the name of the array you'd like to sort. For example, to sort an array named avarStates, use the following call:

    acbSortArray avarStates( )

    After the call to acbSortArray, your array will be sorted. Remember that acbSortArray is sorting your array in place: once it's sorted, there's no going back! If you don't want to sort your only copy of the array, make a duplicate first.

Discussion

The quicksort algorithm works by breaking the array into smaller and smaller chunks, sorting each one, until all the chunks are one element long. The acbSortArray procedure calls the main sorting routine, QuickSort, passing to it the array and the start and end points for sorting. The QuickSort routine breaks the array into two chunks, then calls itself twice to sort each of the two halves.

At this point, you might be grumbling about recursive routines and how they use lots of memory. Normally, that's true. This version of the sorting algorithm, however, tries to be conservative about how it uses memory. At each level, it sorts the smaller of the two chunks first. This means that it will have fewer recursive levels: the small chunk will end up containing a single element much more quickly than the large chunk. By always working with the smallest chunk first, this method avoids calling itself more often than it has to.

The code for the QuickSort procedure is:

  Private Sub QuickSort(varArray As Variant, _
  
intLeft As Integer, intRight As Integer)
      Dim i As Integer
      Dim j As Integer
      Dim varTestVal As Variant
      Dim intMid As Integer

      If intLeft < intRight Then
          intMid = (intLeft + intRight) \ 2
          varTestVal = varArray(intMid)
          i = intLeft
          j = intRight
          Do
             
Do While varArray(i) < varTestVal
                
i = i + 1
              Loop
              Do While varArray(j) > varTestVal
                
j = j - 1
              Loop
              If i <= j Then
                 
SwapElements varArray( ), i, j
                  i = i + 1
                  j = j - 1
             
End If
          Loop Until i > j
          ' To optimize the sort, always sort the
          ' smallest segment first.
          If j <= intMid Then
             
QuickSort varArray( ), intLeft, j
              QuickSort varArray( ), i, intRight 
         
Else
              QuickSort varArray( ), i, intRight
              QuickSort varArray( ), intLeft, j
         
End If
      End If
  End Sub

The following are the basic steps of the QuickSort procedure. These steps use intLeft  to refer to the beginning sort item and intRight  for the ending item:

  1. If intLeft  isn't less than intRight, the sort is done.
  2. The sort takes the value in the middle of the subset of the array that's being sorted as the "comparison" value. Its value will be the dividing factor for the two chunks. There are different schools of thought on how to choose the dividing item. This version of the sort uses the item that's physically in the middle of the chosen list of items:

    intMid = (intLeft + intRight) \ 2 varTestVal = varArray(intMid) 
  3. The sort starts from the left, walking along the array until it finds an item that isn't less than the dividing value. This search is guaranteed to stop at the dividing value, which certainly isn't less than itself:

    Do While varArray(i) < varTestVal
       i = i + 1
    Loop 
  4. The sort starts from the right, walking backward through the array until it finds an item that isn't greater than the dividing value. This search is guaranteed to stop at the dividing value, which certainly isn't more than itself:

    Do While varArray(j) > varTestVal
       j = j - 1
    Loop 
  5. If the position from Step 3 is less than or equal to the position found in Step 4, the sort swaps the elements at the two positions, then increments the pointer for Step 3 and decrements the pointer for Step 4:

    If i <= j Then
       SwapElements varArray( ), i, j
       i = i + 1
       j = j - 1
    End If
  6. The sort repeats Steps 3 through 5 until the pointer from Step 3 is greater than the pointer from Step 4 (i> j ). At this point, every item to the left of the dividing element is less than or equal to it, and everything to the right is greater than or equal to it.
  7. Choosing the smaller partition first, the sort repeats all these steps on each of the subsets to either side of the dividing value, until Step 1 indicates that it's done:

    If j <= intMid Then
       QuickSort varArray( ), intLeft, j
       QuickSort varArray( ), i, intRight
    Else
       QuickSort varArray( ), i, intRight
      
    QuickSort varArray( ), intLeft, j End If

There are probably sort algorithms that are simpler than the quicksort algorithm, but for arrays that aren't already sorted, quicksort's speed is hard to beat. (For presorted arrays, it doesn't do as well as some other sorts. But most arrays don't come to the QuickSort subroutine in order.) As it is, the QuickSort subroutine is capable of handling only single-column arrays. If you need to sort multicolumn arrays, you'll need to either modify the code to handle those cases or move the data into a table and let Access do the sorting for you.

See Also

See the next solution for an example of using QuickSort.

7.8 Fill a List Box with a List of Files

Problem

You need to present your users with a sorted list of files with a specific filename extension in a particular directory. You found the Dir function, but you can't find a way to get this information into a list box. Is there a way to do this?

Solution

This problem provides the perfect opportunity to use the past three solutions. It involves creating a list-filling callback function, passing arrays as parameters, and sorting an array. In addition, you'll fill that array with a list of files matching a particular criterion, using the Dir function.

Load the form frmTestFillDirList from 07-08.MDB. Enter a file specification into the text box (for example, c:\*.exe). Once you leave the text box (by pressing either Tab or Return), the code attached to the AfterUpdate event will force the list box to requery. When that happens, the list box will fill in with the matching filenames. Figure 7-12 shows the results of a search for c:\\*.*.

To include this functionality in your own applications, follow these steps:

  1. On a form, create a text box and a list box, with properties set as shown in Table 7-6.


    Figure 7-12.  frmTestFillDirList, searching for *.* in the C:\folder

    Table 7-6. Property settings for the controls on the directory list form



    Control

    Property

    Setting

    Text box

    Name

    txtFileSpec

     

    AfterUpdate

    [Event Procedure]

    List box

    Name

    lstDirList

     

    RowSourceType

    FillList

     

    AfterUpdate

    [Event Procedure]


    1. Enter the following code in the text boxs AfterUpdate event procedure. (See the Preface for more information on creating event procedures.) This code forces the list box to requery itself when you enter a value in the text box, and then move to some other control:

      Sub txtFileSpec_AfterUpdate ( )  
         Me.lstDirList.Requery
      End Sub
       
    2. Enter the following code in the list box's AfterUpdate event. This is sample code that pops up a message box indicating which file you chose:

      Sub lstDirList_AfterUpdate ( )
         MsgBox "You chose: " & Me.lstDirList.Value
      End Sub
       
    3. Enter the following code into a global module so that it can be called from any form. Though this code would work fine in a form's module, it's general enough that it will serve you best as part of a global module that can be copied from one database to another. This is the function that fills the array of files:

      Public Function FillDirList(ByVal strFileSpec As String, _
       astrFiles() As String) As Integer
            ' Given the file specification in strFileSpec, fill in the
            ' dynamic array passed in avarFiles().

            Dim intNumFiles As Integer
            Dim strTemp As String

            On Error GoTo HandleErr 
            intNumFiles = 0

            ' Set the filespec for the dir() and get the first filename.
            strTemp = Dir(strFileSpec)
            Do While Len(strTemp) > 0
                intNumFiles = intNumFiles + 
      1
                astrFiles(intNumFiles - 1) = strTemp
                strTemp = Dir
            Loop

      ExitHere:
          If intNumFiles > 0 Then
             ReDim Preserve astrFiles(intNumFiles - 1)
             acbSortArray astrFiles()
          End If
          FillDirList = intNumFiles
          Exit Function

      HandleErr:
          Select Case Err.Number
              Case 9
                 ' The array needs to be resized
                 ' Just add room for 100 more files.
                 ReDim Preserve astrFiles(intNumFiles + 100)
                 Resume
              Case Else
                 FillDirList = intNumFiles 
                 Resume ExitHere
          End Select
        End Function

      Rather than resizing the array for each matching file name, the Fill-DirList function traps the error that occurs when the array is full, and resizes it 100 slots at a time. Using the Redim Preserve statement is quite expensive in VBA, and you should consider looking for ways to call it as seldom as possible. In this example, the code resizes the array to the correct size once it's done filling in all the file names. 
    4. Import basSortArray from 07-08.MDB. This is the same sorting code that we used in the Solution in Recipe 7.7.

    Discussion

    The list box in this example uses a list-filling callback function, FillList, to supply its data. (See the Solution in Recipe 7.5 for information on callback functions.) Here's the code:

      Private Function FillList(ctl As Control, _
      
    varID As Variant, lngRow As Long, lngCol As Long, _
      
    intCode As Integer)
         
    Static astrFiles() As String
         
    Static intFileCount As Integer

          Select Case intCode
             
    Case acLBInitialize
                 
    If Not IsNull(Me.txtFileSpec) Then
                     
    intFileCount = FillDirList(Me.txtFileSpec, astrFiles())
                  End If
                  FillList = True

              Case acLBOpen
                  FillList = Timer

              Case acLBGetRowCount
                  FillList = intFileCount

              Case acLBGetValue
                  FillList = astrFiles(lngRow)

              Case acLBEnd
                  Erase astrFiles
          End Select
      End Function

    In FillList's acLBInitialize case, it calls the FillDirList function to fill in the astrFiles array, based on the value in the txtFileSpec text box. FillDirList fills in the array, calling acbSortArray along the way to sort the list of files, and returns the number of files it found. Given that completed array, FillList can return the value from the array that it needs when requested in the acLBGetValue case. It uses the return value from
    FillDirList, the number of files found, in response to the acLBGetRowCount case.

    There's also an interesting situation you should note in the FillList and FillDirList routines. FillList declares a dynamic array, astrFiles, but doesn't give a size because it doesn't yet know the number of files that will be found. FillList passes the array off to FillDirList, which adds filenames to the array based on the file specification until it doesn't find any more matches. FillDirList returns the number of matching filenames, but it also has the side effect of having set the array's size and filled it in. Here's the code that does the work. This code fragment uses the ReDim Preserve keywords to resize the array every time it finds a matching filename:

      ' Set the filespec for the dir() and get the first filename.
      strTemp = Dir(strFileSpec)
     
    Do While Len(strTemp) > 0
         
    intNumFiles = intNumFiles + 1
         
    astrFiles(intNumFiles - 1) = strTemp
         
    strTemp = Dir
      Loop

    FillDirList uses the Dir function to create the list of files. This function is unusual in that you call it multiple times. The first time you call it, you send it the file
    specification you're trying to match, and Dir returns the first matching filename. If it returns a nonempty value, you continue to call it, with no parameters, until it does return an empty value. Each time you call Dir, it returns the next matching filename.

    Once FillDirList has finished retrieving the list of filenames, it sorts the names in the array. Its return value is the number of files it found. The following code shows how this works:

      If intNumFiles > 0 Then
         
    ReDim Preserve astrFiles(intNumFiles - 1)
         
    acbSortArray astrFiles()
      End If
      FillDirList = intNumFiles

    Note that when Access calls the list-filling callback function, values for the lngRow  and lngCol parameters are always zero-based. Therefore, when you use arrays within callback functions, you should always consider using zero-based arrays to hold the data you'll display in the control. If you don't, you'll always be dealing with "off by one" errors. Using a zero-based array will mean that the row values (sent to your code in lngRow) will match your array indices.

    7.9 Handle Object Properties, in General

    Problem

    You don't understand how to get and set property values in Access. It seems as if there are different kinds of properties, and what works for one object and property doesn't work for another. Is there some way to settle this once and for all?

    Solution

    There really are two kinds of properties for objects in Access. Built-in properties are those that always exist for an object, and user-defined properties are properties that you or Access creates for an object when requested. The syntax for referring to each type is different, but this solution provides a method that works for either type. This solution uses the user-defined Description property as an example, but the techniques will work just as well for any other property. The interesting part of this solution is that the Description property is not a built-in property, and attempting to set or retrieve this property using the standard object.property syntax will fail.

    This solution provides a sample form, which is useful only for demonstrating the technique. The real power of the solution comes from the module, basHandleProperties, which provides procedures you can use to set and get any kind of property. To try out the sample form shown in Figure 7-13, load and run frmTestProperties from 07-09.MDB. Choose a table from the list of tables, and notice the Description property shown in the text box below the list. If you choose a field from the list of fields, you'll also see the description for that field in the text box below the list. You can enter new text into the two text boxes, and the code attached to the AfterUpdate event of either text box will write the text back to the Description property of the selected table or field.


    Figure 7-13.  frmTestProperties lets you set and get the Description property of any table or field 

    The sample form uses two functions from basHandleProperties, as shown in Table 7-7. These functions allow you to get or set any property of any object, as long as the object either already supports the property you're working with or allows you to create new properties to add the property if it doesn't already exist.

    Table 7-7. Using the acbGetProperty and acbSetProperty functions

    Function name

    Usage

    Parameters

    Returns

    acbGetProperty

    Retrieve the value of the specified property of the specified object.

    obj As Object: a reference to any existing object.

    The value of the requested property, or Null if that property or object doesn’t exist.

     

     

    strProperty As String: the name of the property to retrieve.

     

    acbSetProperty

    Set the value of the specified property of the specified object.

    obj As Object: a reference to any existing object.  

    The old value of the property, if it existed, or Null otherwise.

      

    strProperty As String: the name of the property to set.

     
      varValue As Variant: the value of the property;   
      varPropType As Variant (optional): the data type of the new property (if the code has to create it). One of dbBoolean, dbByte, dbInteger, dbLong, dbCurrency, dbSingle, dbDouble, dbDate, dbText, dbLongBinary, dbMemo, or dbGUID. If you skip this, Access will use dbText. 

    The only objects to which you can add properties are databases, tables, queries, fields, indexes, and relations. Attempts to add a new property to any other kind of object will fail.

    To use these new functions in your own applications, follow these steps:

    1. Import basHandleProperties into your application.
    2. To set a property, call acbSetProperty. This function returns the old value of the property. For example:

      Dim db As DAO.Database
      Dim varOldDescription As Variant

      Set db = CurrentDb( ) varOldDescription = acbSetProperty(db, "Description", "Sample Database")
      If Not IsNull(varOldDescription) Then

         MsgBox "The old Description was: " & varOldDescription
      End If
       
    3. To get the value of a property, call acbGetProperty. For example:

      Dim db As DAO.Database
      Dim varDescription As Variant

      Set db = CurrentDb( )
      varDescription = acbGetProperty(db, "Description")
      If Not IsNull(varDescription) Then

         MsgBox "The database description is: " & varDescription
      End If

    Discussion

    Access provides two types of properties: built-in and user-defined. Built-in properties always exist and are part of the definition of the object. For example, the Name and Type properties are crucial for the existence of most objects. These are built-in properties. On the other hand, the Jet engine allows you to create new properties and add them to the Properties collection for all the objects it supports, including TableDefs, QueryDefs, Indexes, Fields, Relations, and Containers. These are user-defined properties.

    In addition, Access itself, as a client of the Jet engine, creates several properties for you. For example, when you right-click on an object in the Database Explorer and choose Properties from the floating menu, Access allows you to specify the Description for the object. That Description property doesn't exist until you request that Access create it, using that dialog or in your own VBA code. The same goes for the Caption, ValidationRule, and DefaultValue properties of fields: those properties don't exist until you request that Access create them for you.

    If you attempt to retrieve or set the value of a property that doesn't yet exist, Access will trigger a runtime error. Your code must be ready to deal with this problem. In addition, you may be used to working with built-in properties, to which you can refer using the simple object.property syntax. This syntax works only for built-in properties. For user-defined (and Access-created user-defined) properties, you must refer to the property using an explicit reference to the Properties collection that contains it. For example, to set the Format property of the City field within tblCustomers, you'll need an expression like this (and this expression will fail with a runtime error if the Format property hasn't yet been set):

      CurrentDb.TableDefs("tblCustomers"). _
       Fields("City").Properties("Format") = ">"

    Because you can always refer to any property using an explicit reference to the Properties collection, you can simplify your code, and ensure that all property references work, by using the same syntax for built-in and user-defined properties. For example, field objects support the AllowZeroLength property as a built-in property. Therefore, this reference will work:

      CurrentDb.TableDefs("tblCustomers"). _
       Fields("City").AllowZeroLength = False

    If you want to refer to the same property with an explicit reference, you can use this syntax:

      CurrentDb.TableDefs("tblCustomers"). _
       Fields("City").Properties("AllowZeroLength") = False

    This ability to refer to built-in and user-defined properties using the same syntax is the secret of the code presented in this solution.

    To create a new property, you must follow these three steps:

    1. Create a new property object, using the CreateProperty method of an existing object.
    2. Set the properties of this new property, including its name, type, and default value (you can merge this step with the previous step by supplying the information when you call CreateProperty).
    3. Append the new property to the Properties collection of the host object. For example, to add a Description property to the current database, you might write code like this:

      Dim db As DAO.Database
      Dim prp As Property

      Set db = CurrentDb( )

      ' Step 1
      Set prp = db.CreateProperty( )

      ' Step 2
      prp.Name = "Description"
      prp.Type = dbText
      prp.Value = "Sample Database"

      ' Step 3
      db.Properties.Append prp

      To combine Steps 1 and 2, you could set the properties of the new property at the time you create it:

      ' Steps 1 and 2
      Set prp = db.CreateProperty("Description", dbText, "Sample Database")

      ' Step 3
      db.Properties.Append prp

      Once you've followed these steps, you should be able to retrieve the database's Description property with a statement like this (note that you must use the explicit reference to the Properties collection in this case, because Description is a user-defined property):

      Debug.Print CurrentDb.Properties!Description

    To relieve you from worrying about the differences between user-defined and built-in properties and whether or not a property already exists for a given object, we've provided the acbGetProperty and acbSetProperty functions.

    The acbGetProperty function is the simpler of the two: it attempts to retrieve the requested property. acbGetProperty may fail for two reasons: the object itself doesn't exist, or the property you've tried to retrieve doesn't exist (errors acbcErrNotInCollection and acbcErrPropertyNotFound, respectively). If either of these errors occurs, the function returns Null. If any other error occurs, the function alerts you with a message box before returning Null. If no error occurs, the function returns the value of the requested property. For an example of calling acbGetProperty, see Section 7.9.2 and 07-09.MDB.

    The source code for acbGetProperty is:

      Public Function acbGetProperty(obj As Object, _
      
    strProperty As String) As Variant
          ' Retrieve property for an object.
          ' Return the value if found, or Null if not.

          On Error GoTo HandleErr

          acbGetProperty = obj.Properties(strProperty)

      ExitHere:
          Exit Function

      HandleErr:
          Select Case Err.Number
              Case 3265, 3270   ' Not in collection, not found.
                 ' Do nothing!
              Case Else
                 
    MsgBox Err.Number & ": " & Err.Description, , "acbGetProperty"
          End Select
          acbGetProperty = Null
          Resume ExitHere
     
    End Function

    The acbSetProperty function is more interesting. It attempts to set the value of the property you pass to it. This function has several interesting characteristics:

    1. If you ask it to set a property that doesn't currently exist, it attempts to create that property and then sets its value.
    2. The data type is declared optional, using the DataTypeEnum enumerated type, with dbText as the default value. If you don't tell it what the data type of the new property is to be (i.e., if you leave that parameter blank), the code will use the dbText type by default.
    3. The function returns the old value of the property, if there was one, so you can store it away and perhaps reset it once you're done with your application.
    4. To make sure the code will work with either user-defined or built-in properties, the code uses an explicit reference to the Properties collection.
    5. To tell if it needs to try to create the property, the function traps the acbcErrPropertyNotFound error condition (error 3270); if that error occurs, it uses the CreateProperty method to try to create the necessary property.
    6. If you try to assign an invalid property value, Access triggers the acbcErrDataTypeConversion error condition (error 3421). In that case, there's not much acbSetProperty can do besides alerting you to that fact and returning Null.

    The source code for acbSetProperty is:

      Public Function acbSetProperty( _
      obj As Object, strProperty As String, varValue As Variant, _
      Optional propType As DataTypeEnum = dbText)

         ' Set the value of a property.
         On Error GoTo HandleErr

         Dim varOldValue As Variant

         ' This'll fail if the property doesn't exist.
         varOldValue = obj.Properties(strProperty)
         obj.Properties(strProperty) = varValue
         acbSetProperty = varOldValue

      ExitHere:
         Exit Function

      HandleErr:
         Select Case Err.Number
             Case 3270       ' Property not found
                 ' If the property wasn't there, try to create it.
                 If acbCreateProperty(obj, strProperty, varValue, propType) Then
                    
    Resume Next
                 End If
             Case 3421       ' Data type conversion error
                 MsgBox "Invalid data type!", vbExclamation, "acbSetProperty"
             Case Else
                
    MsgBox Err.Number & ": " & Err.Description, , "acbSetProperty"
         End Select
         acbSetProperty = Null
         Resume ExitHere
     
    End Function

    Only objects that are maintained by the Jet engine allow you to create new properties. That is, you can add properties to the Properties collections of Database, TableDef, QueryDef, Index, Field, Relation, and Container objects. You won't be able to add new properties to any object that Access controls, such as forms, reports, and controls. If you attempt to use acbSetProperty to set a user-defined property for an invalid object, the function will return Null. You can, however, use acbSetProperty and acbGetProperty with any Access object, as long as you confine yourself to built-in properties for those objects that don't support user-defined properties. For example, this code fragment will work as long as frmTestProperties is currently open:

      If IsNull(acbSetProperty(Forms("frmTestProperties"), "Caption", _
       "Test Properties")) Then
         MsgBox "Unable to set the property!"
      End If

    User-defined properties are persistent from session to session. That is, they are saved in the TableDef along with the built-in and Access-defined properties. You can, however, delete a user-defined property using the Delete method on the property's parent collection. For example, you could delete the user-defined property defined earlier using the following statement:

      CurrentDb.TableDefs("tblSuppliers").Fields("Address"). _
       Properties.Delete "SpecialHandling"

    7.10 Detect Whether an Object Exists

    Problem

    You create and delete objects as your application runs. At some point, you need to be able to tell whether an object exists and make decisions based on that fact. But you can't find a function in Access that will tell you if a specific object already exists. Are you missing something? This ought to be a basic part of the product!

    Solution

    You haven't missed anything: Access really doesn't supply a simple method of determining if a specific object already exists. On the other hand, this is really quite simple, as long as you understand two important concepts: Accesss support for DAO Container objects, and the ways you can use error handling to retrieve information. This solution uses these two subjects to provide a function you can call to check for the existence of any object.

    Load and run frmTestExist from 07-10.MDB. This form, shown in Figure 7-14, lets you specify an object name and its type and then tells you whether that object exists. Certainly, you wouldn't use this form as-is in any application--its purpose is to demonstrate the acbDoesObjExist function in basExists (07-10.MDB). To make your exploration of frmTestExist easier, Table 7-8 lists the objects that exist in 07-10. MDB. Try entering names that do and don't exist, and get the types right and wrong, to convince yourself that the acbDoesObjExist function does its job correctly.


    Figure 7-14.  frmTestExist lets you check for the existence of any object in the current database

    Table 7-8. The sample objects in 07-10.MDB

    Object name

    Object type

    tblTest

    Table

    qryTest

    Query

    frmTest

    Form

    frmTestExist

    Form

    basExists

    Module

    Follow these steps to use acbDoesObjExist in your own applications:

    1. Import the module basExists from 07-10.MDB. This module contains the acbDoesObjExist function.
    2. To check for the existence of any object, call acbDoesObjExist, passing to it the name of the object to check for and a value from the AcObjectType enumeration indicating the object's type. The type parameter must be chosen from the values acTable, acQuery, acForm, acReport, acMacro, or acModule. For example, to check for the existence of a table named "Customers", call acbDoesObjExist like this:

      If acbDoesObjExist("Customers", acTable) Then
         ' You know the table exists.
      Else
         MsgBox "The table 'Customers' doesn't exist!"
      End If

    Discussion

    The acbDoesObjExist function, shown in full here, checks for the existence of an object by attempting to retrieve that object's Name property. Because every object that exists exposes a Name property, this action can't fail unless the object doesn't exist. In skeleton format, the code works like this:

        Dim strName As String
        On Error Goto acbDoesObjExist_Err

        strName = obj.Name
        acbDoesObjExist = True

      acbDoesObjectExist_Exit:
         Exit Function

      acbDoesObjectExist_Err:
         acbDoesObjExist = False
         Resume acbDoesObjectExist_Exit

    That is, the code sets up an error handler and then attempts to retrieve the Name property of the requested object. If it succeeds, the code falls through, sets the return value to True, and returns. If it triggers an error, the procedure can be assured that the object doesn't exist, and it will return False.

    The only other issue is how to convert a string containing the name of the object and an integer containing its type to a real object reference. This is where the Jet engine's Container objects come in handy. The Container collections, supplied by Access so the Jet engine can support security for all the Access objects, contain collections of Document objects (one for each saved object in your database). The Containers collection contains collections named Tables, Forms, Reports, Scripts (that's macros for us users!), and Modules. Except for tables and queries, the code checks in those collections of documents, looking for the document whose name you've supplied. For tables and queries, it's simpler to use the TableDefs and QueryDefs collections directly. Access lumps tables and queries together in the Tables container, but keeps them separate in the TableDefs and QueryDefs collections. If the code looked in the Tables container, it would have to take an extra step to distinguish tables from queries; that step isn't necessary if it uses the collections instead.

    The code for acbDoesObjExist is as follows:

      Public Function acbDoesObjExist( _
      
    strObj As String, objectType As AcObjectType)
          Dim db As DAO.Database
          Dim strCon As String
          Dim strName As String

          On Error GoTo HandleErr

          Set db = CurrentDb()
          Select Case objectType
              Case acTable
                  strName = db.TableDefs(strObj).Name
              Case acQuery
                  strName = db.QueryDefs(strObj).Name
              Case acForm, acReport, acMacro, acModule
                  Select Case objectType
                      Case acForm
                          strCon = "Forms"
                      Case acReport
                          strCon = "Reports"
                      Case acMacro
                          strCon = "Scripts" 
                      Case acModule
                         
    strCon = "Modules"
                  End Select
                  strName = db.Containers(strCon).Documents(strObj).Name
          End Select
          acbDoesObjExist = True

      ExitHere:
          Exit Function
      HandleErr:
          acbDoesObjExist = False
          Resume ExitHere
     
    End Function

    Note that in the Select Case statement, the code first checks to see if you're asking about a table or a query. If so, it looks in the appropriate collection:

      Select Case objectType
         Case acTable
            strName = db.TableDefs(strObj).Name 
         Case acQuery
            strName = db.QueryDefs(strObj).Name
      .
      .
      .
      End Select

    If not, it assigns to strCon  the name of the container it will need and then attempts to retrieve the Name property of the particular document within the selected container:

      Case acForm, acReport, acMacro, acModule
         Select Case objectType
            Case acForm
               strCon = "Forms"
            Case acReport
               strCon = "Reports"
            Case acMacro
               strCon = "Scripts"
            Case acModule
              
    strCon = "Modules"
         End Select
         strName = db.Containers(strCon).Documents(strObj).Name

    See Also

    If you haven't done much investigation of DAO in Access, you may find it useful to study the appropriate chapters in the Building Applications manual that ships with Access. Though complete coverage of DAO is beyond the scope of this book, there are several examples using DAO in other chapters, especially Chapter 4 and Chapter 6. In addition, DAO Object Model: The Definitive Reference, by Helen Feddema (O'Reilly), provides complete documentation of the DAO object model.

     
blog comments powered by Disqus
MICROSOFT ACCESS ARTICLES

- Microsoft Access 2010: How to Add, Edit, and...
- Microsoft Access 2010: How to Format Reports
- Microsoft Access 2010: How to Customize Form...
- How to Create Reports in Microsoft Access 20...
- Microsoft Access 2010: How to Format Forms
- How to Create Forms in Microsoft Access 2010
- Microsoft Access 2010 Tips and Tricks
- Link Data from Excel to Access
- Import Excel Data into Microsoft Access
- How to Create a Relational Database in Access
- Improving Construction of Statistical Proces...
- How to Monitor Website Traffic using Statist...
- Chi Square Test of Independence with MS Excel
- Two-Way ANOVA (Analysis of Variance) in Micr...
- Converting a MySQL Database to an Excel Work...

ASP Web Hosting ASP.Net Web Hosting Windows Web Hosting
ASP Free Forums 
 RSS  Tutorials RSS
 RSS  Forums RSS
 RSS  All Feeds
Site Map 
Request Media Kit
Write For Us Get Paid 
Weekly Newsletter
 
Developer Updates  
Free Website Content 
Privacy Policy 
Support 


© 2003-2012 by Developer Shed. All rights reserved. DS Cluster 2 - Follow our Sitemap
Most Popular Topics
All ASP.Net Tutorials