Many-to-many checkboxes revisited, part 12: The template

by Unknown user

Read Part 11

I've written a few templates over the years. Not as many as template mavens like Mike Hanson and Lee White, but I've touched just about every aspect of the template language at one time or another. And more often than not I find myself peering into a dark place somewhere in the bowels of the template engine, wondering what gyrations I need to go through to get the AppGen to spit out the code I want. That's not because the template system is bad, it's just very powerful and very complex. 

This complexity is one of the reasons I rarely start with a template. I almost always write code to solve the problem, and then figure out how to get the AppGen to generate the exact same code. Debugging source code can be tricky; debugging template code can be trickier; doing both at the same time is just plain dumb. 

I had already created a browse procedure with embedded source that worked. In preparation for the template I made a copy of that procedure and the ripped out all of the embedded source. 

I decided the simplest way to implement the template was as an extension (although down the road a control template would be a nice addition).

Here's the declaration, the very beginning of the extension template:

#Extension(ManyToManyCheckboxForABCBrowse,'Many-to-many checkboxes for an ABC browse'),REQ(BrowseBox(ABC)),PROCEDURE

I'm a huge fan of Scooter Software's Beyond Compare, and have been for years. As I almost always generate each procedure into its own source module, it only took a moment to set up a comparison of the source for the two procedures. I then added the extension and began tweaking the template and comparing the source until everything matched up.

There are two important attributes on this extension: it's a PROCEDURE level extension, and it also needs to be attached to an instance of the ABC BrowseBox template. Originally I also had the WINDOW attribute on the extension, out of habit, but it seemed redundant since there would have to be a window in order to populate the browse box. Something made me check the help for #EXTENSION and I was surprised to see that the WINDOW attribute isn't even documented. 

Because the extension requires the ABC BrowseBox template, you have to be on the extensions tab and select a browse box before you can add this extension.

The prompts

The extension has the following prompts. If you've read the previous installments you'll be familiar with the "left" and "right" terminology for the browses involved. There's also a checkbox for when the left side is a browse (the default); I'll get to that in a little bit. 

First up is the data section declaration of the local class that does most of the work:

#At(%DataSection),Priority(3100)
%[20]M2MClassInstanceName class
ListCheckbox           &CML_UI_ListCheckbox
Persister              &CML_Data_ManyToManyLinksPersisterForABC
Links                  &CML_Data_ManyToManyLinks
Construct              procedure
Destruct               procedure
DisplayCheckboxData    procedure
Init                   procedure
LoadEnrollmentData     procedure
SaveEnrollmentData     procedure
SetCheckboxIcon        procedure
                     end                      
#EndAt

I've used the %[offset] technique that pads the contents of %M2MClassInstanceName to 25 characters so my class declaration formats nicely. I've even kowtowed to Clarion's annoying and cloying two space indents so my code aligns with the generated code. Nice, eh?

The next #At section embeds code in the window manager's Init method:

#At(%WindowManagerMethodCodeSection,'Init','(),BYTE'),PRIORITY(8005),DESCRIPTION('Initialize M2MCheckboxList')
%M2MClassInstanceName.Init()
    #If(not %LeftFileIsInBrowse)
%M2MClassInstanceName.LoadEnrollmentData()  
    #EndIf
#EndAt

This is where that checkbox comes into play. Greg Fasolt, who has been alpha testing the classes and the template (and who set me on this journey last fall) told me he had a number of forms where he wanted to use this template. In those situations there is no left side browse, which actually makes things a bit simpler. The "left" side primary key value is already in memory so there's no need to reload and redisplay the list box while it's being displayed.

On exiting the procedure (whether a browse or a form) any remaining changes are saved:

#At(%WindowManagerMethodCodeSection,'Kill','(),BYTE'),PRIORITY(1100),DESCRIPTION('Initialize M2MCheckboxList')
%M2MClassInstanceName.SaveEnrollmentData()
#EndAt

This raises an issue for further development: depending on how the class and template are used it may be better to ask whether the changes should be saved, or in the case of a form the saving of changes should probably be aligned with the form action. 

The next block of template code (potentially) refers to both the left and right browses:

