Back and Forward
Version: FileMaker 17
Tuesday, January 22, 2019
I've always been intrigued with how to mimic a web browser's back and forward buttons... as have many other developers. While I don't usually include them in a professional solution, I do find the exercise of creating an airtight historical navigation solution challenging. I prefer well designed navigation over history buttons. Therefore, I present this article as a brain teaser and not necessarily as a recommended solution. But, I'll let you make the decision. I'll go into why I don't think history navigation works, as well as the best ways to overcome shortcomings so you can make an educated decision for yourself.
The first thing you need to do is figure out exactly how a web browser's back and forward buttons work. It's not as simple as you might think so I'm going to take you through the research I performed on Safari. Other web browsers may differ slightly but they all have the same basic functionality.
Start by clearing the history in your web browser so we can start with a clean slate. In Safari, this can be found at the bottom of the History menu. Once you do this, the back and forward buttons should be dimmed and unresponsive. Next, type in a web site. As my example, I'll use the Database Pros web site so it's easier to walk you through an example.
When the web site loads, the back and forward buttons are still dimmed because there is no history except for the current site. Click on the Resources link on the databasepros.com home page and the back button is now functional. However, the forward button remains unavailable until you click the back button. In other words, the forward button only moves through web sites where you clicked back through. As soon as you click another link or type in a URL, the forward button disappears again.
The back and forward buttons in a web browser walk linearly backwards and forwards through a path of your making. Once you retraced your path back to the beginning, there's nowhere else to go and the back button ceases to function. The opposite is true for the forward button. If you haven't clicked the back button or you've clicked through all the sites you backed through, there's nowhere else to go.
Browser history for the back and forward buttons is also window dependent. If you switch windows, the history will reflect how you've navigated in the current window only. History will also disappear as soon as you close a browser window. Let's not confuse the history of the forward and back buttons with the overall history that displays underneath the history menu. The history menu displays your entire historical travels, regardless of the window and has nothing to do with the back and forward button functionality.
History of What?
When you design a back and forward button, you have to decide what you want to track. I'm going to start off with a history of layouts visited because it's the most helpful and fairly bullet proof. Tracking files, windows and records is much more prone to errors so I will only touch on these subjects.
When I first started programming this solution back in the FileMaker 3.0 days, I used a global repeating field. When variables were introduced in FileMaker 8.0, I started using a global repeating variable. Repeating values are great cause they can hold multiple values, much like an array. Just point to the repetition you want and you can grab the value. A calculation call to a global repeating variable named $$Back storing the history of layout numbers might look like this:
FYI: An array is a data structure that contains a group of elements.
$$Pointer should contain the value of the repetition being retrieved. It is incremented each time a new layout is navigated and decremented each time a layout is backed over. It's important to note that a local variable with a single dollar sign ($) cannot be used in this solution as the history needs to persist beyond the script that declares and/or modifies the variable.
While this was easy to program, it created a bunch of clutter in the Data Viewer. Each time I navigated to a new layout, a new global variable repetition would appear in the Data Viewer. It's sloppy programming to clog up the Data Viewer with so much noise, especially for just one feature. Imagine scrolling up and down through tens or even hundreds of variables just to find the field or variable in the script you are debugging.
A Better Approach
My second idea was to track the layout history in a return-separated list. Return-separated lists are easy to create using simple concatenation and parse with Value functions that it's always a good alternative over repeating values. It gets a little more complex to remove values but nothing that will cause any hair pulling code.
FYI: Just about every function that returns multiple values does so in a return-separated list. This makes parsing return-separated lists a very important skill to have. Become familiar with the Value functions of GetValue, RightValues, LeftValues, MiddleValues and ValueCount.
Let's start with the concatenation of the layout numbers. It's a single line script:
Set Variable [$$Back; Get(LayoutNumber) & Case(not IsEmpty($$Back); "¶" & $$Back)]
This is an example of the Append technique where the target field is referenced inside the formula, allowing the existing value to remain instead of being replaced. This technique allows layout numbers to be stacked on top of each other, separated by a return character.
The Case statement prevents an extra return from being added onto the front of the stack. It's best to keep return-separated lists in a standard format with returns between each value only. Problems will occur with this technique if a rogue return character is left at the beginning so consider yourself warned for this technique and others. Be precise!
NOTE: Returns are represented by the pilcrow character (¶) in a calculaton formula. Quotes are not needed around a single pilcrow character in a formula but I continue with the practice so all static text is designated by surrounding quotes. It just makes it easier to recognize where text strings begin and end.
What turns out to be more difficult is when to run this script. My first thought was to attach it to any button that allowed me to navigate to another layout. This approach is ill advised since you will need to remember to attach a sub-script every time you create a script that navigates. If you're anything like me, this is gonna to lead to a slough of bugs simply because you forgot to attach the sub-script. It also makes attaching a single Go to Layout script step to a button impossible. And, forget about manual navigation!
In this case, a script trigger is the best solution. I won't have to remember to attach it to each layout because I always duplicate an existing layout when I create a new layout. But, what script trigger to choose? There are two possibilities: OnLayoutEnter and OnLayoutExit. Both seem to be reasonable choices, accomplishing the same task. The issue occurs when trying to restore the previous layout by clicking the back button. While we haven't programmed this button yet, we still need to consider how it will work.
For example, if a user clicks a button on layout number 1 to navigate to layout number 2, OnLayoutEnter will place a 2 on the stack. If the user then clicks the back button, the only layout on the stack is 2, causing the script to do nothing. On the other hand, if OnLayoutExit is used, the previous layout of 1 will be placed on the stack and the back button will function as expected.
The Back Button
The back button is a little more complicated than simply restoring the history of layouts. Let's program it in chunks so you can more easily understand why certain pieces of code need to be added. The core of the back button is to grab the value at the top of the stack and visit that layout so we'll start there:
Go to Layout [GetValue($$Back; 1); Animation: None]
The formula in the script step above is entered into the "layout number by calculation" option. Don't mistake this with the "layout by name" option! Once the layout is visited, the value needs to be removed from the stack by grabbing all the values except the first one:
Set Variable [$$Back; Value: RightValues($$Back; ValueCount($$Back) - 1)]
The RightValues function simply grabs everything but the first value in the stack and overwrites the $$Back values. This occurs because the Append technique, mentioned previously, is not employed in this case.
You might have thought it would be easier to remove the first value from $$Back without overwriting but it turns out FileMaker doesn't work that way very easily. We'd need to use the Substitute function, pass it the layout number and be careful not to substitute similar value like 1 and 10. I know it seems backwards in your head but this is truly the best way to remove the value.
Don't forget about the forward button! Before removing the layout at the top of the stack, it has to be moved to the forward stack.
Set Variable [$$Forward; Value: Get(LayoutNumber) & Case(not IsEmpty($$Forward); "¶" & $$Forward)]
Here's the entire script in the proper order:
But, we're still not done. There needs to be some way to disable the OnLayoutExit script trigger when navigating via the back button. In other words, no navigation history should be recorded when walking through the history. All that needs to be done is to transfer the back layout to the forward stack. The easiest way I've found to do this is to set a flag while the back button script is running. Look at the $$Nav global variable at the beginning and ending of the following script:
NOTE: I've also included a simple dialog to let users know when there's no more history to navigate.
Now look at the modifications to the script that stores the history when navigating:
Set Variable [$$Back; Get(LayoutNumber) & Case(not IsEmpty($$Back); "¶" & $$Back)]
So, whenever the back button is clicked, it temporarily turns the script trigger tracking history off by testing if the back button is being clicked. This trick is handy anytime you're running a script with navigation that shouldn't be recorded in the history. For example, unbeknownst to a user, a script might quickly navigate to another layout to establish context, set a value and return without ever displaying the layout.
The Forward Button
Luckily, moving forward through history is almost exactly the same as moving backwards. Just duplicate the back button script and change all the references from $$Back to $$Forward:
As I said previously, tracking and restoring a history of visited layouts is fairly safe. In other words, it would be hard for a user to break the history trail of layouts. Even if they did, it wouldn't cause that much trouble since there would just be a small hole in their navigation history. The biggest issue I'd like to cover is what happens if the user closes the current window. Should the history stay in the remaining windows? If you're trying to mimic a web browser, the history should be erased.
This is not hard to do with a script trigger that runs OnWindowClose but what if the user is trying flipping between windows. How do you determine which history to follow? How do you even track a separate history for each window? It's not even possible to track window changes with native FileMaker features. I'm sure some industrious developer could solve the problem with JSON but it doesn't seem very practical. It's always important to keep things simple so you don't have to ask for your client's first born as payment.
It all comes down to how much time do you want to spend plugging holes with crazy code. Why not just use some well designed navigation that allows your users to easily move back and forth between point A and B. It's unlikely they need a solution wide back button but just a conscientious developer who pays attention to what his client needs.
Using the same technique for tracking layout history, the existing scripts can be modified to create and edit return-separated stack of records visited. Instead of the Go to Layout step with the calculated layout number option, the Go to Record/Request/Page with the calculated record number is used. Since I don't think this is a viable solution, I'd rather discuss the shortcomings rather than show the exact script. However, here's the bit that would represent the back button, using the calculated record number option:
Go to Record/Request/Page [With dialog: On; GetValue($$Records; 1)]
The biggest roadblock to navigating a history of records is sorting and finding. Since the Get(RecordNumber) function returns a value representing the current record in relation to the other records, once the order of the records or the found set is changed, the record number is no longer relevant. You might think the best idea is to store a serial number and then use a Go to Related Record or Perform Find step to locate the historical record. This works fine even when the record order is changed but always keep or restore the desired found set, creating user confusion.
I guess you could try storing the sorts and finds also and reconstitute them when needed. The only problem is sort orders can't be preserved on the fly with a script. While finds can be stored and restored with a variety of methods, it often involves complex code and/or slow performance. The more you try to fill the holes in the technique for restoring record history, the more complex the solution becomes. In my opinion, it's just not worth it.
File and Window History
Again, file and window history can easily be preserved with the techniques discussed already. The big difference is the window and file names have to be stored instead of a number. It makes your lists of values bigger but that's not really the issue. The problem turns out to be how to determine when someone is switching files or windows. There's no script trigger that tracks mousing to another window.
If you decide to require buttons to move from window to window and file to file, you still have a problem of how to restore the correct window or file. The Select Window step can select a window by name but window names can change. I often change window names based on the current record using a combination of a calculation and script trigger. I'm not willing to give up adaptive window naming to accommodate a back button feature that will likely be rarely utilized. If you're really adventurous, you could track and restore windows by ID but this is starting to get ridiculous.
The same is true for files. You can use the Open URL script step to open a file that was in the history but was closed. The code would look like the following:
Open URL [With dialog: Off; "fmp://www.databasepros.com/" & GetValue($$Files; 1)]
That's right! Even though you can specify a variable path with just about every script step that looks at files, the Open File script requires a data source which can only be specified manually through the script interface.
What's Right for Me?
All I can tell you is what works for me. You have to decide for yourself if I'm wrong or right. While I consider tracking a history of layouts a semi-viable solution, I rarely implement it in a professional solutions unless my client insists. While there are very few holes in the layout history solution, I just don't like the addition of too many script triggers since they tend to conflict. I'd rather figure out the way the solution is used by my customer and program in the navigation to get them back and forth between the layouts they commonly use, rather than make a back button across the entire solution. And, you know how I feel about tracking records, file and windows... it's just not solid enough to be a viable solution. But, you be the judge.
John Mark Osborne
Example File (available to patrons at $5.00 per month): Download here if you are a patron
This blog is completely free. Please support it by clicking on one of the advertisers at the left side of the window or becoming a patron. Thanks so much!