WebSnap is now in session

By: Nick Hodges

Abstract: This article discusses how to maintain session information in a WebSnap application. By Nick Hodges.

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.


Server Response from: ETNASC02