KamalPatel.net
Back to Downloads/Articles

Create Intelligent Grids 

By Kamal Patel

Grids are an integral part of an application front-end. This time we shall see how we can make more intelligent grids.

VFP allows us to provide capabilities to our grids where users can move columns around and change their widths, RowHeight, HeaderHight etc at run-time. The problem though is that these settings are temporary and remain only for the period a form is open. Once the form is closed the changed settings disappear and do not appear again when we load the form again. This article explains how you can add some functionality into your grids so that they are capable of holding their state. When user moves columns around or changes their widths the grids can restore the new settings next time when loaded.

Let us begin by seeing how things should work. Once we know what is required we will be into specifics.

What is required?

Let us assume that we have a form that has a grid. When we run this form, the grid will request a Handler object for its pervious state. The state information for each grid is stored in a VFP table. The Handler will be responsible for fetching the appropriate state for that grid and then update the grid with that state. Moreover, when the Grid is destroyed it will ask the Handler object to save the grid’s state to the VFP table.



From our understanding we will require to achieve the following:
 . Ability to capture & Restore the sate of each grid
 . Ability to capture & Restore the sate of each column within a grid
 . Ability to capture & Restore the sate of header within each column

A state in our discussion means a set of properties. From the figure we also understand that the objects that communicate here are the grid, the handler and the VFP table.

Implementation

In order to do the implementation these questions need to be answered:
What would be the structure of the underlying table so that we can capture the state of one or more grids occurring in multiple forms?
How does the Handler know which specific properties to be capture for each object (grid, column & header)?
How does the grid works together with the Handler object?

Structure of the underlying table

We will have to capture the properties of the Grid, properties of all the columns in the grid and properties of the Header within each column. There could be multiple grids in multiple forms so we need a way by which each one can be distinct. We could use the form’s name (SCX name) and the object hierarchy for the object names to get a unique object. For example our form name could be MyForm.scx and our object hierarchy could be form1.grdList.

This way we will know which information to capture. The structure of the underlying VFP table (memento.dbf) holding the state of each grid will look as follows:

CREATE TABLE Memento ;
	(cForm c(60), cobject c(240), cProperty c(60), cType c(1), csetting c(60))

*-- cForm:	Specifies the name of the form (SCX Name)
*-- cObject:	Specifies the full path of the object heirarchy
*-- cProperty:	Specifies the name of the property whose state is captured
*-- cType:	Specifies the Data Type of the Property (Char, Numeric etc)
*-- cSetting:	Specifies the Actual value of the property (Char Value)
							

For example if we want to capture the RecordSource of a grid, grdList, which is in a form named MyForm.scx our data would look like this:

<table>

The handler object will also be responsible for capturing and saving the state of each column within this grid object and also header within each column.

How does the Handler know which properties to capture?

We need to tell the handler object which properties to capture. Here also we shall use the memento.dbf, hence allowing to solve two purposes
a. To store the specific properties which need to be captured
b. To store the actual state of each grid and the objects within the grid
The specific properties that need to be captured will always have “SETUP” in the cForm field and the actual object name (Grid, Column, Header) in the cObject Field. This way we can query this table for cFrom=”SETUP” to get those properties which we need to capture. This also allows us an easy way to extend our handler to capture more properties later on simply by adding a record in this vfp table.

For example if we want to capture the RowHeight & HeaderHeight for the Grid, and Width and ColumnOrder for the columns then our memento.dbf will look as follows:

<table2>

After running the testgrid.scx our Memento.dbf will look something like this…

<table3>

How does the grid works together with the Handler?

The grid (Originator) is responsible for calling the Handler object (Memento) object. We shall refer to the Handler object as a Memento object from now on. In the later part of the article we do discuss why this weird name…

The grid’s Init() calls the memento object’s GetState() passing itself as a parameter and asking the memento object to reset the grid to its previously stored state.

*-- Grid.Init()
LOCAL loMemento
loMemento = CREATEOBJECT(“cGridMemento”)
loMemento.GetState(this)
						

In the same way the Destroy() event of the grid requests the memento object to save the state of the grid.

*-- Grid.Destroy()
LOCAL loMemento
loMemento = CREATEOBJECT(“cGridMemento”)
loMemento.SetState(this)
						

Together the grid and the memento work as illustrated in this diagram. The memento object is actually based on a class cGridMemento.

The cGridMemento Class exposes two Public methods SetSate() and GetState(). GetState() is responsible for restoring the state of the grid from a previously stored state. GetState() further calls Protected methods RestoreGrid(), RestoreColumns() and RestoreHeader(). SetSate() on the other hand is responsible for saving the state of the grid. SetState() further calls Protected methods SaveGrid(), SaveColumns() and SaveHeader(). Each of these protected do what the name of these methods implies. The implementation is hidden within so now let us look at how the cGridMemento is implemented in code.

