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).
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.
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.
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.
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.
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().
* 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
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.
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.
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
The process may take a number of different paths depending on the parameters passed.
* 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
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
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
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