Procedure #EXTENSION won't generate into a browse's ResetQueue method — %ActiveTemplateInstance doesn't resolve to the browse?

Clarion 11.1, ABC. I’m writing a procedure #EXTENSION template that should inject a few lines into a browse’s ResetQueue method (to DISABLE/ENABLE the Insert button based on a record count). The template registers fine, installs on the procedure fine, prompts accept values fine — but it generates NOTHING into the browse method. No error; the code simply never appears.

Here is the embed section:

#AT(%BrowserMethodCodeSection, %ActiveTemplateInstance, 'ResetQueue', '(BYTE ResetMode)'), PRIORITY(5000)
#IF(%UseEditionGuard)
IF %EditionGuard
IF RECORDS(%BrowseClass) >= %RecordLimit
DISABLE(%InsertControl)
%BrowseObject.InsertControl = False
ELSE
%BrowseObject.InsertControl = %InsertControl
ENABLE(%InsertControl)
END
DISPLAY(%InsertControl)
END
#ELSE
IF RECORDS(%BrowseClass) >= %RecordLimit
DISABLE(%InsertControl)
%BrowseObject.InsertControl = False
ELSE
%BrowseObject.InsertControl = %InsertControl
ENABLE(%InsertControl)
END
DISPLAY(%InsertControl)
#ENDIF
#ENDAT

The embed code: BRW1.ResetQueue PRIORITY 2500

IF Glo:Edition = 1
  IF RECORDS(Queue:Browse) >= 10
    DISABLE(?Insert)
  ELSE
    ENABLE(?Insert)
  END
END

What I’ve already verified:

  1. The embed point exists and is reachable. If I hand-type “!here” directly into the BRW1.ResetQueue embed in the Embeditor, it generates into the .clw correctly, and the method BRW1.ResetQueue PROCEDURE(BYTE ResetMode) appears.

  2. The signature is exact. I used the EmbedInfo / “create #AT from embed” template inside that same ResetQueue embed, and it printed:

!#AT(%BrowserMethodCodeSection,%ActiveTemplateInstance,'ResetQueue','(BYTE ResetMode)'),PRIORITY(5000)

which matches my template’s #AT character-for-character.

  1. It’s not a caching/stale-generation issue — a fresh app, a brand-new simple browse (single sort order, ResetQueue present in the generated class), same result: nothing generates from the extension.

My working theory: because this is a procedure #EXTENSION, %ActiveTemplateInstance refers to the EXTENSION’s own instance, not the browse (BRW1) instance — so the #AT has no browse method to match and silently generates nothing. The EmbedInfo output showing that signature was presumably generated in the context of the BrowseBox control template, where %ActiveTemplateInstance DOES mean the browse.
My questions:

Q1. Is that theory correct — that a procedure #EXTENSION cannot use %ActiveTemplateInstance to target a specific browse’s method?

Q2. If so, what is the correct way, FROM A PROCEDURE #EXTENSION, to embed code into a specific browse’s ResetQueue method? Is there a symbol for the browse instance I should use instead, or do I need to #FOR/loop over the browse control instances (e.g. over %Control or a browse-instance symbol) and reference that instance in the #AT?

Q3. If a procedure #EXTENSION genuinely can’t do this cleanly, is the accepted approach to make it a BrowseBox #CONTROL template instead? If so, how is such a control template ATTACHED to an existing browse list control in the app (the step-by-step in the IDE), since it isn’t a visual control I’d drop on the window?

Context on why an extension is preferred: I want to add this limiter to ~12 browse procedures across an app (and reuse across other apps), toggling behavior via a single global (Glo:Edition). Hand-placing works but I’d like the template to manage placement. Whichever mechanism is correct (extension with the right instance reference, or control template), a short example of the working #AT line plus how it’s installed would be hugely appreciated.
DataLimiter.tpl (3.3 KB)

Thanks in advance.

Try changing the #AT with the following, which I found in some of the shipping 3rd party templates.

