In my last article, "Loggin'
in ain't hard to do," I showed you how incredibly easy it is to set up your site for logins, and how
you can do it without writing a single line of code. This time, I'll show you the next pathetically easy thing
to do with WebSnap -- maintain session data. I regret to say, however, that you'll will have to write a
couple of lines of code this time around. Sorry about that.
Note: The demo code for this article can be downloaded from
CodeCentral.
Session school is in session
You have already been keeping track of session data, though maybe you didn't know it. When you logged
into the application you built in the login article, your user name was emblazoned across the top of the very
stylish and hip Web pages in the application. That information was stored in the EndUser scripting object,
which used session information to keep track of it.
For more information about these scripting objects, see
Jim Tierney's
article and the file WebScript_TLB.pas.
As you doubtlessly know already, the HTTP protocol -- the protocol used to transport 99.9% of
all Web pages -- is stateless. That means that once your web server answers an HTTP request, it couldn't
care less about the hapless user on the other end of the connection. It is therefore up to you to do something
to make your Web application remember who that user was. This is normally done by leaving information
behind on the client after each request, commonly with a cookie, a hidden field, or a "fat URL."
WebSnap provides the TSessionService component to handle most of the work of maintaining state for you.
It generates unique session ID values and writes them out to the client for you as
cookies. For each subsequent request, the user sends cookie information containing
the unique Session ID, and your application is able to find session information for that particular user. This
lets you store information about your users
and provide it back to them in your application across page views.
So how do you overcome the innate
heartlessness of your Web server and build a kind, thoughtful, friendly Web site that remembers your users?
Here's how.
Time to roll up our sleeves
We'll use the Login
application from "Loggin' in" as a basis for our new application. Load
the Login code into Delphi 6, then save the project and all the units in a different directory, naming
the new project SessionDemo.dpr. Note that the application is a Web App Debugger
application -- we'll
run it via the Web App Debugger, which can be fired up via the Tools menu. I am going to assume that
you understand the basic workings of WebSnap, so that when I say to add a page and give it this and name it
that, you know what to do. If not, review some of the earlier articles on WebSnap.
If we want to maintain session data about a user, it would be helpful to have some
session data. As it stands, we have none. Personally, I think that
knowing a user's favorite movie is critically important to crafting a memorable
Web-site experience. Knowing a user's predilection for certain days of the week is essential for that personal touch.
And knowing
whether or not a user likes coffee will likely make or break your ability to keep and hold
the user's eyeballs
on your site. So we'll build a site that gathers all of this critical information and tracks
it throughout the user's visit.
Start by adding a page to the site. The page should have a TAdapterPageProducer, it should
be a Login Required page, and you should name it GetPrefs. Save the file as
wmGetPrefs.pas. Drop a
TAdapter onto the page and name it PrefsAdapter. (If you don't know how to do all this, review the Login article.)
Now let's add some fields to hold user data. Start by right-clicking on the PrefAdapter
component and select Fields Editor. Click the New Component button and add an
AdapterField, naming it FavoriteMovieField. (Note that when you rename
the AdapterField, the DisplayLabel and FieldName properties change as well.)
Change the DisplayLabel property to Favorite Movie:.
Next, add an AdapterBooleanField and name it LikesCoffeeField. Set its DisplayName property to
Do you like coffee?
Add an AdapterMultiValueField and name it DaysOfWeekField. Set its DisplayLabel property to
Select the days you like:.
Go to PrefAdapter in the web module, select it, right-click, and choose Actions
Editor... Add an action and name it SubmitPrefsAction.
Now we need to provide some values for DaysOfWeekField. This is done with a TStringsValueList. Drop one
on the GetPrefs web module, name it DaysOfWeekList, and add the days of the week to its strings property, one day to
a line. Go to the PrefAdapter Fields page and select the DaysOfWeekField and assign DaysOfWeekList to its ValuesList
property. You should now have something that looks like this:
The well-tempered Web page
Now we are ready to build the page for gathering this information.
(Just as a parenthetical aside, I think we should note that we are able to do all of this without
writing a single line of code. I know you are
getting anxious to write some code, but you'll just have to wait a little longer. You'll get to write some Object Pascal in a minute.)
Double-click on the AdapterPageProducer and bring up the Web Surface Designer. Add an AdapterForm to the
page, then add an AdapterFieldGroup to the AdapterForm.
I, for one, am sick of the way WebSnap always stacks these controls on
top of each other vertically. I am feeling horizontal today, and I'm the one
calling the shots, so we'll do it my way: Add a LayoutGroup to the AdapterFieldGroup and set
its DisplayColumns property to 3. Then right-click on the LayoutGroup and select
Add All Fields. Voila! The fields
are now laid out horizontally, just the way I like 'em.
Those checkboxes seem a little clumsy, so select the DaysOfWeekField item in the upper
right panel and change its InputType property to iftSelectMultiple. That looks
better, doesn't it?.
Finally, go back to
the AdapterForm and add an AdapterCommandGroup. Set its Display Component to
AdapterFieldGroup1 and set its Caption
to Submit Preferences. Right click on it and select Add All Actions. Now you should have something that looks
like this:
You can mix and match LayoutGroups throughout your HTML in order to place the controls in your AdapterPageP wherever
you like. The LayoutGroup uses an HTML table to lay out the controls, and if you don't like the default settings of the
table, you can change them using the Custom property. The Custom property adds
text to the <TABLE> tag. For instance, the controls probably seem a little crowded right now, so add
'"CELLSPACING="6" CELLPADDING="6"' to the Custom property in order to space things out a little
more. You can also add custom styles with the Style and StyleRule properties. Thus, you can pretty
much control all aspects of the HTML code in your AdapterPageProducer. In addition, each of the controls inside
the AdapterPageProducer has properties that allow you to control HTML output. Play around with them
and see the different effects that they can have.
Now you can run the application, use the Web App Debugger to navigate to the page,
log in, and enter
your preferences. The button doesn't do anything yet, but it sure looks cool. (I know, I know, you've been waiting
a long time to write some code. Your chance is coming up Real Soon Now.TM)
Now you've got a nice looking page and a button for submitting all the user information.
(Note that you can use
the Shift and Control keys to select more than one day of the week -- sharp,
huh?) When the user presses the Submit
button, we want to gather up all of the preferences and store them in the Session object. That's pretty easy to
do...but it will require you to break down and write some code.
Finally, some code
Let's get some housekeeping out of the way first. Somewhere in your app, add the following string constants -- I
put mine in the implementation section of the wmGetPrefs unit:
const
sFavoriteMovie = 'FavoriteMovie';
sLikesCoffee = 'LikesCoffee';
sDaysOfWeek = 'DaysOfWeek';
These strings will serve as indexes for session variables. Next, we need to
handle the SubmitPrefsAction and store the retrieved values in the Session variable. Go to the GetPrefs
page and right-click on the PrefsAdapter component. Select Actions Editor... and select
SubmitPrefsAction. Go to the Object Inspector,
select the Events tab, and create an event handler for the OnExecute event. Add the following code:
procedure TGetPrefs.SubmitPrefsActionExecute(Sender: TObject;
Params: TStrings);
var
Value: IActionFieldValue;
i: integer;
SL: TStringList;
begin
Value := FavoriteMovieField.ActionValue;
if Value.ValueCount > 0 then
begin
Session.Values[sFavoriteMovie] := Value.Values[0];
end;
Value := LikesCoffeeField.ActionValue;
if Value <> nil then
begin
if Value.ValueCount > 0 then
begin
Session.Values[sLikesCoffee] := Value.Values[0];
end;
end else
begin
Session.Values[sLikesCoffee] := 'false';
end;
SL := TStringList.Create;
try
Value := DaysOfWeekField.ActionValue;
for i := 0 to Value.ValueCount - 1 do
begin
SL.Add(Value.Values[i]);
end;
Session.Values[sDaysOfWeek] := SL.Text;
finally
SL.Free;
end;
end;
(You finally got to write some code! Feels good, doesn't it? I mean, it just
seems strange to wield this much power with just a few mouse-clicks, so writing
some code is a blessed relief, no doubt.)Caution -- programmer at work
The results for each of the fields will be placed
in the variable Value, which is of type IActionFieldValue -- an interface declared as
follows:
IActionFieldValue = interface
['{C5D4E556-A474-11D4-A4FA-00C04F6BB853}']
function GetFieldName: string;
function GetValueCount: Integer;
function GetValue(I: Integer): Variant;
function GetFileCount: Integer;
function GetFile(I: Integer): TAbstractWebRequestFile;
property ValueCount: Integer read GetValueCount;
property Values[I: Integer]: Variant read GetValue;
property FileCount: Integer read GetFileCount;
property Files[I: Integer]: TAbstractWebRequestFile read GetFile;
property FieldName: string read GetFieldName;
end;
There is a lot of information there! For simple fields, we'll be interested
primarily in the Values property, which contains the values entered or selected by the user.
Each field has an ActionValue property that returns an IActionFieldValue
interface after an action is taken on the field. So we simply set the Value variable to hold
the interface.
From there, we can grab the user input from the interface and store it. For the FavoriteMovieField,
for instance, we
simply get the first value and place it in the Session.Values property.
The Session object is where all the work gets done. The Values property is a string-indexed array of Variants,
and thus very flexible and easy to use. Basically, you can add as many items into this array as you
like and
index them by strings. What could be easier?
The value will always be saved based on each user's
unique session ID. We'll take a look at session IDs in a minute -- for now, rest assured that each
user has a unique ID which is stored in a cookie on the client machine. Each request sends that value back, so the Session.Values array is set up uniquely for each user.
The LikesCoffeeField portion of the code works much the same as
FavoriteMovieField except that we store a string value of either true or false. The DaysOfWeekField values are a little
trickier, as the user can specify more than one value. We have to trick the system a little bit by storing the values in a temporary
TStringList, then putting
the Text property into the Session.Values array.
Retrieving user data
So far so good. We've gathered user preferences and placed them in the Session object.
But how do we get at them?
Luckily, TAdapter provides the means to get values out, just as
it has the means of getting them in! And it is actually quite simple. For the two single-value
fields, all we need do is provide a handler for the OnGetValue event. Go to the GetPrefs
web module,
double-click on the PrefsAdapter, and select the FavoriteMovieField object. Go to the Object Inspector
and create an event handler for the OnGetValue event. Make it look like this:
procedure TGetPrefs.FavoriteMovieFieldGetValue(Sender: TObject;
var Value: Variant);
begin
Value := Session.Values[sFavoriteMovie];
end;
This code should be pretty much self-explanatory. Go ahead and do the same thing for the LikesCoffeeField.OnGetValue
event. (You'll want to change the string constant to the appropriate value, of
course, but I didn't have to tell you that,
did I? I know you are way ahead of me on all of this.)
DaysOfWeekField takes a little more code, as it may hold multiple values. Select DaysOfWeekField and go to the Events Page of the
Object Inspector. Provide event handlers for OnGetValueCount and OnGetValues like so:
procedure TGetPrefs.DaysOfWeekFieldGetValueCount(Sender: TObject;
var Count: Integer);
var
SL: TStringList;
begin
SL := TStringList.Create;
try
SL.Text := Session.Values[sDaysOfWeek];
Count := SL.Count;
finally
SL.Free;
end;
end;
procedure TGetPrefs.DaysOfWeekFieldGetValues(Sender: TObject;
Index: Integer; var Value: Variant);
var
SL: TStringList;
begin
SL := TStringList.Create;
try
SL.Text := Session.Values[sDaysOfWeek];
Value := SL[Index];
finally
SL.Free;
end;
end;
See the little trick we pull with storing the string values
and using a TStringList to get the individual values? Pretty neat little hack, if I do say so myself.
(You, of course, must know an even better scheme -- so email me with
it!)Gathering up all of these preferences isn't very useful if we lack a place to display them.
So let's add a page to the project. Make sure users have to log in to see it
(review the "Loggin' in" article if you've forgotten how) and save the new page as
wmPrefDisplay. Leave it with a plain PageProducer if you like. We are going to do a little
JavaScript now
to get the values out of the Adapter and onto the page. Since this is the page that we'll go to
after doing the SubmitAction, we need to tell that to the application. Go to the wmGetPrefs
page and
double-click on the AdapterPageProducer. Navigate to the CmdSubmitPrefsAction and set its PageName property
to PrefDisplay. This will tell it to go to that page after the action is executed.
Use the Code Editor to navigate to the HTML page attached to wmPrefDisplay. You should see the
default template HTML page there. Right above the </body> tag, add the following
JavaScript:
<P>
<B>Favorite Movie:</B> <%= Modules.GetPrefs.PrefsAdapter.FavoriteMovieField.Value %>
<P>
<% s = ''
if (Modules.GetPrefs.PrefsAdapter.LikesCoffeeField.Value)
s = 'You like Coffee.'
else
s = 'You do not like coffee.'
s = '<B>' + s + '</B>';
Response.Write(s);
%>
<P>
<%
// Display all the values of a multiple value adapter field.
function ListValues(f)
{
var s=''
var v=''
var n=''
var c=0;
if (f.Values == null) return s;
var e = new Enumerator(f.Values.Records)
for (; !e.atEnd(); e.moveNext())
{
s+= '<li>'
// Use DisplayText here to the name of the item rather than
// the value.
s += f.Values.ValueField.DisplayText;
s += '</li>'
c++
}
e.moveFirst()
r = new Object;
r.text = s
r.count = c
return r;
}
%>
<B>Favorite Days of the Week:</B>
<% obj=ListValues(Modules.GetPrefs.PrefsAdapter.DaysOfWeekField)%>
<ul>
<%=obj.text%>
</ul>
Now when you run the program you can log in and enter values on the GetPrefs page. When
you submit them, you are taken to a page that displays them. Now here's the really cool part -- go
back to the GetPrefs page, and you'll see that your selections are remembered and displayed properly on the
page -- even your selected days of the week. That's really nice, huh? No more
hassling with filling those values out each time you visit the Web site.
Finishing touches
Lets add one more page. This one will simply show off the Session value, just
to convince the skeptics that we really do have a unique Session ID each time we
run the application. Create a new page and call it SessionID.
Save the page as wmSessionID. Then, in the HTML, right above the </body> tag,
add this:
<P>
Your Session ID is: <%= Session.SessionID.Value %>
This time, don't run the application. Simply request this page in your browser.
Each request will
return a different value, because each time the application is run it creates a new session for your request.
Since the session values are held in memory, they are lost when application closes, and you get a new
Session ID for each request. Each session ID is a 16-character string with random
values in each character.
Take a quick look at the SessionService component on the wmHome page. You'll see that you can set
a DefaultTimeOut value (in minutes) for each session. This lets you expire sessions for users who don't
return and refresh their sessions after a given period of time. You can also limit the total number of
sessions, but it seems likely that you'll want to leave this at -1, which allows unlimited sessions to
exist.
Remember that session information is held in memory, and that means that you won't be able
to use the session information in a CGI application. Also note that the login scheme accesses the session
variable, and will terminate the current session when a user logs out. When a
user logs out, the current session is terminated, but a new one is immediately created. As long as you
have a component assigned to the TWebAppComponents.Sessions property, every request will be assigned a
session variable.
So long for now
That does it for this installment of Nick's WebSnap Adventures. You likely have already spotted
the glaring hole in this scheme -- the values for each user aren't saved between logins. I'll
write another article about how to make these values persistent between sessions, so that your users can
keep their preferences over the long haul.
Nick Hodges is The Big Cheese at HardThink,
Inc., a
consulting shop specializing in Delphi Development. He is a TeamB
member and is trying to learn American Sign Language.
But mostly he like to hang with his family and enjoy their new home in St. Paul, MN.