|
KamalPatel.net Back to Downloads/Articles |
|
Create Intelligent GridsBy Kamal Patel
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.
ImplementationIn order to do the implementation these questions need to be answered:
Structure of the underlying tableWe 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:
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
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 cGridMementocGridMemento 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. FinallyThe 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.
|