Page 1 of 1
Can Properties (DATA) be added dynamically to an Object (CLA
Posted: Tue Apr 29, 2008 12:40 am
by xProgrammer
Hi All
I can't see how to do it and I suspect it might well be impossible, particularly having looked at the code for TDatabase (which has a very clever, if not very pretty, workaround).
But before I spend too much time on alternatives, I thought that it was worth asking the question.
Thanks
Doug
(xProgrammer)
Posted: Tue Apr 29, 2008 12:56 am
by ShumingWang
__ObjAddData(oyourbject,cnames)
Shuming Wang
Posted: Tue Apr 29, 2008 2:12 am
by xProgrammer
Thanks
That should make what I want to do so much easier and neater!
Regards
Doug
(xProgrammer)
Posted: Tue Apr 29, 2008 11:18 am
by xProgrammer
Thanks Shuming
Works like a charm
Posted: Tue Apr 29, 2008 1:51 pm
by Antonio Linares
Doug,
Please notice that Class TDataBase does not add new DATAs.
It uses the Class "error handler" OnError() method to route the desired msgs to access "virtual" DATAs. So the same Class can be used to manage multiple different DBFs without having to inherit a new Class for each DBF
Posted: Tue Apr 29, 2008 10:19 pm
by xProgrammer
Hi Antonio
I noticed that and now understand the reasoning behind it although I'm not convinced that it's strictly correct. If DATAs are added dynamically to an object (ie a class instance) they would presumably be independant of the class of that object - one class would still suffice wouldn't it?
In my architecture a class such as PATIENT doesn't inherit from a database class but instead contains a database object (or in reality a pointer to that object). The database object would ideally dynamically add properties not to the database object but to the calling object (in this case an object of class PATIENT).
Whilst I have not yet written this code I think it would look something like this in the PATIENT class constructor
Given the smarts I want to build into the database class that code might be a bit complicated but if one didn't want to support any translation of fieldnames etc it would be fiarly straight forward along the lines of:
Code: Select all
METHOD AddDatas( oCalling ) CLASS MyDataBaseClass
SELECT ::WorkArea
FOR ii := 1 TO FCOUNT()
__ObjAddData( oCalling, FNAME( ii ) )
NEXT
RETURN nil
Doing a databsae read would be fiarly simple under these circumstances too with something like:
Code: Select all
METHOD ReadData( oCalling )
aVals := ARRAY( 0 )
aThis := ARRAY( 2 )
SELECT ::WorkArea
FOR ii := 1 TO FCOUNT()
aThis[1] := FNAME( ii )
aThis[2] := FIELDGET( ii )
AADD( aVals, aThis )
NEXT
__ObjDataSetFromArray( oCalling, aVals )
RETURN nil
Please note that last call is wrong name - it exists but I don't have the documentation with me. Code is not optimised. Field Names could be preset once in the array aVals and not need to be reset each time. This code is to illustrate the principle only. oCalling would possibly be a property of the databse object and not need to be passed in each time.
Happy to receive comments / ideas / criticisms.
Regards
Doug
xProgrammer
Posted: Wed Apr 30, 2008 6:04 am
by Antonio Linares
Doug,
If you add new DATAs, you add them to the Class not to the object,
so all that Class instantiated objects are affected
Thats why the Class error handler approach is better, as it does not modify the original class.
Posted: Wed Apr 30, 2008 6:30 am
by xProgrammer
Hi Antonio
Thanks for the advice. The documentation I saw was a bit misleading in that it mostly talked about adding DATA to an object and the function is so named.
Certainly that would be a problem in some architectures, but I don't see it as an issue in mine. If I am adding the DATAs to a PATIENT class (using a reference to an object of that class) it surely won't affect my database class or other classes that contain objects of that database class?
Anyway I will certainly do some testing.
Actually I think that I will probably create a VIEW class, supporting multiple views for a table and presumably also support views across multiple tables.
Regards
Doug
Posted: Wed Apr 30, 2008 10:22 pm
by xProgrammer
As you said Antonio, __ObjAddData() adds DATAs to the class and not just the particular object it is called against. For my purposes that isn't a problem though.
It looks like I will end up with a RECORD class and a RECORD_LIST class.
Initial version will subclass these. Subclass will basically have a controlling array (set up by a series of #xcommands).
Subsequent version will not need subclassing but read parameters from a file.
I will be supporting:
data name conversion (UI independant of data storage) (in and out)
type conversion (in and out)
default values (for Blank() functionality)
field lengths (which may not be available from other modes of persistence)
Only just started but #xcommand version is looking like this:
Code: Select all
XLATE PT_KEY TO sKey AS PKEY
XLATE PT_NMFAMLY TO sNmFamly AS STRING LENGTH 32
XLATE PT_NMGIVEN TO sNmGiven AS STRING LENGTH 32
XLATE PT_NMOTHER TO sNmOther AS STRING LENGTH 32
XLATE PT_NMPREV TO sNmPrev AS STRING LENGTH 32
XLATE PT_NMPREF TO sNmPref AS STRING LENGTH 16
XLATE PT_NMTITLE TO sNmTitle AS STRING LENGTH 8
XLATE PT_DOB TO dDOB AS DATE
XLATE PT_GENDER TO cGender AS CHAR
XLATE PT_ADLINE1 TO sAdLine1 AS STRING LENGTH 32
XLATE PT_ADLINE2 TO sAdLine2 AS STRING LENGTH 32
XLATE PT_ADSUBRB TO sAdSubrb AS STRING LENGTH 24
XLATE PT_ADSTATE TO sAdState AS STRING LENGTH 3 DEFAULT "NSW"
XLATE PT_ADPCODE TO sAdPCode AS STRING LENGTH 4
XLATE PT_ADCNTRY TO sAdCntry AS STRING LENGTH 24
XLATE PT_PHHOME TO sPhHome AS STRING LENGTH 16
XLATE PT_PHWORK TO sPhWork AS STRING LENGTH 16
XLATE PT_PHMOB TO sPhMob AS STRING LENGTH 16
XLATE PT_PHFAX TO sPhFax AS STRING LENGTH 16
XLATE PT_EMAIL TO sEmail AS STRING LENGTH 24
XLATE PT_MEDIC TO sMedic AS STRING LENGTH 10
XLATE PT_MEDPOS TO sMedPos AS CHAR
XLATE PT_VETAFF TO sVetAff AS STRING LENGTH 20
XLATE PT_ACTIVE TO cActive AS CHAR
XLATE PT_LUBY TO sLUBy AS SKEY
XLATE PT_LUWHEN TO sLUWhen AS DATETIME
XLATE PT_LUACTN TO cLUActn AS CHAR
"AS STRING" is optional as it is the default. This is processed into an array along the following lines:
Code: Select all
#xcommand XLATE <fname> TO <pname> AS STRING LENGTH <length> => AADD( ::aPROPERTIES, { <"pname">, <"fname">, , "S", <length>, 0 } )
Since it is all controlled by an array conversion to being data driven should be straight forward.
Code generators were never quite adequate, but data driven is IMHO the way to go. I have done that in a Web based system (with a series of major commercial systems now using that technology). The thing that made it work where generators fail was the ability to include user defined script almost anywhere and the fact that everything was dynamic. xBase with its code blocks might be one of the few other systems where this approach could be as successful.
Regards
Doug
(xProgrammer)
Posted: Thu May 01, 2008 1:17 pm
by xProgrammer
Basic code is up and running. Doesn't yet cover all possibilities but I'm happy so far.
Method AddDatas performs following functions:
adds DATAs (not to it's class but to the class passed in)
writes field positions into the aPROPERTIES array (so that only has to be done once)
writes variable names into the aVALUES array (again so that only has to be done once)
It looks like this:
Code: Select all
METHOD AddDatas( oCalling ) CLASS VIEW
LOCAL aTHIS
oCalling:oDBF:Select()
oCalling:oDBF:GoTo( 100 )
::aVALUES := ARRAY( 0 )
FOR ii := 1 TO ::iProperties
__ObjAddData( oCalling, ::aPROPERTIES[ii][1] )
::aPROPERTIES[ii][3] := FieldPos( ::aPROPERTIES[ii][2] )
aTHIS := ARRAY( 2 )
aTHIS[1] := ::aPROPERTIES[ii][1]
AADD( ::aVALUES, aTHIS )
NEXT
Method ReadValues reads values from the data base table, translates the names, converts type as appropriate and sends the values to the object passed in. It looks like:
Code: Select all
METHOD ReadValues( oCalling ) CLASS VIEW
LOCAL cDataType
FOR ii = 1 TO ::iProperties
cDataType := ::aPROPERTIES[ii][4]
DO CASE
CASE cDataType = "D"
::aVALUES[ii][2] := STOD( FieldGet( ::aPROPERTIES[ii][3] ) )
OTHERWISE
::aVALUES[ii][2] := FieldGet( ::aPROPERTIES[ii][3] )
ENDCASE
NEXT
__ObjSetValueList( oCalling, ::aVALUES )
Method BlankValues sets values in the passed in object to blanks or defaults as appropriate. It looks like:
Code: Select all
METHOD BlankValues( oCalling ) CLASS VIEW
LOCAL cDataType
FOR ii = 1 TO ::iProperties
cDataType := ::aPROPERTIES[ii][4]
DO CASE
CASE cDataType = "C"
::aVALUES[ii][2] := " "
CASE cDataType = "D"
::aVALUES[ii][2] := CTOD(" / / ")
CASE cDataType = "P"
::aVALUES[ii][2] := "[NOT_YET_SET]"
CASE cDataType = "Q"
::aVALUES[ii][2] := SPACE( 16 )
CASE cDataType = "S"
IF ::aPROPERTIES[ii][7] != nil
::aVALUES[ii][2] := ::aPROPERTIES[ii][7]
ELSE
::aVALUES[ii][2] := SPACE( ::aPROPERTIES[ii][5] )
ENDIF
CASE cDataType = "T"
::aVALUES[ii][2] := SPACE( 16 )
ENDCASE
NEXT
__ObjSetValueList( oCalling, ::aVALUES )
That just leaves method WriteValues which effectively does the reverse of ReadValues. It looks like:
Code: Select all
METHOD WriteValues( oCalling ) CLASS VIEW
FOR ii = 1 TO ::iProperties
cDataType := ::aPROPERTIES[ii][4]
DO CASE
CASE cDataType = "D"
FieldPut( ::aPROPERTIES[ii][3], DTOS( oSend( oCalling, ::aPROPERTIES[ii][1] ) ) )
OTHERWISE
FieldPut( ::aPROPERTIES[ii][3], oSend( oCalling, ::aPROPERTIES[ii][1] ) )
ENDCASE
NEXT
This is basically operational code so I am convinced this approach will work. I think it may offer some advantages over other approaches.
xProgrammer