Procedures that are triggered automatically when the user performs some normal Panorama action are said to be “implicitly triggered.” (In Panorama 6 and earlier versions, this was called a “hidden trigger.”) Examples of user actions that can cause a hidden trigger to activate include adding new records to a database, deleting records, opening a file, bringing a form to the front and many more. The trigger is “implicit” because the user is not explicitly asking Panorama to activate a procedure by pressing a button or selecting a menu choice. Procedures that are implicitly triggered can modify (or even override) the way Panorama reacts to many standard user actions.

Some implicitly triggered procedures are embedded into the property panels for forms, fields or form objects. For example, the Form Properties panel allows you to set up code that responds to form related events.

Other implicitly triggered procedures are simply ordinary named procedures with a special name. This special name always begins with a period, for example .Initialize, which is automatically called when a database opens.

The procedure name must be spelled exactly as described in the documentation below, including upper and lower case. So of these three variations on the word .Initialize, only the first will work.

.Initialize ☞ Ok
.initialize ☞ Will not trigger
.INITIALIZE ☞ Nope

The operation of many implicit procedures is tightly intertwined with the system’s run loop for dispatching events. See Understanding the Run Loop for more on this topic.

Multi Function Implicit Procedures

Some implicit procedures can perform different functions depending on what the user has done. To identify the code for each function, add a label at the start of the code for that section.

To learn more about labels, see shortcall, goto and info(“labels”).

Temporarily Disabling Implicitly Triggered Procedures

If you write implicitly triggered code that causes Panorama to malfunction, you’ll need to temporarily disable that code in order to work on that database and fix the problem. See Opening a Database in Diagnostic Mode to learn how diagnostic mode can be used to temporarily disable implicitly triggered code.

Triggering code when initializing a database

When a database opens, Panorama checks to see if it contains an .Initialize procedure, and if so, runs it. You can use this procedure to initialize variables, set up custom menus, pre-sort or pre-select the data … anything that needs to be done automatically whenever the database file is first opened.

Warning: Most procedures can only be triggered from the data sheet or a form. However, the .Initialize procedure will start running immediately when the file is opened, in whatever window happens to be open. If this window is not a data sheet or form, the procedure may not operate correctly. Many procedure statements (sort, group, select, etc.) will not operate properly from a non-data window. If this may happen, the first thing the .Initialize procedure should do is open a form or the data sheet.

Warning: If several databases are opened at once, Panorama will open the databases first, then run the .Initialize procedures.

Note: If the database is opened without any visible windows, Panorama will not run the .Initialize procedure.

Opening a database without running .Initialize – If you want to open a database but skip the .Initialize procedure, use the File>Find & Open dialog (see “Special Operations Menu” in Find & Open). Locate the database in the dialog, then right click on it and choose Data Sheet Only from the pop-up menu. This will open the database but skip the procedure.

Another way to open the database without running the .Initialize procedure is to use diagnostic mode. See Opening a Database in Diagnostic Mode to learn how this mode is used.

Triggering code when modifying individual database records

Panorama supports three implicit procedures related to modifying data - .NewRecord, .DeleteRecord, and .ModifyRecord.

If a database contains a .NewRecord procedure, that procedure will be called whenever a single new record is added to the database, for example by pressing the Return key or clicking on the Add icon in the toolbar. The info(“trigger”) function can be used to determine which of the three possible actions triggered the procedure:

New.Add ☞ Add New Record tool or menu item
New.Insert ☞ Insert New Record tool or menu item
New.Return ☞ Return key or Insert New Record Below menu item

Here is a typical .NewRecord procedure that will not allow new records to be added if there already at 100 or more records in the database:

if info("records")≥100
    message "Sorry, this database is limited to 100 records."
else
    if info("trigger") = "New.Add"
        addrecord
    elseif info("trigger") = "New.Insert"
        insertrecord
    elseif info("trigger") = "New.Return"
        insertbelow
    endif
endif

Here’s another .NewRecord example that only allows new records to be added to the end of the database, not inserted in the middle. If the trigger is New.Insert, or if the Return key is pressed in the middle of the database, no record is added.

if info("trigger") = "New.Add"
    addrecord
elseif info("trigger") = "New.Return" and info("eof")
    addrecord
