To exert more rigorous control, it can be helpful for many scripts to actively suppress the behavior of any script triggers that may be set off. This makes the behavior of a script more predictable, since triggered scripts would be known to have no effect while the main script is running. The main script could also disable and enable triggered scripts at will, selectively suppressing or taking advantage of triggered behaviors. This is accomplished with two scripting patterns and four custom functions.

Script Patterns

Any script written to be called by a trigger should begin with an If [] script step checking whether or not script triggers are currently being suppressed (after any header comments) using the TriggersAreActive custom function. That If [] statement should contain all other steps within the script, so that, in effect, the script does nothing while triggers are suppressed.

If [ TriggersAreActive ]
	# The rest of the triggered script goes here...
End If

Any script that may be disrupted by setting off script triggers can use Set Variable [] script steps to invoke the TriggersDisable and TriggersEnable custom functions as needed. These two functions should always be used in pairs.

Set Variable [ $ignoreMe ; TriggersDisable ]
# Perform operations without complications from triggered scripts...
Set Variable [ $ignoreMe ; TriggersEnable ]

Custom Functions

TriggersAreActive

/**
 * =====================================
 * TriggersAreActive
 *
 * PURPOSE:
 * TriggersAreActive checks a global variable to identify whether or not script
 * triggers should be allowed to run. This function is best used in concert with
 * the TriggersDisable, TriggersEnable, and TriggersReset
 * functions.
 *
 * RETURNS:
 * False (0) if triggers should be suppressed; True (1) otherwise.
 *
 * PARAMETERS: none
 * =====================================
 */

TriggersDisable

/**
 * =====================================
 * TriggersDisable
 *
 * PURPOSE:
 * TriggersDisable sets global variables to indicate that the current
 * script is suppressing triggers. This function must be called from the context
 * of a script; otherwise, it will not suppress triggers, and it will return
 * False. In order to be suppressed, a script called via trigger should use the
 * TriggersAreActive function to decide whether or not to run.
 *
 * RETURNS:
 * True (1) if called from the context of a script, and therefore triggers have
 * been disabled; False (0) otherwise.
 *
 * PARAMETERS: none
 * =====================================
 */

TriggersEnable

/**
 * =====================================
 * TriggersEnable
 *
 * PURPOSE:
 * TriggersEnable sets global variables to indicate that the current
 * script is no longer suppressing triggers. This function will also turn off
 * trigger suppression altogether if there are no other scripts suppressing
 * triggers. In order to be suppressed, a script called via trigger should use
 * the TriggersAreActive function to decide whether or not to run.
 *
 * RETURNS:
 * True (1) if TriggersEnable successfully removed the current script from
 * the list of scripts suppressing triggers; False (0) otherwise.
 *
 * PARAMETERS: none
 * =====================================
 */

TriggersReset

/**
 * =====================================
 * TriggersReset
 *
 * PURPOSE:
 * TriggersReset sets global variables to indicate that script triggers
 * should be allowed to run, regardless of any persisting instructions to the
 * contrary from any script. This may be useful to recover from a situation
 * where a script forgot to call TriggersEnable at the end of operation.
 * In order to be suppressed, a script called via trigger should use the
 * TriggersAreActive function to decide whether or not to run.
 *
 * RETURNS: "" (null)
 *
 * PARAMETERS: none
 * =====================================
 */

Sample implementations of these functions are available at the fmpstandards Github repository:

These linked functions are presented here only as examples. Different implementations may be used to achieve the same effect.

  • No labels

