Paging Dr. WebSnap!

By: Nick Hodges

Abstract: Use the TPagedAdapter component for fun and profit! By Nick Hodges.

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.


Server Response from: ETNASC02