WebSnap Tips and Tricks

By: Nick Hodges

Abstract: Nick Hodges shares some of his tips and tricks to make WebSnap do the things you need it to do.

by Nick Hodges
Lemanix Corporation

WebSnap is tricky, and it certainly takes a bit of getting used to before you can really make it sing. It's a powerful framework that has a lot of interesting nuances, but it is a bit unapproachable. Well, it's really not all that bad, but to be a good WebSnap programmer, you do have to learn a lot, which is true with any other application framework. I've used WebSnap pretty extensively, and have run across a number of tips and tricks, culled from hours of hacking around, to make WebSnap do some of the things that you might want it to do. In addition, I read the Delphi newsgroups a lot, and I see the questions that people ask. Below are a few of those tips and tricks that I've gathered, and I hope you find them useful.

Use Adapters to Manage Content

I don't know if I do things different than the rest of all of you, but I build my websites using what I call the “chunk management” technique. I construct a page out of different “chunks” of HTML, and I use different techniques for managing those chunks. One way is to have a main page that lays out the lowest common denominator of a page – a page that has a header, three columns, and a footer for example. You see a lot of web sites that are layed out something like the one below:

<TABLE WIDTH=100% BORDER=1 CELLPADDING=4 CELLSPACING=3>
        <COL WIDTH=85*>
        <COL WIDTH=85*>
        <COL WIDTH=85*>
        <TR>
                <TD COLSPAN=3 WIDTH=100% VALIGN=TOP>
                        <P ALIGN=CENTER>Header</P>
                </TD>
        </TR>
        <TR VALIGN=TOP>
                <TD WIDTH=25%>
                        <P>Left Content Column</P>
                </TD>
                <TD WIDTH=50%>
                        <P>Middle Content Column</P>
                </TD>
                <TD WIDTH=25%>
                        <P>Right Content Column</P>
                </TD>
        </TR>
        <TR>
                <TD COLSPAN=3 WIDTH=100% VALIGN=TOP>
                        <P ALIGN=CENTER>Footer</P>
                </TD>
        </TR>
</TABLE>

The resulting table ends up looking something like this: (Well, this is more of an abstract view, but you should get the idea).

Header

Left Content Column

Middle Content Column

Right Content Column

Footer

The header will have title information, general links, graphics, etc. The left column will have a menu, the middle column the main content for the given page, and the right column will have advertisements, related items, or things of general interest. Whatever. You see sites like this all the time all over the Internet. Shoot, try to find one these days that isn't laid out like this. The trick here is to build and manage chunks of HTML to fill in the areas in the table. That's how I think of building a site. Each area gets filled differently based on what page is being requested, and what specific information is required.

For instance, the Header portion will probably stay almost the same for each request. Perhaps it will include the current time or greet the user by name. You'll probably keep this chunk in a header file, and maybe even manage it via an include directive. However, the left column will usually contain a menu, and that menu needs to change automatically as pages are added to the site, and as different pages are selected. Perhaps the list of pages is in a database, or the menu changes depending on the privileges and access level of the current user. Thus, the menu becomes a chunk of HTML managed separately from any other content on the page. I might want to build the menu in a method of the webdatamodule. I might want to build it in server-side Javascript. I might be doing it one way, and I want to change it to another way without altering how the rest of the page is built. WebSnap let's me do this.

So How Do I Chunk?

There are a couple of ways to chunk. One is the PageProducer way using custom tags and the OnHTMLTag event of a page producer. You all with a WebBroker background are very familiar with this way of managing HTML. You can easily use TPageProducer and it's siblings to create content for each section of a page, and use a single template full of custom tags to fill it in. For instance, the code for the table above might end up looking like this:

<TABLE WIDTH=100% BORDER=1 CELLPADDING=4 CELLSPACING=3>
        <COL WIDTH=85*>
        <COL WIDTH=85*>
        <COL WIDTH=85*>
        <TR>
                <TD COLSPAN=3 WIDTH=100% VALIGN=TOP>
                        <P ALIGN=CENTER><#HEADER></P>
                </TD>
        </TR>
        <TR VALIGN=TOP>
                <TD WIDTH=25%>
                        <P><#LEFTCOLUMN></P>
                </TD>
                <TD WIDTH=50%>
                        <P><#MIDDLECOLUM></P>
                </TD>
                <TD WIDTH=25%>
                        <P><#RIGHTCOLUMN></P>
                </TD>
        </TR>
        <TR>
                <TD COLSPAN=3 WIDTH=100% VALIGN=TOP>
                        <P ALIGN=CENTER><#FOOTER></P>
                </TD>
        </TR>