22 Comments

  1. I like it! This is a really good practice to put into place before you start coding a solution or even midstream. You inevitably find that one layout or field trigger firing when you don't want it too - just because you take the user into the field for some reason.

    My initial read had me wondering about a more concise syntax.

    Initially "Suppress" is there because Script Triggers are ALWAYS ON as far as FMP is concerned. However, for the sake of brevity, yet maintaining clarity, how do these sound?

    Triggers or TriggersState
    TriggersOn
    TriggersOff
    TriggersClear
    

    In use it would become the following

    If [ Triggers = True ]
    	# The rest of the triggered script goes here...
    End If
    

    I only mention it because in this form it's declared as an absolute. Triggers are either ON or OFF. And I don't have to type the word "suppress" (which I would presume that someone [like me] will miss a "p" or an "s" when typing fast - not that I'm entirely worried about it.) (wink) - it's your baby after all.

    I'd also move the Script Patterns section to the top under the Overview and before the functions. This provides context relative to the overview description.

    1. I was expecting (and hoping) for the names to change before moving this out of proposal mode. I always like having a more concise name (short of using abbreviations), but removing "Suppress" and "Start" and "Stop" also removes the verbs (with the token exception of "clear"). These custom functions exist for doing something more than for the value they return, which puts them in the minority of custom functions, and makes verbs in the names key. (I know, SuppressTriggers really is only used for the value it returns, but that value is still used to decide a course of action.) I might have tried "RunTriggers", "DisableTriggers", "EnableTriggers", and "ClearTriggers"; but I really like grouping in alphabetical sorts. "TriggersDisable" and "TriggersEnable" are both awkward, but so are the names I started with; at least these are shorter. "TriggersAreActive" or "TriggersAreEnabled" do have a nice ring, though. Any other thoughts?

      1. If I had to choose, I would much rather go with TriggersAreActive. Reading that within the If is cleaner. I'm finding that there are more coders who code in the positive, where it takes a while before you start to head into the world of "not" - which I find myself using more and more.

        Taking off the equality operator make it read really clean too.

        If [ TriggersAreActive ]
        	# The rest of the triggered script goes here...
        End If
        

        or the shorter?

        If [ TriggersActive ]
        	# The rest of the triggered script goes here...
        End If
        

        The later here has an implied verb. (smile)

        1. "TriggersAbsoluteEnable" is still really funky. Ideas?

          1. "TriggersReset" is the best I can come up with. Run the word 'reset' through the OS X dictionary and see the computing definition and related Wikipedia articles. It's really what we're doing with that CF...

  2. Anonymous

    For the sake of concision, wouldn't it make sense to combine TriggersEnable, TriggersDisable & TriggersReset into a single CF, say 'Triggers' for example, that takes a single parameter (which is your action verb)? Then you could set it up to receive any number of text, numerical or boolean parameters, all of the text parameters basically just synonyms for 'Enable', 'Disable' and 'Reset', and produce the desired output.

    Examples:

    Triggers ( "enable" ), Triggers ( "disable" ), Triggers ( "clear" )

    Triggers ( "allow" ), Triggers ( "disallow" ), Triggers ( "reset" )

    Triggers ( True ), Triggers ( False ), Triggers ( "null" )

    It would still create the desired globals and still feed TriggersAreActive, but it would allow the user to use the terminology they feel comfortable with. They could even add their own terms to the CF, if the included lists don't meet their needs.

    I did a quick and dirty version, basically just combining the 3 CFs, setting the parameter lists to produce either a 0 (disable), 1 (enable) or 2 (reset), then using the choose function to produce the globals and the CF result, and it worked perfectly. I'm sure with a little more work, some redundancy could be eliminated as well.

    I've always just set a global ($$suspend) to 1 and set an If (not $$suspend) at the beginning of the triggered script, then cleared the global in the parent script. Same result, less work, and 2-4 CFs don't have to be imported into every file.

    Just my thoughts, Knight

    1. As a personal rule (and a proposed best practice), I never pass commands as parameters (and I try not to pass any enumerated values). It creates a discoverability problem, and it's inconsistent with "the FileMaker way." In a calculation dialog, all the commands available to you show up in the list of calculation functions. By passing the commands as a parameter to a Triggers ( command ) function, we'd be hiding three commands behind the mask of one function — developers would have to open the function up to know what to pass. (I know the standards on this site assume serious developers will be using Advanced, but any clients who work on any of their own code will have a problem opening up the custom function with Pro.) The particular enumerated values you suggest also invite inconsistent code, which defeats the purpose of coding conventions; I think code should use the same words if it means the same thing.

      For simpler implementations, I used to set a global exactly how you describe; but it runs into problems with more complex scripting. If a parent script disables triggers, and a sub-script of that script also disables triggers, then when the sub-script re-enables triggers, triggers will also be enabled during the rest of the parent script, which may lead to unwanted behavior. To control that, you have to keep track of which scripts are suppressing triggers, so that one script doesn't re-enable triggers while another script still needs them disabled. The custom functions handle that for us.

      The other advantage of keeping this functionality in custom functions, even just for the "TriggersAreActive" function, is that it makes change easy. After some discussion yesterday about the name choices for these functions, I changed both the names of the functions and the names of the global variables they use in a file I'm working on, and that change was instantly propagated to every script that uses the technique. If I ever need to make any change to how any of these functions work, I only need to make the change once (per file); I don't have to go and rename every If [ not $$suspend ] to If [ not $$DISABLED ], for example.

      1. Anonymous

        Your second 2 paragraphs are all valid points, and I understand your reasoning behind 'never pass commands as parameters' but that does just seem to be a personal rule. I don't believe, at all, that it's "the FileMaker way." What about GetLayoutObjectAttribute? Or TrimAll? TextStyleAdd? Even Get(Active/TriggerModifierKeys). I have to look up the acceptable parameters every time I use one of those! If that were truly the FM way, there would be 17 versions of TextStyle (ie, TextStyleBold, TextStyleItalic, etc). To every rule, there is an exception, even to the rule that every rule has an exception.

        In keeping with the spirit of your proposed best practice, how 'bout "TriggersAllow ( True, False or "" )"?

        Knight

        1. Just because there are exceptions in FileMaker's functions doesn't make it a worse idea or not worth aspiring to. There are indeed exceptions to the "don't pass commands as parameters" trend, but Get ( flag ) isn't necessarily one of them. In the calculation dialog, selecting constraining the list of available functions to Get () functions shows all the available flags. FileMaker can do that for the Get () functions, but, alas, we can't do that for custom functions. TextStyleAdd is one of several functions for separate text format manipulations: TextColor, TextColorRemove, TextFont, TextFontRemove, TextFormatRemove, TextSize, TextSizeRemove, and TextStyleRemove. FileMaker does have a handful of calculation functions with enumerated parameters, but they are in the overwhelmed minority.

          I'd rather have a couple more custom functions to choose from than ask developers unfamiliar with my work spend time reading the definitions of those functions or fishing for examples of them used in context. I prioritize discoverability over conciseness. ("TriggersDisable" and "TriggersEnable" do take up two spaces in the function list compared with "TriggersAllow ( trueOrFalse )", but they're also both shorter than either "TriggersAllow ( True )" or "TriggersAllow ( False )" in a calculation.)  You may have different priorities, and you're welcome to implement this (or not) however you see fit. That's why this is proposed as a best practice, and not as a standard. The principle of the what's getting done is more important than the exact custom functions used to accomplish it; but I do really like the names we came up with yesterday.

          (By the way, bonus points for invoking Hofstadter!)

          1. Anonymous

            "Just because there are exceptions in FileMaker's functions doesn't make it a worse idea or not worth aspiring to." Yeah, and I wasn't trying to say that it wasn't worth aspiring to. I was just giving examples of functions that don't fit into that ideology. And I know they definitely are the minority; I think I listed the only examples.

            And I, in no way, was trying to say that there was anything wrong with your method. I just approached it a little bit differently and wanted to present my case. Thinking back over my custom functions and scripting, I've generally gone the consolidation route. I've got a CF that uses common abbreviations for character sets to filter the input, and a second parameter to add characters to the filter. Like Fltr ( input ; "num" ; "." ) filters the input for "0123456789", same with lowercase letters, uppercase letters, etc. I created it because I got tired of typing Filter (input ; "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"). But, I guess FilterNumbers, FilterCaps, etc, would have their benefits, as well.

            I'm a bit newer to the game than you guys and I'm trying to get myself involved in the community.

            Knight

            1. Not that it's relevant to the proposal, but another plurality-of-functions approach to your filter example would be to use custom functions for each enumerated character set, which can be passed to FileMaker's Filter () function and expanded by concatenating additional characters:

              Filter ( input ; Numbers & "." )
              //Numbers = "0123456789"
              

              Now that I see that, I really like it. I may have to use it in the future. Thank you for the idea.

              1. I'd love to get an associated "authoritative" set of Custom Functions going for this. Have you been able to invest any time with git?

                Create a free github account. I'll give you access to my repository where I've started keeping them.

                1. I know I said I'd be up for it before; but this time, I mean it. I'm not interviewing or moving cross-country or adjusting or anything. I'm now registered as jbante.

                  1. Make a fork of the repo from here https://github.com/petrowsky/fmpstandards

                    It should give you directions. You can then add your functions it it, modify existing functions and make pull requests. I'll then pull in your additions and changes.

                    Anyone else who wants to do this can too. Guess I should make a wiki page about it. It's a great method for allowing multiple people to work on a consolidated set of agreed upon Custom Functions - and you always know you've got the latest.

              2. Anonymous

                No problem. I kinda like how that looks too. The one thing that wouldn't provide that my CF has (which I didn't mention earlier) is the ability to remove members of the filter list. Say, for example, you want to filter your input for all numbers except for 0. By placing characters in curly braces ({}) within the 'added characters' parameter, it activates a 'Filter Out' custom function, adding everything outside of the braces to the filter and removing everything inside the braces from it. So, to filter on a character set of "12346789.A", it would look something like this:

                Fltr ( input ; "num" ; ".A{05}" )
                

                But to apply the same functionality to FM's Filter function, I guess you could just apply a 'Filter Out' CF to the enumerated character set, ie:

                Filter ( input ; ".A" & FilterOut ( Numbers ; "05" ) )
                

                I know this all completely proves your point about having to have documentation on the function or having to open it up and look under the hood. Because there's no way you'd know about the curly brace thing, otherwise. And I do like the idea of enumerating character sets behind a CF name. Passing parameters as text always felt a little lowbrow to me ("num" vs Numbers). I'll have to bat it around a little bit and see which way I like better. 

                Sorry for going completely off-topic.

                Knight

        2. Knight, I've been on the fence about this one myself and I tended to code with enumerations because I was the only one creating my own solutions.

          However, I'm starting to "lean" towards Jeremy's points. Not because it's the "FileMaker Way" (I'm not impressed with a lot of FileMaker's way of doing things.) (wink)

          But more because it's REALLY easy to copy and paste custom functions within FMP 11 and higher now. There's a simplicity to only having to know the function name itself.

          Because the functions are being "classed" so to say (because of their "Triggers" prefix) it makes them easy to find. I think if you sit with this for a while, you may find that the mental reluctance is because it "feels" like you're adding "extra" stuff to the database by having "yet one more function" - at least that's my issue when swallowing this pill.

          When you look at PHP or Java (oh my!!!!) you find the number of functions in FileMaker is laughable compared to those other languages - not that we want to make FileMaker into Java, but rather the "hunt and peck" method of programming in FileMaker is much higher and this model accommodates that.

          Personally, I'm fine with both methods as long as documentation is accessible and examples are prevalent. But I think the direction Jeremy is taking is one that has good merit.

          1. Anonymous

            I haven't done much work with PHP or Java, but I have worked with VB, and you ain't kiddin'! VB is ri-DICULOUS with the amount of methods and functions and objects... FM doesn't even come close. And it seems like half of them are redundant.

            And I have gone both directions on this, depending on the situation. I can see validity in both.

            Knight

  3. Anonymous

    When implementing this Custom Function I would like to add three lines to the top of all my trigger scripts any simply exit the script if triggers are disabled. Any thoughts on why or why not to do it this way? I see that you recommend using a if-statement that incapsulates the entire script, but I would prefere simply exiting the script if it does not effect performance.

    

    If [not TriggersAreActive]
        Exit Script [Result: 1]
    End if
    1. Anonymous

      I prefer to use the exit script step approach. Just three lines to paste in, and no need to scroll to the bottom of the script for the end If statement.

      I also prefer to use a global variable $$triggersLock rather than CFs, just makes the whole process more visible for my co-developer

      Brian

      1. Whether to use the guard clause type of approach (exit at beginning of script) or to wrap the whole script in an If [] statement is just a matter of personal taste, as I see it.

        I originally also used a simple global variable for suppressing triggers; simple problems deserve simple solutions. But my motivation for using custom functions was that it isn't always that simple. The problem I ran into was that a script that suppresses triggers may call a subscript that also suppresses triggers. When the subscript is done, it re-enables triggers. The parent script would proceed expecting triggers to be disabled when they are not. The custom functions keep track of which scripts are suppressing triggers so that triggers aren't re-enabled prematurely. I'd rather explain the custom functions to my co-developers than repeat that logic throughout a system.

        1. Anonymous

          Hi Jeremy! I just read this thread for the first time. I try to improve my trigger management and look for solutions.

          I am still in the state of using a simple global variable, and I also ran into the problem you describe above. But this is not an argument for not using a global variable. A simple solution for that problem:

          Instead of setting the global variable to 1 or 0, I always use:

          Set Variable [ $$disableTriggers ; $$disableTriggers +1 ]
          and:
          Set Variable [ $$disableTriggers ; $$disableTriggers -1 ]

          Of course always used as a pair in one script (and in subscripts) ... Then subscripts are not a problem ...

          And Set Variable [ $$disableTriggers ; 0 ] can be used for a TriggerReset.

          But I like the custum function approach, too, because it's more simple. Simplicity rules, because: "Progress means simplifying, not complicating." (Bruno Munari)

          Best regards,
          Patrick Horn