The OnObjectValidate Trigger

FileMaker 11 added just one new object trigger, and it is a strange little dude: `OnObjectValidate`. This trigger fires after you edit a field, before it is validated. If you thought FileMaker already had enough after-you-change-a-field triggers, you were mistaken. In this article, I explain how the various field triggers fit together and why `OnObjectValidate` exits. You then get a simple example of how it can be used. And as a special bonus, I show you how to extend the technique so it can easily be reused over and over again.

>Update: (September 4, 2010) I corrected the title of this post because, by golly, there *is no trigger called `OnBeforeValidate`*. Whoops. >Update: (September 4, 2010) Thanks to excellent advice from [Corn Walker][corn], I have simplified the final version of the script. Corn’s method doesn’t require a script parameter and is much less complicated. Thanks!

[corn]:http://proofgroup.com/about/corn_walker

Field Triggers Demystified

FileMaker has *a lot* of triggers that are loosely related to changing a field:

1. `OnObjectKeystroke` fires when you type in a field.
2. `OnObjectModify` fires when you modify the contents of a field.
3. `OnObjectValidate` and `OnObjectSave` fire when you exit a field after making changes.
4. `OnObjectExit` fired when you exit a field, even if you haven’t made changes.

> Note: Throughout this article, I’ll be focusing on *field* triggers. Many of these triggers do different things when attached to other layout objects. But for today, assume you’ve attached these triggers to a field object.

I wanted to make a diagram that shows how FileMaker processes field triggers in an effort to make it clearer. My [first attempt][laffo] sort of had the opposite effect, which just demonstrates that this stuff is kind of complicated. Luckily, I went back to the drawing board and tried again.

[laffo]:http://sixfriedrice.com/wp/wp-content/uploads/2010/09/field-trigger-diagram1.png

First, this is what happens, from a trigger perspective, when you exit a field in FileMaker Pro 10:

Field triggers in FileMaker 10

At the top, you start to exit the field. (This can happen in a lot of ways. You can tab to the next field in the tab order, click outside the field, change records, switch layouts, close the window, run the `Go To Field` or `Commit Records` script step, switch to Preview Mode, and, and, and…)

FileMaker checks to see if you made any field changes. If not, it skips right ahead to the `OnObjectExit` trigger. But if you *did* make changes, FileMaker goes through all the steps. First, it validates the field. This takes two forms:

1. FileMaker makes sure what you typed makes sense for this field. For instance, if this is a date field, and you typed “Hi Mom” you’ll get an error.

2. If you set up any validations in the field options, FileMaker runs them now as well.

As you are no doubt aware, failed validations result in an error message, something like this:

FileMaker Pro's standard error message for invalid dates

Once a validation error happens, FileMaker drops you back in the field and the process ends. You have to fix the error or revert the field before you can really exit.

If validation passes, FileMaker goes on to run your `OnObjectModify` and `OnObjectExit` triggers. As usual, if the triggered scripts exit with a `False` result, FileMaker stops the exit and leaves you in the field. Otherwise, the exit is done. All this happens in the microsecond after you try to exit a field.

OnObjectValidate

That is good stuff. You can inject your special scripted behavior into the field exit process, and you decide if you want it to happen only when the field was changed, or any time the field is exited (or, I suppose, both). But you don’t have quite as much control as you might want. FileMaker runs those pesky validations *before* your triggers. If you want to do something with the field value before validations run, you’re out of luck.

This limitation was, apparently, annoying enough that the FileMaker PTB decided to do something about it. In FileMaker Pro 11, the exit field process now looks like this:

Field Triggers in FileMaker 11

Sharp-eyed readers will note that the OnObjectValidate trigger politely fires after you start exiting the field, but *before validations run*.

Script triggers are all about inserting customized behavior into FileMaker’s normal processes, and with this new trigger, you get one more angle of attack (or, some might say, one more “hook,” as in, “I can hook in to the process in a new place.) `OnObjectValidate` scripts can process field data before FileMaker validates the field, and before the user ever sees an error message.

Put It To Use

So what can you do with it? Probably lots of things. Here’s the example I showed in my presentation at DevCon 2010. Suppose you have a date field into which you often enter dates in the near future. Someone might call and say, “I want an appointment on Thursday.” If you’re like me, this request triggers a thought process something like this:

>”Ok, so today is, um, Monday. And it is the tenth. No, the eleventh. No, what date is it? [checks]. Oh, the 28th. Wow. Where did the time go? Anyway, Monday, the 28th. And Thursday is [on my fingers] Tuesday, Wednesday, Thursday — three days from now. That’s the 31st. Wait, this is September. That has, um, [on my knuckles] January, February, March, April, May, June, July, August, September — 30 days. So the first. I’m about 75% sure Thursday is October 1st. Man, somebody should make this easier.”

Ok, so most of you probably aren’t quite as bad as I am at this stuff, but who wouldn’t prefer to just type “Thursday” and be done with it?