Code for cGridMemento

cGridMemento is based on the VFP line class.

**************************************************
*-- Class:        cgridmemento 
*-- ParentClass:  line
*-- BaseClass:    line
*-- A customized memento class which captures and restores the state of a grid
*
DEFINE CLASS cgridmemento AS line


   Height = 68
   Width = 68
   Name = "cgridmemento"


   *-- Saves the property settings for the grid object
   PROTECTED PROCEDURE savegrid
      LPARAMETERS toObject, tcObjectPath, tcFormName
      LOCAL laGridProperties[1]

      *-- First we get all the information needed
      SELECT cProperty, cType ;
         FROM memento ;
         WHERE cForm = 'SETUP' AND cObject = 'GRID' ;
         INTO ARRAY laGridProperties

      *-- Loop through the array and store each property
      FOR lnCount = 1 TO ALEN(laGridProperties, 1)
         SELECT memento
         LOCATE FOR cForm = tcFormName AND cObject=tcObjectPath AND cproperty = laGridProperties[lnCount, 1]

         IF !FOUND()
            APPEND BLANK
            REPLACE cForm WITH tcFormName, ;
               cObject WITH tcObjectPath, ;
               cproperty WITH laGridProperties[lnCount, 1], ;
               ctype WITH laGridProperties[lnCount, 2]
         ENDIF

         *-- Now save the setting
         lcVariant = EVAL('toObject.'+ ALLT(laGridProperties[lnCount, 1]))

         lcSetting = this.GetStringFromVariant(lcVariant, ctype)

         REPLACE cSetting WITH lcSetting
      NEXT
   ENDPROC


   *-- Saves the property settings for each column object in grid
   PROTECTED PROCEDURE savecolumns
      LPARAMETERS toObject, tcObjectPath, tcFormName
      LOCAL laColumnProperties[1]

      *-- First we get all the information needed
      SELECT cProperty, cType ;
         FROM memento ;
         WHERE cForm = 'SETUP' AND cObject = 'COLUMN' ;
         INTO ARRAY laColumnProperties

      *-- Loop through the array and store each property
      FOR lnColumns = 1 TO toObject.ColumnCount

         lcObjectPath = tcObjectPath + [.Columns(] + ALLT(STR(lnColumns)) + [)]

         FOR lnCount = 1 TO ALEN(laColumnProperties, 1)
            SELECT memento
            LOCATE FOR cForm = tcFormName AND cObject=lcObjectPath AND cproperty = laColumnProperties[lnCount, 1]

            IF !FOUND()
               APPEND BLANK
               REPLACE cForm WITH tcFormName, ;
                  cObject WITH lcObjectPath, ;
                  cproperty WITH laColumnProperties[lnCount, 1], ;
                  ctype WITH laColumnProperties[lnCount, 2]
            ENDIF

            *-- Now save the setting
            lcVariant = EVAL([toObject.Columns(] + ALLT(STR(lnColumns)) + [).] + ALLT(laColumnProperties[lnCount, 1]))

            lcSetting = this.GetStringFromVariant(lcVariant, ctype)

            REPLACE cSetting WITH lcSetting
         NEXT
      NEXT
   ENDPROC


   *-- Saves the property settings for each header object in column
   PROTECTED PROCEDURE saveheaders
      LPARAMETERS toObject, tcObjectPath, tcFormName
      LOCAL laHeaderProperties[1]

      *-- First we get all the information needed
      SELECT cProperty, cType ;
         FROM memento ;
         WHERE cForm = 'SETUP' AND cObject = 'HEADER' ;
         INTO ARRAY laHeaderProperties

      *-- Loop through the array and store each property
      FOR lnColumns = 1 TO toObject.ColumnCount

         lcObjectPath = tcObjectPath + [.Columns(] + ALLT(STR(lnColumns)) + [).Controls(1)]

         FOR lnCount = 1 TO ALEN(laHeaderProperties, 1)
            SELECT memento
            LOCATE FOR cForm = tcFormName AND cObject=lcObjectPath AND cproperty = laHeaderProperties[lnCount, 1]

            IF !FOUND()
               APPEND BLANK
               REPLACE cForm WITH tcFormName, ;
                  cObject WITH lcObjectPath, ;
                  cproperty WITH laHeaderProperties[lnCount, 1], ;
                  ctype WITH laHeaderProperties[lnCount, 2]
            ENDIF

            *-- Now save the setting
            lcVariant = EVAL([toObject.Columns(] + ALLT(STR(lnColumns)) + [).Controls(1).] + ALLT(laHeaderProperties[lnCount, 1]))

            lcSetting = this.GetStringFromVariant(lcVariant, ctype)

            REPLACE cSetting WITH lcSetting
         NEXT
      NEXT
   ENDPROC


   *-- Retrieve's the property settings for the grid object
   PROTECTED PROCEDURE retrievegrid
      LPARAMETERS toObject, tcObjectPath, tcFormName
      LOCAL laGridProperties[1]

      SELECT cProperty, cType, cSetting ;
         FROM memento ;
         WHERE cForm = tcFormName AND ALLT(cObject)==ALLT(tcObjectPath) ;
         INTO ARRAY laGridProperties

      *-- Make sure there was a saved state
      IF INLIST(TYPE('laGridProperties[1]'), 'L', 'U')
         RETURN
      ENDIF

      *-- Loop through the array and store each property
      FOR lnCount = 1 TO ALEN(laGridProperties, 1)

         *-- Now retrieve the setting
         lcSetting = ALLT(laGridProperties[lnCount,3])
         lcSetting = 'toObject.'+ ALLT(laGridProperties[lnCount, 1]) + '=' + lcSetting
         &lcSetting

      NEXT
   ENDPROC


   *-- Retrieve's the property settings for the each column object
   PROTECTED PROCEDURE retrievecolumns
      LPARAMETERS toObject, tcObjectPath, tcFormName
      LOCAL laGridProperties[1]

      FOR lnColumns = 1 TO toObject.ColumnCount
         lcObjectPath = tcObjectPath + '.Columns(' + ALLT(STR(lnColumns)) + ')'
         SELECT cProperty, cType, cSetting ;
            FROM memento ;
            WHERE cForm = tcFormName AND ALLT(cObject)==ALLT(lcObjectPath) ;
            INTO ARRAY laColumnProperties

         *-- Make sure there was a saved state
         IF INLIST(TYPE('laColumnProperties[1]'), 'L', 'U')
            RETURN
         ENDIF

         *-- Loop through the array and store each property
         FOR lnCount = 1 TO ALEN(laColumnProperties, 1)

            *-- Now retrieve the setting
            lcSetting = ALLT(laColumnProperties[lnCount,3])
            lcSetting = 'toObject.Columns(' + ALLT(STR(lnColumns)) + ').' + ALLT(laColumnProperties[lnCount, 1]) + '=' + lcSetting
            &lcSetting

         NEXT
      NEXT
   ENDPROC


   *-- Retrieve's the property settings for the each header object
   PROTECTED PROCEDURE retrieveheaders
      LPARAMETERS toObject, tcObjectPath, tcFormName
      LOCAL laGridProperties[1]

      FOR lnColumns = 1 TO toObject.ColumnCount
         lcObjectPath = tcObjectPath + '.Columns(' + ALLT(STR(lnColumns)) + ').Controls(1)'
         SELECT cProperty, cType, cSetting ;
            FROM memento ;
            WHERE cForm = tcFormName AND ALLT(cObject)==ALLT(lcObjectPath) ;
            INTO ARRAY laHeaderProperties

         *-- Make sure there was a saved state
         IF INLIST(TYPE('laHeaderProperties[1]'), 'L', 'U')
            RETURN
         ENDIF

         *-- Loop through the array and store each property
         FOR lnCount = 1 TO ALEN(laHeaderProperties, 1)

            *-- Now retrieve the setting
            lcSetting = ALLT(laHeaderProperties[lnCount,3])
            lcSetting = 'toObject.Columns(' + ALLT(STR(lnColumns)) + ').Controls(1).' + ALLT(laHeaderProperties[lnCount, 1]) + '=' + lcSetting
            &lcSetting

         NEXT
      NEXT
   ENDPROC


   *-- Receives any data type and converts it to character data type and returns the string
   PROTECTED PROCEDURE getstringfromvariant
      LPARAMETERS tcVariant, tcType

      LOCAL lcString
      lcString = ''
      DO CASE
         CASE tcType = 'C'
            lcString = ["] + tcVariant + ["]
         CASE tcType = 'N'
            lcString = ALLT(STR(lcVariant))
         CASE tcType = 'L'
            lcString = [.] + IIF(tcVariant, "T", "F") + [.]
         OTHERWISE
            lcString = ''
      ENDCASE

      RETURN lcString
   ENDPROC


   *-- Receives a grid object as a parameter and sets its state to last stored set
   PROCEDURE getstate
      *-- Recevies an object as a parameter and resets its settings
      LPARAMETERS toObject, tcObjectPath

      IF !USED('memento')
         USE memento IN 0
      ENDIF

      LOCAL lcFormName
      lcFormName = SYS(1271, toObject)
      lcFormName = STRTRAN(UPPER(SUBSTR(lcFormName, RAT('\', lcFormName) + 1)), '.SCX','')

      IF PCOUNT() # 2
         *-- Usually this happens and is at run-time
         lcObjectPath = SYS(1272, toObject)
      ELSE
         *-- Possible that called at design time
         lcObjectPath = LOWER(ALLT(tcObjectPath))
      ENDIF

      *-- Retrieve the settings for grid, columns and headers
      this.RetrieveGrid(toObject, lcObjectPath, lcFormName)
      this.RetrieveColumns(toObject, lcObjectPath, lcFormName)
      this.RetrieveHeaders(toObject, lcObjectPath, lcFormName)

      *-- Finally close the memento table
      USE IN memento
   ENDPROC


   *-- Receives a grid object as a parameter and saves the state
   PROCEDURE setstate
      *-- Recevies an object as a parameter and saves its settings
      LPARAMETERS toObject, tcObjectPath

      IF !USED('memento')
         USE memento IN 0
      ENDIF

      LOCAL lcFormName
      lcFormName = SYS(1271, toObject)
      lcFormName = STRTRAN(UPPER(SUBSTR(lcFormName, RAT('\', lcFormName) + 1)), '.SCX','')

      IF PCOUNT() # 2
         *-- Usually this happens and is at run-time
         lcObjectPath = SYS(1272, toObject)
      ELSE
         *-- Possible that called at design time
         lcObjectPath = tcObjectPath
      ENDIF

      *WAIT WIND lcFormName + ',' + lcObjectPath + ',' + toObject.Name

      *-- Save the memento for grids, Columns and Headers
      this.SaveGrid(toObject, lcObjectPath, lcFormName)
      this.SaveColumns(toObject, lcObjectPath, lcFormName)
      this.SaveHeaders(toObject, lcObjectPath, lcFormName)

      *-- Finally close the memento table
      USE IN memento
   ENDPROC


ENDDEFINE
*
*-- EndDefine: cgridmemento
**************************************************
						
						

Extending the Memento to work like a builder You can even use this at design time. Form example you have a state captured for a grid which you would like to implement at design time, you could do this. Open the form designer which contains the grid, position the mouse over the grid. Make the command window the active window and type the following:

loObject = SYS(1270)
loObject = loObject.Parent		&& Make loObject as Grid object, as default is column
SET CLASS TO libs
LoMemento = CREATEOBJECT(“cGridMemento”)
loMemento.GetState(loObject, "form1.grdCustomer")

You will see that at design time the state was set. This is also the functionality of a builder.

Finally

The table memento.dbf is user specific so the data will be different for each user. It is recommended that you do not add this table to a DBC unless each of your users owns a DBC with tables. This way each user will have their own setups for their grids. In theory, the Windows Registry is supposed to be used to store information that is application specific. The registry would not be a good solution in our case as it would be very slow. Moreover, the table approach also allows us simply add SETUP records and extend the functionality. Memento can be an expensive design pattern in terms of time. For example in our case if we implement such functionality for all the grids in the system, it could slow down the system. This is because we might be saving the state of each grid even when none of the settings have been changed. So it is very important that you use this optimistically. You may speed up the process by separating the logic for saving the state from the Destroy() event of the grid and provide it manually example via RightClick() on grid or through a button. Whether it is the last form the user was working on or the window positions for each form you wish to capture. The concept still remains the same and is pretty cool too.

A look at a Design Pattern - Memento:

A Design Pattern can be thought of as an architectural layout to solve a problem whose actual implementation could be in millions of ways. The approach used to solve this problem is by a Design Pattern called Memento. The intent of a Memento is to capture and externalize an object’s internal state so that the object can be restored to this state later.

Every Design pattern has a motivation. The Motivation behind the Memento Pattern arose from the need to store the objects current state and then be able to restore it when requested. Let us look at an example where a Memento Pattern could be used. Consider that we want to implement Undo features into our application. We will need to keep a track of changes as the user works by capturing each state via a Memento object and then when the user requests an Undo, we will request the Memento to reset to an the last stored state. We would achieve this by the two general interfaces exposed by the Memento; GetState() and SetState().

A memento pattern is constructed by three objects:

Memento
Stores internal state of the Originator object.
Protects against access by objects other than the originator. Mementos effectively have two interfaces (methods).

Originator
Creates a Memento containing a snapshot of its current internal state
Uses the Memento to restore its internal state

CareTaker
Is responsible for the Memento’s safekeeping
Never operates on or examines the state of the Memento

Generally a memento is responsible for storing a snapshot of the internal state of another object. In our case the Memento was responsible for storing the state of a grid and restoring the state of the grid upon request. Mementos are usually dynamic in nature but we used the same concept to store persistent state of the grids into a VFP table. This is because it was faster an easier to maintain.

Remember that Design Patterns provide us with a guidance to solve a problem for the most commonly occurring software problems. We use this as a basis for our design and then enhance or extend the design depending on our requirements.