Plateforme Level Extreme
Abonnement
Profil corporatif
Produits & Services
Support
Légal
English
Articles
Recherche: 

A Visual FoxPro Collection Class
James Edgar, April 1, 2003
We all know what a collection is. Visual FoxPro uses a lot of them. The _SCREEN has a "forms" collection, a Form owns the "controls" collection. The Pageframe has a "pages" collection and every grid its "columns" collection. Each container class also has an "objects" collection (which turns out...
We all know what a collection is. Visual FoxPro uses a lot of them. The _SCREEN has a "forms" collection, a Form owns the "controls" collection. The Pageframe has a "pages" collection and every grid its "columns" collection. Each container class also has an "objects" collection (which turns out, on closer examination, to be just a another name for the "forms", "controls", "pages", "columns" etc. collections).

These native collections make is possible to address and manipulate objects on a container without actually knowing what's on the container. We can read and set properties of the objects contained in the collection, and execute their methods. But we cannot directly add an object to the collection, nor remove an object, nor even substitute one object for another. We cannot tell VFP what objects to include in the collection or what objects to exclude. The collections are populated by Fox using its own rules - and we cannot change the rules.

What FoxPro has always needed is a generic collection class which we could then subclass. Other development languages have long had such a class. Visual Basic has a very flexible collection class for which I find all sorts of uses when developing in that language. Visual C++ owns a number of different collections. But, for whatever reason, the Visual FoxPro designers did not give us a collection class until Version 8.0.

So, what a lot of us did to remedy this glaring deficiency was to write our own collection class. In FoxPro that's not at all hard to do. I have several different collections that I have used for five years or so.

This article is the first in a series about collections. In it we are going to explore the concept and implementation of a collection class. In the process we will develop our own collection class in FoxPro using just FoxPro tools, partly just to see how it works, and partly because, unless you install Version 8.0 -- which there seems to be some reluctance to do -- you may not have access to the native collection class.

In subsequent article we will look at the new FoxPro collection class, examine its features and add a little power and flexibility to the basic class.

Finally, we will use a collection class to build a useful tab bar class in FoxPro. FoxPro has its native pageframe class, and there is a fairly workable ActiveX tab class available, but both have severe limitations. The pageframe class is cumbersome, and slow to instantiate unless you use a number of suggested workarounds (all of which seem to be aimed at making the pageframe work more like a simple tab bar). The usual workaround is to not actually place controls on a page until it is displayed - which sort of defeats the purpose of the control.

The ActiveX tab bar control is simpler, and potentially much faster, but not very VFP-friendly. In fact it was my inability to get son-of-a-gun to work consistently that motivated me to develop my own, purely FoxPro, tab bar class.

A Collection Defined

A Collection is an ordered set of locations in a framework that each contains or may contain data. It must have some means of pointing to location, of determining the number of locations in the framework and of adding data to and removing data from the framework.

Framework: Collection frameworks may take at least three forms that I know of (and probably more): linked-list, array and dictionary (or map).

  • A linked-list is an ordered group of elements structured as a double-linked list. The first item points to the second in memory, the second points to the third, and so on until the "tail" item is reached. Each items also points back to its predecessor - hence the term "double-linked". These cannot be indexed and for that reason finding an item in a large linked collection can be tedious.

  • A dictionary framework is a group of pointers to things that reside somewhere outside of the collection. Because its "content" is actually external, manipulation of the things referenced in the framework can become complex.

  • An array is a dynamically sized framework. Each item in the collection is associated with an index, and can be directly addressed through the index without having to sequentially poll items. Adds and deletes are a little slower because memory has to be reallocated for each add and delete. But the ability to directly address members more than makes up for this slight speed penalty.

Synchronous or Asynchronous: A framework may be synchronous, meaning that all locations must contain data, or asynchronous, in which locations may be empty. A synchronous framework never has empty places because locations are not created until there is some data to put in them. If data is removed, the location that formerly held the data is also removed. By contrast, in an asynchronous framework, creation of locations is independent of adding data. Often the framework is created empty then populated later.

Zero-Based or One-Based: The index of the first item in a zero-based collection is zero and its range is 0 to Count - 1. In a one-based collection, the first item is 1 and the range is 1 to Count. A zero-based collection has a number of advantages when it comes to mathematical manipulations. For example, in a zero-based collection finding out how many items are between the 17th item in the collection and the Count is a matter of n = Count - 16 (the 17th item has a zero-based index of 16). In a one-based collection, the process must adjust for the one-base, n = Count - 17 + 1. This may seem trivial, but many if not most collections are zero-based just for this reason.

A linked-list framework is by nature synchronous because it actually does not have locations independent of its data - the data are the locations. Map or dictionary frameworks are typically asynchronous in nature while array frameworks may be either.

Items: Items (or "members" or "elements" - terminology varies even among Microsoft® products) are arranged as discrete locations for content in the collection framework. In fact, an item can be thought of as nothing more than an addressable location within the collection framework at which content is or may be stored.

The items of a collection need only be related by the fact that they are in the collection. They do not have to share the same data type. One can intermingle strings, logicals, numbers and object references in the same collection. A collection may even contain other collections, which may also contain collections, and so on, making possible nested, multi-dimensional collections to any depth.

Index: A long (integer) used to point to the place in the collection where a particular content is stored is usually termed an index. Items are addressed as oCollection.Item[n] where n is any number between 1 and Count.

Key: The index is like a surrogate key in a table. It has no inherent meaning. A Key, on the other hand, is an index with meaning. Usually a string, it is an alternate means identifying an item in the collection and must be, for that reason, unique.

Count: The collection must know or be able to discover, and accurately report, the number of locations it contains. This is usually the Count. In some collections Count is a function, in others a property.

Adding and Removing Items: All collections must have some means of increasing and decreasing the number of locations in its framework and of adding and removing content at the locations. Where content is added or removed varies.

Some collections work like a stack, adding and removing from the top of the framework. Others add and remove from the bottom. Neither approach seems to have an inherent advantage.

Why Do I Need a Collection Class?

The short answer is, you don't.

You can have a long, happy and productive life as a FoxPro programmer and never use a collection class of any kind.

But a collection class can make working with sets of objects much easier than the current options afforded by the language.

It is one more tool to make programming life simpler and faster.

  • Example 1: You have a large number of objects on a form containing calculated numeric values based on the values of other controls on the form. When the values of these other controls change, we need to recalculate the values of the read-only textboxes.

    The typical way to do that now (without actually hard-coding the textbox names in your code or placing them on a separate container) is to plow through the Controls collection examining each object one at a time looking for these few textbox controls. If these are only four out of forty-five controls on the form, you are going to have to cut through a lot of chaff to find the few kernels.

    With a collection object on the form the process is much easier. Each text box is instructed by its Init() event to add itself to the "CalculatedTextBox" collection. Now each time you need to find a calculated text box you need scan only the four items in the collection, not the forty-five on the form.

  • Example 2: Your form contains a number of numeric textboxes from which we need to periodically determine the highest or lowest value. Again, using a collection object on the form to reference just these few controls, scanning the collection for the highest or lowest value is just a matter of a few lines of code.

  • Example 3: We want to arrange a number of controls on a form in two columns. This is very easy. All of the controls to be arrayed in column 1 are referenced in one collection, all the controls for column 2 in a second collection. Repositioning the controls in their "columns" is a now a trivial exercise, as you will see if you download and run the collection form example (see the link below).

But the real power of a collection class is that it can become the core of a lot of other very useful classes. Almost all of the complex classes we write typically constructed of objects on a container of some kind. The ability to group like objects on the container and then deal with them as a discrete set makes the implementation of some very complex classes a lot simpler.

Design Considerations

Usage: The collection may contain any valid data type. This collection, however, is intended primarily for the management of objects. This intended usage has an effect on both the structure and functionality we provide the collection. We do not need, for example, a two-dimensional framework. This greatly simplifies coding since we do not have to figure out which column to address in adding, deleting or accessing objects.

Framework: Choosing a framework for our collection is simple. The array form is unquestionably the most flexible framework. Plus (and this is the biggy) Visual FoxPro already owns a splendid native array structure with scads of array-manipulating functions. What better choice?

Actually it is a little more complicated that that. Because we are using our collection primarily to manage objects, what we end up with is a framework that is a combination of array and dictionary styles. The results from the fact that our collection does not actually contain objects themselves, but pointers to objects that are somewhere outside of the array.

When VFP instantiates an object, it instantiates one instance only and never more than one instance of the object. We as programmers have no direct access to the object. What we have access to are "references" to the object, i.e. pointers to the object in memory.

At any one time there exists numerous references to a single object. Form.myObject is one reference, as is Form.Controls[1] (assuming that .Controls[1] points to the same object as Form.myObject), and Form.Objects[1] is a third. When we add the object's reference to our collection, we create another pointer to the same object, myCollection.Item[1].

This arrangement actually is a good thing because it is a lot easier to manage the allocation of memory for pointers than for objects, pointers take a lot less memory, and we can be assured that if we change any property of the object in myCollection.Item[1], the change will be reflected instantly in all of the other object references.

So, in exchange for all this power and flexibility, we are going to pay a slight speed penalty. But it is very slight. The Fox development team has really done a good job of optimizing the memory allocation/deallocation process that occurs when an array is dimensioned. Referencing objects is also very quick, considering all the work that has to be done (finding the object in memory, reading its internal directory, finding the property or method in the object before setting or reading the property or executing the method instructions).

Consequently, given all the possibilities, including the option of laboriously constructing our own custom framework, there is no doubt that using an array as our collection framework is by far our best choice.

Our framework could be either synchronous or asynchronous. An array framework would support either option. If asynchronous, we would allow empty locations in the array. If synchronous, all array elements would be required to have content. Because the native FoxPro array is such a good memory mananager that we do not pay much of a speed price for adding and removing locations, and because a synchronous framework is easier to work with (since we do not have to constantly test each location to determine whether it is empty), a synchronous framework is, I believe, the best option. As we will see, maintaining synchronicity will have a considerable effect on how we implement add and remove functions.

Selecting the array for our framework also pretty much decides the issue of zero-based vs. one-based. Since the FoxPro array is one-based, there is very little advantage in trying to make it support a zero-based collection -- especially since in a collection intended to hold object references, there is unlikely to be a lot of array mathematics. So our choice is one-based.

Functionality: The basic functionality of a collection class is somewhat sparse. We certainly want to add a little more flexibility to our collection.

  • Add/Insert/Update. Many collection classes do not support an insert or update process. Items are "updated" by being removed, modified, then added back to the collection. A collection is much more useful, however, if items can be inserted at any point in the framework and if its content can be updated in place. We could create three separate functions to add, insert and update content, but one function can be made to do all these things with the help of parameters.

  • Remove/Clear. Rather than removing an item only from the top or bottom of the collection, an item to be removed could be pointed to by passing its index as an argument. We also want to identify an item to be removed by its content as well as than its index, and we want the ability to remove all items in the collection with one process call. In our collection, these processes are handled by the RemoveItem() and Clear() methods.

  • Accessing Content. The usual way of addressing content in a collection is to point to the location containing the desired content using an index, for example

    myCollection.Item[n],

    where n is a number between 1 and the Count.

    Some collections, however, to not allow direct addressing. They permit addressing only through a function. The chief advantage of a function is that n can be checked for out-of-range before addressing the collection and throwing an error. A typical syntax example is,

    Value(n)

    If n is out of range, a null value of some kind is returned.

    In addition to locating items by index, it would also be useful to find items containing a specified content. Normally this is done to find the index address of the item, so what is returned is the index pointer. This functionality is incorporated into the Index() function discussed below.

Base Class: Foxpro gives us a lot of choices of base classes upon which to build our collection.

One obvious choice is the custom class. This class is not visible and has very few native properties and methods, and thus, a small footprint with low object overhead. When I originally wrote a collection class, this was my first choice. But now I like the label class, primarily because it is self-documenting at design-time.

If you use few collections on your forms, self-documentation may not be a consideration. But if like me you continue to find more and more uses for the collection class, you may end up with many on the same container. By using the caption property of the label class to label the collection, I can easily distinguish the "CalculatedTextbox" collection from the "ButtonImages" collection without having to click on a bunch of identical, anonymous icons to find out which is which.

Keeping the collection invisible at run-time is no great trick with an Assign() event attached to the Visible property. We'll see more of this below.

Creating the Collection Class

Create a new label class named "Collection" in the class library of your choice.

In the property sheet set

      Alignment   = 2 (Center)
      BackColor   = 128, 128, 192
      BorderStyle = 1 (Fixed Single)
      Caption     = "Collection"
      FontItalic  = .T.
      ForeColor   = 225,225,225
      Visible     = .F.
      WordWrap    = .T. 

When you subclass the collection on a form or container, you will, of course, change the caption to identify the content of the sub-classed collection.

New Properties

We are going to need three new properties: Count, Item[1], and NewItemIndex. Item[1] is, of course, the array that provides the framework for the collection. NewItemIndex specifies as a number the index of the last item added, inserted or updated. This property is set in the AddItem() method. Count is the property that reports the number of items in the collection.

  • Count: Add the Count property to the class, and check the "Access-Method" box. On the property sheet, set Count to zero. Count is the only added property that must exist when the class is instantiated. Otherwise, the Count_Access() event we are going to write below will sometimes not work properly.

  • Item[1] and NewItemIndex: These properties are added in code in the Init() event.

    There is no reason these "internal" properties cannot be added to the property sheet. But I have found it to be generally good practice to minimize the number of properties appearing in the property sheet by including only those that may need to be set. If the property is intended solely for internal use or otherwise never needs to be set in the property sheet, there is no reason to expose it for someone to tinker with, and lots of good reason not to. But if you want to add them to the property sheet, the initial setting for NewItemIndex is 0 (zero).

We need modify only two native methods, Init() and Destroy().

  • Init() is used solely to add internal properties. If you have elected to add them via the property sheet, you can skip the code example below, but be absolutely sure to set Item[1] to .NULL. Otherwise, all sorts of unfortunate consequences will result.

  • Destroy() calls the Clear() method (see below) of the collection to set all object references in the collection to .NULL. To fail to do so is to risk hanging references which may prevent your form from closing.
* Collection.Init()
*
IF DODEFAULT()

   WITH this
      * The array that holds the collection members. This array is always initialized to .NULL.
      * .NULL. signifies to the Count_Access() event that the collection has no members.
      .AddProperty( "Item[1]", .NULL. )
   
      * Specifies the index of a new member added to the collection. The index is a pointer to
      * the location of the member in the collection.
      .AddProperty( "NewItemIndex", 0 )
   ENDWITH

   RETURN .T.
ENDIF

RETURN .F.

* Collection.Destroy()
*
* If the members are composed of object references, the Clear() method sets the references to 
* .NULL. before collapsing the Item[] array.  This eliminates any hanging object references
* which may prevent a form from closing.
IF DODEFAULT()

   this.Clear()
	
   RETURN .T.
ENDIF

RETURN .F.

New Methods

AddItem(): Provides add, insert and update functionality.

The AddItem() syntax is,

AddItem( uContent [,nIndex] [,lInsert] ), where

  • uContent specifies the content to be added. This may be of any valid data type - even empty data -- but it cannot be omitted and it cannot be .NULL. In this collection .NULL. is used to indicate an empty location. We do not allow empty locations. So any attempt to add .NULL. to the collection is ignored.

    Similarly, since this is a synchronous collection, we do not create a location in the framework unless there is content to add to the location. If uContent is omitted, there is no content to add, so the location will not be created.

  • nIndex specifies the item to be added, inserted or replaced. If this parameter is omitted, zero or greater than Count, the new item will be appended at the end of the collection.

    If nIndex points to an item that already exists, then, unless lInsert is true, it is presumed that the content of the item is to be replaced.

    Items must be added to the collection in sequence. Skipping is not allowed. Item 5 cannot be added before Item 4. If there are two items in the collection, an attempt to add content at item 5 will actually cause it to be added as item 3.

    Because the location at which content was actually added may not be the one specified in nIndex, we need a means of finding out where the content finally ended up. This is the purpose of the NewItemIndex property, which contains the actual index of the last item added.

  • lInsert if true, forces the insertion of a new item at the index specified. If omitted or false, the content of an existing item will be replaced with the new content.

    If the item specified in nIndex does not exist, uContent will be added to the end of the collection irrespective of the setting of lInsert, which is, in effect, ignored.

 
* Collection.AddItem()
*
* OVERVIEW:   Add a new item to a collection object or replace an existing item with new content.
*
* PARAMETERS:   
*
*    tuContent   Specifies he content to be associated with the item. May be any data type, but  
*                may not be .NULL. or omitted.
*
*    tnIndex     Specifies position in the collection at which the new item is to be inserted or 
*                where existing content is to be replaced.  If omitted, zero, or larger than
*                Count, the item is appended to the end of the collection.
*
*    tlInsert    Specifies whether an item is to be inserted into the collection at the   
*                specified tnIndex. If .F. or omitted and there is already an item at tnIndex, the 
*                content at tnIndex will be overwritten by the new tuContent. 
*                If .T., a new item will be inserted at tnIndex and the item formerly at that 
*                location will be moved down in the collection.
*
*    RETURN:   (L) True if the content was successfully added to the collection.
*
LPARAMETERS tuContent, tnIndex, tlInsert

LOCAL lnCount, lnNewCount, lnItems

WITH this

     IF PCOUNT() = 0 .OR. ISNULL( tuContent )
         
         * The content to be added to the collection was not passed or passed as .NULL.  
         * Just exit.  Empty or .NULL. content cannot be added to the collection.
         RETURN .F.
      
      ENDIF
      
      * Since Count triggers Count_Access() we do not want to trigger it more than once.  
      * Read it once and store the result in a local variable.
      lnCount    = .Count   
      lnNewCount = lnCount + 1

   IF PCOUNT() = 1 ;
      .OR. VARTYPE( tnIndex ) # "N" ;
      .OR. !( BETWEEN( tnIndex, 1, lnNewCount ) )

      * If the index was not passed, is the wrong data type, is less than 1 or greater 
      * than Count + 1 execute the default operation, i.e. the new item is to be added 
      * at the end of the collection.
      tnIndex = lnNewCount
   ENDIF
         
   * Items must be added to the collection in sequence without skipping an item. The index 
   * parameter cannot, therefore, be greater than lnNewCount. If the item index is greater 
   * than lnNewcount, reduce it to lnNewCount. 
   tnIndex = MIN( tnIndex, lnNewCount )
      
   * Find out if the content is to be inserted into the collection or replace existing content.  
   IF ( ;
         PCOUNT() = 3 ;
         .AND. VARTYPE( tlInsert ) = "L" ;
         .AND. tlInsert ;
      ) ;
      .OR. tnIndex > ALEN( .Item )
      
      * If three parameters were passed, and the 3rd parameter is logical and true, force an 
      * insert.  Otherwise we are updating existing content at the location specified in tnIndex.
      tlInsert = .T.
      lnItems  = ALEN( .Item ) + 1
   ELSE
      tlInsert = .F.
      lnItems  = ALEN( .Item )
   ENDIF
         
   IF tlInsert .OR. ( tnIndex > ALEN( .Item ) ) .OR. ( lnItems > ALEN( .Item ) )
            
      * If item[ 1 ] is .NULL., the collection is empty, so the new item will be Item[ 1 ], but 
      * if item[ 1 ] is not .NULL., then we must redimension the array to add a row.
      IF ISNULL( .Item[1] )
      
         * The collection should have already been cleared if the Item[ 1 ] is .NULL.  
         * But, in case it has not been, clear it now by calling the Clear() method.
         .Clear()
      ELSE
         
        * We must add a blank row to the Item array before calling AINS(). AINS() does not re-
        * dimension the array before inserting a new item.  If the array is not re-dimensioned
        * first, the last item in the collection will be pushed out of the array and lost when
        * the new item is inserted.
        DIMENSION .Item( lnItems )
            
        * Insert an item into the array at tnIndex.  The new item is inserted just before the
        * the existing item which is pushed down in the array.
        AINS( .Item, tnIndex )
      ENDIF
   ENDIF

   .Item[ tnIndex ] = tuContent
        
   * Save tnIndex as a property.  NewItemIndex is used to preserve the actual index of the last 
   * item added, inserted or updated.
   .NewItemIndex = tnIndex
ENDWITH

RETURN .T.

RemoveItem(): Removes a single item from the collection.

An item to be removed may be specified by passing either its index or its content as an argument to RemoveItem().

Its syntax is,

RemoveItem( [nIndex | uContent ][, lContent ] ), where

  • nIndex specifies the index of the item to be removed, or
  • uContent specifies the content of the item to be removed, and

  • lContent specifies whether a number in the first parameter is an index or content.

The process may take a number of different paths depending on the parameters passed.

  • If an index is passed, the item at the location specified by the index is removed.

  • If content is passed, the item having that content is located and removed. If the content is contained in more than one item, the first item from the top of the collection having the content is removed.

  • If neither nIndex nor uContent are passed in parameters, the last item in the array is removed.

  • If uContent is a number, it may look like an index. The second parameter of the method, lContent, sorts this out.

  • If the first parameter is numeric, it is assumed to be an index unless lContent is passed as true. In such case, the number is treated as content and the collection is searched for the number in content.
* Collection.RemoveItem()
*
* OVERVIEW:   Removes an item from a collection.
* 
* PARAMETERS:   
*
*    tuParm   The content of an value to be located in the collection OR an integer index 
*             specifying the location from which the item is to be removed.
*
*    tlContent   Specifies whether a numeric tuParm represents content of an item to be located 
*             in the collection or the index of an item to be removed.  If omitted or false, 
*             tuParm is presumed to be an index.  If true, tuParm is content to be located in 
*             the collection.  tlContent has no effect unless tuParm is numeric.
* 
*    RETURN:  (L) True if the specified item is removed.  False if an error prevents its removal.
* 
LPARAMETERS tuParm, tlContent

LOCAL lnElement, lnIndex, lnCount

lnIndex = 0

WITH this
   * Get the number of Items in the collection. 
   lnCount = .Count

   DO CASE

   * No parameters passed. 
   CASE PCOUNT() = 0
       
      * Index not passed.  Assume the item is to be removed from the bottom of the collection.
      lnIndex = ALEN( .Item, 1 )
   
   CASE VARTYPE( tuParm ) = "O"
       
      * If tuParm is an object reference to be located in  the collection, ASCAN() cannot be used 
      * because it will not accept an object as a search expression. We are therefore going to have 
      * to use the brute force method and just look at every item in the collection until a match is 
      * found or we run out of items to look at.
      FOR lnI = 1 TO lnCount
   
         IF VARTYPE( .Item[ lnI ] ) = "O";
            .AND. .Item[ lnI ] = tuParm
      
            lnIndex = lnI
               
            EXIT
         ENDIF
      NEXT
               
     CASE VARTYPE( tuParm ) # "N" ;
      .OR. ;
      ( ;
         PCOUNT() = 3 ;
         .AND. VARTYPE( tlContent ) = "L" ;
         .AND. tlContent ;
      ) 
       
      * tuParm is a value to be looked up.  If tuParm is found in the collection, its location 
      * index is returned.
      lnIndex = INT( ASCAN( .Item, tuParm ) )

      OTHERWISE
       
      * tuParm is a numeric row index.  Make certain it is valid.  If it is outside the range of 
      * valid indices, revert to default behavior which is to remove the last item in the list.
      IF BETWEEN( lnIndex, 1, lnCount )
      	lnIndex = INT( tuParm )
   	  ELSE
   	  	lnIndex = lnCount
   	  ENDIF
   ENDCASE

   IF lnIndex > 0
       
      * A FoxPro array cannot contain fewer than one element.  Consequently if the last array
      * element is removed, the element count will still be 1, but the value of the array will be .F.
      * We can't tell whether this .F. is due to removing the last element or if it is an actual 
      * value.  Therefore, when the last element is removed from an array, set Item[1] to .NULL.
      IF ALEN( .Item ) = 1
          
         * We don't have to write code to do this since the Clear() method will set the 
         * array to .NULL.   
         .Clear()

      ELSE

         * Remove item from the collection after setting it to .NULL. just in case it is
         * an object reference.  There is some dispute whether object nullification here is really
         * necessary.  But just in case...
         .Item[ lnIndex ] = .NULL.
         ADEL( .Item, lnIndex )
                
         * Redimension the collection to remove the last row that is now empty.
         * But, since an array cannot be dimensioned to zero rows, make certain it will remain
         * dimensioned to at least one row.  Otherwise an error will be thrown.
         DIMENSION .Item( MAX( ALEN( .Item, 1 ) - 1, 1 ) )

      ENDIF
          
      * Adjust lnCount in case lnCount - 1 = 0. This a little obtuse, but stems from
      * the fact that an array cannot have zero  elements in Visual FoxPro.
      * If Item[] had one row, and one is removed, its row count is still one (albeit
      * an empty row), so we don't let the element count fall below 2 so the test below works.
      lnCount = MAX( lnCount, 2 )

   ENDIF
       
   * If the item was removed and the collection resized, the current length of the collection will 
   * be less than the former length.  By comparing present and former length, we know whether the
   * process has succeeded or failed.
   RETURN ALEN( .Item ) < lnCount

ENDWITH

Clear(): Removes all items from the collection. Clear() is already familiar to FoxProers who encounter it in list box and combo box objects. There is, therefore, no doubt as to its purpose.

Calling Clear() has three effects. It

  • Removes all of the items from the collection,

  • Nullifies any object that was in the collection to prevent orphaned objects from hanging up our form (because we don't want to take the time to test each item to see if it is indeed an object, we just set everything to .NULL.), and

  • Sets Item[1] to .NULL. which signifies to the Count_Access() method (discussed below) that the collection is empty.

In a "cleared" collection, the Count property returns zero even though there is actually one (.NULL.) element in the collection's Item array property. The Count_Access() method determines whether any items are in the collection by reference to the content of Collection.Item[1]. If it is .NULL. and ALEN( Collection.Item ) = 1, the collection is considered empty.

This code has gone through a number of metamorphoses. In an earlier version, the code in Clear() parsed the array, setting every item to .NULL., then collapsed the array to one element. Something like this:

WITH this

   FOR lnI = TO ALEN( .Item, 1 ) TO 1 STEP -1
      .Item[ lnI ] = .NULL.
      ADEL( .Item, lnI )
   NEXT

   DIMENSION .Item( 1 )
ENDWITH

Sometime later, I learned from a much smarter person than I the two-line process I now use, reproduced below. It works just fine.

* Collection.Clear()
*
* OVERVIEW:	Clears all of the items in the collection setting any object references to .NULL.
*  
WITH this

   DIMENSION .Item( 1 )
   .Item[ 1 ] = .NULL.
	
ENDWITH

RETURN

Index(): We discussed earlier the need for functionality to return the index of the location of a specific content passed as an argument. Index() is the method that performs this function. It returns the item index of the first item in the collection having the content specified in its argument. Its syntax is,

Index(uContent [,lCaseSensitive ]), where

  • uContent specifies the content to be located and

  • lCaseSensitive specifies whether a string content search is case sensitive.

By default, the search for string content is case insensitive. Only if lCaseSensitive is true will case be considered searching for character content. If the specified content is not found in the collection, 0 (zero) is returned. If the content is found, its location is returned as the numeric index of the location at which the content was found.

* Collection.Index()
*
* OVERVIEW:   Locates content in the collection and returns  index of the location in the collection
*       at which the content was found. 
*
* PARAMETERS:   
*
* tuValue   The content to be found.
*
* tlCaseSensitive   Specifies whether strings are to be matched by case. Default is .F. -matches 
*       case- will be insensitive.  Has no effect on non-string content.
*
* RETURN:   (N)    The pointer to the item in which the content was located. If the content was 
*       not found, returns 0 (zero).
*
LPARAMETERS tuValue, tlCaseSensitive

LOCAL lnCount, lnI, lnIndex

WITH this

   LnCount = .Count
       
   * If there are no members in the collection, return 0 since
   * tuValue cannot possibly be in an empty collection.
   IF lnCount = 0
      RETURN 0
   ENDIF

   * Initialize lnIndex to zero.
   lnIndex = 0
       
   * Look for tuValue in the collection.
   FOR lnI = 1 TO lnCount

      DO CASE
                   
      * Case insensitive search for a string.
      CASE !( tlCaseSensitive ) ;
         .AND. VARTYPE( .Item[ lnI ] ) = "C" ;
         .AND. VARTYPE( tuValue ) = "C" ;
         .AND. UPPER(.Item[ lnI ] ) = UPPER( tuValue )
         
         lnIndex = lnI

         EXIT
          
      * Search for any other data type or case sensitive search for a string.   
      CASE VARTYPE( .Item[ lnI ] ) = VARTYPE( tuValue ) ;
         .AND. .Item[ lnI ] = tuValue
      
         lnIndex = lnI

         EXIT
      
      ENDCASE

   NEXT

ENDWITH

RETURN lnIndex

Value(): Is the reciprocal of Index(). It returns the content of an item at the location in the collection specified by an index passed as an argument. The syntax is,

Value([nIndex]), where

  • nIndex points to the item whose content is to be returned. If nIndex is omitted, the content of the first item in the collection is returned. If the specified location does not exist, .NULL. is returned.

In many, if not most collection classes, items in a collection cannot be addressed directly, but must be addressed through a method. For example, in Visual Basic, the method oCollection.Item(n) is used to return the content of Collection.Item[n].

In this class, we give the user a choice of using the method oCollection.Value(n) to return the content of Collection.Item[n] or to address the item directly, such as uContent = oCollection.Item[n].

The difference is that, unlike direct addressing, Value() first determines that the location addressed actually exists before trying to read its content, thereby reducing the potential for error, and returns a testable result (.NULL.) if the location does not exist.

Value() should be used when seeking content and the return tested for .NULL. before continuing. Only when absolutely certain that an Item exists should direct addressing be used.

* Collection.Value()
*
* OVERVIEW:   Returns the content of the item at the position specified in tnIndex.

* PARAMETERS:   
*
* tnIndex     An integer representing the location in the collection of the value to be returned,
*
* RETURN:     (U) The content of the member at the location specified by tnIndex, if found, 
*             otherwise .NULL.
* 
LPARAMETERS tnIndex

LOCAL lnCount

* Test the parameters.
IF PCOUNT() = 0
   tnIndex = 1
ENDIF

WITH this
   
   * Since reading Count triggers Count_Access() we do not want to trigger it more than once. 
   * Read it once and store the result in a local variable.
   lnCount = .Count
   
   * If the collection contains no items, tnIndex cannot possibly be found in the collection.
   IF lnCount > 0
      
      * Test for a valid tnIndex
      IF VARTYPE( tnIndex ) = "N" ;
         .AND. BETWEEN( tnIndex, 1, lnCount )
                  
         * Return the content at tnIndex
         RETURN .Item[ tnIndex ]
   
      ENDIF
   ENDIF

ENDWITH

* Item was not found.  Return .NULL.
RETURN .NULL.

Access() and Assign() Methods

To finish up, we are going to attach two properties to Access() or Assign() methods.

Count_Access(): As indicated above, the Count property is created with an Access() method. We use the method to ensure that Count is always accurate and to prevent spurious assignments from corrupting our collection.

The Count_Access() event occurs when the setting of the Count property is read. Count specifies the number of items in the collection. Since the number of items is not always the number of elements in the Item array, some adjustment has to be made.

A VFP array can never contain zero elements, but a collection may contain zero items. Consequently, if the array contains one element, this method determines whether that element is empty, and should not, therefore, be counted. Count is treated like a read-only property. Its value cannot be set except in this method. If it is set elsewhere, the setting is ignored.

The Count property does not actually need to be set. It cannot be read except through this method which ignores its actual setting. The sole reason it is set is to allow its setting to be visible in the Debugger. Otherwise, its setting would always appear to be zero - something that caused me considerable consternation until I figured out what was going on.

* Collection.Count_Access()
*
* OVERVIEW:	Returns the number of items in the collection.
* 
WITH this
   
   * If nothing has been added to the collection or all of  its members have been removed, the 
   * value of .Item[ 1 ] will be .NULL. and the length of the Item[] array will be 1.
   IF ALEN( .Item ) = 1 .AND. ISNULL( .Item[ 1 ] )

      .Count = 0
   ELSE

      .Count = ALEN( .Item )
   ENDIF

   RETURN .Count
ENDWITH

Visible_Assign(): We will use the Visible_Assign() method to prevent the collection object from ever becoming visible during run-time. Here is the code that does that.

* Collection.Visible_Assign()
*
* OVERVIEW:  Prevents the Visible property from being set to .T.
*
* Note the parameter, tlVisible, is required to prevent Fox from throwing an error, but is ignored.
* 
LPARAMETERS tlVisible

this.Visible = .F.

RETURN

Conclusion

And that is our basic collection class. Pretty simple, really.

Below is a link to the download section from which you may download among other things, a form named (with great imagination) "Collection" that demonstrates some of the speed and flexibility of a collection object. Run the form with the "DO FORM collection" command from a Foxpro session.

The collection on the form holds five textboxes scattered around the form. The first thing you should do is align the textboxes in one of the arrangements specified in the option group at the top left corner of the form. Then you can use the various command buttons and spinners to add, delete, move the objects back and forth between collections and around the form just to see how quickly the collection responds.

Have fun.

Source Code

James Edgar
J. M. Edgar has been a software developer in xBase languages since 1984. He received a Masters degree from the University of Maryland and studied law at McGeorge School of Law, the University of Nebraska and Georgetown Law School, earning a Juris Doctor in 1980. He is a member of Phi Beta Kappa, Phi Kappa Phi, Alpha Kappa Delta, and an American Jurisprudence Fellow. He is admitted to practice law in California, the Federal Eastern District of California and the United States Tax Court. He has worked for the U.S. Justice Department, as an Assistant Chief of Police of a major California city, a lawyer in private practice and president of a multi-state insurance agency as well as heading his own software development company. He can be contacted on the Universal Thread or at jmedgar@yahoo.com.
More articles from this author
James Edgar, December 1, 2003
One of the most universally useful compound abstract classes is a tab bar. Unfortunately, Visual FoxPro does not have a native tab bar class. The VFP designers evidently figured that since we have a page frame class, we don't need a tab bar because a page frame works like a tab bar.
James Edgar, July 1, 2003
Lots of Visual FoxPro components include a splendid native scroll bar. The grid, list box, edit box and form classes all contain embedded bars. The VFP designers wisely concluded that these classes are likely to display information that might not be all visible in the same view, and therefore some ...
James Edgar, January 1, 2001
This paper was written in 1993 and has been modified periodically from time to time since that date. It has absolutely no application to the law in alien lands such as Canada or Louisiana. There is an ancient truism among attorneys that "An oral contract is not worth the paper it wasn't written on....
James Edgar, January 1, 2001
(Originally published under this title in Virtual FoxPro Users Group Newsletter of January 2001.) Mouse management in Visual FoxPro can be a little trying. It’s hard to determine in code where the mouse is positioned on a form and it seems almost impossible to force FoxPro to consistently ...