Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
by Unlicensed user

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. 

...

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 '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 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:

Code Block
#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. 

...

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

This is one the part of two parts of the template that caused me a lot of grief. I have a 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 found understood/realized/remembered the answer. 

#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:

...

The help points out that the instance values are omittable:

But 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:

Code Block
#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:

Code Block
#!----------------------------------------------------------------------------
#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 needed, 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. 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 or you will come across it. 

Code Block
#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:

Code Block
#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. 

It's been a long road from that first question Greg Fasolt raised about the original class and template.