#AT(%BrowserMethodCodeSection, %ActiveTemplateInstance, ‘ResetQueue’, ‘(BYTE ResetMode)’), PRIORITY(5000)
  #FOR(%ActiveTemplate),WHERE(%ActiveTemplate='BrowseBox(ABC)')
    #FOR(%ActiveTemplateInstance)
      #CONTEXT(%Procedure,%ActiveTemplateInstance)
#!....Your code here
      #ENDCONTEXT
    #ENDFOR
  #ENDFOR
#ENDAT

It’s not something I have tried, but might get it to work.

Mark

As a follow up, in case this is a problem for you:

Your code is checking the record limit using the browse queue, that will have a problem if you are page loading the browse, as records(queue) will not give you a full count of the records in the table.

If your template is a child of the browse template, then you need to use %ActiveTemplateParentInstance

SOLVED - thank you Mark and Rick. Posting the working solution in case it helps someone else.

The core problem: from a procedure #EXTENSION, I was trying to use %ActiveTemplateInstance directly as the instance argument in #AT(%BrowserMethodCodeSection, …). In an extension context %ActiveTemplateInstance came back blank, so the #AT had no instance to target and generated nothing (silently).

Two things fixed it:

  1. Get the browse instance with the INSTANCE() function, not %ActiveTemplateInstance. Loop %ActiveTemplate in #ATSTART, and when the template is the BrowseBox, capture INSTANCE(%ActiveTemplate) into a declared symbol. Then use that captured symbol as the instance argument on a single #AT (the #AT cannot be inside the #FOR - Clarion throws “#ENDFOR expected”).

  2. #Priority to place the code before the parent call. PRIORITY(2500) puts it ahead of PARENT.ResetQueue; 5000 landed after it.

Working extension (trimmed to the essentials):

#EXTENSION(LimitBrowseRecords, 'Cap browse records'), PROCEDURE
#PROMPT('Table (file) to count:', @S64), %LimitTable, REQ
#PROMPT('Records:', @S8), %RecordLimit, DEFAULT('10'), REQ
#PROMPT('Insert control:', CONTROL), %InsertControl, DEFAULT('?Insert'), REQ
#!
#ATSTART
  #DECLARE(%LimitBrowseInst)
  #FOR(%ActiveTemplate)
    #IF(INSTRING('BrowseBox', %ActiveTemplate, 1, 1))
      #SET(%LimitBrowseInst, INSTANCE(%ActiveTemplate))
    #ENDIF
  #ENDFOR
#ENDAT
#!
#AT(%BrowserMethodCodeSection, %LimitBrowseInst, 'ResetQueue', '(BYTE ResetMode)'), PRIORITY(2500)
IF RECORDS(%LimitTable) >= %RecordLimit
   DISABLE(%InsertControl)
ELSE
   ENABLE(%InsertControl)
END
#ENDAT

A couple of notes for anyone following:

  • %Primary did not resolve from the extension context (came back blank), so I made the table name a prompt rather than trying to derive it. If someone knows the correct symbol to get a browse’s primary file from an extension, I’d be glad to hear it.

  • On the record count itself: as Mark pointed out, RECORDS(queue) is unreliable with page-loaded browses, so I count the file with RECORDS(tablename) instead. That gives the true record count for an edition-gating limit.

  • Debugging tip that finally cracked it: I had the template write diagnostic comments into the generated .clw (into %DataSection) dumping each %ActiveTemplate and its INSTANCE() value. That showed me the instance was blank, which pointed straight at the fix. Being able to SEE what the template was resolving, rather than guessing, was the turning point.

Thanks again - you got me started on the right path.

#IF( %Primary )

returns the name of the %File so can be treated like a %True or %False in the #If() statement.

From the help docs.

The key to understanding the use of the #CONTEXT structure is “as if the source code for the named section were being generated.” This statement means that the statements are evaluated as if #GENERATE were executing.

In the context of the Instance/named section, this isnt strictly true in that only a part of #Procedure will run, the whole #Procedure template still runs, so its easy to get caught in loops, trigger #Restrict/#EndRestrict & #Prepare/#EndPrepare code if not careful.


Create a #Code template which lists the builtin variables and their current value on whatever embed the #code template is placed as seen here.

This website is saying I’m editing this post in another tab when I’m not!