#AT(%BrowserMethodCodeSection,,'TakeNewSelection','()'),PRIORITY(9000)
    #If(%LeftFileIsInBrowse)
        #If(%ControlInstance = %LeftBrowseControlInstance)
%M2MClassInstanceName.LoadEnrollmentData()  
        #EndIf
    #EndIf
    #If(%ControlInstance = %RightBrowseControlInstance)
%M2MClassInstanceName.DisplayCheckboxData()
    #EndIf
#EndAt

This is the bit that caused me a lot of grief. This template that is attached to one browse control via the REQ attribute, and it needs to generate code into another browse control. First I tried all kinds of crazy ways to get at the context of the other browse, none of which were successful. I had actually been down a similar road years before when I wrote a template chain to generate ASP.NET MVC code from Clarion, but it took a while and a few conversations with Mike Hanson before I understood/realized/remembered the answer. 

Pay attention, because this is really good stuff.

#AT statements have the ability to progressively filter the possible locations where they can generate code. For instance, the very next #AT statement in the template looks like this:

#AT(%BrowserMethodCodeSection,%ActiveTemplateParentInstance,'SetQueueRecord','()'),PRIORITY(9500)
#! #AT(%BrowserMethodCodeSection,,'SetQueueRecord','()'),PRIORITY(9500)
%M2MClassInstanceName.SetCheckboxIcon()
#EndAt

Because my extension template instance is a child of the browse template instance, by specifying %ActiveTemplateParentInstance as the second parameter I can restrict generation to just that browse, and by further specifying the procedure name and the prototype list (empty in this case) I can say I want to generate the code into a specific method. The number of parameters on the #AT depends on the number of parameters in the corresponding #EMBED statement, which in this case is:

#EMBED(%BrowserMethodCodeSection,'Browser Method Code Section'),%ActiveTemplateInstance,%pClassMethod,%plassMethodPrototype,PREPARE(,%FixClassName(%FixBaseClassToUse('Default'))),TREE(%TreeText & %CodeText)

The help points out that the instance values are omittable:

Because there's just one #Embed statement for browser method code sections I really can generate the source for one browse from a template attached to another browse, as long as I can identify that other browse from my template. 

The first step was to identify the two browse instances, and Mike provided me with some code that did the trick. 

I have this code in my #AtStart section:

#AtStart
    #Declare(%LeftBrowseControlInstance)
    #Declare(%RightBrowseControlInstance)
    #Declare(%LeftBrowseControlName)
    #Declare(%RightBrowseControlName)
    #Set(%RightBrowseControlInstance,%GetTemplateInstanceForControl(%RightBrowseControl))
    #Set(%LeftBrowseControlInstance,%GetTemplateInstanceForControl(%LeftBrowseControl))
    #If(%LeftFileIsInBrowse)
        #Set(%LeftBrowseControlName,%GetBrowseManagerName(%LeftBrowseControl))
    #EndIf
    #Set(%RightBrowseControlName,%GetBrowseManagerName(%RightBrowseControl))   
#EndAt

There are two template "function" calls, one to %GetTemplateInstanceForControl and one for %GetBrowseManagerName:

#!----------------------------------------------------------------------------
#GROUP(%GetTemplateInstanceForControl,%pControl),PRESERVE
    #FIX(%Control, %pControl)
    #Return(%ControlInstance)
#!----------------------------------------------------------------------------
#GROUP(%GetBrowseManagerName,%pControl),PRESERVE
    #CONTEXT(%Procedure, %GetTemplateInstanceForControl(%pControl))
        #RETURN(%ManagerName)
    #ENDCONTEXT
#!----------------------------------------------------------------------------

This code provides me with two vital bits of information for the right and when needed the left browses: the control instance so I can generate the code when appropriate, and the name of the left browse's manager class instance. 

Here again is that block of template code that can generate into both browses. Because I've omitted the template instance parameter, the block will generate for all browses. Unfortunately it's not possible to put a filter outside of the #AT block, but I can add the filtering I need inside the #AT block, so that when the block is generated for a specific browse I include just the code I need. 

This is a very useful technique; if you write a lot of templates you either have come across it or you will come across it. 

