VHP Computing Group. Home Page.  
TechnologiesSpecialistsDownloadLinksSign GuestbookView Guestbook

December, 2002
Vladimir Trukhin
Senior Software Engineer
JSC "Votkinsk Hydroelectric Power Plant"
Fax: +7 (34241) 63297
E-mail:
vlt@votges.ru

This article was first published in the July, 2002 issue of FoxTalk

Friendly Grid and Learned Search

It's a fairly easy to organize data searching in the columns of the Grid object that's placed in the form. It's easy to do it in the second and the third form as well, but from here, this process tires a developer with its monotony. It's an appropriate time to create some classes that will be able to deal with both the Grid objects and their columns, and Vladimir Trukhin shows you how.

One day when I'd tired of building yet another regular form with elements for record searching, I decided to formulate requirements for a way to provide the simple and effective method of searching in all my forms. I came up with the following requirements:

• An object that provides a search must be a form floating above another form with needed data.

• The newly created searching object must be linked to the active form if it exists. If there isn't an active form, the object must not be created.

• Each form with Grid objects must have its own searching object (two open forms don't share the same search form).

• The searching object must be destroyed once the form has closed.

• It must find all objects of the Grid class that are flagged as being searchable in the form.

• It must determine a list of columns, each of which is flagged for being searchable, for each Grid object.

• It must provide the user with the list of the detected Grid objects and the list of their columns.

• It must provide the user with not only the searching service but also with record selection. (This was an insistent wish of advanced customers.) The established criterion of selection must operate even after the searching object has been closed but the form still is running.

• Linking the searching object with the data form must not require a serious modification of the existing forms.

• The searching object must easily link with forms in any application. (Later in this article, I'll show how to implement this class in an existing application on a sample of the Tasmanian Traders project.)

Most of the listed considerations are obvious ones. They don't require special solutions, but some of them deserve discussion.

There are two actors on this stage...
Naturally, the searching object can find all Grid objects in the active form—but it must select only available ones meant for this operation.

In this phase, two players are appearing in the game. The first player is Mr. Search—he's scanning the form. The second is Mr. Grid—he's signaling Mr. Search with diligence. How and what Mr. Grid can divulge about himself?

Certainly, Mr. Grid can have special properties for this purpose, but it's undesirable, as this approach will demand a definition of some subclass of the Grid class. That means that this implementation may require a replacement of the Grid objects in the existing forms. As they may have been working well for a long time, it's a poor idea to mess with them. Let's find another way. As rule, nobody uses the Comment property of the Grid object. However, this property can be a good flag, to be used as a signal about the ability of common business with Mr. Search. Furthermore, its value will look appropriate in the list of the grids that are available for searching.

Now about columns... There can be a large number of columns, and there isn't necessarily a good reason to do a search in each one. Similarly to the previous case, I'll use an unused property to flag the column as being available for searching—in this case, the Tag property. It can store a character value such as "Find" to indicate the ability of searching in this column.

The usage of existing properties for flags satisfies the requirements of a minimum modification in the existing forms.

Where are the magicians hiding?
The first time through, a problem of detection of all of the needed Grid objects in the form looked like a simple job. I thought that it was enough to perform the actions to find Mr. Grid:

1. Get an array of the objects that are placed on the form.

2. Get references to the objects of the Grid class.

3. Store these references in an array to use them later.

However, it turned out that the problem is much more difficult. It seems that Mr. Grid doesn't always like to sit on the surface of the form. Sometimes he prefers the depths of the object hierarchy inside the form.

As a result, it was necessary to create a method that recursively calls itself to sink into the object's hierarchy. Initially, it receives the reference to the form as a parameter and finds all objects that are placed in this level. Then the method scans those found objects with a goal to find the Grid objects with a non-empty Comment property. If a scanned object isn't a grid, then there's still a reason to suppose that it can contain such an object. In this case, it's appropriate to call this method recursively and to pass reference to the object.

If the scanned object is a grid, then it's necessary to record it in the array, which represents the list of the found grids. Every row of such a list stores two values:

• The name of the grid (from the Comment property of the grid)

• The reference to the Grid object

The code of the method looks like this:

PROCEDURE DetectGrids(loCurObject)

 LOCAL ARRAY laGrids(1)
 LOCAL lnElement, lcObjRef, loObjRef
 IF AMEMBERS(laGrids,loCurObject,2)>0
   FOR lnElement=1 TO ALEN(laGrids,1)
     lcObjRef='loCurObject.'+laGrids(lnElement)
     loObjRef=&lcObjRef
     IF UPPER(ALLT(loObjRef.BaseClass))=='GRID' ;
         and !EMPTY(loObjRef.Comment)
       IF THIS.NumberOfGrids=0
         THIS.NumberOfGrids=1
       ELSE
         THIS.NumberOfGrids=THIS.NumberOfGrids+1
         DIMENSION THIS.Grids(THIS.NumberOfGrids,2)
       ENDIF
       THIS.Grids(THIS.NumberOfGrids,1)=loObjRef.Comment
       THIS.Grids(THIS.NumberOfGrids,2)=loObjRef
       IF EMPTY(loObjRef.Tag)
         loObjRef.Tag='gc'+;
         ALLT(STR(INT(RAND(SECONDS())*1000000)))+;
         ALLT(STR(THIS.NumberOfGrids))
       ENDIF
     ELSE
       THIS.DetectGrids(loObjRef)
     ENDIF
   ENDFOR
 ENDIF 

 ENDPROC

 

Every ball has its own hole...
Perhaps the manipulation with the Tag property of the grid in the preceding bit of code gives rise to some perplexity. It's invoked to resolve the next problem...

It's a problem of record selection. Both Mr. Search and Mr. Grid should decide who will control the filter variable. Let's suppose that it's Mr. Search's privilege. He stores the filter clause into some variable, starts the filter, and goes away. Oops—our friends have some serious problems:

• If the name of the variable to search on is fixed, then Mr. Search can't do a selection on multiple forms. Several forms could each be open and each of them could have search forms open at the same time.

• The alternative is for Mr. Search to create a new variable with a random name at his every appearance, but it's frightful to think how many variables he'll create. The variable must stay in memory after Mr. Search disappears since the filter is still running.

• And what will happen if Mr. Grid wants to quit the stage before Mr. Search returns a result? (The search form is "always on top," but it's not modal That's right—he'll leave all filter variables in the labyrinth of infinite memory.)

It's quite another matter when Mr. Grid assumes responsibility for the filter variable:

• He can keep it in his pocket for as long as he needs.

• If Mr. Search will need the name of variable to set the filter, he can ask Mr. Grid about it—for example, with code like this:

  PROCEDURE SelectedFilterVariable

         LOCAL loGridRef
         loGridRef=thisform.SelectedGridRef()
         return ALLT(loGridRef.Tag)

   ENDPROC 

• And, at last, leaving the stage, Mr. Grid will release the variable, which isn't needed any longer by anyone. He should do something like the following:

  IF !EMPTY(this.Tag)

         LOCAL lcVarName
         lcVarName=this.Tag
         RELEASE &lcVarName

   ENDIF

The Tag property is used to ease the modification of the existing forms.

An implementation of the class into an existing project
I won't give a detailed description of the source code here because it's all included in the Download file for this article and doesn't contain any tricks. It will be more interesting and useful to describe an implementation of this class into an existing project. I've chosen the Tasmanian Traders project as an example since every Visual FoxPro programmer has this one. An example of the searching class, which is working with the Customers form, is represented in the next example (see Figure 1).

The implementation of the class requires several simple steps:

1. Place the class definition file in the project's directories structure.

2. Modify the Customers form.

3. Modify the main menu of the application.

Class definition and directories of the Tasmanian Traders project
The definition of the class is contained in the file GRIDSEARC.PRG file, which, in turn, is contained in the file 07TRUKHIN.ZIP. It's enough to unpack it and to place into the program directory of the project. This directory is C:\Program Files\Microsoft Visual FoxPro 7\Samples\Tastrade\Progs, which coincides with the standard installation of Visual FoxPro 7.0. However, different PCs can have different paths to this directory. If your samples are installed elsewhere, you can call the HOME(2) function to get the path to it.

Modification of the Customers form
After the class definition is placed in a suitable location, it's time for the next step—modifying both the Customers form and the Grid objects that are placed in this form.

First of all, it's necessary to create the form's FinderRef property and to initiate it with a NULL value. This property will store a reference to the searching object.

 FinderRef = NULL

Second, it's necessary to add the StartFinder() method to the form. It will create an instance of the searching class.

 PROCEDURE StartFinder

 thisform.FinderRef=NEWOBJECT('GridSearch', ;
     'PROGS\GridSearch.PRG','',THIS)
 LOCAL loRef
 loRef=thisform.FinderRef
 loRef.Show()

 ENDPROC

Then it's necessary to override the Destroy() method of the form. It must destroy the searching object at the closing of the form.

PROCEDURE Destroy

   IF TYPE('thisform.FinderRef')='O' ;
       and !ISNULL(thisform.FinderRef)
     LOCAL loRef
     loRef=thisform.FinderRef
     loRef.RELEASE()
     thisform.FinderRef=NULL
   ENDIF
   DODEFAULT()

 ENDPROC

 

The following step is a modification of the grdList object, which is placed in this form. It may be done like this:

1. To override the Destroy() method of the grid, use the following method. It will release the filter variable:

PROCEDURE Destroy

 IF !EMPTY(this.Tag)
   LOCAL lcVarName
   lcVarName=this.Tag
   RELEASE &lcVarName
 ENDIF
 DODEFAULT()

 ENDPROC

2. To identify the grid as searchable, with a more friendly name that the user will see in the Search form list box, replace the Comment property of the grdList object with the "List of Customers" value:

  grdList.COMMENT="List of Customers".

3. Replace the Tag property with the "Find" value for all columns, which have the "Character" datatype and which are needed for a search. For example:

grdList.grcName.TAG="Find"
grdList.grcContactName.TAG="Find"

Modification of the main menu of the application
The main menu of the project is defined in the file MENUS\MAIN.MNX. It's enough to add a new item—for example, "Search in Active Form"—in the Edit menu and to write a procedure for this item.

LOCAL loActiveForm, loMrSearch
 IF TYPE('_screen.ActiveForm')!='O' ;
   OR ISNULL(_screen.ActiveForm)
   RETURN
 ENDIF
 loActiveForm=_screen.ActiveForm
 IF IsMember(loActiveForm,'StartFinder')
   loActiveForm.StartFinder()
 ENDIF 
 RETURN

  FUNCTION IsMember(loObject,lcMember)

 LOCAL llPresent,lnNumberOfMembers, lnIndex
 llPresent=.F.
 LOCAL ARRAY laMembers(1)
 lcMember=UPPER(ALLT(lcMember))
 lnNumberOfMembers=AMEMBERS(laMembers,loObject,1)
 IF lnNumberOfMembers>0
   FOR lnIndex=1 TO lnNumberOfMembers
     IF UPPER(ALLT(laMembers(lnIndex,1))) == lcMember
       llPresent=.T.
       EXIT
     ENDIF
   ENDFOR
 ENDIF
 RETURN llPresent

 ENDFUNC

So all modifications in the project are made. It's now time to type in the command window a magic "DO MAIN" command to start up Tas Traders, and then open the Customers form.

Conclusion
This class isn't a panacea for all problems that appear in connection with a data-searching requirement, but it can be used easily and with success in most cases. All that's required is that you create both a Form class and a Grid class (shown in Figure 2) that include all properties and methods described earlier, and you're all set for less work in creating your search forms.


Home Page | Technologies | Specialists | Download | Links | Sign Guestbook | View Guestbook