Long, long ago...
Man, how many articles has it been? Five, six? I've lost count. We've come a long way from the humble beginnings of this
modest sample application when all it could do was log you in
as a full-fledged registered user. Since then we've added session
tracking, persistent sessions,
and access rights.
Okay, that's only four articles. Well, here's number five -- and it's going to be a doozy.
There's plenty to do. First we'll do a little fixing up. I've found a better way to do some of the
things that we've done before, so we'll get that stuff installed. And then we'll work on adding a little content.
So far we've worked mostly on the plumbing of WebSnap, and I thought it time for you to be able to display
some cool content. As a result, this article will be a lot longer than normal, but hey, you get what
you pay for. (Wait, this article is free...?) Anyway...
In a galaxy far far away...
Let's start by improving some of the work we've already done.
The main improvement is going to be in how we handle persistent information.
The original technique for storing persistent data works nicely, but an astute reader
(whose name I have regretfully forgotten, or I would gladly name him so that his
brilliance is known to all) pointed out that
although it seems that we can store session information in the database when the
session ends via a logout, in fact we
cannot: The UserID value has gone blank by that time. So we need a better way to
make sure that we capture session state changes before
the user logs off. Think of the horror of forgetting whether a person likes
coffee! <shudder>
Well, hey, I think to myself, no big deal. There's got to be an OnLogout event around somewhere.
So of course I go hunting for one. And there isn't one!
But my steel-trap memory keeps telling me, You've seen that somewhere, somewhere, somewhere... So I keep looking.
Finally I find it. It is slyly hiding on the TEndUserAdapter -- the very component we abandoned in
favor of the more powerful
TEndUserSessionAdapter. Well, we thought it was more powerful. We were mostly right, but
TEndUserSessionAdapter
lacks this useful -- essential -- event.
Why, you ask? I'll tell you. Although TEndUserSessionAdapter and TEndUserAdapter descend from a common
ancestor, TEndUserSessionAdapter never implements the OnLogout event!
At this point, many of you may be worrying. Some of you may even be panicking. You ex-VB developers
(we know you're out there) are throwing up your hands and saying, Well, there's nothing to do
now but hire a C++ programmer to build me an ActiveX control or something.
How wrong you are! Despair not! This is Delphi, my good fellow. We can fix anything!
Delphi rocks!
Let's do what real programmers do and fire up the New Component Wizard to solve this problem. Select
Component | New Component... from Delphi 6's main menu and set it to look like
this. Well, mostly like this. You'll probably want to set the path, or even the component name, to something different.
(As always, the code for this
article is on CodeCentral):
You should end up with a unit already to be filled out with all kinds of cool code. Then add the little
bit of cool code shown below to make the component work:
unit nxEndUserSession;
interface
uses
Windows, Messages, SysUtils, Classes, WebAdapt;
type
TnxEndUserSessionAdapter = class(TCustomEndUserSessionAdapter)
private
FOnLogout: TEndUserUserIDEvent;
FOnLogin: TEndUserUserIDEvent;
{ Private declarations }
protected
{ Protected declarations }
procedure ExecuteLogout; override;
procedure ExecuteLogin(AUserID: Variant); override;
public
{ Public declarations }
published
{ Published declarations }
property OnLogin: TEndUserUserIDEvent read FOnLogin write FOnLogin;
property OnLogout: TEndUserUserIDEvent read FOnLogout write FOnLogout;
property OnProduceLoginPage;
property LoginPage;
property OnHasRights;
property Data;
property Actions;
property OnBeforeExecuteAction;
property OnAfterExecuteAction;
property OnBeforeGetActionResponse;
property OnAfterGetActionResponse;
property OnGetActionParams;
end;
implementation
uses WebComp;
{ TnxEndUserSessionAdapter }
procedure TnxEndUserSessionAdapter.ExecuteLogin;
begin
inherited;
if Assigned(OnLogin) then OnLogin(Self, AUserID);
end;
procedure TnxEndUserSessionAdapter.ExecuteLogout;
begin
// Call the logout event handler *before* killing the session, and thus losing the UserID
if Assigned(FOnLogout) then FOnLogout(Self, UserID);
inherited;
end;
end.
Note that we did derive our component from TCustomEndUserSessionAdapter. That's the class
that is designed to be inherited from -- the "Custom" part indicating that it purposely doesn't add any published properties, leaving us the
option of publishing what we want.
We publish two new events -- OnLogin and
OnLogout -- in addition to all the properties and events published by TEndUserSessionAdapter.
That way it will behave just like the component we are replacing, but with the new functionality.
(That's pretty much the point of creating a new component.)
The new
functionality is evoked in two overridden virtual methods -- ExecuteLogin and ExecuteLogout.
They do pretty much what you'd expect. They get called upon log-in and log-out,
and call their event handlers.
The trick is in ExecuteLogout. See how it
calls the event handler before the inherited functionality? It does this because the
inherited functionality tosses out the UserID. We call the OnLogout event first so the
UserID is still valid. This allows us to save the user's session information (Perhaps the event should be called OnBeforeLogout, but let's not quibble. I like
the symmetry of OnLogin and OnLogout.)
Now we need to install the component. We'll install it just like a regular component,
but we also want to install it into the New WebSnap Application wizard.
The principles of good
component design and management require that we separate functionality from registration, so
we'll create a new unit as below:
unit nxWebSnapReg;
interface
procedure Register;
implementation
uses Classes, WSvcReg, nxEndUserSession;
procedure Register;
begin
RegisterComponents('WebSnap', [TnxEndUserSessionAdapter]);
RegisterWebAppServices([TnxEndUserSessionAdapter]);
end;
initialization
finalization
UnRegisterWebAppServices([TnxEndUserSessionAdapter]);
end.
This unit should be placed in a design-time package. Note that it uses the unit WSvcReg,
which is not on your hard drive. It is found in the DCLWebSnap.dcp file,
which should be added to your package's requires list.
I named this unit nxWebSnapReg.pas,
a rather generic name, as we'll likely be using it later to add more WebSnap components.
Each call to RegisterWebAppServices should be matched up with a corresponding call to
UnRegisterWebAppServices. The unit here takes care of that for you.
Once you install your package into the IDE, you'll be able to select the component as part of the New
WebSnap Application Wizard:
Now that our new component is on the palette, replace the EndUserSessionAdapter
on your home page with your brand-spanking-new nxEndUserSessionAdapter. Set its LoginPage property to
Login, add all the actions and fields to it (including the
UserRightsField as
you did in
the AccessRights article), and make the event handlers for its two new properties look like this:
procedure THome.nxEndUserSessionAdapterLogin(Sender: TObject;
UserID: Variant);
begin
// Get session information for the new UserID
SessionDatamodule.GetSessionInformation(UserID);
end;
procedure THome.nxEndUserSessionAdapterLogout(Sender: TObject;
UserID: Variant);
begin
SessionDatamodule.SaveSessionInformation(UserID);
end;
Next, go to the login page and remove the code for the TLoginFormAdapter.OnLogin event
handler -- it has been rendered redundant.
Now you can manage all of your login and logout logic on the home page itself,
rather than having it spread out over multiple modules.
That's it for maintenance and upgrades. Let's move on to new features.
Paging Dr. WebSnap
As I said, we've been doing a lot of plumbing in my previous articles. So this time around, I thought
we'd change the pace a bit and show some of WebSnap's content management and presentation
capabilities.
It is often necessary to present tables of data to a user. The data may have lots of records, and you won't want to show all the records at once.
Maybe you'll want to limit the amount of data displayed and let the user move
from page to page. Well, guess what -- WebSnap makes it a snap to do this sort of
thing. (Sorry. I just can't resist the play on words.)
You've seen this behavior all over the Internet. Go to Google and do a
search for "smelly cats." You'll see hundreds of hits, with a little page menu of
numbers at the bottom to break up the data for you. WebSnap will build these types of data-navigation links for you
automatically, assuming you're prepared to write a little bit of code. We'll show two
examples -- one with a table of data and one with a collection of records that you
build manually.
Fish for dinner again?
We'll start by building a simple data table to look at the infamous BioLife
database table. John
Kaster wrote a good article on
how to build an app using this data, but we'll build just a simple table to show
how we can add paging and limit the number of records. It's really easy to do:
First, get the data. Hunt up the biolife.xml file in your demos directory and
copy it
into the same directory as your project. That's the data we'll display.
Create
a new page, calling it PagedDataGridPage, and save it as wmPagedDataGridPage.pas. (You naturally
gave it a TAdapterPageProducer, right?)
Drop a TClientDataset and a TDatasetAdapter on
the page. Set the clientdataset's Filename property to "biolife.xml" and set Active
to True. Right click on the clientdataset and add all the fields. Set the DatasetAdapter's
Dataset property to the clientdataset.
Don't forget to update your new HTML page with the fix that we did in the
last article. Change line 32 to
if ((e.item().Published) && (e.item().CanView))
This, you may remember, is the line of JScript that shows a link to a page only if the user has proper access rights.
Double-click on the AdapterPageProducer and add an AdapterForm. To that, add an AdapterGrid and
an AdapterCommandGroup.
Let's fill out the AdapterGrid first. Set the AdapterGrid's Adapter property to
the DatasetAdapter. Right click on the AdapterGrid and select "Add Columns..." Add all the columns there
except the Notes column, which takes up too much space in a grid.
Right-click on the AdapterCommandGroup and add the PrevPage, GotoPage, and NextPage Commands.
Now the fun begins. Go to the DatasetAdapter and set its PageSize property to 4.
You'll
see something like this:
Cool, huh? And we didn't write a single line of code!
Run the application, leave it running, log in,
and look at the page. You can view all the data, four records at a time. WebSnap does all the
calculations for you and creates links to the appropriate pages in the data. Now you
can present large amounts of data to users and allow them to navigate through it
easily.
When grids just aren't enough
Sometimes grids don't cut it. You may want to
present each record in a specific format instead of simple tabular form. This is a
little trickier, but it's still pretty easy.
As an example, we'll build a simple
WebLog application that you can use to post your rantings and other random thoughts on the
Web, making them all available for the world to peruse -- a sort of public
diary. (If you want to see what I am
talking about and you don't mind my conservative rantings, then take a look at
http://www.nickhodges.com/bin/WebLogCGI.exe.)
We'll start with a database table. The code for this article includes an
InterBase database called weblog.gdb -- you can use it. It already has some entries, so we
can see the WebLog at work right away. The schema looks like this:
/* Table: WEBLOG, Owner: SYSDBA */
CREATE TABLE "WEBLOG"
(
"ID" INTEGER NOT NULL,
"ENTRYDATETIME" TIMESTAMP NOT NULL,
"TITLE" VARCHAR(128),
"ENTRYTEXT" BLOB SUB_TYPE 0 SEGMENT SIZE 80,
CONSTRAINT "WEBLOGPRIMARYKEY1" PRIMARY KEY ("ID")
);
It's just a simple table that has a unique ID, a timestamp for the entry,
a title, and a blob field to hold as much text as you want.
We have to nab the data somehow. Let's use dbExpress, just for the fun of it.
Go to the dbExpress tab in the Delphi 6 IDE and drop a TSQLConnection
and a TSQLDataset on the form. Then, from the Data Access tab, grab a TDatsetProvider and
a TClientDataset. Finally, add a TPagedAdapter -- from the WebSnap tab -- on
the form.
I found through experimentation that the TPagedAdapter really prefers
to get its data from a TClientDataset, and that is why we are using it. That,
and because the TSQLDataset component is unidirectional, so it won't work
with controls like a grid.
Double-click on the TSQLConnection, and create a new
connection called WebLog. Fill in your username and password. (The demo
application uses the default SYSDBA and masterkey.) Point the database value
to the full path of the WEBLOG.GDB file on your hard drive. And be sure to
set the LoginPrompt to False, as we don't want the Web app to have to log in
manually. Test the connection by making sure you can set the Connected property
to True. (Leave it there while you are at it.).
Next, go to the TSQLDataset component and set its CommandType property to ctQuery.
Set its SQLConnection to SQLConnection1. Then set the CommandText property as follows:
select * from WEBLOG ORDER BY ENTRYDATETIME DESCENDING
Set this component to Active as well. There's no reason not to have all these datasets active and
ready to go when the app is called. Right-click and add all the fields.
Hook the DatasetProvider's Dataset property to the TSQLDataset and the Clientdataset's
Provider property to the DatasetProvider. Then activate the clientdataset, right-click
on it, and add all the fields to it as well.
Now name the components as shown in this table:
| Component Type |
New Name |
| TSQLConnection |
IBConnection |
| TSQLDataset |
dbeWebLogQuery |
| TDatasetProvider |
dspWebLog |
| TClientDataset |
WebLogQuery |
| TPagedAdapter |
WebLogPagedAdapter |
Phew!
We have just a little more to do. Well, a lot more, actually. This is turning into a really
long article!
On to the TPagedAdapter!
Right-click on it and add an ordinary
TAdapterField. Name it WebLogEntryField. Then right-click on the PagedAdapter, select
Action Editor..., and add all the actions. It should look like this:
Now we need to provide a place for the paging to work.
One thing I figured out
fast was that paging works only inside a grid. So if you want to format a set
of fields, you need to do it in a grid with one column.
Go to the
AdapterPageProducer on your page and give it an AdapterForm with a grid and a command
group. Set the grid's Adapter property to PagedAdapter. Add a single DisplayColumn
to the grid and set its FieldName property to WebLogEntryField. Then add "All Commands"
to the CommandGroup. Set the CommandGroup's DisplayComponent property to the
AdapterGrid. This grid will hold all the content -- one record per
cell in the grid's single column.
Now -- finally -- we get to write some code!
Select the PagedAdapter and go to the Events
tab of the Object Inspector. The events that interest us are the last five events for
the TPagedAdapter.
I won't take any time explaining the code, because the event names should
make it all self-explanatory. You can easily see that the code gives the TPagedAdapter
the information it needs to navigate around your data and build the paging menus:
procedure TWebLogPage.WebLogPagedAdapterGetEOF(Sender: TObject;
var EOF: Boolean);
begin
EOF := WebLogQuery.Eof;
end;
procedure TWebLogPage.WebLogPagedAdapterGetFirstRecord(Sender: TObject;
var EOF: Boolean);
begin
WebLogQuery.First;
EOF := WebLogQuery.Eof;
end;
procedure TWebLogPage.WebLogPagedAdapterGetNextRecord(Sender: TObject;
var EOF: Boolean);
begin
WebLogQuery.Next;
EOF := WebLogQuery.Eof;
end;
procedure TWebLogPage.WebLogPagedAdapterGetRecordCount(Sender: TObject;
var Count: Integer);
begin
Count := WebLogQuery.RecordCount;
end;
procedure TWebLogPage.WebLogPagedAdapterGetRecordIndex(Sender: TObject;
var Index: Integer);
begin
Index := WebLogQuery.RecNo - 1; // Subtract 1 because RecNo is 1-based, and the system expects 0-based
end;
Ready to add the content?
Go to the PagedAdapter again
and select the field you created earlier by right-clicking and bringing up the
Fields Editor. Select the WebLogEntryField and go to the Events tab on
the Object Inspector. Make the OnGetValue event handler look like this:
procedure TWebLogPage.WebLogEntryFieldGetValue(Sender: TObject;
var Value: Variant);
const
FormatStr = '<B><FONT COLOR="RED">%s</FONT> -- %s</B> <FONT SIZE="-1">%s</FONT>';
begin
// This just formats a string holding the weblog entry from the current record.
Value := Format(FormatStr, [WebLogQueryTitle.AsString, WebLogQueryEntryDateTime.AsString, WebLogQueryEntryText.AsString]);
end;
This code formats an entry based on the data in the WEBLOG table that we created so
long ago. (Well, it seems so
long ago, doesn't it?)
One final thing. Set the TPagedAdapter's PageSize property
to 2, meaning we'll have two entries per page. (The database in the sample code has a
few silly entries. If you created your own database, you'll have to add some data for
the application to display anything.)
Run the app. Assuming you haven't
missed any steps -- and assuming I haven't missed any steps -- you should see something like this:
Finally, a bit of housekeeping.
Go to the webmodule and add this code to the
OnBeforeDispatch event to make sure the Clientdataset picks up changes to the table
as you make them:
procedure TWebLogPage.WebPageModuleBeforeDispatchPage(Sender: TObject;
const PageName: String; var Handled: Boolean);
begin
WebLogQuery.Refresh; // Ensures that the clientdataset has any updates made since the last request.
end;
Hm. We've come this far, might as well add a little gold-plate.
You
can change the Caption of the actions on the PagedAdapter to whatever you want -- like
Next instead of NextPage, for example. In addition, you can spruce up the page
numbers a bit by adding this code to the GotoPage action's OnGetPageName event:
procedure TWebLogPage.ActionGotoPageGetPageName(Sender: TObject;
APageIndex: Integer; var Value: String);
begin
// The standard output is boring. Lets make the entries bold and put the little <#> dealies around them
Value := Format('<B><%d></B>', [APageIndex + 1]); // Add one so we don't have the "zero-eth" page
end;
Finally, we're done
Wow -- I thought this one would never end. But it was worth it, eh?
Now you can create a WebLog and start posting all kinds of outrageous diatribes. Wait,
that's actually a pain, as you have to do it manually. We don't have an application
to add WebLog entries.
I guess my next article will be about how to add
items to the WebLog, huh? I guess you'll just have
to stick around until next time.
Thanks for coming over.
Nick Hodges is Lead Sled Dog at HardThink Inc., a
consulting shop specializing in Delphi development. He is a TeamB member
and he wants to put a wood-burning stove in his house.
But mostly he like to hang with his family and enjoy their new home in St. Paul, MN.