</TABLE>

And then you can use the main page's OnHTMLTag to replace each of the above tags with the HTML chunks for the appropriate section.

Of course, the other thing to remember is that you can nest TPageProducer content within other pageproducers, to there might even be “chunks within chunks.” It is fairly easy to keep things well organized by using discrete TPageProducers to produce individual portions of your page, and then piece them together in a logical fashion.

TPageProducers are the “normal” way to deal with larger chunks of HTML. Typically, for small pieces of information, Adapter fields have been used. For example, you might want to put the current date and time on your page, so you'd create an Adapter field that kept track of that, and then put some Javascript in your HTML like this:

<b>The current time is <%= MyTimeField.Value %></b>

and you get that discrete piece of information placed in your page. But there is no reason why you can't use Adapter fields to handle larger chunks of HTML. You could easily create a field that had an OnGetValue event handler that looks like this:


procedure THome.SomeBigChukFieldGetValue(Sender: TObject; var Value: Variant);
begin
  Value := MyPageProducer.Value;
end;

Then, instead of using custom tags and an OnHTMLTag event handler with a big ugly if statement that compares strings, you can use Javascript

<%= SomeBigChunkField.Value %>

to put the chunks of text where you want.  I suppose it is a matter of taste, but this method seems cleaner and easier, and avoid the aforementioned ugly if statement. I find that I do things this way more and more, as the Javascript is easier to write, and the Delphi code ends up more modularized and easy to read.

Use the TLocateFileService

The TLocateFileService component is easily has the lowest “recognized coolness to actual coolness” ratio on the WebSnap palette tab. The component is really powerful, lets you do all kinds of cool things, and I almost never see anyone ever mention it on the newsgroups. In fact, it is so important to good web application design that I dare say WebSnap really wouldn't be worth using without it. One of the key strengths of WebSnap is its ability to separate the functionality code (the Delphi part) from the presentation code (the HTML). 

The TLocateFileService is the component that does that precise thing.  It allows you to retrieve HTML files – from virtually any source or any location – as the HTML is needed by a WebSnap application. By default, WebSnap will look for your HTML file in the same directory as the binary, using the name of the unit as the filename it is looking for. But certainly you won't always want to leave your HTML there in the web server's virtual directory, now, will you. You might want to store it in a completely different place on your network – maybe in a spot where your HTML specialists can more easily access it. Maybe you want to store it in a database. Maybe you want to create it all totally on the fly. Who knows. All I know is that the TLocateFileService component will allow you to do all of these things.

The OnFindStream event passes the file that it is looking for and a reference to a TStream. This event allows you to assign any stream at all to the AFoundStream parameter – a TBlobStream, a TMemoryStream, a TCompressedStream, whatever – as long as that stream contains HTML text. You can, naturally, hunt up your HTML from any source, based on any criteria you want. You may have some pages in files on your server's hard-drive, and others in a database. It doesn't matter. It is all up to you. Since the event passes you the name of the file it is looking for, you can provide content from many different places based on the page name. The component will even allow you to do the same for files referenced with an include tag. How cool is that?

Here’s a really quick, simple example.


procedure THome.LocateFileService1FindStream(ASender: TObject;
  AComponent: TComponent; const AFileName: String;
  var AFoundStream: TStream; var AOwned, AHandled: Boolean);
begin
  AFoundStream := TFileStream.Create(MyHTMLDirectory + AFilename, fmOpenRead);
  AHandled := True;
  AOwnded := True;
end;

A couple of notes on the code above – the AHandled parameter tells WebSnap that you have taken care of hunting up this HTML file, and that there’s no more hunting needed. The AOwned property tells WebSnap who will manage the destruction of the stream. Setting it to True tells WebSnap “I don’t care what happens to this stream; do what you need to with it and destroy it when you are done.” Setting it to False says “Hey, WebSnap, this is my stream. You can read the HTML out of it, but don’t do anything else with it, and definitely don’t destroy it.” One more note – the AFilename parameter will always have the filename with the ‘.html’ extension attached to it.

