Maintaining State in Web Applications

By: Corbin Dunn

Abstract: How to maintain state with either Fat URLs, hidden fields, or cookies.

Advanced Web Applications

Maintaining State in Web Applications
By Corbin Dunn

There are several ways to maintain state in a web application. Take a look at DavidI's document about maintaining state (http://community.borland.com/article/0,1410,10265,00.html) for some pro's and con's of each method. In this document, I will describe ways to use each method and provide an example.

Maintaining state with cookies
The first way, and one of the most common ways, is to maintain state with cookies. This document was written with Delphi 5 in mind, but it should work fine in previous versions.

Cookies are stored in a file on your client machine and can be set and retrieved by a Web Application program with the help of a web browser. Cookies are sent as part of the HTML request/response header so it is transparent to the user. A cookie has a Name and Value pair, an expiration date, a domain name that the cookie is valid for, and a set of URLs that the cookie is valid for. In addition, you can set the Secure property of a cookie to only allow it to be sent and received via a secure connection (typically, HTTPS).


Delphi Example

This best way to learn is to start up Delphi and create a new Web Application (this requires the Enterprise version of Delphi). In the new web module, add a single action and make it the default action (set Default to True). Now, add the following code in the OnAction event:

procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  I: Integer;
  strVisitNum: string;
begin
  // This is the main "entry" action.
  // This example will count the number of times a person
  // has been to this page using a cookie.

  strVisitNum := '1'; // Initialize the visitor number to 1

  // First check to see if there is a cookie present
  if Request.CookieFields.Count > 0 then
  begin
    // See if the 'Visit Number' cookie is present
    strVisitNum := Request.CookieFields.Values['Visit Number'];
    if strVisitNum <> '' then
    begin
      try
        // Try to convert it to an integer and inc it
        strVisitNum := IntToStr(StrToInt(strVisitNum) + 1);
      except // Eat exceptions (conversion exceptions)
      end;
    end
  end;

  // Set the 'Visit Number' cookie 
  with Response.Cookies.Add do
  begin
    // You should set the domain to your domain to
    // get only cookies sent back to that domain (yours).
    // I'm leaving it commented out because in testing
    // you usually reference your machine by http://machinename/scripts
    // with no domain, and you can test this app by just compiling.
//    Domain := 'inprise.com';

    // Expire (quit sending the cookie) in one month from now
    Expires := IncMonth(Now, 1);
    // Limit the cookie to the path to this DLL
    Path := Request.URL; // Such as '/scripts/StateKeeper.dll'
    Secure := False;
    // The most important parts of the cookie are the Name and Value
    Name := 'Visit Number';
    Value := strVisitNum;
  end;

  Response.Content := '<html><body>Welcome!<br>You have visited ' +
    strVisitNum + ' times!</body></html>';

end;

When receiving cookies, the main thing that you will be looking at is Request.CookieFields.Values['Cookie Name'];. If the cookie is not present, the returned string will be blank.

When setting a cookie, the most important things to set are the Name and Value properties.


Maintaining state with Hidden Fields
In this next example, we will see how to save state with forms using the POST method and hidden fields.

Conceptually, you can send a custom form to the user based on previous selections. You can also maintain state with hidden fields. This example does both. It allows the user to select an option, and based on what option they selected, it will set it as selected in the resulting page. In addition, it keeps track of the number of times they have gone through that form by saving a counter in a hidden field.


Delphi Example

To build this example, simply add another action in your web module and change the PathInfo to be any string (I chose '/hidden'). Then, access the page with: http://MyMachineName/scripts/DllName.dll/hidden (or something similar based on the names you used).
procedure TWebModule1.WebModule1WebActionItem2Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  nOptionSelected: Integer;
  strOptionString: string;
  strVisitNum: string;
