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