by Mark Trescowthick - GUI Computing
We recently decided to adopt a new standard here at GUI central. Specifically, for any project over about four weeks duration, we decided to use an Intranet/Extranet site to expose all those myriad pieces of paper which any project generates (yes, even we RAD developers are plagued by paper!).
This was, it turns out, about the best decision we've made for a long while. However, having got a test site going in next to no time, we (naturally!?) decided we needed something more. Specifically, both Jim and Ross decided that they wanted a discussion forum, which they both felt would be more effective than large numbers of mail messages hurtling to and fro.
It sounded reasonable to me and, as I was sure that I'd seen something on the Net, I volunteered. I always do that. You'd think that, after all these years, I know better. But no. I just can't resist putting my hand up!
In any event, I quickly navigated to one of my favourite ASP sites, ActiveServerpages.com, and downloaded Matthew Griffith's freeware ForumExpress. I'd seen it was available, and that it only needed a "bit of tweaking" to get it fully operational. Not quite, as it turned out, but it still ended up providing the inspiration for at least the basis of what finally eventuated.
Having had a look at this, my next thought was to use VideoSoft's excellent off-the-shelf component. Trouble was, we'd be deploying this on multiple servers, not all of them ours. That didn't seem like the "cheap and cheerful" option I was seeking.
So, in the end, I wrote one. The full source is available for download.
ForumExpress has a very spiffy interface (basically it is a clone of Outlook in most respects) but to do that it uses a lot of styles, which simply don't translate to IE3 or Netscape3 at all well. And it uses a lot of client-side scripting to manage frames and such. As this was to be the "write once, deploy many" easy maintenance solution, I really didn't want to be deploying a lot of client-side code. So I stripped the interface down until it looked like this :
Next came the big decision - to thread or not to thread? Funnily enough, both Jim and I don't really think that threaded forums work all that well on the Web, so this one was easy. No threads. We did, however, decide to allow for multiple Topics… which sort of provides the same functionality in many ways.
Anyone could add a new topic, we decided, but only "Administrators" could close one - after which no more messages could be added, but everyone could still view them.
All this agreement had to end, of course. Jim was firmly of the belief that the actual topic should be presented as one long thread. I couldn't see that that was to my liking and wanted a more message-based view. Naturally, Jim decided that we would compromise - I could provide both views! He's a generous man, Jim.
We only added one extra element to the design we'd initially come up with, and that was to note the number of messages added since a user's last visit, and to highlight those topics in which the messages appeared.
This came from our beta testing with Ross Mack's current project and he and Jim beat me unmercifully around the head until I capitulated. They're cruel men, but fair. After all, I had transgressed the unwritten rule… I'd initially provided a solution which met their requirements. I suspect that they were just enjoying being on the giving rather than the receiving end for a change. I've always felt it was better to give than to receive, and this proved it. But I digress (and anyway, I'll get them back Real Soon Now!).
Under the Covers
As usual frame manipulation soon became a real pain. I dream of the day a redirect can be done to a nominated frame, even though I know deep down that that can never really be…
My standard approach to frames is to use two ASP scripts (which I tend to name default.asp and content.asp) to generate the standard 3-panel frame structure most everything seems to use these days. This allows me to pass parameters to both outer and inner frames provides the maximum in flexibility.
I use default.asp as the outer frameset, and also as a handy replacement for Global.asa - which I've just about stopped using entirely unless an application has multiple entry points; it just causes far more pain that it's worth. In this case, default.asp just passes any parameters straight through to content.asp, as the outer frames will always be the same, while content.asp decides on the basis of the parameters (or lack thereof) just what will be placed in each frame it creates. You can get the same effect using multiple static HTML pages, but this is a much more flexible approach.
This standard approach worked well enough in this case too, though right at the end of development I realised that I'd overlooked one key factor - this app was to be deployed mostly as part of a greater site, so it would, as often as not, be working within an existing frame. I had to cope with that and to allow it to exit gracefully back to the main site, as well as being careful not to take up more than my apportioned real estate.
This meant that (a) I needed to go back and rename my frame names to something less common than "main" and "sidebar" and (b) I needed some way to know just what my "home" frame was. It also meant some client-side scripting, but more of that in a moment…
I'm rapidly coming to the conclusion that naming frames "main" is about the logical equivalent of naming a VB form "Form1", and about as dangerous to your health and productivity, for what it's worth…
I also realised that I would, in all likelihood, have more than one of these apps running on any given server (especially ours) at a time. And that I wanted to make this customisable, but to do it in such a way as to ensure that I didn't need any setup scripting.
After a bit of thought, I came up with an include file as the solution, which I call character.inc.
<% ' character.inc ' Include file to provide forum name etc incForumName = "AVDF" ' Forum Name incCookieName = "AVDFForum" ' Cookie Name incExitURL = "http://www.avdf.com/" ' Exit leads here incParentFrame = "_top" ' Frame in which we're working ' (or "_top" if in whole browser) incODBCDSN = "AVDFForum" ' DSN Name dim incForumAdmin(1) ' array of forum admins ' used to determine if user is Admin incForumAdmin(0) = "Trescowthick" incForumAdmin(1) = "Karabatsos" %>
Most of this is pretty self-explanatory, but doing things this way hadn't actually occurred to me before. When I started to create it, I'd really assumed that it would run to quite a few items, but as you can see there's not much to it.
This is actually the character.inc for the demo Forum you can use.
As we wanted to keep the database design as clean as possible, and to aid in making setup as easy as possible, I chose to determine whether a user was Admin level or not by the simple expedient of doing an InStr on their login name! OK, so it's not the most elegant solution, but it did allow me to keep things very simple indeed. I chose an ODBC DSN data source rather than a DSNless connection simply to maintain 100% database independence.
One wrinkle which came from this desire to make things "drop in" was how I ensured that any session which timed out displayed the appropriate message in the appropriate frame. I normally skip this, and just take over the topmost frame to tell the user that they must log in again, but in this case that didn't seem right - after all, the rest of the site might still be perfectly valid. And, yes, I do know I could use an application scope object and 'avoid the problems' - been there, done that, prefer 'the problems', thank you very much!
The comments are pretty self-explanatory, but what it does is loop back up the frame hierarchy until it finds the name of our ultimate parent frame or (as a safeguard against client-side looping) it hits the top level. Then the script simply sets the contents of the frame it has located to my session timeout file. I'm certain there's a better way to do it (please email me) but it works nicely and means that any session timeout is handled in the topmost frame the app is allowed to use.
If I was really keen, I could make DeadSession an ASP file and then give the user the "click here to restart" option. But too much like hard work <g>. Let them eat cake. Or some such.
You'll also note that CheckSession ends with an unterminated if statement. That's deliberate, as I have a companion include file (CheckSessionEnd.inc) which I place after the mainline ASP code. This effectively prevents any ASP trying to run if the Session has timed out. As I've pretty much standardised now on doing all HTML output in one or more Subs, my mainline ASP is pretty small, and I find that the second include file actually helps my code readability as it provides a visual reference as to where the main code ends.
I'd prefer an End statement in VBScript itself, of course, but as we don't have that…
I also considered coding this by using the FileSystem Object to create the HTML, and then redirect to it, but on the whole this approach seemed neater (and easier!).
Len() doesn't, I've now learnt, force any type conversion on a memo field which is text prior to trying to handle it as text. Which might, I can see, be a reasonable (if annoying) thing. But when Replace() does, that qualifies as a bug, me thinks!
Consider this, where MessageText is in fact a memo field extracted from a Recordset :
sText = replace(MessageText, vbcrlf,"<br>")
ITextLength = Len(MessageText) sText = Replace(MessageText, vbcrlf,"<br>")generates a Type Mismatch error on the first line. Somebody wasn't paying attention…
A simple fix, of course, is to append a null string to MessageText before acting on it, but that really shouldn't be necessary.
There's not really much more to this little app. From go to whoa it took only about a day to get up and running, but then it's hardly rocket science.
It is an obvious candidate for being turned into a single server-side VB DLL, and I must admit I considered that. But the fact is that to do that I would need to register it on the server, install runtimes and such and generally have access. And it would complicate the setup process no end. As it stands, I can drop this code onto a server, set up a single virtual directory in IIS and an ODBC DSN and be operational.
If you're interested in the final result, there's a live demo here.