else
    message "Sorry, new records must be added"+
        " to the end of the database, not inserted in the middle."
    return
endif
call .ModifyRecord

Warning: The .NewRecord procedure is only triggered when individual new records are added, it is not triggered when multiple new records are appended to the database, for example by importing text.

If it exists, the .DeleteRecord procedure will be triggered when the user attempts to delete a record from the database. This procedure could be triggered by the Delete Record tool or menu item, or by pressing the Delete key in data sheet or view-as-list windows. The example below allows records created today to be deleted immediately, but requires confirmation before allowing older records to be deleted.

if Date < today()
    alertsheet "Are you sure you want to delete this record? "+
        "It was created "+pattern(today()-Date,"# day~")+" ago.",
        "Buttons","No,Yes"
    if info("dialogtrigger") = "No"
        return
    endif
endif
deleterecord

Notice that the record is not deleted unless the procedure deletes the record. The .DeleteRecord procedure interrupts the normal deletion process and takes over. This puts you, the programmer, in control.

If it exists, the .ModifyRecord procedure will be triggered when the user modifies any field in the database. Warning: The .ModifyRecord procedure will not run if a procedure is already running, or if the field has its own procedure (see Automatic Field Code). In those cases you may want the other procedure to call the .ModifyRecord procedure as a subroutine (more on this in a moment). The .ModifyRecord procedure is also not called if the data is modified with a command in the Fill menu, see .ModifyFill below.

The .ModifyRecord procedure example below automatically marks the latest date and time when a record was modified. This example assumes that the database has two fields for time/date tracking: ModifyDate (a date field) and ModifyTime (a numeric field).

ModifyDate=today()
ModifyTime=now()

Note: This example illustrates the .ModifyRecord procedure, but a better way to perform this task would be to use the File>Database Options dialog to designate a numeric (Integer) field as the Time Stamp field. Once a field has been designated as a time stamp field, Panorama will automatically copy the current date and time into this field every time any other field in the record is modified. Setting up a time stamp field allows you to reliably track when each record in the database was last modified. The modification date and time are stored in this field using the SuperDates format.

If your database has other procedures that modify the database, they should call the .ModifyRecord procedure to make sure that the time stamp is kept up to date. For example, here is a procedure that automatically subtracts one from the QtyInStock field.

QtyInStock=QtyInStock-1
call .ModifyRecord

When the .ModifyRecord procedure is running, Panorama automatically defers display of any database changes until the code is completely finished. Normally you won’t even notice this, but to learn more, see the showlater statement.

Triggering code when data editing is blocked

Panorama allows individual database fields to be disabled so that they can’t be edited (see Disable Editing of Individual Fields). Usually Panorama simply beeps when you click on a field that is disabled (whether you click on a data sheet cell, or on a Text Editor, Data Button, or other item in a form.) However, if the database contains a .BlockedEdit procedure, that procedure will run when a disabled field is clicked on (it does not run, however, when a disabled field is skipped while tabbing). For example, this procedure could be used to notify the user why their editing is being blocked, or even to ask the user for a password to unlock the field.

If there is more than one disabled field, the .BlockedEdit procedure will be triggered no matter which field was clicked on. However, the procedure can find out which field was blocked – the field name is passed in the first parameter to the procedure. Here’s an example of how a .BlockedEdit procedure could be written to notify the user that the field is disabled, instead of simply beeping.

let blockedField = parameter(1)
nsnotify "Cannot edit.","TEXT","Editing of the "+blockedField+" field is disabled."

Note: The .BlockedEdit code can enable the field (see below), but if it does so the user will have to click again to edit the field. There is no way that Panorama can be told to allow the original editing request. By the time the .BlockedEdit code runs, the original request has already been blocked.

To see a practical example of how the .BlockedEdit procedure can be used, see the Disable Editing of Individual Fields help page.

Triggering code when a column is modified

If it exists, the .ModifyFill procedure will be triggered when the user uses most commands in the Field>Morph sub menu (Morph, Propagate, etc.). The .ModifyFill procedure will not run if a procedure is already running, or if the field has its own procedure. In those cases you may want the other procedure to call the .ModifyFill procedure as a subroutine (more on this in a moment).