begin
  // Hidden field example. 

  // The first thing we do is check to see what method this
  // action was invoked by. If it was from a GET method,
  // we will return a form for the user to use and send
  // a selected option.
  // In addition, we will maintain "state" of how many visits
  // the user has made to the page with a hidden field.

  if Request.MethodType = mtGet then
  begin
    // Send a form back as the response so the user can
    // invoke this same action with it (with a POST method)
    Response.Content := '<html><body>' + #10#13 +
                        '<form action="' +
                        Request.URL + Request.PathInfo +
                        '" method="post">' +
                        'Test saving state! Select '
						 +
                        'something for me to remember: ' +
                        '<select name="selection">' +
                        '<option value="1">First Option' +
                        '<option value="2">Second Option' +
                        '<option value="3">Third Option' +
                        '<option value="4">Forth Option' +
                        '</select>' +
                        // Notice the hidden field below to
                        // keep track of the number of "visits"
                        '<input type="hidden" ' +
                        'name="Visit Number" value="1">' +
                        '<input type="submit">' +
                        '</form></body></html>';

  end
  else if Request.MethodType = mtPost then
  begin
    // On a post, data is sent to us in the ContentFields
    // string list.
    try
      nOptionSelected :=
        StrToInt(Request.ContentFields.Values['selection']);
    except // Catch conversion exceptions
      nOptionSelected := -1;
    end;
    // Set the selected option based on which one was sent
    // in the post data.
    case nOptionSelected of
      1: strOptionString := '<option value="1" selected>First Option' +
                         '<option value="2">Second Option' +
                         '<option value="3">Third Option' +
                         '<option value="4">Forth Option';
      2: strOptionString := '<option value="1">First Option' +
                         '<option value="2" selected>Second Option' +
                         '<option value="3">Third Option' +
                         '<option value="4">Forth Option';
      3: strOptionString := '<option value="1">First Option' +
                         '<option value="2">Second Option' +
                         '<option value="3" selected>Third Option' +
                         '<option value="4">Forth Option';
      4: strOptionString := '<option value="1">First Option' +
                         '<option value="2">Second Option' +
                         '<option value="3">Third Option' +
                         '<option value="4" selected>Forth Option';

    else   // None selected (some error occured if this is executed)
      strOptionString := '<option value="1">First Option' +
                         '<option value="2">Second Option' +
                         '<option value="3">Third Option' +
                         '<option value="4">Forth Option';
    end;

    // Now try to inc the Visit Number
    try
      // Try to convert it to an integer and inc it
      strVisitNum := Request.ContentFields.Values['Visit Number'];
      strVisitNum := IntToStr(StrToInt(strVisitNum) + 1);
    except // Eat exceptions (conversion exceptions)
      strVisitNum := '1';
    end;

    Response.Content := '<html><body>' + #10#13 +
                        '<form action="' +
                        Request.URL + Request.PathInfo +
                        '" method="post">' +
                        'You selected option number ' +
                        Request.ContentFields.Values['selection'] +
                        ' and this is vist number ' +
                        strVisitNum + '<br>' +
                        'You can select another: ' +
                        '<select name="selection">' +
                        strOptionString +
                        '</select>' +
                        // Add the hidden field back in
                        '<input type="hidden" ' +
                        'name="Visit Number" value="' +
                        strVisitNum + '">' +
                        '<input type="submit">' +
                        '</form></body></html>';

  end
  else // An "Unsupported" MethodType
    Response.Content := '<html><body>Method not supported</body></html>';
end;
One of the key things to notice here is the line:
'<input type="hidden" name="Visit Number" value="' + strVisitNum + '">'
that adds the hidden field to keep track of the number of visits.

This example is probably one of the more elegant ways of saving state. You could easily extend it to database browsing. Instead of saving the "Visit Number" you could save the current record (or range) that the user was viewing.


Maintaining state with Fat URLs
The third way to maintain state is with "Fat URLs" like the following which shows how a search on HotBot can be saved: http://hotbot.lycos.com/?MT=Corbin%27s+treehouse

You can easily implement this in Delphi, and because of its simplicity it is one of the more common ways of saving state that is easily 'bookmarkable.'