As a FileMaker power programmer, you can probably think of 42 ways to let a person type “Thursday” and have FileMaker put in the right date. But with script triggers in 11 you can do it with style:

* This method requires no extra fields or pop-up windows.
* Your date field stays a real date field, which is just better.
* You don’t have to click any special buttons, or layer buttons over the field, and rely on spurious timing assumptions.

Instead, just type a word directly in a date field, then use a script to fix it up when you exit the field. Best of all, it is pretty simple. The trickiest part is writing a calculation that turns words into dates. Here’s mine:

SmartDate :=
 Case(
 value = "today"; Get(CurrentDate);
 value = "tomorrow"; Get(CurrentDate) + 1;
 value = "yesterday"; Get(CurrentDate) - 1;
 value = "sunday" or value = "sun"; DateOfDay ( 1 );
 value = "monday" or value = "mon"; DateOfDay ( 2 );
 value = "tuesday" or value = "tue"; DateOfDay ( 3 );
 value = "wednesday" or value = "wed"; DateOfDay ( 4 );
 value = "thursday" or value = "thu"; DateOfDay ( 5 );
 value = "friday" or value = "fri"; DateOfDay ( 6 );
 value = "saturday" or value = "sat"; DateOfDay ( 7 )
 )

I put this in a custom function. As you can probably guess, it turns words like Today, Tomorrow, Yesterday, Saturday, and Mon into reasonable dates. It uses this (complicated) custom function, which figure out the date for a given day number:

DateOfDay :=
 Let(
 x = dayNum - DayOfWeek ( Get(CurrentDate) );
 If(x <= 0; x + 7; x) + Get(CurrentDate) ) >

Note: You could easily extend the `SmartDate` function to handle more complex expressions like “Next Thursday” or “Last Friday” or even “Bastille Day.” Whatever makes your socks go up and down.

Then I use this function in a very simple script:

If [ Not IsEmpty(SmartDate(My Table::My Date Field)) ]
 Set Field [ My Table::My Date Field ; SmartDate(My Table::My Date Field) ]
 End If

Finally, set this script as the `OnObjectValidate` trigger script for the date field. Now, when you type “Thursday” into the field, FileMaker does all the thinking for you, and swaps in the right date.

> Note: You may be wondering why I bother with the `If` in my script. Why not have `SmartDate` return the original value if it doesn’t match one of the special conditions? This is how I first wrote it. But it has an annoying side effect: When you put something invalid in the date field, it changes to a question mark when you try to exit the field. You still see FileMaker’s date error dialog box, but you’ve lost what you typed, so it is harder to go back in and fix a typo. This happens because the script uses the `Set Field` step to insert invalid data into the date field, and FileMaker puts a question mark in the field in this case. I decided it was best to leave the field alone if I couldn’t figure out what to do with it. Hence, the `If`.

This kind of technique wouldn’t work at all in FileMaker 10. Because validations run before `OnObjectSave` and `OnObjectExit`, FileMaker’s invalid date error message will always appear before your script can do anything about it. This is exactly why `OnObjectValidate` exists. It gives you a little more control.

Which to Use?

You might think I’m saying `OnObjectValidate` is *better* than `OnObjectSave`, but you’d be wrong. The truth is, in most cases, `OnObjectSave` is probably the right trigger to use. That way you can be sure you’re dealing with valid data in your trigger script. `OnObjectValidate` should only be used when you explicitly want your script to run *before* validation for some reason.

Bonus Technique

I’m going to channel my inner [Don Levan][don] here, and propose a slight modification to the script. This is by no means a necessary change. But for the adventurous, this version of the script is *generic*. By that, I mean one script can be used with as many different date fields as you want.

[don]:http://vanguardcs.net/resources.php

>Note: I changed this technique significantly based on suggestions from Corn (see the update at the top of this post). Click here to see the old way.

Here’s the script:

If [ Not IsEmpty(SmartDate(Get(ActiveFieldContents))) ]
 Set Field [ SmartDate(Get(ActiveFieldContents)) ]
 End If

I’ve made three changes:

1. In the `If` step, I use the `Get(ActiveFieldContents)` function so I’m checking `SmartDate` against whatever field the user happens to be in.
2. I turned off the “Specify target field” checkbox for the `Set Field` script step so it targets the current field.
3. For the value side of the `Set Field By Name` I again use `Get(ActiveFieldContents)`.

With these changes in place, your trigger works exactly as it did before. But now, you can wire a second, third, or 144th date field to the same script. Easy Peasy Pumpkin Squeezy. Or something like that.

Cool, huh?

Leave a Comment