The .ModifyFill procedure example below automatically marks the latest date and time when a fill command is used. This example assumes that the database has two fields for time/date tracking: ModifyDate (a date field) and ModifyTime (a numeric field).

field ModifyDate
formulafill today()
field ModifyTime
formulafill now()

If your database has other procedures that use fill statements they should explcitly call the .ModifyFill procedure to make sure that the time stamp is kept up to date. The example below selects all items that have over 500 in stock and have not been touched in 30 days. For those items, it reduces the price by 10%, then marks the modification date and time.

select QtyInStock>500 and today()-ModifyDate>30
field Price
formulafill Price*0.90
call .ModifyFill

Since the .ModifyFill procedure is not triggered by a FormulaFill statement in a procedure, the procedure must update the modification date and time itself by explicitly calling .ModifyFill.

Triggering code when moving to a different record

If it exists, the .CurrentRecord procedure will be triggered when the user shifts to a different record. For example, this procedure is triggered when you move up or down in the database with the vertical scroll bar, or with the Find or Find Next commands. It could be used to initialize a variable or graphics object to display the newly current record. However, we recommend that you keep this procedure as short as possible, and actually strongly recommend avoiding use of the .CurrentRecord procedure altogether if at all possible, since it can cause performance problems. This procedure should definitely not ever change the current window or display a dialog or alert, and it should not ever cause a further shift in the database position (for example doing a further search). It’s possible to get Panorama into an infinite loop where the only way to stop is to force quit the program (or reboot the entire computer).

Note: In some situations, the .CurrentRecord procedure will be triggered more than once when the position shifts. For example, this may happen when adding a new record to the database. You should not rely on the ..CurrentRecord procedure being triggered once and only once when the current record changes.

When the .CurrentRecord procedure is running, Panorama automatically defers display of any database changes until the code is completely finished. Normally you won’t even notice this, but to learn more, see the showlater statement.

Logging Changes (Audit Trail) with .ModifyRecord, .ModifyFill and info(“modifiedfield”)

It’s possible to create a log of all changes made to a database, so you can see who changed what when. A Simple Journal example database illustrates this.

As the illustration above shows, the Journal field contains a log of all changes made to the database. This log is created by the .ModifyRecord and .ModifyFill procedures in the database. Here is the .ModifyRecord code:

if info("modifiedfield")="Journal" rtn endif
let cellValue=grabdata("",info("modifiedfield"))
let jline="«"+info("modifiedfield")+"»="+constantvalue("cellValue")+
    " on "+datepattern(today(),"YYYY-MM-DD")+" @ "+timepattern(now(),"hh:mm:ss")
Journal=jline+sandwich(cr(),Journal,"")

And here is the .ModifyFill code:

let wasField=info("fieldname")
let jprefix="«"+info("modifiedfield")+"»="
let jsuffix=" on "+datepattern(today(),"YYYY-MM-DD")+" @ "+timepattern(now(),"hh:mm:ss")
field Journal
if datatype(info("modifiedfield"))="Text"
    formulafill jprefix+quoted(grabdata("",info("modifiedfield")))+jsuffix+sandwich(cr(),Journal,"")
else
    formulafill jprefix+str(grabdata("",info("modifiedfield")))+jsuffix+sandwich(cr(),Journal,"")
endif
field (wasField)

One possible problem with this logging mechanism is that the log quickly becomes larger than the actual data! We’re sure, however, that some of you will find this a valuable tool.

Form Event Procedures

Panorama can run your custom code when certain form related events occur, including opening the form, making the form the front window, and resizing the form. Place the code in the Form Properties panel.

This code should include a special label for each event you want to customize. (To learn more about labels, see shortcall, goto and info(“labels”).)

formOPEN: ☞ triggered when form is first opened (including with the goform statement).
formCLOSE: ☞ triggered when the form is closed (see the special notes below).
formRECTANGLE: ☞ invoked when a form is opened to calculate the window position and size.
formWINDOWTITLE: ☞ invoked when a form is opened to calculate the window title. 
formFRONT: ☞ triggered when form becomes the front window
formRESIZE: ☞ triggered when form is resized
formGRAPHICSMODE: ☞ triggered when the form switches into graphics mode
formDATAMODE: ☞ triggered when the form switches into data mode

