Using a QueryOpen agent to render HTML pages
It makes sense: with LotusScript, you have almost unlimited control. So the choice of rendering entire HTML pages on the fly with a QueryOpen agent could well be the next step in Domino web development, providing you can make it fast enough. Let's find out.
The launch form
Again: my favorite way of opening Domino applications on the web: a dummy Navigator with its $$NavigatorTemplate form. But this time, the form is almost completely empty: a few hidden fields to trap CGI variables and one RichText field:
The RichText field 'HTML' will be filled by the QueryOpen agent 'wqo-launch'. But first, I want to make a few classes: a StringBuffer class to speed things up and a HtmlPage class to help generating an HTML page. Both of them will be stored in a LotusScript library 'HtmlEngine' so that I can use them in other QueryOpen agents as well.
The StringBuffer class
This class acts as a dynamic array of strings: pushing elements to an array is a lot faster than concatenating strings. BlogSphere developers: if your code is running slow: I noticed you use string concatenating all the time, so using a StringBuffer class is a big quick win.
(see elswhere for the StringBuffer class code)
The HtmlPage class
I invented this class to make it easy to work with HTML. Being an expert IBM Lotus Notes developer does not imply that you are also an expert in HTML, CSS or JavaScript. Everyone has his domain of expertise. The HtmlPage class is also fun to work with: you can add things to the metadata, include things in the head, add things to the body and even add debug information as you go along. So if you come across a content block that needs additional CSS or JavaScript, you can simply add it to the head tag instead of being forced to put them in the body. When finally the HTML is rendered, the class sorts the different parts of the page out and displays them in the correct place:
- In the head tag, the metadata come first, then the includes. This is important for Search Engines.
- The debug information can be rendered in a fieldset at the bottom of the page.
- I also included a timer to show how much time the agent took: we want to make sure it stays fast, so we need to measure it.
Class HtmlPage
Private meta As StringBuffer
Private include As StringBuffer
Private body As StringBuffer
Private debug As StringBuffer
Private docType As String
Private pageTitle As String
Private debugMode As String
Private showTime As String
Sub New(Byval dType As String)
Set meta=New StringBuffer(100)
Set include=New StringBuffer(100)
Set body=New StringBuffer(500)
Set debug=New StringBuffer(500)
docType=dType
debugMode="on"
showTime="yes"
pageTitle="(undefined)"
End Sub
Sub setTitle(Byval value As String)
pageTitle=value
End Sub
Sub setDebug(Byval value As String)
debugMode=value
End Sub
Sub displayTime(Byval value As String)
showTime=value
End Sub
Sub addMeta(Byval key As String, Byval value As String)
meta.push |<meta name="| + key + |" content="| + value + |" />|
End Sub
Sub addStyle(Byval href As String)
include.push |<link rel="stylesheet" type="text/css" href="| + href + |" />|
End Sub
Sub addScript(Byval src As String)
include.push |<script type="text/javascript" src="| + src + |"></script>|
End Sub
Sub addInclude(Byval value As String)
include.push value
End Sub
Sub add(Byval value As String)
body.push value
End Sub
Sub trace(Byval value As String)
debug.push value
End Sub
Sub render(Byval RtItem)
If docType="strict" Then
RtItem.AppendText |<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">| + CRLF
Else
RtItem.AppendText |<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">| + CRLF
End If
RtItem.AppendText |<html><head><meta http-equiv="content-type" content="text/html;charset=UTF-8" />| + CRLF
RtItem.AppendText |<title>| + pageTitle + |</title>| + CRLF
RtItem.AppendText meta.collapse(CRLF)
RtItem.AppendText include.collapse(CRLF)
RtItem.AppendText |</head><body>|+CRLF
RtItem.AppendText body.collapse(CRLF)
If debugMode="on" Then
RtItem.AppendText |<fieldset><legend>Debug Info</legend>|+CRLF
RtItem.AppendText debug.collapse(CRLF)
RtItem.AppendText |</fieldset>|+CRLF
End If
If showTime="yes" Then
RtItem.AppendText |<p>Page rendered in | + Cstr(Round(Timer-startTime, 2)) + | sec.</p>| + CRLF
End If
RtItem.AppendText |</body></html>|
End Sub
End Class
The QueryOpen agent
The sample page is just a test... but it renders a valid HTML page. There is error trapping, but be very careful not to break the HtmlPage class. If this breaks, you also lose your error trapping.
Option Public
Option Declare
Use "HtmlEngine"
Sub Initialize
Dim session As New NotesSession
Dim doc As NotesDocument
Dim page As HtmlPage
Dim dbPath As String
Set doc=session.DocumentContext
Set page=New HtmlPage("strict")
page.trace "Agent started."
On Error Goto catch
dbPath=doc.DbPath(0)
page.setTitle "Test QueryOpen"
page.addMeta "description", "This is the description"
page.addStyle DbPath & "default.css"
page.addScript DbPath & "common.js"
page.add |<h1>Test QueryOpen</h1>|
page.add |<p>DbPath: | + DbPath + |</p>|
'page.setDebug "off"
'page.displayTime "no"
Goto finally
catch:
page.trace "Error " & Err & " in line " & Erl & ": " & Error$
Resume finally
finally:
page.trace "<br />Agent finished."
page.render doc.GetFirstItem("HTML")
End Sub
The HTML rendered:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html><head><meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Test QueryOpen</title>
<meta name="description" content="This is the description" />
<link rel="stylesheet" type="text/css" href="/blog-lotusnotes/htmlengine.nsf/default.css" />
<script type="text/javascript" src="/blog-lotusnotes/htmlengine.nsf/common.js"></script>
</head><body>
<h1>Test QueryOpen</h1>
<p>DbPath: /blog-lotusnotes/htmlengine.nsf/</p>
<fieldset><legend>Debug Info</legend>
Agent started.
<br />Agent finished.
</fieldset>
<p>Page rendered in 0 sec.</p>
</body></html>
On screen, it looks like:
What next?
Next step maybe would be: making a BlogSphere lite version that includes all the optimisation for performance and Search Engines I have in mind. Maybe the BlogSphere developers will allow me to join :-)
Have fun with the sample database (don't forget to sign the agent and LotusScript library):
Comments
04/07/2007 07:45:25, Erwin Heeren
Are you sure this will not cause performance problems on a heavily loaded server? I always assumed that @Functions and a rich text field were to be prefered over agents and saved the Web Query Open technique for those cases where there are no @fomula alternatives.
04/07/2007 17:41:41, Michel Van der Meiren
Hello Erwin. Yes, that's what I also heard all the time in my more than ten years of Lotus Notes/Domino development. But it seems to become a habit. If we could persuade BlogSphere and IBM Blog users to add a timer to their QueryOpen agents, we would know. Maybe the Domino server performance regarding LotusScript agents has improved enormously while we were not looking?
06/07/2007 03:16:43, Kevin S Pettitt
Hi Michel, sorry I wanted to comment sooner but I'm still having trouble with the comment system...This is a really great post. I'm happy you took the time to dig into Blogsphere like this, and I'm sure Declan would welcome any contribution to improving the template you'd like to make. I know I would.
You really should have a conversation with him as he put a lot of thought into the new "render engine" that drives the wqo agents in version 3. There were some good reasons for going in that direction, but I'm not the one to explain them.
Feel free to contact me directly for further Blogsphere assistance. Among other things, I'd like to point out that earlier versions of Blogsphere had dozens of skins built in, and for some reason I think later versions did not. I can send these all to you. Also, there is a configuration wizard which kicks in when you first launch a new Blogsphere database that doesn't have a "blogconfig" profile document. It sounds like you didn't see the wizard when you tried it out, which probably means yours had a profile already.
05/08/2007 01:28:39, Ben Poole
You've got some great posts on this site!
I use a WebQueryOpen agent to render the core page content in DominoWiki. It works quite well, and seems to behave under stress. The next version of DominoWiki optimises the approach taken by only parsing live page links in the Queryopen agent, whereas currently live page links and wiki mark-up are both parsed by the agent code.
Web agents aren't the bogeymen they once were: I think the agent manager has had quite a lot of spit and polish over the years (especially with Notes 7).
Note: I have no empirical evidence to back any of this up :)
05/08/2007 22:29:12, Michel Van der Meiren
Hello Ben. You are right: when I started developing, using a WebQueryOpen agent was considered as the worst you could do. I did some rough tests, and indeed, there has been spectacular improvement. And in some cases, using a WebQueryOpen agent is almost the only solution, like for computing the live page links for Wiki pages. I am really looking forward to the next DominoWiki release.
13/08/2007 08:05:04, Ayhan Sahin
Iam developing on a wCMS for 8 years. Iam working with HTML-Templates, they are rendered by the client and i set some flags these tell the server, if a wqo is needed to fired. Especially in the app-common language (like in ben pooles dominowiki) i need some regexp to e.g. evaluate formulas and so on...
i tried the html-class and it rocks ;-)
09/10/2007 17:53:31, Eric Romo
Have you tried using List(s) instead of Array(s), perhaps like so:
Class StringBufferLst As StringBuffer
Private lst List As String
Sub New(Byval a As Integer)
increment = a
count = 0
Erase lst
End Sub
Sub push(Byval a As String)
count = count+1
lst(count) = a
End Sub
Function collapse(Byval a As String) As String
Dim sTempLst As String
sTempLst = Join(lst, a)
collapse = sTempLst
End Function
End Class
10/09/2007 05:56:47 PM, Eric Romo
FYI: Sorry, but my previous post looked better before Submit
09/10/2007 20:16:16, Michel Van der Meiren
@Eric: great stuff. Sometimes we just plain overlook the obvious...
10/09/2007 09:15:49 PM, Eric Romo
NOTE: you'll need to change the Join call to use a function like the following to convert the list to an array. Apparently Lotuscript doesn't allow a List datatype to be used as an array for the Join function.
Function listToArray(tLst As Variant, count As Integer) As Variant Dim tmpArr() As String Redim tmpArr(count) Dim idx As Long For idx = 1 To count tmpArr(idx) = tLst(idx) Next listToArray = tmpArr End Function
To add a comment, log in or register as new user. It's free and safe.