![]() |
|
Published 1999-08-10 Printer-friendly version
I'm an inveterate template tinkerer. It's a mixed blessing. By the time I abandoned my CPD 2.1 model file for the Logix Project Manager, there was more logic devoted to branching conditionally around various options than to perform the actual task at hand. By the time CDD 3007 rolled around, I had so much invested in modifying Todd Carlson's templates that I didn't dare switch to newer Clarion templates. But while they lasted these modified tools made me a lot more productive than I would otherwise have been.
I'm still tinkering, but the introduction of OOP and the ABC library has meant that I can now work within the TopSpeed framework rather than around it. Steve Parker's article on control files in the previous issue provides an opportune way to illustrate the convenience and power of OOP/ABC while at the same time extending my toolkit.
What I want to build is a "set and forget" method of handling
control files. As Steve points out, some file structures require a
SET/NEXT approach, others do better with
GET/POINTER. Still others may require other access
methods. I want to spend an absolute minimum of time and effort
incorporating control files into future applications, independent
of the file system.
Steve has enumerated the things our tools need to be able to do:
The following narrative mimics the way I go about building tools for my own use. First, list the things that have to be done; second, establish a general design; third, build a skeleton; fourth, fill in the details. This works for a small shop, but if you're building tools for sale or working in a larger organization you may want to skew this pattern toward heavier documentation of the design in advance of coding.
The ABC FileManager class provides facilities to accomplish five
of these six tasks, so it makes sense to start with that as a
basis. A class based on FileManager can inherit all of
its functionality and minimize the new work that needs to be
done.
MyFileManager CLASS(FileManager)
Fetch PROCEDURE,BYTE,PROC
END
The new method provides a place to add Steve's access logic, but it doesn't include provision for specifying which version of that logic to use. ABC classes are insensitive to driver, but in this case that luxury is not available.
Two possibilities come to mind. First, the fetch algorithm could
accept a flag to designate how access to the one and only record in
the file is to be gained. A byte should be sufficient, since more
than 255 variations on the get-only-record theme are unlikely.
However, if the file driver changes, it could be inconvenient to
chase down every Fetch and alter it. The second
approach, adding a property to the derived file manager, permits
the fetch algorithm to be specified once at the beginning of the
program.
MyFileManager CLASS(FileManager)
FetchOnlyType BYTE
Fetch PROCEDURE,BYTE,PROC
END
The compiler can distinguish between the new Fetch
method and the one specified in the standard ABC FileManager class
because their prototypes can be differentiated by the rules for
procedure overloading. (See pp. 89-90 in the Language Reference
Manual.)
A residence for record fetching logic having been established, the logic itself can be added.
First, set up an EQUATE for each value
MyFileManger.FetchOnlyType can assume:
ITEMIZE,PRE(FetchMethod)
GetByPosition EQUATE
GetNext EQUATE
END
Although these are the only two options at the moment, setting up this structure provides a clean way to add other options in the future.
The new Fetch method, by using the
EQUATEs, becomes self-documenting.
MyFileManager.Fetch PROCEDURE
ReturnValue BYTE,AUTO
CODE
ASSERT(SELF.FetchOnlyType)
SELF.Open
SELF.UseFile
CASE SELF.FetchOnlyType
OF FetchMethod:GetByPosition
! "get by position" code here ...
OF FetchMethod:GetNext
! "get by set/next" code here ...
END
IF ReturnValue
CLEAR(SELF.File)
ReturnValue = SELF.Insert()
END
SELF.Close
RETURN ReturnValue
This "shell" contains everything except the actual logic to access the configuration record if it exists. In adapting the logic from Steve's article, I've made a few assumptions based on my own expected use of the method:
Your programming style may suggest a different set of design assumptions.
The only thing left to do is add appropriate code for accessing
the configuration record. Whatever the method used,
ReturnValue should be set to zero if the fetch is
successful, something else if not. The CASE structure
is the only part of the shell which changes:
CASE SELF.FetchOnlyType
OF FetchMethod:GetByPosition
GET(SELF.File,1)
IF ERRORCODE()
ReturnValue = Level:Notify
ELSE
ReturnValue = Level:Benign
END
OF FetchMethod:GetNext
SET(SELF.File)
ReturnValue = SELF.Next()
END
The new method behaves very much like the standard ABC
Fetch except that it doesn't require
specification of a key, it expects one and only one record in the
file, and, if the file is empty, it adds a cleared record.
A little plagiarism is a wonderful thing. All the information
needed to make making the new class look to Clarion like an ABC
class is sitting in the \LIBSRC directory. First, set up files for
the new class prototypes and methods. Call them something like
MyClasses.inc and MyClasses.clw.
Referring to similar files shipped with Clarion, set up these two
files to match their style.
Here's the general setup of MyClasses.inc:
!ABCIncludeFile
OMIT('__EndOfInclude__',_MyClassesPresent_)
_MyClassesPresent_ EQUATE(1)
INCLUDE('ABFILE.INC'),ONCE
! class prototypes here ...
__EndOfInclude__
The comment (!ABCIncludeFile) tells the IDE that
this code follows the ABC pattern and should be treated as any
other ABC include file. The OMIT structure lets this
file be included anywhere the definitions are needed without fear
of duplicating those definitions. For example, the definitions of
the ABC FileManager are included above so that they
can be used in the definition of the MyFileManager
class.
The new ONCE attribute provides another mechanism
for avoiding duplicate definitions, but that protection depends on
the ONCE attribute appearing in every
INCLUDE. Older code may still require the protection
provided by the OMIT structure, so it's a good
idea to leave it in place.
A little more creative plagiarism provides the general setup of
MyClasses.clw:
MEMBER
_ABCDllMode_ EQUATE(0)
_ABCLinkMode_ EQUATE(1)
MAP
END
INCLUDE('MyClasses.inc')
The MEMBER statement identifies this as a source
module, something the compiler needs to know. The MAP
structure is required, since it causes the compiler to include
prototypes for Clarion language statements and functions. The
INCLUDE of MyClasses.inc makes the new
class definitions available to code in this module and, if the nest
is not too deep (LRM page 95), also includes definitions from
ABFILE.INC.
The two equates, for _ABCDllMode_ and
_ABCLinkMode_, implement the ABC library's method
of determining where and how these methods will be linked and
referenced. These definitions work with attributes in each
CLASS statement which are required and will be added
next.
In the skeleton version of the CLASS prototype,
some necessary attributes were ignored. The full statement should
have been:
MyFileManager CLASS(FileManager),|
TYPE , |
MODULE('MyClasses.clw'), |
LINK('MyClasses.clw',_ABCLinkMode_), |
DLL(_ABCDllMode)
The TYPE attribute identifies this class as a
prototype only. Actual instances of the class will be created
(instantiated) when needed. The MODULE attribute tells
the compiler where to find the code for the class methods. The
LINK attribute tells the compiler to compile and link
these methods if, and only if, _ABCLinkMode_ is True.
The DLL attribute tells the compiler to look for these
methods in another DLL if, and only if,
_ABCDllMode_ is True.
The full code for MyClasses.inc and
MyClasses.clw is available for download at the end of this article.
Having built a new class, the next challenge is to use it. There are at least two likely situations:
The first type of use is easy. Just use the Fetch
method as you would the standard ABC Fetch:
IF NOT Access:MyConfigFile.Fetch ! do something, possibly including ... Access:MyConfigFile.Update END
(This code assumes that nothing goes wrong during the update. You are of course free to be as conservative as the situation warrants.)
The second is even easier. In the ThisWindow.Init
method of a form, before the code stores
GlobalRequest, insert:
IF Access:MyConfigFile.Fetch RETURN Level:Fatal ELSE GlobalRequest = UpdateRecord END
This allows the form to be called directly from a menu and, no matter how it is called, causes it to update the one and only record in our configuration file.
To make this capability available for a particular file, set
Access:MyConfigFile.FetchOnlyType to a value
indicating how the single record should be accessed. This can be
done anytime after the FileManager instances are
initialized and before the Fetch is called.
It would be nice if all of the necessary housekeeping could be wrapped up in a simple package so that implementing a configuration file would require nothing more than, say, adding a word to the user options of that file in the dictionary. Making things that simple is probably overkill, though, since most projects will have only one file of this type.
An application level extension template can do the same thing
cleanly and without the extra processing needed to check every file
in the dictionary for a user option. Given the name of a file, the
template can initialize the corresponding file manager's
FetchOnlyType property and add setup code to any form
procedure which uses the file.
This is as good a time as any to set up a file for home grown
templates. Call it MyTemps.tpl. A single line
identifies the template chain:
#TEMPLATE(MyTemps,'Homegrown Templates'),FAMILY('ABC')
Another line establishes the extension template:
#EXTENSION(ConfigFile,'Implement Configuration
File'),APPLICATION,MULTI
The APPLICATION attribute tells the generator that
this extension is applied at the application level rather than the
procedure level. The MULTI attribute permits more than
one instance of the extension in a single application.
A couple of lines of documentation describing what this template is supposed to do are in order:
#DISPLAY('This extension adds code to handle a specified file')
#DISPLAY('as a configuration file containing one and only one')
#DISPLAY('record. Forms for which this file is the primary file')
#DISPLAY('can be called directly without an intervening browse.')
#DISPLAY(' ')
A prompt allows specification of the file:
#PROMPT('Configuration File:',FILE),%MyConfigFile,REQ
There are at least two ways to specify the access method. The easiest is to add a second prompt (or more accurately, prompt structure):
#PROMPT('Choose Access Method:',OPTION),%MyAccessMethod
#PROMPT('Get by position',RADIO)
#PROMPT('Get next',RADIO)
Another alternative is to have the template select the access
method based on driver. This has the advantage of hiding the access
method from the programmer, thereby reducing the number of things
that have to be remembered when using the template. A good place to
put this selection logic is in an #ATSTART
section:
#ATSTART
#FIX(%File,%MyConfigFile)
#CASE(UPPER(%FileDriver))
#OF('CLARION')
#OROF('DBASE3')
#OROF('DBASE4')
#SET(%MyAccessMethod,'Get by position')
#OF('TOPSPEED')
#SET(%MyAccessMethod,'Get next')
#ELSE
#ERROR('File driver not recognized by ConfigFile extension')
#ENDCASE
#ENDAT
At this point everything necessary to generate code is in place.
First, provide for initialization of
Access:MyConfigFile.FetchOnlyType:
#AT(%ProgramSetup)
#CASE(%MyAccessMethod)
#OF('Get by position')
Access:%MyConfigFile.FetchOnlyType = FetchMethod:GetByPosition
#OF('Get next')
Access:%MyConfigFile.FetchOnlyType = FetchMethod:GetNext
#ENDCASE
#ENDAT
Adding code to the input form is a little trickier. The basic structure is easy:
#AT(%WindowManagerMethodCodeSection,'Init','(),BYTE'),PRIORITY(0) IF Access:MyConfigFile.Fetch RETURN Level:Fatal ELSE GlobalRequest = UpdateRecord END #ENDAT
Specifying PRIORITY(0) puts the code at the very
beginning of the method, which is necessary because
GlobalRequest is internalized very early in the
initialization process.
This code should only be generated if the window is an update
form for MyConfigFile. The template language provides
a WHERE attribute to allow this kind of selection.
With that attribute in place, the #AT statement
becomes:
#AT(%WindowManagerMethodCodeSection,'Init','(),BYTE'), %|
PRIORITY(0), %|
WHERE(%ProcedureCategory = 'Form' %|
AND %Primary = %MyConfigFile)
One of the benefits of writing articles is that you learn things in the process. Until I had to split a template instruction to fit the printed page I was unaware that the template language has a line continuation symbol and that it differs slightly from the continuation symbol used in Clarion code. You'll find it documented on page 503 of the Programmer's Guide.
Unfortunately, when I tried to use this syntax in an example,
the registry rejected the template. You'll find a note which
suggests that this might happen on page 525 of the
Programmer's Guide. So the continuation symbols
(%|) in the above example are there for readablity
only and should not be used in actual code.
The point here is not that the documentation is bad. In fact, it's very good. But the template language has always had more little vagaries and nuances than the Clarion language itself, which makes documentation a daunting task. With both template and Clarion code, I read the documentation, code accordingly, test, and modify until it works. But with templates I don't worry too much if I can't explain in detail why what works, works.
Testing also reveals that %Primary, a built-in
symbol which the documentation suggests should contain the label of
the procedure's primary file, is blank when the
WHERE clause is evaluated. Why? I don't know. But
replacing %Primary with %File, a
multi-valued built-in symbol, works. Why? I don't know.
The three files which define the MyFileManager class and the ConfigFile extension are included in a downloadable ZIP file. Using them is very simple:
You will also need to tell the generator to use MyFileManager instead of FileManager for any configuration files. You can do this either on the Individual File Overrides tab or the Classes tab. The difference will be whether all files use MyFileManager (Classes tab) or just the ones you want handled as configuration files (Individual File Overrides tab).
I usually choose the more general case, expecting to add other functionality to the derived file manager.
In summary, once you have found a solution to a particular coding problem, it makes sense to take the extra step and build tools which implement that solution. Clarion's tool-building facilities are within the abilities of the average programmer, and skill in using those facilities improves rapidly with practice. I hope this example not only proves useful to you, but tempts you to build other tools on your own. You have nothing to lose and a world of productivity to gain.
After this article first appeared, I received an email from Jan Jacob de Maa. He had downloaded the associated ZIP file and was trying to use it. Unfortunately, he was encountering compile errors.
Jan Jacob's experience led to the discovery of three errors in the article, which errors are repeated in the ZIP file:
MyFileManager class, an
underscore is missing in the DLL attribute. It should read
DLL(_ABCDllMode_) rather than
DLL(_ABCDllMode). The missing underscore causes the
DLL attribute to remain off when it should be on,
wreaking havoc when compiling and running in 32-bit mode.IF
Access:MyConfigFile.Fetch is missing the parentheses
necessary to tell the compiler that this is a function rather than
a variable. It should read IF
Access:MyConfigFile.Fetch().ThisWindow.Init causes the
wrapper to execute its Fetch operation before the file
is open. Changing the priority to 8000 places the code
correctly. Also, because at this new position
GlobalRequest has already been internalized,
GlobalRequest = ChangeRecord has been changed to
SELF.Request = ChangeRecord.Download the updated source code
Nik Johnson stumbled into the programming racket in 1959 when his boss at Grumman Aircraft insisted on his attending a Fortran class. Since 1986 he has been using Clarion to help clients solve information handling problems.
|
Posted on Sunday, March 20, 2005 by Murray Oldfield This did not work with Clarion 6.1
Posted on Monday, March 21, 2005 by Nik Johnson I suspect that it didn't work in earlier versions either.
Posted on Monday, March 21, 2005 by Nik Johnson I suspect that it didn't work in earlier versions either.
|
To add a comment to this article you must log in.
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $189
(includes all back issues since '99)
Renewals from $139
Two years: $289
Renewals from $239