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).
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.
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: 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. 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.
Last Modified: 17-DEC-99