So, if you aren't using the TLocateFileService component, check it out, and start using it. It will really rev up your application, and make it much easier to mange your HTML – and you are properly managing your HTML aren't you?

Store Session Information in a Database

As you probably already know, HTTP is a stateless protocol, and thus if you want to maintain information about a specific visitor to your site, you need to maintain that information in the Session variable. The Session variable works great, but it has some limitations that you may want to overcome. The first is that the standard TSessionsService stores its information in memory, and thus it won't work in a CGI application. Even in an ISAPI application, the Session information is lost when the session expires, or the server is shut down. Very often, the user enters preferences and other information that you want to store for the next time the user visits your site. In addition, session information is stored in memory on a single machine, and thus WebSnap applications, by default, don't have the ability to run on "server farms" where multiple machines might respond to a single user over different requests. 

Fear not – WebSnap is once again up to the task.  Now, the ideal solution to this would be to create a new TSessionService descendent that implements storage in a DB.  (There are some TDBSessionService components floating around out there.)  I am too lazy to do that here – I keep meaning to get around to it – but I will give you the basic tools you need to store session information in a database, typically in a BLOB field, but in any type of TStream descendent you like.  Here you go:


procedure SaveSession(AID: TSessionID; aStream: TStream);
var
 TempSessions: TSessions;
 TempItem: TSessionItem;
begin
 if Assigned(aStream) then
 begin
   TempSessions := TSessions.Create;
   try
     TempItem := TSessionItem.Create(TempSessions);
     if Sessions.GetSession(AID, TempItem) then
     begin
       TempSessions.SaveToStream(aStream)
     end else
     begin
       Assert(False, 'Session not found');
     end;
   finally
     TempSessions.Free;
   end;
 end;
end;

// Update or Add all name/value pairs from the saved session to a new or existing session
procedure RestoreSession(AID: TSessionID; aStream: TStream);
var
 TempSessions: TSessions;
 Item: TSessionItem;
 I: Integer;
begin
  if aStream <> nil then
  begin
    if aStream.Size <> 0 then // if there is no data there, don't do anything
    begin
      TempSessions := TSessions.Create;
      try 
        TempSessions.LoadFromStream(aStream);
        if AID = '' then
        begin
          // Create a new session
          AID := SessColn.Sessions.StartSession
        end else
        begin
          Assert(Sessions.SessionExists(AID), 'Could not find session ' + AID);
        end;
        Item := TempSessions.Items[0] as TSessionItem;
        for I := 0 to Item.Items.Count - 1 do
        begin
          Sessions.SetItemValue(AID, Item.Items.Names[I], Item.Items.Variants[I]);
        end;
      finally
        TempSessions.Free;
     end;
    end;
  end;
end;

These two routines ought to be pretty easy to use.  They both take the same two parameters.  The aID field is a TSessionID, which is easy to find with WebContext.Sessions.SessionID.  The second parameter can be any valid instance of a TStream descendent, though if you want to store it in a database, it probably would be easiest to make this a TBlobStream instance (Created with a call to CreateBlobStream, of course.)

Now, once you can do that, you can save your session out to a database, probably in a table indexed on the username of the given user, allowing you to retrieve the session information the next time the user makes a request, or even the next time the user logs in.  You can call RestoreSession right before responding to a request, and SaveSession right after.  It's not the most elegant way – call it a poor man's TDBSessionService – but it will get the job done.

Returning Custom Content in a Stream

Lot of people ask for this one. They want to be able to send a Word document or a PDF document or some other type of non-HTML based information down the pipe in response to a client request. More often than not it's a document or an image that has been created on the fly, and so it isn't a file that can simply be referenced as HTML. Since you can't point to an existing file, you'll have to send the content down to the client as a stream. And – you guessed it! – WebSnap make this really easy.  One of the things that WebSnap does for you is to wrap up each HTTP request that comes in into a nice neat class, TWebRequest, that you can read from. (In fact, most of the fields in the class are read-only). More importantly, it creates a class that represents the HTTP response that you are going to send, TWebResponse. You can set the properties of this class to be exactly what you want them to be in order to have control over the response that you send back to the client. One of those properties is the ContentStream property, which can hold a stream of data that represents the content of the response. You can set the ContentType property to tell the client what type of data is being returned. The ContentType property takes a MIME type name, such as 'image/gif'. It's really that simple – just get the data you want into a stream, assign it to the ContentStream property, set the ContentType property, and that's it.

