![]() |
|
Published 1998-02-01 Printer-friendly version
| Publisher's Note |
| At several points in this article, the code is much too long to fit in a normal width document. We've used continuation characters ( the rule character | ) to show where the code is continued. This code will not work as shown in this article. You should use the enclosed templates instead. We hope this message helps eliminate the confusion that could be caused. |
A few months back I wrote an article on accessing custom views in your hand-code, with the help of an extension template. You could specify the view in much the same way as you would for a Report or Process procedure. First you specified the primary file and any secondary files in the File schematic window. Then you declared the range, filter, ordering options, and various other settings.
The problem was that it was designed to work with the old 2.003 template chain. Now that Clarion 4 has arrived, it's time to move it over. This is not as easy as it might seem. Of all the new techniques of the new ABC templates, the most different is the handling of Views. Rather than dealing with them directly, Views are handled through the ViewManager. This is an Application Base Class that works in concert with all the other ABCs to pull it off.
As I mentioned in my former article, the API interface into the ViewManager is not extremely simple or memorable. Therefore, it's a perfect situation to utilize a template. We can describe what we want to achieve, and the template translates our request into the necessary API calls.
Now you may be asking why we need such a tool. Have you ever written some code like this?
Pre:KeyField1 = SomeValue CLEAR(Pre:KeyField2, -1) SET(Pre:Key, Pre:Key) LOOP NEXT(File) IF ERRORCODE() THEN BREAK. IF Pre:KeyField1 <> SomeValue THEN BREAK. DO Something END
It's these little bits of hand-code that are the bugbear of all programmers. It's overkill to use a Process procedure, but it's annoying to have to write it by hand. There are also a number of "problems" with the above code.
First, we are accessing the file directly, which is a no-no with ABC. Second, we have to know which key to use, along with all the key components. Third, we have to initialize any range values, and clear unneeded key components with either high or low values. Fourth, we must check the filter constraints ourselves. Not only is this requirement for intimate knowledge of the database onerous, if the file and/or key structure ever changes, it could easily break our code. Using my mhView template with 2003, this code is much simpler and more generic:
DO OpenMyView LOOP NEXT(MyView) IF ERRORCODE() THEN BREAK. DO Something END DO CloseMyView
Now changes to the database structure are much less likely to affect the workings of our code. With the object-oriented nature of ABC, I decided to take the plunge and go OOP. Now the code will look like this:
Manage:MyView.Open LOOP WHILE Manage:MyView.Next() = LEVEL:Benign DO Something END Manage:MyView.Close
We've managed to eliminate two more lines of code, and we've gained all the benefits of the ABC infrastructure.
Well, if we've removed all of the complexity from our code, that work must be done somewhere. Much of it is handled by the VIEW structure itself (like automatic key selection and filtering). The old template handled creation of the VIEW structure as well as all accesses to it. Using the ViewManager under ABC, the template must tell it what to do, so that the ViewManager can perform its job to our specifications.
This is actually a good exercise to help us to understand the new Clarion 4 with its ABC persona. Our template will be borrowing elements from both the base classes and the ABC templates. We must have some understanding of how things are done by Clarion, so we can apply those techniques to our own creations.
Here's what our template has to produce:
This may seem like a lot of work, but we can probably salvage a bunch of code from our old template, as well as utilizing #GROUPs in the ABC templates that are supporting Clarion's Process and Report templates. It will take some research and experimentation, but isn't that the fun part of software development?
It looks like most of the template #PROMPTs regarding VIEW declarations are the same as they were in 2003. That means we can utilize much of our old template code for this task. Most of this logic is the same as that in the Process template, so we can use it as a sample. The template derives the majority of the VIEW declaration from the file tree. This produces the VIEW and JOIN lines within the VIEW definition.
The PROJECT lines representing the fields are normally determined by scanning the browse or report format, and adding any optional Hot Fields specified by the developer. Since we are hand coding, this approach would force us to manually declare all of our desired fields as hot fields. To alleviate this tedium, our template assumes that all fields will be accessed. If you have a very complex file schematic with many fields, this could slow down access somewhat. However, I believe that the benefit of increased development speed outweighs this minor drawback.
The hot fields are stored in multi-valued token variables called %QueueField and %QueueFieldAssigment, which we must #DECLARE. In our case, these variables will have the same value. (In fact, they will have different values only in a Browse, where there are corresponding queue fields.) We just add all fields from our files:
#ATSTART
#DECLARE(%QueueField),UNIQUE
#DECLARE(%QueueFieldAssignment,%QueueField)
#INSERT(%AddHotFields, %Primary)
#FOR(%Secondary)
#INSERT(%AddHotFields, %Secondary)
#ENDFOR
#ENDAT
#!
#GROUP(%AddHotFields, %FileParm)
#FIX(%File, %FileParm)
#FOR(%Field)
#ADD(%QueueField, %Field)
#SET(%QueueFieldAssignment, %Field)
#ENDFOR
Next we have to figure out where to generate the view structure. Although there are many local data embeds, the one that exists in all procedure types is %DataSection.
#AT(%DataSection) #INSERT(%ConstructView(ABC)) #ENDAT
Notice that the #INSERT statement explicitly includes the (ABC) parameter, because this #GROUP is not in our template chain.
In this case, it makes sense to derive our class from the ViewManager itself. It means that we have only one object to declare, and one less object name with which to be concerned. We need to modify the way in which the class is initialized (to add our own filter and order settings). Our class definition will look like this:
Manage:%ListView ViewManager Init PROCEDURE END
Because we're using the ViewManager class, we must also let the system know that we need the necessary includes in the module data section. This is done with the following:
#CALL(%AddModuleIncludeFile(ABC),'ViewManager')
First, we should decide which embed to use. In the case of the data, there was a single embed in all procedure types that was suitable for our class declaration. Unfortunately, no such common embed exists for the code section. The sore thumb here is the Source procedure, which contains only the %ProcessedCode embed in its code section. The rest of the procedure types contain the %LocalProcedures embed. Therefore, our #AT statements will look like this:
#AT(%ProcessedCode),WHERE(%ProcedureTemplate = 'Source'),PRIORITY(9000) #INSERT(%Methods) #ENDAT #AT(%LocalProcedures),WHERE(%ProcedureTemplate <> 'Source') #INSERT(%Methods) #ENDAT
We could probably omit the WHERE clause on each of these, as they wouldn't normally collide (they exist only in their respective procedure types). I prefer to include them, though, as it makes my intent obvious. The PRIORITY attribute is used to ensure that our method procedures are written after any code that belongs to the procedure itself. It is only necessary in the Source procedure to cause your methods to be generated after your own Source embeds.
We need to generate only an Init method, as the rest of the functionality is provided by the ViewManager itself. We must call some or all of the ViewManager's Init, AddRange, AddSortOrder, AppendOrder, and AddFilter members. It's not all that hard, though, because we can copy a bunch of template code from the %ProcessViewManager #GROUP. For this to work, it's important to maintain consistency between our #PROMPTs and those of the Process procedure templates. In the case of the Range #PROMPTs, we can directly #INSERT(%RangeLimitOptions(ABC)).
After hacking and slashing, we get the following:
#FOR(%ListViewBoundField)
#FIND(%Field,%ListViewBoundField)
#IF(NOT %FieldFile OR %FieldName)
BIND('%ListViewBoundField',%ListViewBoundField)
#ENDIF
#ENDFOR
SELF.Init(MyView, Relate:%Primary)
#IF(ITEMS(%OrderSegment) AND NOT %UsePrimaryKey)
SELF.AddSortOrder()
#ELSE
SELF.AddSortOrder(%PrimaryKey)
#ENDIF
#FOR(%OrderSegment)
SELF.AppendOrder('%(CHOOSE(%OrderSequence='Ascending', '', '-'))%OrderField')
#ENDFOR
#IF (%RangeField)
#CASE (%RangeLimitType)
#OF ('Single Value')
SELF.AddRange(%RangeField,%RangeLimit)
#OF ('Range of Values')
SELF.AddRange(%RangeField,%RangeLow,%RangeHigh)
#OF ('File Relationship')
SELF.AddRange(%RangeField,Relate:%Primary,Relate:%RangeFile)
#OF ('Current Value')
SELF.AddRange(%RangeField)
#ENDCASE
#ENDIF
#IF (%RecordFilter)
SELF.SetFilter('%'RecordFilter')
#ENDIF
#SET(%ValueConstruct, '0')
#FOR(%FieldPairs)
#SET(%ValueConstruct, (%ValueConstruct + 1))
SELF.SetFilter('%LField %'Operator %RField', |
'%(CHOOSE(~%FilterAfter,'6','4')) |
%(FORMAT(%ValueConstruct, @N02))')
#ENDFOR
SELF.Reset
BIND
First we must BIND any variables that are not part of our file structures, so that they can be used in the Filter and Order expressions.
Init
The call to Init simply includes the name of our VIEW structure and the RelationManager for our View's primary file. Notice that I've used SELF rather than PARENT. We are able to do this because our new class is a ViewManager. One word of caution: If our new Init member had the same prototype as the Init member of the base class, then we would have to have used PARENT.Init instead.
AddSortOrder
My old mhView template ignored the %PrimaryKey if any custom sort order segments were present. With this new version, I want it to be optional for the custom sort to be alone, or added to the %PrimaryKey. That explains the conditional call to AddSortOrder.
AppendOrder
If any custom order segments actually exist, they are individually passed to AppendOrder. Notice that we don't have to combine them into a string, as this is automatically performed internally by the ViewManager. (We could have if we wanted to, but why make the template more complex than it needs to be.)
AddRange
If any range settings are specified, they are accommodated by the various possible calls to AddRange. The range settings are actually converted to high-priority filter items by the ViewManager. This method just makes it easier to specify the options to the ViewManager.
SetFilter
Any custom filter is initialized with SetFilter. If we have additional field comparisons, they are added with aSetFilter('Expression', 'p nn'), where "p" is either 4 or 6 (depending on one of the prompts, and "nn" is an auto incremented number. These are automatically added to the filter expression after the first call to SetFilter (which uses "5 Standard" as its default parameter).
You may recall that the ViewManager is able to support multiple SortOrders. Each of these sort orders can have one or more filter segments. This is very handy for tabbed Browses, but it is not needed for our much simpler mhView template.
Reset
Finally, this prepares the View for reading the first record. If you need to re-read your View from the start, you would issue the commandManage:MyView.Reset.
Whenever you override a class, you must be sure to call any overridden parent methods, where appropriate. This is especially true in the case of the Init and Kill methods. If the base class is not properly initialized, then the derived class will probably not work. When initializing, you will normally call the base Init method before anything else. The opposite applies to the PARENT.Kill method, which will normally be called after you've processed your own code.
In most procedures, we can call our Init method in the same place as other classes, in the embeds designed for this purpose. In the case of the Source procedure, however, this is not quite as simple. As I mentioned earlier, there is only one embed in the code section.
A similar problem applies to the opening and closing of files. Normally we can ask for the existing template infrastructure to help us. This is done with the following code:
#AT(%CustomGlobalDeclarations) #INSERT(%FileControlSetFlags(ABC)) #ENDAT #ATSTART #INSERT(%FileControlInitialize(ABC)) #ENDAT
At least this gets the file noticed for global code generation. Without the necessary support in the Source procedure, we have to do a little more work. The easiest approach is to add an extension template to generate OpenFiles and CloseFiles routines that we can call manually.
The mhSourceFiles template is handy for all Source procedures, with or without the View template. I'll discuss this template a little later in the article. For now, we'll have our template generate the calls to the Init and Kill procedures in the "After Opening Files" and "Before Closing Files" embeds.
For all the other procedure types, we'll use the regular embeds. All of the others are based upon the WindowManager class, so we can probably use the embeds in the Init and Kill methods for this class. To determine the best location, I create a sample APP, then go into "Source" view for a sample window. Then I search for ".Init" to find out where the other Init calls are being generated. Once I find the "Description" of the embed (WindowManager Method Data Section), I can search for this description in the AB*.TPW files for the actual name of the embed. The #EMBED line looks like this:
#EMBED(%WindowManagerMethodCodeSection,|
'WindowManager Method Executable Code Section'),|
%pClassMethod,|
%pClassMethodPrototype,|
WHERE(%MethodEmbedPointValid()),|
PREPARE(%FixClassName(%FixBaseClassToUse('Default')))
This is rather scary looking. The important things to notice are the name (%WindowManagerMethodCodeSection) and that the #INSERT command will take two parameters (%pClassMethod and %pClassMethodPrototype). To get help with this, we'll search the AB*.TPW for "#INSERT(%WindowMangerMethodCodeSection".
Once we figure out how to use it, our template section to handle all procedure types becomes:
#AT(%WindowManagerMethodCodeSection,'Init','(),BYTE'),|
PRIORITY(7800),WHERE(%ProcedureTemplate <> 'Source')
%ObjectName.Init
#ENDAT
#!
#AT(%AfterFileOpen),WHERE(%ProcedureTemplate = 'Source')
%ObjectName.Init
#ENDAT
#!
#AT(%WindowManagerMethodCodeSection,'Kill','(),BYTE'),
PRIORITY(2500),WHERE(%ProcedureTemplate <> 'Source')
%ObjectName.Kill
#ENDAT
#!
#AT(%BeforeFileClose),WHERE(%ProcedureTemplate = 'Source')
%ObjectName.Kill
#ENDAT
The PRIORITY attribute will require some experimentation on your part before you fully understand its workings. Suffice to say that when you are inserting code into a particular embed, your priority settings control the order of the insertions from the various sources. When adding embeds in the IDE, you can control the PRIORITY directly. With templates, you use the PRIORITY attribute on the #AT line. If you're not sure what the priority should be, view the procedure in "Source" mode to get a better idea. If you're still not sure, try a few different settings to see where the code ends up.
This is a handy template that adds support for opening and closing files in a Source procedure. It generates two routines, OpenFiles and CloseFiles, at the bottom of your code. You must call these manually, as the template cannot predict what you have entered in the %ProcessedCode embed. Therefore, your source might look like this:
DO OpenFiles Manage:MyView.Open LOOP WHILE Manage:MyView.Next() = LEVEL:Benign Count# += 1 END Manage:MyView.Close DO CloseFiles RETURN(Count#)
The template looks like this:
#EXTENSION(mhSourceFiles,'mh Add Open+CloseFiles to Source Procedure'),|
DESCRIPTION('mh Add Open+CloseFiles')
#LOCALDATA
FilesOpened LONG
#ENDLOCALDATA
#BOXED('mh Source Files')
#DISPLAY('This will add two routines called OpenFiles and CloseFiles to your ' & |
'Source procedure. They also call PUSHBIND and POPBIND.'),AT(,,172,28)
#DISPLAY('*** You must call these routines yourself! ***')
#ENDBOXED
#!----------
#AT(%CustomGlobalDeclarations)
#INSERT(%FileControlSetFlags(ABC))
#ENDAT
#!----------
#ATSTART
#INSERT(%FileControlInitialize(ABC))
#ENDAT
#!----------
#AT(%ProcessedCode),PRIORITY(8000)
#INSERT(%OpenFiles)
#INSERT(%CloseFiles)
#ENDAT
#!**********
#GROUP(%OpenFiles)
!--------------------------------------
OpenFiles ROUTINE
PUSHBIND
#EMBED(%BeforeFileOpen,'Beginning of Procedure, Before Opening Files')
#DECLARE(%CumFileOpen),MULTI
#FOR(%ProcFilesUsed)
#IF(~INLIST(%ProcFilesUsed,%CumFileOpen))
#ADD (%CumFileOpen,%ProcFilesUsed)
#INSERT(%AddRelatedFiles(ABC),%CumFileOpen,%ProcFilesUsed)
Relate:%ProcFilesUsed.Open
#ENDIF
#ENDFOR
#FOR(%OtherFiles)
Access:%OtherFiles.UseFile
#ENDFOR
FilesOpened = True
#EMBED(%AfterFileOpen,'Beginning of Procedure, After Opening Files')
#!**********
#GROUP(%CloseFiles)
CloseFiles ROUTINE
#EMBED(%BeforeFileClose,'End of Procedure, Before Closing Files')
IF FilesOpened
#DECLARE(%CumFileOpen),MULTI
#FOR(%ProcFilesUsed)
#IF(~INLIST(%ProcFilesUsed,%CumFileOpen))
#ADD (%CumFileOpen,%ProcFilesUsed)
#INSERT(%AddRelatedFiles(ABC),%CumFileOpen,%ProcFilesUsed)
Relate:%ProcFilesUsed.Close
#ENDIF
#ENDFOR
END!IF
#EMBED(%AfterFileClose,'End of Procedure, After Closing Files')
POPBIND
Notice that it mimics the style of the regular file opening and closing done in other procedures, including the #EMBEDs. This provides a location for the generation of our calls to Init and Kill.
One interesting thing to notice here is the %CumFileOpen variable. To fully understand the implications of this, you must realize something about the %Relate:Filename.Open method. It will open not only the specified file, but also all child files with appropriate relationships with the specified file. It would be redundant to ask to open files that have already been handled by the prior Open statements, so we use the %CumFileOpen multi-valued variable to hold a list of files that have already been opened, either explicitly or implicitly.
While developing templates for 2.003, I usually placed my template and source files in directories separate from Clarion's, to aid in organization. Clarion 4, however, contains various automatic support for files that are placed into Clarion's TEMPLATE and LIBSRC directories. If you decide to use these directories, be sure to maintain a consistent naming style for your files so that they don't get lost in the shuffle. My public domain files for C4+ABC are all prefixed with "MHAB". A couple of examples of the benefits that I've discovered so far:
There is a new template attribute in Clarion 4 called "FAMILY". It allows you to have sets of templates that apply to different types of applications. Depending on the family of your application's template (ABC, Clarion, etc.), you will be able to add only procedure, control, extension and code templates from template chains that specify that they are from the same family. If your template omits the family attribute, it defaults to FAMILY(Clarion).
This can cause some confusion when converting applications. Most templates designed to work with the old 2.003 template chain will not work with C4+ABC. When you convert an application, it changes the template chain on any templates of which it is aware. If you have "unknown" custom or third party templates, it does not change the chain to a compatible template for the new family.
That means you could have a C4+ABC application mixed with 2003+Clarion templates after conversion. (Remember that the family is checked only when the templates are added during development. The converter will handle it, only if it has the necessary conversion rules.)
This means that you must have C4+ABC compatible versions of all of your templates before converting your application. You can maintain developing the APP with the old chain using Clarion 4 until these templates are available.
If some of your templates are from a 3rd party, then you may get extra conversion rules to help you automatically. (By the way, they are very difficult to create.) Otherwise, at the very least you must change the template chain. This requires following these steps:
If you also have source code referencing entities of your template that have a new name (like our "DO OpenMyView" changed to "Manage:MyView.Open"), then you must edit these manually after conversion. It may be possible for you to do a bulk search and replace in the TXA before importing it into the new APP.
Yes, conversion is a tedious job, but remember that it has to be done only once for the APP.
When you are developing this type of template, you must always ask the following questions:
Always keep in mind that you want to take a generic approach, whenever this is feasible. In most cases you won't have the foresight to include all possible options. When new requirements make themselves apparent, though, try to extend an existing template, rather than develop a new (almost identical) one from scratch. This approach will give you a robust maintainable toolbox.
Well, now you have a couple of new templates for your development arsenal. Hopefully you have also learned a little more about how the AB classes and templates approach things.
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: $184
(includes all back issues since '99)
Renewals from $134
Two years: $274
Renewals from $224