The labels are case-sensitive and must match the examples above exactly. Here is an example of form event code that customizes what happens when the form is opened or brought to the front. This example assumes that the window is opened with the openform clone option, so that there could be multiple open instances of this form. When the form first opens, the contents of the ID field are saved into a variable, and the text editor object Letter is opened. Later, if a differnt window is brought to the front and then this window is brought back to the front, the code automatically searches for the record that corresponds to that window, bringing the database back to the same spot. (There is no formRESIZE label, so nothing extra happens in that case. For performance reasons, you should omit any label you aren’t using.)

formOPEN:
    letwindowglobal windowID=ID
    objectaction "Letter","open"
    return

formFRONT:
    windowglobal windowID
    find ID=windowID
    return

Here is what this code looks like in the Form Properties panel.

Keep in mind that this code is only triggered for events involving this form. If you want other forms to be customized, you’ll need to add code to their properties panel. (However, you can use a subroutine to share code between different forms, see below.)

Note: When the form event code is running, Panorama automatically defers display of any database changes until the code is completely finished. Normally you won’t even notice this, but to learn more, see the showlater statement.

Unlike the other form events, the formRECTANGLE: and formWINDOWTITLE: events should only be used to calculate the specified value, which is then returned to Panorama using the functionvalue statement. See Newly Opened Database Window Arrangement to learn the details about how to use these special form events to customize the initial position, size and title of a form window.

The formCLOSE: code must not do anything except for copy values into variables. For example, you could save the window position into a variable, so that you can later re-open the window in the same spot. The code must not display or change anything (you could, however, start a timer that would display or change information).

Form Event Procedure Subroutine

If you prefer, you can move the form event code to a separate named subroutine, and call it from the form properties panel. This can be more convienient for editing and debugging, and also allows you to share the same code among multiple forms. If you do this, the code in the form properties panel can only contain one statement – the call statement that calls the subroutine. If Panorama sees just this one statement, it will look in the subroutine for the event labels. If there is more than one statement, the event labels must be in the properties panel.

Including Procedure Triggers in Debug Instrumentation

Implicitly triggered code normally runs silently, if it works correctly you won’t be aware of it at all. If code is malfunctioning, however, it may be important to be able to find out how that code was triggered, so that you can track down the problem. If you enable the Debug Instrumentation CodeTriggers option, Panorama will add an entry to the Instrumentation Log every time a procedure is triggered, whether explicitly or implicitly. Whenever a menu is chosen, a button is pressed, or implicit code is triggered, the exact trigger will be noted for later examination. This is especially useful for implicit code since you may not be aware that the code is even being triggered.

Before you can use this feature you have to configure instrumentation on your system. See the Debug Instrumentaiton help page to learn how to set this up.

Once instrumentation is configured, open the Instrumentation panel in the Preferences dialog. Then click on Objective-C Classes at the bottom of the list on the left, then click on CodeTriggers on the right.

With this option enabled, any action that triggers a procedure will be added to the instrumentation log. For example, suppose you have the Help>Font Awesome Icons window open and you click on the button that opens a new window:

If the CodeTriggers option is enabled, two new lines will appear in the instrumentation log:

TextDisplayObject RUN CODE --> call "New Catalog Window"
RUN FORM EVENT --> OPEN (Catalog)

The first line records that you clicked on a Text Display Object, and that this object had code associated with it which was run. The log even includes the code itself (if the database is locked down then the code will be redacted). This is an explicit trigger.

The button code opens a new cloned Catalog form. The Catalog form has been set up to implicitly trigger code when it opens. This implicit code trigger has been recorded on the second line of the log (RUN FORM EVENT). If there was a problem in opening the window, the log shows that Panorama got as far as the OPEN FORM EVENT code, so that might be a good place to start looking for the problem.

As this example shows, the instrumentation log not only shows code in your own databases that are triggered, but code in Panorama itself. Much of Panorama is written in Panorama code, so it can be educational to enable the CodeTriggers option and then start clicking on things in Panorama to see what is being triggered.


See Also


History

VersionStatusNotes
10.2UpdatedImplicit triggers can be recorded in Debug Instrumentation log. New form events for triggering code when going in or out of graphics mode, and when the window is closed.
10.1UpdatedForm event procedures automatically trigger when form opened, brought to front, or resized.
10.0No ChangeCarried over from Panorama 6.0