Domino is good at lots of things yet awful at lots of other things. Ever since Notes databases were made available to the web browser the Domino server has been making a complete hash of some of the most simple tasks. It has been our responsibilty to improve the code generated and, in most cases, over-ride Domino's code with our own.
This article is going to look at an area of Domino code that need serious attention - view/document navigation. When you open a Domino view you hardly ever see all the documents, so a link to the next page is always needed. Similarly a link to the previous page is needed after you've moved to the next page. It's also nice to have next and previous links when you've opened a document - otherwise you have to return to the view and click on the link below/above the current document. Domino can create all these links for us failry easily, but it doesn't do a very good job of it. We need to address this ourselves.
What's the problem?
With lots of things Domino the fact that it's so easy to do most things is often a double-edged sword. The code generated does work but can be lacking in certain aspects. Consider navigating between pages of a view. The easiest way to do this is to create two Action Hotspots on the $$ViewTemplate. The formula is simply:
Or ViewNextPage for a next link. Here's what it would look like in Domino Designer:
As I said earlier, this works. So what's the problem? Well, let's look at the HTML it creates:
<a href="" onclick="return _doClick('80256F4200690CDB.de7341d87e5fbf1380256f4300397381/$Body/0.438', this, null)"><< Previous</a>
Now, most Domino database exist in an intranet environment where search engines don't trawl and we can pretty much rest assured that most browsers (or users) aren't disabled in any way.
On the Internet it's different. There's no common environment that all users share. Not only this but the search engine is king. It's imperative that you make the site as accessible as possible; both to users and search engines. In some instances accessiblilty is even a legal requirement!
A similar problem exists at the document-level. Here we can create similar links to the previous and next document using formula like @Command([NavigateNext]). The code produced in this case isn't as bad but still problematic. It creates a link to a URL like this:
When you click this link the server returns the browser a HTTP status code of 302. This tells the browser that the document it is looking for is located elsewhere and passes it the correct URL. The URL in this case would be that of the next document.
It's not known for sure, but it's thought that search engines - Google in particular - don't like this kind of link. They can be used to trick the likes of Google in to wrongly indexing or scoring a page's importance, based on its PageRank.
Accessibility aside, there's another less serious problem that is worth mentioning. It's more of a logic thing and is due to Domino not behaving as you might expect it to. When you first open a view or you open the first document in a view you will always see the "previous" link. In the case of the view the link will simply reload the page over and over. With the document it will return the user to the view. The same goes for the last page of the view and the last document. There's also a oddity when moving from one page to the next in a view. The next page always starts with the last document from the first page.
You might think I am just nit-picking now, but this is the level of control that many users/customers demand of a website nowadays. Gone are the days where we could simply impress with any old code. The fact that it was in a browser was impressive enough. Now we need to cater for every sngle whim of the customer.
Solving The Problem:
Don't despair, the problem can be solved. But, as I said, we need to do the coding ourselves.
In both cases the solution involves using a WebQueryOpen Agent for the document and the $$ViewTemplate. If you really don't like this idea, which I don't particularly, then this solution probably isn't your cup of tea. It's taken me a long time to overcome my irrational fear of using Agents in this way. After some self-councilling I've given in and realised that the benefits far out-weigh the performance hit (if there is one at all).
Using an agent we can get a handle on the view and work out how many documents are in it. Using this information we can then use some simple @Functions on the form to create the proper links. We can also tell the user which page they are on and how many there are in total.
At the document-level the agent code locates the current entry in the view and looks for entry before and after it. Based on this information it knows whether to display previous or next links. Not only this but it can provide advanced information about the document, such as their titles.
A Quick Demo:
This article will end with a demo database that you can download. In the mean time let's see what it looks like. Here's the normal view with Domino's non-logical next and previous links. Now look at the improved view where there's no previous link on the first page, nor next link on the last, and there's a counter to tell the user which page they are on. Now look at a document from the view and the links at the bottom. Nice aren't they.
Not only does all this make it easier for our user but the links are now "real" links. Google et al will now be able to get to all the information on the site.
Coding The View:
With the view the agent simply needs to tell us how many documents are in the view in total. We can do the rest using @Functions on the actual "form".
To find the total I first tried the following code:
Set nav = view.CreateViewNav()
Call doc.ReplaceItemValue( "Total", nav.Count)
It worked. But adding thousands of documents to the view soon highlighted the performance problem. The same was true of using a NotesViewEntryCollection. After some testing and re-writing I found the quickest method was to use the EntryCount property of the view.
Call doc.ReplaceItemValue( "Total", view.EntryCount)
However this is an R6 only feature and doesn't cater for categorised views. Sometimes we will still need to use the .Search() method of the database, passing it the same search formula as the view uses. Like so:
Set col = db.Search("Form='article'", Nothing, 0) Call doc.ReplaceItemValue( "Total", col.Count)
If your view is categorised you can make this formula as complicated as it needs to be. Overall this seems to be the best way to get the count. Some very un-scientific testing led to the following results when using different approaches to find the total.
So, with the "Total" field on our $$ViewTemplate form, which holds the count of all documents, we can go on to create our links.
To work out whether to show links and what they should point to we need a few more fields. First a numeric field called "Start". This field checks for &start=X in the URL. If that parameter is there its value is used. If not it defaults to 1. Then we need another numeric field called "ShowPerPage", which tells us how many documents each page of the view contains. This checks whether there's a URL parameter called Count. If there is its value is used. If not we need to hard-code a number here that is also the actual rows the view displays. In this case I chose 5 and I've set the "Lines to display" property to the same number in the Embedded View's properties, as below:
Now we can work out which links to show and what they should point to. First the "previous" link. This is simply a Computed Text area with the following formula:
@If(Start<=1; @Return(" "); ""); newstart:=@If(Start-ShowPerPage<=1; 1; Start-ShowPerPage); "<a href=\"super/?open&start=" + @Text(newstart) +
"&count=" + @Text(ShowPerPage) + "\">« Previous</a>"
The first thing we do here is check whether it's the first page or not. If it is then the text area shows nothing, which is a simple way of hiding the link - as it just doesn't get created.
If there is a page to display it works out where it should start and creates a link to it.
The code for the next page is similar so I won't talk about that. The only other thing to do is display the position of the page. Here's the code:
@If(Total=0 | Total<=ShowPerPage ; @Return("Page 1 of 1"); ""); div:=Total/ShowPerPage; dif:=div-@Integer(div); pages:=@Integer(div)+@If(dif=0;0;1); "Page " + @Text(@Integer(Start/ShowPerPage)+1) + " of " + @Text(pages)
The logic is fairly simple. Divide the total by the number on each page. If there's any remainder from the division at all - no matter how small - then we need to round the number up by one. This is the number of pages.
Coding the Form:
Coding the form is a similar process to the view, but all the work is done in the WebQueryOpen Agent. All we need to do is get a handle on the current document and then locate it within the view.
Using the information available via the object we can work out whether it's the first and/or the last document in the view. Knowing this we can then decide if we need to create a link and to what the link should point.
Whereas with the view we couldn't use the NotesViewNavigator object, for performance reasons, the size of the view doesn't seem to affect the time taken to compute information about the current document.
Before we do any of this computing, we need some fields on the form to contain the links. No need to save them so they can be Computed For Display and called whatever you like. I called one PrevDocLink and the other NextDocLink. With the fields there to accept the text for the link we can get on with the coding.
The first thing the code needs to do is create a NotesViewNavigator object and find the current document in it. Without worrying about how we define the objects the code is simply two lines:
Set nav = view.CreateViewNav() Set ThisEntry = nav.GetEntry(doc)
With this information at hand we can easily find out if it's the first or last entry. To do this we create two other NotesViewEntry objects. One called FirstEntry and one called LastEntry. We can then test whether these are one and the same entry as our current document. Here's the code to work out if it's the first entry:
Set FirstEntry = nav.GetFirstDocument If ThisEntry.UniversalID = FirstEntry.UniversalID Then Call doc.ReplaceItemValue( "PrevDocLink", "None Found") Else Call doc.ReplaceItemValue( "PrevDocLink", "<a href=""all/"+nav.GetPrev(ThisEntry).UniversalID+""">Open
<b><i>"+nav.GetPrev(ThisEntry).Document.GetItemValue("Title")(0) +"</i></b></a>") End If
Make sense? Nothing too compicated about it really. First, test whether the current document and the first document have the same ID. If they do then it's the same thing. If so, set the value of the "link" field to something to say so, or you could just leave it blank. If it isn't then there's a document before the current one and we set the field to a link to it. To make it a little fancy we make the text of the link the actual title of the document.
The code for the next document link is too similar to bother mentioning. And you can see it all in the demo db anyway.
That's pretty much all there is to it with the form. Two fields and some code to fill them in.
Taking it Further:
The code and sample database we're talking about are quite simple. The view is "flat" and there's only one form in it. This doesn't mean it can't handle complicated though. The fact that we are using the .Search() method means we can reproduce any view and any categorisation it might contain.
With not a lot of code we've managed to create a much-needed addition to Domino's arsenal. What's not to love about it? The only reason you might not want to use it is the performance hit. Whether this is a factor depends on the size of the database and is down to you to decide.
They say that a user on the internet will wait something like 7 seconds for a response from a server. Personally I suspect it's more like 2 or 3 seconds. If you find that computing the total of your view is taking anything more than a second it might be worth dropping this approach.
I know that, for me, this is a definite must-have for any Domino site that is going to live on the internet. Not only is it good for the whole user experience but it makes it possible for search engines to trawl effectively. Which is more important? It's hard to say, but ignoring either issue is to be avoided at all costs...