Blogs

Blog Tags Help

Enter a tag to filter the current view

Main  | Next

Every time you use window.open, God kills a kitten

Tim Tripcony  

I was booking some travel recently and couldn't select a date on one of the travel sites because they were using window.open, so my browser blocked it as a popup. They weren't trying to sell me anything I wasn't planning on buying anyway, they weren't trying to install malware, it was just a date picker. But I couldn't use it without modifying my browser's security settings (or switching to a less secure browser). It's 2008... window.open? Seriously?

So, for any of you who still use window.open to display a date picker, or a comment form (see Andrew's blog as an example of the right way to do this), or... well, anything else... and the only reason is because you asked someone for an alternative and were simply told, "duh, use a positioned div" - but not told how - hopefully this post will show you how easy a better alternative can be.

To display something that looks like a popup but really isn't, all you really need is:

  • a div that hides until needed and has a higher z-index than the rest of the page
  • code to show and hide that div, and position it where you want it to display
What's a z-index? If you use layers in your Notes client development, you're already familiar with this concept: think of your user interface as existing in three-dimensional space. The x axis is horizontal, the y axis is vertical, and the z axis measures the conceptual distance between the "front" and the "back" of what's available to the user. Unlike x and y, however, which are typically measured in pixels or some other unit associated with the available width of the user's screen, the z index is theoretically limitless, and is simply identified by an integer. So on a web page (or a Notes layer), anything with a higher z-index is "closer" to the user; anything with a lower z-index is "further" away, and therefore displays "behind" the higher elements. Mozilla.org has a great tutorial on this concept if you want to dig deeper, but for the moment, just remember that the element with the highest z-index displays on top of everything else.

Here is an example (also available for download) of using a "positioned div" to display additional content that you might otherwise launch in a separate window with window.open. To enable this, start out by creating a div somewhere in your page markup, give it an id and a class (in my example, I'm using "pseudoWindow" as the id and "layerwindow" as the class), and define some CSS for that class:

div.layerwindow {
  background-color: #eeeeee;
  border: 3px solid #abcdef;
  display: none;
  overflow: scroll;
  padding: 3px;
  position: absolute;
  z-index: 2;
}


This causes that div to hide initially, as well as defining how it will look when it's no longer hidden.

Next, you'll need some JavaScript code for showing the div, positioning it, and hiding it again:

var LayerWindow = function(){
  var getViewPort = function(){
    var viewPortWidth;
    var viewPortHeight;
    var vYscroll;
    if (window.innerWidth) {
      viewPortWidth = window.innerWidth;
      viewPortHeight = window.innerHeight;
      vYscroll = window.pageYOffset;
    } else if (document.documentElement && document.documentElement.clientWidth) {
      viewPortWidth = document.documentElement.clientWidth;
      viewPortHeight = document.documentElement.clientHeight;
      vYscroll = document.documentElement.scrollTop;
    } else {
      var bodyTag = document.getElementsByTagName('body')[0];
      viewPortWidth = bodyTag.clientWidth;
      viewPortHeight = bodyTag.clientHeight;
      vYscroll = document.body.scrollTop;
    }
    return {
      w: viewPortWidth,
      h: viewPortHeight,
      yScroll: vYscroll
    };
  };

  return {
    open: function(id, options){
      var vp = getViewPort();
      if (document.getElementById(id)) {
        overlayer = document.getElementById(id);
        overlayer.style.width = options.w + 'px';
        overlayer.style.height = options.h + 'px';
        overlayer.style.top = (vp.yScroll + parseInt(vp.h / 2, 10) - parseInt(options.h / 2, 10)) + 'px';
        overlayer.style.left = (parseInt(vp.w / 2, 10) - parseInt(options.w / 2, 10)) + 'px';
        overlayer.style.display = 'block';
      }
      return false;
    },
    close: function(id){
      document.getElementById(id).style.display = 'none';
    }
  };
}();


Finally, update whatever would be triggering the window.open to instead just display the hidden div:

<a href="#" onclick="return LayerWindow.open('pseudoWindow', {'w':300, 'h':250});">Open layer window</a>



There are a multitude of ways to add further elegance and functionality to this (and nearly every widely used JavaScript framework has their own implementation) - for example, loading the content of the hidden div via AJAX prior to displaying it instead of using pre-populated content - but hopefully this will give you a head start toward eliminating your own use (if any) of window.open... please, think of the kittens.

0.01 nanoseconds

Tim Tripcony  

Ever since Peter von Stöckel posted about using NotesDatabase.GetProfileDocCollection to quickly create an empty NotesDocumentCollection, I've been meaning to benchmark it against a database with hundreds of thousands of documents, but keep forgetting to. Earlier tonight I had a reason for needing an empty collection, was reminded of the benchmarks Nick Wall provided on Peter's post (tested on a Domino 6.5.3 server), and realized that my web server log would be an ideal candidate to see if the speeds are similar in the Notes 8 client... at the time, it contained 541,269 documents. Here's how it turned out:

 db.Search
({@False}, Nothing, 0)
db.Search
({@False}, datTomorrow, 0)
db.GetProfileDocCollection
( "EmptyCollection" )
13.0740.1910.04
23.2950.2010
33.1040.2010
43.1250.20
53.0540.2010
63.2050.190.01
73.0840.20.01
83.1150.20
93.0940.20
103.2250.20
Average
(in seconds)
3.13750.19840.006
Per document
(in nanoseconds)
5.97650.36650.011


Still scales pretty well. Let me know if you've done any benchmarking against a huge database that also contains a significant number of profiles... my DomLog only contains one.

PNG support in the Notes client? Maybe they should start with full support for GIF...

Tim Tripcony  

A couple of disturbing observations:

First, in a 2006 post from Julian about animated favicons, the little animated banana dances at the exact same tempo as the song "Your Star" by the All-American Rejects. Try it... it's mesmerizing.

But secondly, to explain the title of this post, I noticed an odd behavior related to image resources in the Notes client: GIF support is incomplete. ICO files are actually stored in the same format as GIF, just with a different extension. So another approach to creating animated favicons is to simply rename an animated GIF to .ICO. That's how Dean Edwards created a favicon that periodically blinks. Theoretically, then, we should be able to download any site's favicon, rename it to .GIF, import it as a file resource, and then display it in the Notes client. No such luck.

Since real browsers (read: Firefox, Safari, and Opera... of course, as always, IE fails) don't distinguish between the two formats and simply trust the server-supplied MIME type, navigate to the same image resource via a URL and the image displays as expected. But try to reference the same image resource anywhere in the Notes client (or even just preview it in the image resource list), and you get nothin' but gray.

Sorry, Bruce, I tried...

JSFactory

Tim Tripcony  

Back when I was experimenting with XIDED,  I wrote a utility database to mitigate my primary annoyances when dealing with JavaScript - in Domino in particular, but also a few general pet peeves (you'll probably notice that the recently released beta of 8.5 addresses the first of these):

  • The JavaScript editor in Domino Designer is an IDE pretending to be Windows Notepad, failing in that imitation only when it occasionally maintains the current indentation from one line to the next.
  • As the complexity of a web application increases, so does the hassle of maintaining all of its application-specific JavaScript in a single source file... the ink was starting to rub off on my Ctrl and F keys.
  • Conversely, if you split out your source into a bunch of smaller, more maintainable files, it's a pain to merge them all back into one, so the temptation is to just throw a script tag in for each one, which tends to negatively impact page load times (even if the browser is pulling each from the cache, it still has to check each one separately to see if it's in the cache, so it might not be downloading each again, but there's still a performance hit). But then you still have to refresh the file resource for each that has been updated.
  • At one point, the combined uncompressed size of all of my source (including the frameworks I was using in addition to my application-specific code) was just over 1.5 MB, which is ridiculous. But minification and compression added two additional steps to my "build" process.
So out of pure old-fashioned laziness, I wrote what I now call "JSFactory": once I've defined a project version document (to allow multiple versions to be maintained in different directories), listing the location of the source files in the order that they should be combined, an output location for the combined file (and - optionally - compressed, and/or gzipped... since I've configured my server to deliver gzipped content), and a file resource element to update, I can refresh that element with a single click. So I can maintain lots of little source files, doing my coding in Aptana, and then update my application without even opening Designer. It works in Windows and Linux (and presumably Mac, though I don't have one to verify that).

So if I've had this for years, why am I just mentioning it now? Well, two reasons. The first is that Chris Toohey's SOTU Sidebar Widget Demo inspired me to add a similar widget to JSFactory, which I did yesterday:



The second is that I decided to try an experiment: in a singular (and possibly only) departure from my traditional "give everything away" approach, I'm offering this for sale. As its usefulness is restricted to a very specific niche task, and future versions of Domino may make it entirely obsolete (here's hoping, anyway), I'm only charging $9.99, and I didn't bother putting in a bunch of licensing restrictions; if you buy it, it's yours to use as you see fit: donate it to your whole team (assuming you have one), put it on as many workstations or servers as you wish, modify the source (yes, I left it open), etc. I realize this means that one person could buy it and just send it around to everyone else, but I'm not looking to "make my millions" with this... I've got a dream job. I just request that if you receive a copy without having purchased it and find it useful, consider buying your own license... it's the same price as most albums on iTunes.

SnTT: clickable URL's in a view column... in the Notes client

Tim Tripcony  

Bruce just pinged me with a dilemma: he wanted a way to to make URL's clickable when displayed in a view column from within the Notes client. I wracked my brain momentarily, and the only idea I could come up with was using the Inviewedit trick that I first saw Chris Blatnick allude to in his CoolMail demo (explained later in greater detail). This isn't ideal, of course, as that technique requires that you click an icon column - perhaps immediately adjacent to the URL - not the URL itself...

...or does it? One of the parameters included in the Inviewedit event is Continue, something we're typically used to toggling in other events to prevent that event from completing. For example, you might perform some validation in the QuerySave and toggle Continue to False if the validation fails, or toggle it to False in the QueryOpen if you want to load a DialogBox when the user tries to open the document using the standard form. So what happens if Continue is False in the Inviewedit? Even if the column in question is a standard text column (in other words, not displayed as an icon), if Continue is immediately toggled to False, the column never becomes editable! But the event has already been triggered, so you can still determine the row that was clicked (via Source.CaretNoteID), and can therefore get a handle on the document, retrieve the URL, and navigate to it. Voila:

Let Continue = False
Dim workspace As New NotesUIWorkspace
Dim session As New NotesSession
Dim dbCurrent As NotesDatabase
Dim docSelected As NotesDocument
    
Set dbCurrent = session.CurrentDatabase
Set docSelected = dbCurrent.GetDocumentByID(Source.CaretNoteID)
Call workspace.URLOpen(docSelected.GetItemValue("URL")(0))

Yellowcast Episode 3

Tim Tripcony  

Episode 3 was released earlier this evening. Just a quick one (by comparison) - only 36 minutes - but hopefully informative and mildly entertaining. Don't let the title throw you... all is explained in the Community News segment.

Pimp My Fields

Tim Tripcony  

Back in December, Nathan asked me for a quick proof of concept of using Decepticon in conjunction with ExtJS to provide easy styling of Domino forms. It was even easier than I thought it would be, so a couple days later (actually, I think it was the same night... can't remember now) I sent him a link to an example implementation... and promptly forgot all about it until he mentioned recently that somebody else was interested in the same functionality. I figured I might as well clean it up a bit to allow it to be even more modular. The result is a subform that, when added to any form, automatically renders all fields in their Ext.Form equivalent. And... it works with or without Decepticon. It's just easier if you do use Decepticon.

To see this in action, this is how a fairly typical form might look when developed specifically for Notes client usage. Pretty standard, right? Two-column table, one column for field labels and another for the fields. This is what the same form looks like when the subform is included. The table still looks crappy, of course, so you'd want to do some additional cleanup to make the form itself look nice, but for the actual fields, here are a couple of the "easy wins":

  • Date-only date fields get a date-picker... not 'cause you flagged them somehow, just because the subform knows it's a date field.
  • Similarly, time-only fields get a time-picker. NOTE: if you've specified that the field should display date and time, this still gets confused and renders a boring text input.
  • Dialog lists, comboboxes and listboxes get converted to the fancy Ext type-ahead combo thingy. Ironically, "Allow values not in list" currently has the same limitation as above: boring text input.
  • If you populate the "Help Description" property for a text field, it renders it similarly to how the "Field Hint" property behaves in Notes: the description displays as a faded background to the field, but unlike in Notes, if you blur out of the field when it has no value (even if it used to), the hint returns. In retrospect, I probably should have pointed this to the "Field Hint" property instead, so feel free to tweak the XSL accordingly.
  • Rich text fields get rendered as full-blown WYSIWYG HTML editors. The usefulness of this feature is the most questionable of the bunch, as you probably don't want your rich text fields stored as HTML. On the other hand, if you're already setting your rich text to "Store contents as HTML and MIME", this may be a perfect fit for you. In all honesty, I haven't used any of this in a real-world application yet, so I'm curious to hear what your results are if you do.

The example database can be downloaded here.

Guess What I'm Talking About

Tim Tripcony  

There's a software platform that I'm a huge fan of, as most people know, but sometimes it can be frustrating to use, for the following reasons:

  • The usability and interface have come a long way over the last few years, but it's still not quite up to the standard set by Microsoft and Apple interfaces.
  • It's more secure, reliable and powerful than anything else out there, but it seems like every fix I install breaks 3 other things that I need to just work.
  • Most importantly, while awareness of the platform is gradually increasing, it's primarily still restricted to geeks who are passionate about the platform. So occasionally I see a piece of software or hardware device that integrates with it that didn't used to, but the implementation tends to be subpar in comparison to what is available for integration with Microsoft. But more often than not, it gets ignored entirely, and the software/device vendor just provides a Microsoft integration point.

So, obviously, I'm talking about Linux... wait, what did you think I was referring to? Oh.... Bummer.

Lotus Advisor: Beyond IBM Lotus Domino AJAX

Tim Tripcony  

It's always nice to find out that something I've written proved useful to a fellow Yellowbleeder. Back in '06, I described an approach to performing @DbLookup / @DbColumn operations in AJAX applications. Sasha Oros has now written an article for Lotus Advisor in which he explains how he has significantly expanded upon that technique. It's a fascinating article, and a great example of (in my opinion) well-written JavaScript, and the article includes a downloadable example database. Although a subscription is required to view the article, online subscription is free.

yellowcast

Tim Tripcony  

Last Friday evening I had the pleasure of co-hosting with Chris Toohey episode 1 of a brand new podcast: Yellowcast. Apparently we're both more talkative than I realized: what was slated to be an hour episode ballooned to 2 and a half hours, and was subsequently trimmed to just under two hours... so you may want to listen to this one in installments. We covered (at great length) topics ranging from my favorite plaything - DXL - to the new Blackberry client for Lotus Connections, as well as a new community project we've been starting to architect in our spare time. I'm sure that our future episodes will be far shorter, but despite the length of this one, we had a blast recording it. If you have time, give it a listen and let us know if you have ideas for future episodes. At the moment we're planning on making this a weekly thing.

By the way, thanks to Bruce for the shout out... with luck, in time our ramblings will be half as polished as yours.

Magic Button

Tim Tripcony  

For as long as I can remember (possibly 2001) I've always maintained some variation on a SmartIcon toolbar button that a former coworker called the "magic button": it allows new fields to be added to a document and existing fields to be overridden or deleted from the document, either while editing the document or while it's selected in a view. I've seen various approaches to this basic concept over the years, and each time I install Notes on another computer, I either consult the Google to find one I've used before or just reconstruct it from memory. It finally occurred to me that if I just posted the one I'm currently using here, not only would it be easier for me to find later, but some of you might find it useful as well. So here's the current incarnation of said magic button:

targetField := @Prompt([OkCancelEditCombo]; "Select Field"; "Select a field to override:"; ""; @DocFields );
updateTypes := "Text":"Number":"Time":"Delete Field";
updateType := @Prompt([OkCancelList]; "Select Type"; "Choose a type or action:"; "Text"; updateTypes );
@If(updateType = "Delete Field"; @Return(@SetField(targetField;@DeleteField)); "" );
newValue := @Prompt([OkCancelEdit];"New Value";"Enter the new value:"; @Text(@GetField(targetField )));
newTypeValue := @Select(@TextToNumber(@Replace(updateType; @Subset(updateTypes;3);"1":"2":"3" )); newValue; @TextToNumber(newValue); @TextToTime(newValue));
@SetField(targetField; newTypeValue)

I'm sure you can decipher the above, but here's what it does:

  1. Asks you which field you want to override. Type in a new one if you're adding a field. I've seen other versions where something like "---NEW---" is included in the field list prompt, and if you select that, it asks for the new field name... figured I might as well skip that extra step by just using [OkCancelEditCombo].
  2. Asks what data type the new value will be (text, number or time), with an option to delete the field instead - in which case the formula just deletes the field and exits.
  3. Asks what the new value will be, defaulting to the current value (if any).
  4. If the new value's data type is not text, converts the value to the selected type.
  5. Writes the new field value to the document.
This little button has saved me oodles of time over the years...

PercentRem.com

Tim Tripcony  

I just bogarted registered PercentRem.com. At the moment it's just a default instance of DominoWiki (although I did update the logo), but my current plan is to dedicate that wiki entirely to LotusScript. The language definitely has its limitations, but it's amazing what it can do when properly coaxed. My hope is to make this a living, breathing, centralized repository of tips, tricks, pitfalls, righteous hacks, and so forth. Let me know if you have any thoughts, ideas, suggestions, etc. Due to Ben's excellent versioning engine, I'ma just leave it wide open for the time being, so feel free to drop in any content that you feel would be a good fit. Over time I hope it will coalesce into a semi-structured reference on what the language truly can and can't do.

Cheap date

Tim Tripcony  

I just had an epiphany... or maybe just an apostrophe. I'm in the middle of some code that does some date matching (determines which document to process based on date information posted from a web application). I don't have control over the date format posted to the agent, and currently the format is [Month abbreviation][unpadded day number][day number suffix] - i.e., Apr15th. I already knew that passing "April 15th" to the constructor of a NotesDateTime object would create an instance representing [04/15/2008 12:00:00]. But did you know that "Apr15th" will too? As it turns out, it actually ignores the suffix: "Apr15bogus", for example, creates a date with the same properties. Always good to find these things out before I resort to string parsing...

Everything old is new again

Tim Tripcony  

As you may have noticed, I've recently become quite the Linux fanboy. Well, today I took it a step further. Yesterday my Dell Inspiron (which I'm using to post this) was running Windows XP with Ubuntu 7.10 running as a VMware virtual machine... today it's the opposite. Of the five computers in our apartment, the ratio is now 3 Linux (1 Ubuntu, 1 OpenSUSE, 1 Xandros) to 2 Windows... and one of those two isn't even mine; the other is Laura's Inspiron, and she hinted today that she might switch to Ubuntu soon too, since she uses exactly two programs: Firefox and OpenOffice - and she knows that both just come pre-installed with Ubuntu. Scratch that; I just remembered that she recently installed a third program: Aptana (which on Linux you don't install, you just unzip to a folder of your choosing, much like Eclipse on Windows). She's dabbling in PHP and had asked which IDE I would recommend. In short, we may soon be a Linux-only household; to the owner of the Latitude, don't worry... I'm leaving Windows alone on that machine.

Unlike on OpenSUSE, installing VMware on Ubuntu was a breeze:

apt-get install vmware-server

Only caveat: although it's free, you still need a valid serial number, and apt won't configure the package installation until you've entered one. Once installed, I spun up a new virtual machine (pointing it to an ISO of my XP CD), and in less than an hour had Notes reinstalled, complete with a data folder overwritten from the backup I'd saved to our NAS before converting. So Notes looks and behaves precisely as it did before. I decided to install Office 2003 because I loathe 2007. Nothing else... nice clean registry, so Windows is actually far snappier running as a VM than it used to be with all of the extra clutter.

On top of the base Ubuntu distro, I've added Skype, Eclipse, Aptana, and of course, Notes 8.0.1. When I tried to install it, the wizard window popped up, but it was just an empty panel. Mayhap I just didn't wait long enough, but after about five minutes, I killed it and just opted for the silent approach:

./setup.sh -silent -V licenseAccepted="true"

This installed Notes with Sametime, but without Symphony or the composite app editor. I'm actually not using Sametime at the moment; although it doesn't have all the fancies of Sametime 8, I actually prefer Pidgin (which is also pre-installed in Ubuntu) because it bundles all of my IM accounts (AIM, Yahoo, MSN, GTalk, MySpaceIM, and Sametime) into a single interface.

By the way, Jens Bruntt posted a great article last week on how he installed Notes on Ubuntu. The fix he outlined for the embedded browser isn't working for me yet (for instance, xulrunner didn't install in /opt, and including a reference to /usr/lib/xulrunner doesn't seem to fix the problem), but to be honest, I don't use the embedded browser much anyway. So far, the only thing I miss is iTunes, but between gtkpod and the Amazon MP3 Downloader, I'm pretty much covered on that front as well.

UPDATE: Designer works flawlessly in wine. My Windows VM is now sleeping peacefully, doubtless dreaming of a day when I'll come crawling back, telling it I can't live without it. Don't feel bad, Windows... it's not you, it's me.

SnTT: Ajax history manager

Tim Tripcony  

Julian's post about  JavaScript stack traces reminded me of something I wrote late last year and had been meaning to mention but never did.

Web applications have steadily become more interactive over the last couple of years, and have been evolving from a collection of pages to a sequence of interactions and events. On the one hand, this can be very positive for the user, as we're no longer forced to trigger an entire page refresh for every mouse click. On the other hand, one challenge this presents is that many users are still in the habit of conceptualizing site navigation and interaction in a context of page history; in other words, they expect to be able to click something (i.e. a button or a link), examine the result, then click the Back button and see what they saw before... and click Forward to return. This was already a problem before Ajax when POST requests were involved, but adding Ajax to the equation can compound this even more: by default, not only will clicking Back not show the user a previous state of the current page, in a "one-page application", it'll actually exit the application, possibly even your entire site.

So how can we get around this? Ironically, most workarounds I've seen (including the one that I implemented and am about to describe) make use of the HTML element that was often used to provide Ajaxy behavior before what we now consider to be "true" Ajax gained widespread use: namely, the iframe. When an iframe's location changes, an entry is added to the browser's history (in some browsers... more on that later), even though the location of the page containing it has not changed. As a result, clicking the Back button will simply return the iframe to its previous location without modifying the container's location. Similarly, once that's occurred, the history contains an entry in the forward direction, so clicking Forward at that point will again navigate only the iframe, still leaving the container location intact.

How does that help us? Chances are, those familiar with JavaScript's Function.apply() have already guessed where I'm headed with this. As I commented on Julian's post, a handle on a function can be passed as a parameter to another function. This is used all the time in Ajax requests to specify a callback function: once the request has been processed, the specified callback function is executed to respond to the result of the request. In this case, however, we're not associating a function with a request for remote data (or submission of data); instead, we're actually associating a function (and the parameters that should be passed to it) with navigation. Here's how it works:

  • Instead of calling the function directly, we pass a handle on the function (along with an array of parameters to call it with and an optional scope) to another function - AjaxHistoryManager.register(). For example:

    function sayHello (sender, recipient) { this.innerHTML = recipient + ', ' + sender + ' says hello.'; }
    AjaxHistoryManager.register(sayHello, ['Me', 'World'], document.getElementById('helloDiv'));


    NOTE: the lack of parentheses after the reference to sayHello is intentional; we're not calling the function at this point, just passing it as an argument.

  • The register() function creates an object to store the function handle, parameter array and scope object (which defaults to the passed function if no scope is specified), pushes that object to an array that stores all of the function calls that have been registered since the container page was loaded, and increments a counter.
  • Finally, register() sets the source of a hidden iframe to the URL of a Page design element (assuming you're using Domino; a very minor tweak to this would allow it to behave identically in PHP, ASP, JSP... pretty much anything), including a query string parameter indicating the current counter value.
  • The Page loaded in the iframe has only one job: it includes a script tag that (using computed text) passes the counter value to window.parent.AjaxHistoryManager.execute(); this function retrieves the object stored earlier and calls the associated function with the stored parameters, binding "this" to the correct scope object. The reason I almost never use "this" in JavaScript is that any number of factors can cause a function to lose a handle on the scope you originally intended it to have, which will cause any references to "this" to apply to the new scope. That's not a problem in this case, because we're calling .apply() on the function to be run, which allows us to explicitly specify a scope object. So, in the example above, "this" refers to the HTML element that was passed as the scope object. If the function you're registering doesn't have any references to "this", you can leave off the scope parameter entirely and just pass the function handle and a parameter array.

The result is that the registered function is still called almost immediately (depending on how long it takes your server to send the 94 byte overhead for the iframe Page), but the browser history now contains an entry associated solely with that event. Ergo, executing a series of events in this manner allows the user to execute them again (with the same parameters and scope each time) simply by clicking Back and Forward.

You can see an example of this behavior here (the sample database is also available for download). On that page, click the "Increment Counter" button a few times, then click Back and Forward (Alt+Left / Alt+Right also work)...  the displayed counter will increment and decrement based entirely on those navigation events. This works correctly in Firefox and I.E., but not Safari or Opera. In Opera, navigation does appear to update the iframe, but the stored function is not called; in Safari, clicking Back actually navigates the entire container page. In both cases, though, the function is still executed initially upon registration; in other words, your application will still work, but will not have the benefit of triggering previously executed functions again via navigation events. I still have never had to support either of these browsers within an enterprise context, but if anyone has ideas on how to extend this approach to fully support either or both, let me know.

One last disclaimer: this approach is best suited for "read-only" operations. For example, user interaction causes some change to a portion of a page (i.e. resorting data), and we want reverse navigation to "undo" that change. This approach gets a whole lot messier if a POST is involved: if the methods being executed are actually updating data in the database, then executing them in reverse order needs to revert the database content to its previous state - whether by resetting field values on one or more documents, or actually removing documents that were added by the executed method(s).

Main  | Next