#AT(%BrowserMethodCodeSection,,'TakeNewSelection','()'),PRIORITY(9000)
    #If(%LeftFileIsInBrowse)
        #If(%ControlInstance = %LeftBrowseControlInstance)
%M2MClassInstanceName.LoadEnrollmentData()  
        #EndIf
    #EndIf
    #If(%ControlInstance = %RightBrowseControlInstance)
%M2MClassInstanceName.DisplayCheckboxData()
    #EndIf
#EndAt

The remainder of the template code is a walk in the park. It simply uses values from the prompts along with the previously discovered browse manager class instance name to fill in all of the class's methods:

#At(%LocalProcedures),Priority(9999)
%M2MClassInstanceName.Construct                           procedure
    code
    self.ListCheckbox &= new CML_UI_ListCheckbox
    self.Persister    &= new CML_Data_ManyToManyLinksPersisterForABC
    self.Links        &= new CML_Data_ManyToManyLinks
    
%M2MClassInstanceName.Destruct                            procedure
    code
    dispose(self.ListCheckbox)
    dispose(self.Persister)
    dispose(self.Links)
    
%M2MClassInstanceName.DisplayCheckboxData                 procedure
    code
    self.ListCheckbox.LoadDisplayableCheckboxData()      
    
%M2MClassInstanceName.Init                                procedure
    code
    self.Persister.Init(Access:%LinkingFile,%LinkingFileKey,|
        %LinkingFileLeftField,%LinkingFileRightField)
    self.Links.SetPersister(self.Persister)
    self.ListCheckbox.Initialize(%ListQueue,%ListQueue.%(%IconField & '_Icon'),|
        %ListQueue.%RightFileUniqueField,%RightBrowseControl,,self.Links)
    
%M2MClassInstanceName.LoadEnrollmentData                  procedure
    code
    #If(%LeftFileIsInBrowse)
    self.SaveEnrollmentData()
    %LeftBrowseControlName.UpdateBuffer()
    #EndIf
    self.links.LeftRecordID = %LeftFileUniqueField
    self.Links.LoadAllLinkingData()
    self.ListCheckbox.LoadDisplayableCheckboxData()    
    display(%RightBrowseControl)
    
%M2MClassInstanceName.SaveEnrollmentData                  procedure
    code
    self.Links.SaveAllLinkingData()
    
%M2MClassInstanceName.SetCheckboxIcon                    procedure
    code
    if self.links.IsLinkBetween(self.Links.LeftRecordID,self.ListCheckbox.ListQRightRecordID)
        self.ListCheckbox.ListQIconField = CML_UI_ListCheckbox_TrueValue
    else
        self.ListCheckbox.ListQIconField = CML_UI_ListCheckbox_FalseValue
    end
#EndAt

That's it for the template. It was quite a bit more work than just writing the embed code, so if I only had one procedure that needed it I never would have bothered. The payoff is that it's now so much easier to implement this functionality on another browse or form. 

Both the template and the source can still be improved. The template could pre-fill the primary key values from the file metadata, and the generated class source could use some tightening. And maybe Greg or another reader will find a bug or an obvious feature lack I've missed. But this is a good start. 

Summary

It's been a long road from that first question Greg Fasolt raised about the original class and template. I had the most fun with the classes and the unit tests, and lost the most hair with the template.

This series isn't a perfect analogue for my idealized Clarion development cycle, but it hits a lot of the high points:

  • Test-driven development (TDD): If there's only one thing you take away from these articles, it's the idea that you start with a test. Creating tests first has so many benefits and will drive much of your development in the right direction.
  • Classes: You don't need to develop with classes, but there's a good reason software development is so heavily invested in object-oriented development.
  • Extract business logic into reusable code: Again, much easier with classes.
  • Working code before templates: In fact I don't write that many templates because for simple situations they're not necessary and for complex situation I need a lot of reuse to justify the investment. But when a template is called for it's essential to have working hand code in place for comparison purposes. 
  • Refactor, refactor, refactor: This is an endless process, because almost all code can and usually should be improved in some way. 

And speaking of code improvements, there are still some loose ends to tie up. More to come!

You can get the latest version of the classes and templates at the ClarionMag GitHub repository.