Blogs

Blog Tags Help

Enter a tag to filter the current view

All entries tagged with sntt

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

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

Purty charts in Domino

Tim Tripcony  

If necessity is the mother of invention, boredom must be the weird aunt that has 87 cats and always smells like cheese.TM

I was going stir crazy here in my hotel room, so I started playing with the XML/SWF Charts library I stumbled upon the other night. Here's a sample of what I came up with. That's live data, generated by 11 lines of code (not counting variable declarations). The sample database contains the chart library (comprised of various SWF files stored as file resources), an example page to demonstrate the markup syntax for embedding the main SWF, and an agent to demonstrate generating the source XML. In this case it's navigating the referrers by date view in my blog. Nothing too fancy in this example, but this library allows all sorts of wacky output formats. Check out the gallery to see some of what it's capable of.

(cross-posted from TimTripcony.com)

Create, edit and delete without agents

Tim Tripcony  

I'm really becoming a broken record, eh? Or, for you kids in the audience, a degraded MP3 file. By request, I put together a full demonstration of how documents can be created, edited, and deleted using AJAX, all without the use of any agents. You can try it out here. And you can download the demo database here.

Previously, I described how documents can be created and edited in a Domino database without calling agents. This demo combines the two - as well as deletion of documents - using a simple contact manager as an example. The UI is very primitive, but hopefully it illustrates the premise.

In this example, contact information can be entered and then posted to the server without a page refresh. The function that posts the document (thereby creating a new record in the database) in turn calls the function that loads the view data (thereby displaying the newly created record). The new data could of course be inserted into the table immediately instead of reloading the view, but this approach was intended to illustrate how much faster document creation is when agents aren't involved. Reloading the view also ensures that the new record will already be sorted correctly when it's added to the table. In addition to document creation, each view row includes a link to allow editing and deletion of existing documents.

Please feel free to post any questions or suggestions you may have about this... I was planning to write up a far more detailed explanation of how all of this is tied together, but I'm suddenly very sleepy.

Oh, one thing I should mention: the reference to Event.onDOMReady at the end of ajaxeditor.js isn't part of the core prototype.js. It's an extension I found on Vivabit. This allows functions to be defined in external js files that would otherwise need to be called inline or via window.onload... for example, functions that you want to run immediately, but not until some DOM element that the function is going to muck with is actually ready to receive said mucking. The primary advantage of this extension is that it triggers the functions as soon as it is safe to do so, instead of waiting for the entire page to load (images, for example).

(cross-posted from TimTripcony.com)

SnTT: using the WebBrowser control to view attachments inline

Tim Tripcony  

If you've ever set a form to launch the first attachment upon document open, but wished (or received requests from users) that it wouldn't open a separate application window, there's another option. Nathan and I were experimenting with this, and here's what we came up with:

In the standard perweb.nsf there is a form called "WebBrowserForm", which contains an embedded control of class "Shell.Explorer.1" with its name set simply to "Browser". When you open a URL within Notes, you're essentially creating a new document using this form that, during the PostOpen, gets a handle on the browser control (  Set browserobject=uidoc.GetObject( "Browser" )  ), and tells it to navigate to the web page (  browserobject.navigate doc.url(0)  ). Although I've been unable to find a way to create this control using the Create > Object dialog, you can copy it from the original form into your own applications and take advantage of its ease of use.

The sample database is just a basic e-book reader. Its interface is a horizontal preview - a two frame collapsible frameset, with a table of contents view in the left frame and an empty preview frame on the right side. So, just like the standard mail Inbox, when a document is selected, if the preview frame is expanded, the document is loaded in preview mode. But here's where it gets a wee bit fancy.

The form's QueryOpen extracts whatever file is attached to the user's temp directory. The PostOpen gets a handle on the WebBrowser control, but instead of telling it to navigate to a URL, it navigates to the extracted file's path, which - if the filetype is one that I.E. can load inline - launches the file... inside Notes. So if the file is a PDF, for example, Adobe launches inside the preview pane instead of opening a separate application window just for itself. And, since the browser control is set to fill the window, the external application automatically scales to the preview pane. Imagine Nathan's Sesame Street demo, but a full-fledged Adobe Reader window off to the right instead of Big Bird.

This works not only for PDF's, but for text files, Office (if installed), and of course, good old HTML files. Even Quicktime movies and MP3's... AVI's and MPG's launch Media Player in a separate window. Basically, any file that, if clicked in a web page, would be loaded within the browser instead of triggering a download prompt or launching a separate app can be "previewed" in this manner. It's basically the standard attachment viewer on steroids. NOTE: viewer, not editor. If you open a Word doc, it'll be fully editable (assuming no constraints on the original file), but any changes you save are written back to the extracted copy in the temp folder, not to the copy attached to the Notes document. You'll probably want to make this abundantly clear to your users if you implement this approach in your own applications, particularly if they're now addicted to the standard in-place attachment editing capability.

(cross-posted from TimTripcony.com)