Let's say you have a requirement to prevent users from accidentally editing field values in the main layout of the application. As we all know, the default behavior of Browse mode is to allow direct editing of field values, including fields of related records in portals. So, to achieve this requirement, you will need to make these fields and/or portals read-only on the main layout and provide an alternative method for editing these fields. Let's then say that the requirement further states that the user should be able to cancel whatever changes they have made while editing and all changes should be "rolled back".
A common approach to these requirements is to provide an "Edit" button on the main layout that opens a modal popup window displaying the fields to be edited along with "Cancel" and "Save" buttons. You might then use one of two techniques to collect the data and provide rollback. One, you might use global fields to collect the data and then script the process of copying the data into the real fields when the user clicks "Save". By using global fields, when the user clicks "Cancel" the script only needs to close the window to leave the original data untouched. Alternatively, you might copy the real data into script variables when the window opens and allow the user to edit the actual fields on the layout. If the user clicks "Save", their edits are simply committed, but if they press "Cancel" the script copies the original data out of the variables and re-populates the fields with the data that was present when the window opened. This is a common technique, and it works fine for simple situations.
But let's say that we need to display dozens of fields and/or portals records, and that in order to support the popup functionality, we need to build relationships to multiple tables, etc., and finally, we need to allow the user to create and delete portal records. How can we do all that with global fields or script variables and still provide rollback functionality?
With the requirement to support relationships among tables in the popup, global fields are not going to get you there. Almost certainly you would conclude that it would be easier to present the user with the actual parent and related record fields. And you could use the method described above where data is saved into script variables when the popup opens. This method, however, gets much more laborious when you add portal records to the mix and vastly more complicated when the user is allowed to delete the portal records. Providing rollback functionality is a true scripting challenge. I know, because I've done it; but it's really tedious and required a lot of maintenance. If you add a new field to the list of fields that need to be edited, you need to alter the script every time.
Fortunately, there's a much easier way, and it takes advantage of FileMaker's built-in support for transactions. Much has been said about transactional processing in FileMaker, most prominently by Todd Geist. I've used transactional techniques in a number of systems where the script requires "all or nothing" processing. In the technique I'm about to describe, we are going to use a script trigger to "capture" and prevent the database commit operation that FileMaker natively and silently performs for the user as they work in Browse mode. Only when the user clicks "Save" will we allow all the data to be committed to the database. If the use clicks "Cancel", the edits that has been saved but not committed (including addition and deletion of records in related tables) can be reverted in a single script step. In other words, FileMaker has already done the hard work of remembering the state of the database before the user began their edits and knows how to restore the database to the previously saved state. We're simply taking advantage of the way FileMaker already works. Thus, the technique requires no global fields or script variables to store and restore data. It supports rollback for editing the parent record or creating, editing and deleting portal records. And since the editing layout is built using the actual fields, it supports all the richness of the relationships tied to the table occurrence on which the layout is built.
I'll be using the FileMaker 11 Faculty Staff" starter solution for this demo. I added a "Class" table to the file and created a 1:M realtionship between Faculty Staff and Class. I then added a read-only "comments" field and read-only portal of Classes to the Faculty Staff form layout, as shown. The idea is that each teacher may teach several classes and we need to associate these classes to the teacher.
When the user presses the "Edit" button, the "Class Selector" popup window is displayed as a modal dialog. The layout for this window is built on the Faculty Staff table occurrence, and displays the same Comments field (from Faculty Staff) and the same portal of Classes that we saw above, except that now both sections are editable. Note that we have provided the user "Cancel" and "Save" buttons. The user can select classes in the portal (using a simple drop-down list for this example) or even delete them. Comments can be added as free text in the text box.
Nothing fancy so far. The whole key to this technique, however, is that we also have an "On Record Commit" script trigger on this layout. The script trigger calls a script called "OnCommit". Here's that script (comments omitted for brevity):
Freeze Window Set Error Capture [ On ] # If [ $$allowCommit = 1 ] Exit Script [ Result: 1 ] Else Exit Script [ Result: 0 ] End If
Simple enough, right? Basically this script reads the value of the global variable $$allowCommit. If it is set to 1 (true), the script returns 1, meaning the database commit is allowed to proceed. Otherwise, the commit is "captured" and suppressed. Next we'll look at how we control the value of $$allowCommit.
The controller script for the popup window is called "ClassSelectorController". This is the script that runs when the user clicks "Edit" on the main layout.
Freeze Window Set Error Capture [ On ] # # this global controls whether commits are currently allowed Set Variable [ $$allowCommit; Value:0 ] # # commit record in parent window in case this user has it locked Commit Records/Requests [ No dialog ] # # launch the new task window and isolate this record New Window [ Name: "Class Selector"; Top: -9999; Left: -9999 ] Freeze Window Go to Layout [ “Class Selector” (Faculty Staff) ] Show All Records Omit Record Show Omitted Only # # check for record locks Open Record/Request Set Variable [ $error; Value:Get ( LastError ) ] If [ $error ] Close Window [ Current Window ] Show Custom Dialog [ Title: "Error"; Message: "This record is currently unavailable for editing. Please try again later."; Buttons:“OK” ] Exit Script [ ] End If # # finish window prep Show/Hide Status Area [ Lock; Hide ] Adjust Window [ Resize to Fit ] Move/Resize Window [ Current Window; Top: ( Get ( WindowDesktopHeight ) / 2 ) - ( Get ( WindowHeight ) / 2 ); Left: ( Get ( WindowDesktopWidth ) / 2 ) - ( Get ( WindowWidth ) / 2 ) ] # # Wait for user input: Cancel or Save Loop Pause/Resume Script [ Indefinitely ] If [ Get( ScriptResult ) = "cancel" ] Show Custom Dialog [ Title: "Warning"; Message: "All changes you have made in this window will be lost. Do you want to revert your changes?"; Buttons: “Cancel”, “Revert” ] If [ Get ( LastMessageChoice ) = 2 ] Revert Record/Request [ No dialog ] Close Window [ Current Window ] Exit Script [ ] End If Else If [ Get ( ScriptResult) = "save" ] If [ not IsEmpty ( Faculty Staff::z_validationErrors_ct ) ] Show Custom Dialog [ Title: "Error"; Message: "Please fix any errors before clicking \"Save\""; Buttons: “OK” ] Else # setting $$allowCommit lets the OnCommit script know that it's OK to allow the commit to OnRecordCommit trigger event to proceed # the act of closing the window will trigger the record commit Set Variable [ $$allowCommit; Value:1 ] Close Window [ Current Window ] Exit Script [ ] End If End If End Loop
The first thing this script does is initialize the value of $allowCommit to 0 (suppress commits). Then it opens the popup window, attempts to obtain a lock on the record, and pauses and waits for the user to click "Cancel" or "Save". Meanwhile, the user can add, edit or delete comments and related Class records. When the user clicks out of a field, FileMaker would ordinarily commit changes to the database, overwriting the previous data and preventing rollback functionality. But our script trigger script suppresses this native behavior. Only when the user presses the "Save" button (which passes the paramteter "save" to this script) does the script set $allowCommit to 1 and closes the window. FileMaker attempts to commit the data upon closing the window and this time the OnCommit script will allow it to proceed. On the other hand, if the user presses "Cancel", the controller prompts the user to confirm that they intend to cancel all changes that have been made since the window opened. If they confirm their decision, the single script step "Revert Record/Request" rolls back all the changes the user has made, including all edits and portal record additions and deletions.
That's it. No global fields. No storing values in variables. Just a single script trigger with that calls a very simple script to control when the commit happens.
Just for fun, I've included some custom validation using an unstored calculation field, illustrating the power of being able to use the actual fields on the layout, rather than global fields. If the user selects more than 3 schools, an unstored calculation generates an error message in the window. If the user selects "Save" when the error exists, the controller detects the error, displays a dialog, and prevents the save operation.
I hope you see how easy it can be to provide true cancel/save functionality in your user interface. With FileMaker's built-in support for transactional processing and our ability to take control of the process via scripts and script triggers, we can provide this type of user experience with very little effort.