The first thing we are going to do is take a closer look at a GET request (aka, Fat URL). Typically, they will look something like:
http://www.mydomain.com/scripts/myIsapi.dll/PathInfo?name1=value1&name2=value2
The first part (http://www.mydomain.com/scripts/myIsapi.dll/PathInfo) contains the usual information; the domain name, followed by the path to the ISAPI dll (or CGI), and then the PathInfo that specifies which action to invoke. Immediately after that is a question mark that signifies the start of the query data, or Fat URL. It is then followed by Name=Value pairs separated by ampersands.

In Delphi, the way you access these values is with the Request.QueryFields.Values['Name'] object which returns the value associated with a given Name.

Something to be aware of is that the web browser has to URL encode the data sent via the GET method. If you use a form to send the data with a GET method (as which is done in my example below), then you don't have to worry about anything. If you want to construct a Fat URL in code, you have to be sure to URL encode any possible Name and Value pairs. In Delphi, with the NetMaster's TNMURL component, this is a piece of cake. Here is an example:

function Encode(const str: string): string;
  begin
    NMURL1.InputString := str;
    Result := NMURL1.Encode;
  end;
  
var
  strFatURL: string;
begin
  strFatURL := 'http://MyMachine/scripts/MyIsapi.dll/Path?' +
    Encode('Query Name Is')
    + '=' +
    Encode('Corbin''s Tree House')
    + '&' +
    Encode('&Another Name')
    + '=' +
    Encode('Here"s The Result Value');
end;
Notice the small helper function "Encode" that encodes a given string with the TNMURL component and returns the encoded string. After execution, strFatURL contains: http://MyMachine/scripts/MyIsapi.dll/Path?Query+Name+Is=
Corbin%27s+Tree+House&%26Another+Name=Here%22s+The+Result+Value
which is a properly URL encoded.


Delphi Example

Now it is time to add some code to the Delphi example we have been working on. Add another action to your web module and give it a new PathInfo string, such as '/get' (since GET is the method which a Fat URL is invoked by).

procedure TWebModule1.WebModule1WebActionItem3Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  strVisitNum: string;
begin
  // Fat URL example (using the GET request method).

  // When a web browser sends a Fat URL to the web server, you have
  // to access the data sent via the Request.QueryFields, instead
  // of the Request.ContentFields that are filled in with the POST
  // method.

  // First, check to see if the URL has any data sent in the Query
  if Request.QueryFields.Count <= 0 then
  begin
    // Set the response to an HTML form with the GET method
    Response.Content :=
      '<form action="' +
      Request.URL + Request.PathInfo +
      '" method="get">' +
      'Test saving state with Fat URL''s!<br>' +
      '<input type="submit" name="counter" value="1">' +
      '</form></body></html>';
  end
  else
  begin
    // Inc the counter and send back the count number
    try
      strVisitNum := Request.QueryFields.Values['counter'];
      strVisitNum := IntToStr(StrToInt(strVisitNum) + 1);
    except // Eat exceptions (conversion exceptions)
      strVisitNum := '1';
    end;
    Response.Content :=
      '<form action="' +
      Request.URL + Request.PathInfo +
      '" method="get">' +
      'Test saving state with Fat URL''s!<br>' +
      '<input type="submit" name="counter" value="' +
      strVisitNum + '">' +
      '</form></body></html>';
  end;
end;

You will probably notice that the URL becomes something like: http://MyMachine/scripts/MyIsapi.dll/get?counter=23, which means the user can very easily change the URL and the underlying request that is sent to the Web Application when a Fat URL is sent with the GET method. Of course, it is possible to do this with a POST method too, but it is more difficult because the user has to create an HTML form that sends the custom data to a Web Application.

This is just something to keep in mind. If you try to put counter=1.123 in the URL, and didn't catch the conversion exception like I do, your application would show an exception to the end user (which isn't very pretty).

You should now have a good understanding of how to save state in a Web Application.

Download the complete source code for this web application.


Server Response from: ETNASC04