Here's an example:


procedure THome.WebDispatcher1ImageActionAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  MS: TMemoryStream;
  JPEGImage: TJPEGImage;
begin
  JPEGImage := TJPEGImage.Create;
  try
    JPEGImage.LoadFromFile('c:\graphics\somejpeg.jpg');
    MS := TMemoryStream.Create;
    JPEGImage.SaveToStream(MS);
    MS.Position := 0;
    Response.ContentStream := MS;
    Response.ContentType := 'image/jpeg';
    Response.SendResponse;
  finally
    JPEGImage.Free;
  end;
end;

You could easily do the same with a PDF or DOC file, by simply changing the ContentType parameter to the correct MIME type descriptor and passing that type of data in the stream. There are two key things to note here. First, once you copy the file into the stream, you need to reset the Position of the stream back to the beginning. Second, you should not free the stream itself once you’ve assigned it to the Response.ContentStream property, as the TWebResponse class will do that for you.

Uploading Files

Getting a file from the client back to the server is, in HTML, a pretty tricky proposition. It requires a pretty intimate understanding of how the HTTP protocol works, and an ability to keep track of precise amounts of bytes that are split up into (not necessarily the same size) chunks. No fun at all. But – as if you'd never guess – Websnap makes it, well, easy. Really easy, actually. The TAdapter component has in it a TAdapterField specifically designed for managing the uploading of files, the TAdapterFileField. To use it, simply add a TAdapterFileField to a TAdapter, and then code similar to this in its OnUploadFiles event:

procedure TUpload.AdapterFileField1UploadFiles(Sender: TObject;
  Files: TUpdateFileList);
var
  i: integer;
  CurrentDir: string;
  Filename: string;
  FS: TFileStream;
begin
  // Upload file here
    if Files.Count <= 0 then
    begin
      raise Exception.Create('You have not selected any files to be uploaded');
    end;
    for i := 0 to Files.Count - 1 do
    begin
    // Make sure that the file is a .jpg or .jpeg
    if (CompareText(ExtractFileExt(Files.Files[I].FileName), '.jpg') <> 0)
        and (CompareText(ExtractFileExt(Files.Files[I].FileName), '.jpeg') <> 0) then
    begin
      Adapter1.Errors.AddError('You must select a JPG or JPEG file to upload');
    end else
    begin
      CurrentDir := './';
      if not DirectoryExists(CurrentDir) then
      begin
        ForceDirectories(CurrentDir);
      end;
      FileName := CurrentDir  + ExtractFileName(Files.Files[I].FileName);
      FS := TFileStream.Create(Filename, fmCreate or fmShareDenyWrite);
      try
        FS.CopyFrom(Files.Files[I].Stream, 0); // 0 = copy all from start
      finally
        FS.Free;
      end;
    end;
  end;
end;

Most of the code here actually just ensures that you are receiving the proper type of file, in this case the file must be a *.jpg file. It first checks if the file being uploaded has the ‘.jpg’ extension, and then it makes sure that there is a directory to receive the file. Finally, all it does is create a TFileStream, and then copy the data from the clients harddrive to your server. All in basically a few lines of code. Hard to beat that. And not only that, the TAdapterFileField is smart enough to create the HTML you need to let the user select the file when you add it to a TAdapterPageProducer. See the following graphic:


Conclusion

Well, there’s five or six little tidbits of knowledge to improve your WebSnap skills. As I said, there’s a lot to learn, but the power and control you are looking for, WebSnap provides.

You can find out more about WebSnap, download some code, and read other articles about WebSnap at my WebSnap page, found at http://www.lemanix.com/WebSnap


Nick Hodges is the Chief Technology Officer for Lemanix Corporation. Lemanix is a Borland Solution Partner, Borland Certified Educator and Borland Product Reseller that specializes in enterprise solutions, training, and consulting using Borland Delphi. Nick is a frequent author and conference speaker, certified as a Delphi 7 Developer and Trainer, and a member or Borland's TeamB. Nick lives in St. Paul with his wife and three children. He's working on building one of these for use in the Spring..


Server Response from: ETNASC03