I would think you want Prompt Type to be FILE and a Number @n8 not String @s8:

#PROMPT(‘Table to count:’,  FILE , %LimitTable, REQ
#PROMPT(‘Records:’, @n8), %RecordLimit, DEFAULT(‘10’), REQ

I prefer not to disable controls and leave a mystery. Also you may be fighting other templates doing the same enable/disable.

I would rather insert code when they try to insert:

IF RECORDS(%LimitTable) >= %RecordLimit
   Message('Record Limit is %recordlimit') 
   Return Level:xxx

Carl,

Good catch on the prompt types - fixed both. Table to count is FILE and Records is @n8 now, matching what you suggested:

#PROMPT('Table to count:', FILE), %LimitTable, REQ
#PROMPT('Record limit:', @n8), %RecordLimit, DEFAULT(5), REQ

On disabling vs. intercepting the Insert - agreed completely, and that’s actually where I ended up landing, for exactly the reason you gave. Instead of touching the control’s enabled state at all, I intercept at the point they try to insert, similar to what you sketched:

IF Glo:Edition = 1
  IF RECORDS(%LimitTable) >= %RecordLimit
    Message('%LimitMessage','%LimitTitle')
    SELECT(%ListControl)
    CYCLE
  END
END

One thing I’d like your take on: you wrote it as Message + RETURN Level:xxx, and I went with Message + SELECT(?List) + CYCLE instead - putting focus back on the list and looping the accept cycle rather than returning a level code out of TakeAccepted. Is there a reason you’d prefer the RETURN Level:xxx form over CYCLE here?

The CYCLE is better in .TakeAccepted() as more expected code. Placed in other places like .Ask(BYTE Request) you must Return Level.


If you look at the top of .TakeAccepted() you’ll see how it detects a CYCLE and then does a RETURN Level:Notify. This was so code moved from a Legacy ACCEPT Loop worked the same.

ThisWindow.TakeAccepted PROCEDURE
ReturnValue          BYTE,AUTO
Looped BYTE
  CODE              ! This method receive all EVENT:Accepted's
  LOOP              ! FYI ... LOOP not ACCEPT     
    IF Looped
      RETURN Level:Notify   !<--- CYCLE does this 
    ELSE
      Looped = 1
    END

I went ahead and tried building a Global Extension to share some settings across a few PROCEDURE-scope extensions, and hit a wall.

Setup: an APPLICATION-scope extension holding a few shared values via #PROMPT (product name, a purchase URL, a couple of button captions), and a PROCEDURE-scope extension meant to read them via #ALIAS. I based it on the pattern in the Template Language Reference:

#EXTENSION(GlobalSecurity,'Global Password Check'),APPLICATION
  #DECLARE(%PasswordFile)
  #DECLARE(%PasswordFileKey)

#EXTENSION(LocalSecurity,'Local Procedure Password Check'),PROCEDURE
  #ALIAS(%PswdFile,%PasswordFile,%ControlInstance)
  #ALIAS(%PswdFileKey,%PasswordFileKey,%ControlInstance)

Mine looks like:

#EXTENSION(MyGlobalSettings,'Shared Settings (install once)'),APPLICATION
#PROMPT('Purchase URL:',@s255),%GG_PurchaseURL,DEFAULT('https://example.com'),REQ

#EXTENSION(MyLocalExtension,'...'),PROCEDURE
#FIX(%ApplicationTemplate,'MyGlobalSettings')
#ALIAS(%LocalURL,%GG_PurchaseURL,%ApplicationTemplateInstance)

Registers clean, the Global Extension’s own dialog shows and saves values fine. But generating a procedure with the local extension throws:

GEN: Unknown Variable ‘%LocalURL’

for every aliased symbol - like the #ALIAS never actually bound.

Three things I’m unsure of:

  1. Does #ALIAS work against a #PROMPT-declared symbol, or only #DECLARE’d ones? The reference example only shows #DECLARE, so I can’t tell if that’s a real requirement or just how they happened to write the example.

  2. Is #FIX(%ApplicationTemplate,‘name’) the right way to locate a specific Global Extension instance before reading %ApplicationTemplateInstance?

  3. The reference’s instance parameter is %ControlInstance - is that only valid for control-level templates, and is %ApplicationTemplateInstance the wrong substitute for an APPLICATION-to-PROCEDURE reference?