10 thoughts on “The OnObjectValidate Trigger

  1. Great write up.

    Since you already have your cursor in the field and the script is explicitly firing before you exit the field, could you not also use the SetField[] script step with no target field specified? This would avoid the whole passing the field name as a parameter issue, keeping it über-simple.

    If [ Not IsEmpty(SmartDate(Get(ActiveFieldContents))) ]
    Set Field [ SmartDate(Get(ActiveFieldContents)) ]
    End If

  2. Corn:

    That is absolutely brilliant. Much cleaner and simpler. I always forget Set Field can work in this way. I revised the article to do it your way. Thanks!

    Geoff

  3. Hi Geoff/Corn.

    Thanks, really useful stuff.

    experimented with it a bit.
    – moved the two CF’s into one which gives option of putting all logic in single function script (not sure it’s bullet proof but holding up for now)
    – added few more shorthands to Geoff’s Originals.

    Made rough demo for myself. For anyone interested file here http://bit.ly/cRbNwu

    Olly Groves

  4. Geoff,

    Nicely done. Another side benefit of OnObjectValidate…they’re handy when editing portal records and you want to run a script after the data *changes*. For instance tabbing through multiple line item quantities and adjusting them should update the invoice total.

    OnObjectKeystroke runs with each keystroke (wasteful when typing in multiple digits)

    OnObjectExit runs *every* time you exit a portal field. Even when you don’t change data. Excessive.

    OnObjectSave won’t fire until all portal rows have been exited and the parent record is “saved”, blowing any tab order thing you have going in the portal.

    OnObjectValidate works great, only firing if the portal field has been modified and exited, but preserving the tab order. Showed this at my dev con session. It’s a nice subtle distinction.

  5. Another neat tool for ye olde FMPro toolbox… Thanks!

    Oh, and Bastille Day is definitely a pretty cool holiday 😉

  6. Really clear Geoff, thank you.

    Any reason you don’t just use the first 3 letter of actual days? Other than someone entering something like ‘wedding day’ I don’t see an issue.

    If you were wanting to allow for more options that started with those letters (can’t think of any of the top of my head) then they would just need to be placed at the top of the list?

    Eg

    SmartDate :=
    
    let( val=left(value;3);
    Case(
       value = "today"; Get(CurrentDate);
       value = "tomorrow"; Get(CurrentDate) + 1;
       value = "yesterday"; Get(CurrentDate) - 1;
       val = "sun"; DateOfDay ( 1 );
       val = "mon"; DateOfDay ( 2 );
       val = "tue"; DateOfDay ( 3 );
       val = "wed"; DateOfDay ( 4 );
       val = "thu"; DateOfDay ( 5 );
       val = "fri"; DateOfDay ( 6 );
       val = "sat"; DateOfDay ( 7 )
    )
    )
  7. An update,

    how about this?

    Let( 
    [
    val =   Get ( ActiveFieldContents );
    vs=Substitute(val;["day";""];[" ";"¶"]);
    vd=Left(FilterValues ( vs; "mon¶tue¶wed¶thu¶fri¶sat¶sun¶tues¶weds¶wednes¶thurs¶satur" );3);
    vm=Substitute(FilterValues ( vs; "last¶next" );"¶";"");
    
    cd = Get(CurrentDate);
    td=DayOfWeek(cd);
    dn = Case(							// sets dayNumber based on value entered. if no match returns orginal value
    		vd = "sun"; 1;
    		vd = "mon"; 2;
    		vd = "tue"; 3;
    		vd = "wed"; 4;
    		vd = "thu"; 5;
    		vd = "fri"; 6;
    		vd = "sat"; 7;
    		val 							// rtn's original 'value' if no match.
    		);
    
    isday=dn = Filter(dn;"-123456789");
    diff=If (isday;(Case( dn ? td;dn-td;7+dn-td)));
    modMult = Case(vm="last";-1;vm="next";1;0)
    
    ];
    Case(								// add other quick text values outside days above here
    	dn = "today" or dn = "tod" or dn = "to" or dn = "now"; cd;
    	dn = "tomorrow" or dn = "tom" or dn = "tm" or dn = "t"; cd + 1;
    	dn = "yesterday" or dn = "yes" or dn = "y"; cd - 1;
    	PatternCount(dn;"week")>0 ; cd + (7*modMult);
    	PatternCount(dn;"month")>0 ; cd + (30*modMult);
    	PatternCount(dn;"year")>0 ;cd + (365*modMult);
    	isday=1; cd + diff +(7*modmult);   // If tod (x = 0) add 7 days. eg if Sun + user enters Sun, show's next Sun Date.
        
       	dn							// rtn's orginal 'value' if no match. (then handled by FMPro validation or your script post OOValidate)
    	)
    
    )
  8. Dave: Very cool. I didn’t realize that. Thanks!

    Tim: Absolutely. There are lots of ways to extend/improve the smart date function. Thanks for the tips!

  9. It always gets me that seasoned programmers will call the same function 2 times,
    first to check the results,
    second time to use the results

    Isn’t it smarter to save the results in a variable, this way this function it only called once?

    
    Set Variable [$SDR; value: SmartDate(Get(ActiveFieldContents))]
    If [ Not IsEmpty($SDR) ]
    Set Field [ $SDR ]
    End If