The OnObjectValidate Trigger

Leave a comment

9-3-2010 by Geoff Coffey

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, I have simplified the final version of the script. Corn’s method doesn’t require a script parameter and is much less complicated. Thanks!

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 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.

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 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.

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?

12 Comments

  1. corn walker

    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. Geoff Coffey

    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. Olly Groves

    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. David Knight

    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. Derek Bastille

    Another neat tool for ye olde FMPro toolbox… Thanks!

    Oh, and Bastille Day is definitely a pretty cool holiday ;-)

  6. Tim Anderson

    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. Tim Anderson

    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. Geoff Coffey

    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. Robin

    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
    
  10. Geoff Coffey

    Robin:

    Definitely a matter of opinion, but I would classify that as premature optimization.

    Geoff

  11. Chris W

    I am sad that there are no more updates to this blog :(

  12. Geoff Coffey

    Chris, I am too, sometimes. So many interests, so little time :)

Tell Us What You Think

*
* (will not be published)