If you’ve got a working example of a Global Extension feeding values into procedure extensions via #ALIAS, I’d love to see it - or if this combination just isn’t supported, happy to hear that too and go a different route.

Thanks again for the help so far.

Have you seen the ABShop.tpl?

In this template #Alias appears to be working inside an #At and even though its used inside a #group, the group is called from within an #At

  1. Shouldnt matter, but #Declare is more sensitive to scoping issues in my experience.
  2. In these situation’s I find it easiest to have a template to generate all the template values and instances to source. If one template can see the data, it make’s its easier to code other templates.
  3. Theres 3 types of variables which can have multiple instances. The Built In template symbol’s, List boxes (#button,inline multi) , & #Declared,Multi.

Can you use #Set(%ProcSymbol, %AppSymbol)?

I’m struggling to see the purpose of #Alias over a #Group with parameters, so I might not be much help.

The #With is an alternative, possibly simpler way to use #Prompts if someone didnt want to use #Button,Inline, multi

Edit.
One other thing to bear in mind is all the built in symbols are read only, if you are looking to edit them. Its a shame because the %Procedure %Prototype is one instance which could be stuffed by external templates better.

Hi Jeff,

Unfortunately there’s a bit of over-thinking going on here, and that’s taking you down rabbit holes :slight_smile: Let’s recap a bit, because I think there are perhaps 2 situations you have in play here, and it’s worth dealing with them individually.

Let’s do the second one first. You have a global extension which sets “global settings” for the template, and then a local extension which has more settings perhaps using the global as a default, or modifying the local or whatever. This is a really common pattern. And it’s straightforward;

First the global extension; This has the ,APPLICATION attribute. Something like;

#TEMPLATE(Draw,'CapeSoft Draw Template - Version:4.36'),family('abc','cw20')
#EXTENSION(GloDraw,'Activate CapeSoft Draw - Version:4.36'),Application
#PROMPT('Disable All Draw Features',Check),%gNoDraw,At(10)

You can then have as many prompts as you like.

then the local extension; This could be a #EXTENSION or a #CONTROL. Something like;

#Control(Draw,'CapeSoft Draw Control'),Multi,WRAP(IMAGE),DESCRIPTION(' [Draw] ' & %ObjectName & ' (' & %BaseClass & ')'),Window
#Prompt('Do Not Generate This Object',check),%NoDraw,at(10)

The magic here is that the global extension is already in scope. You don’t need to #ALIAS or #CONTEXT or anything. The %gNoDraw is just available.
So in the local extension/control code it’s possible to just do;

#AT(%LocalDataClasses),where(%gNoDraw = 0 and %NoDraw =0)

Because the local and global extensions / controls are inside the same #TEMPLATE the global one is just in scope.

The original post you made was also interesting. You wanted to make a #EXTENSION which got attached to the Browse control, so that the browse settings would be in scope. The easiest way to do this is to set the template to expliticle be a descendent of the parent. For example we make a SendTo button which attaches to a Browse. The template is declared like this;

#Control(BrowseSendTo,'SendTo Button for Browses'),WINDOW,Multi,Req(BrowseBox(ABC))

Notice the REQ attribute. This #Control is “attached” to an ABC BrowseBox. This makes it easy to add to the procedure (go to Extensions like, highlight the Browse in question in the list, and click Insert.)
The up side of this is that again, all the parent templates (ie the Browse) template variables are then automatically in scope.

There is absolutly a place for things like #ALIAS and #CONTEXT but usually when accessing either global or local extensions which are not “attached” to each other, and not in the same #TEMPLATE. For example one template may “detect” another template and change behavior accordingly. Like say adapting to different PDF engines or stuff lke that.

Cheers
Bruce

Bruce — thank you, this was the last piece I needed.

I’ve restructured my template accordingly and it’s working end-to-end now — global settings feeding defaults into my local extensions with zero extra plumbing. Really appreciate you taking the time to lay out both patterns so clearly.

Jeff