Shiv Kumar
 Hobbyist Filmmaker / Editor

Maintaining State - Using Fat URLs

Not Rated YetNot Rated YetNot Rated YetNot Rated YetNot Rated Yet0votes
January 14, 2008 11:21 AM  Views: 655   Favorited: 0 Favorite It Comments: 0
Filed Under:  Programming
Tags:  Database, Delphi, ISAPI
 

Sometimes, we'd like to be able to show only one record from a database in a web page with the ability to navigate to the previous and next records. In this tutorial this is what we'll set out to achieve. If you're new to ISAPI programming, please take a look at the tutorials prior to this. In this tutorial, I don't give you the step by step instructions on how to start with ISAPI projects but rather assume, you've gone through and understood the previous tutorials. . You can take a look at a demo of the project we're about to create, here http://www.matlus.com/scripts/biolifesingle.dll
BiolifeSingle.png

Since the http protocol is a stateless protocol (after every response the connection to the client is dropped) we need to know what the "next" and "prior" records are for any given client. When a client requests the next record, we need to know either what the current record is, or what the next record is, in order for us to respond with the next record. In this tutorial, we'll use a technique known as "fat URLs" for passing on the state information to our ISAPI application.

What we really do is pass on the ID of the next record when the user clicks on the Next button. Or the ID of the Prior record if the user clicked on the Prior button. Since this information needs to be given to us in each request, we need to have sent this information in the earlier response. A kind of "thinking ahead". You'll find you frequently need to "think ahead" when you're building browser based (ISAPI/CGI) applications. So when we return the current requested record, we also need to know the prior and next records at that time for us to be able to create hyper-links for the prior and next buttons.

For example, if the user requested the first record, we need to know the ID of the next record and create a hyper-link for the next button, that requests that next record. For simplicity sake, lets assume the Ids of our records were 1,2,3 ...for the corresponding records. Lets also suppose we have an action in out ISAPI application with the path info of /ShowRecord, that expected to see a parameter called No that indicated the record we need to respond with. So for the first record the anchor tag for the Next button should be

/ShowRecord?No=2 (Note that we're ignoring the full URL here). In this case, since the current record is the first record the anchor tag for the Prior button should not exist (since there is no record prior to the first). Now if the user clicks on the Next button, we need to respond with the 2nd record (extracted from QueryFields property of the Request object - /ShowRecord?No=2). But we also need to know the IDs of the Prior record and Next record. The anchor tag for the Prior Button should be /ShowRecord?No=1 and that for the Next Button should be /ShowRecord?No=3. Now if the users clicks on either of the buttons, we know which record to respond with.

The function ShowSingleRecord in our ISAPI does this job as shown below:

The function ShowSingleRecord
function TWebModule1.ShowSingleRecord(SpeciesNo : string) : string;
var
  GotRequiredRecord : Boolean;
begin
  with qryData do
  begin
    Open;
    { When a Query is opened it is set to the BOF Crack }
    FFirstRecordID := FieldByName('Species_No').AsString;
    Last;
    { Move it to the Last Record and assign the property its values }
    FLastRecordID := FieldByName('Species_No').AsString;
    { Move it back to first }
    First;
    GotRequiredRecord := False;
    FPreviousID := '';
    FNextID := '';
    while not Eof do
    begin
      { If the Required SpeciesNo has been found then assign the NextCode variable and exit the loop }
      if GotRequiredRecord then
      begin
        FNextID := FieldByName('Species_No').AsString;
        Break;
      end;
      { If this is the Requested SpeciesNo, then assign values to variables for later use }
      { SpeciesNo will be blank for the first and last records }
      if (FieldByName('Species_No').AsString = SpeciesNo) or
         (SpeciesNo = '')  then
      begin
        GotRequiredRecord := True;
        { Assign the Current Records Field values to the corresponding properties of the web module }
        FSpeciesNo := FieldByName('Species_No').AsString;
        FCategory := FieldByName('Category').AsString;
        FCommonName := FieldByName('Common_Name').AsString;
        FNotes := FieldByName('Notes').AsString;
      end;
      if not GotRequiredRecord then
        FPreviousID := FieldByName('Species_No').AsString;
      Next;
    end;
    Close;
  end; { with Query1 do }

  { Construct the HTML now }
  PageProducer1.HTMLDoc.Text := GetDisplayTemplate;
  Result := GetHeader('Biolife Species No:'   SpeciesNo);
  Result := Result   PageProducer1.Content;
  { Add the Previous and Next Button Images with the required hyper links }
  Result := Result   GetNavigationButtons;
  { Add the HTML footer }
  Result := Result   GetFooter;
end;

The Web Module has properties declared for placeholders of the information we need to store across methods. Such as:

Properties of the WebModule
    constructor Create(AOwner: TComponent) ;override;
    property FirstRecordID : string read FFirstRecordID;
    property LastRecordID : string read FLastRecordID;
    property PreviousID : string read FPreviousID;
    property NextID : string read FNextID;
    property SpeciesNo : string read FSpeciesNo;
    property Category : string read FCategory;
    property CommonName : string read FCommonName;
    property Notes : string read FNotes;
    { Public declarations }

The purpose of the function GetNavigationButtons is to construct the required HTML to show the navigation buttons with the appropriate anchor tags for each of the buttons. That is, with respect to the current requested record, the anchor tags for the prior and next buttons should contain the Ids of the prior and next records. Also, if there is no prior record (the current record is the first record), we should use the disabled version of the image and have no anchor tag for the prior button. Similarly, if there is no next record (current record is the last record) we need to use the disabled version of the next button's image and have no anchor tag for it. Since we also have to contend with a First Button and Last Button, the enabled/disabled states of these buttons need to be as we would expect as well.

The function GetNavigationButtons
function TWebModule1.GetNavigationButtons : string;
var
  PreviousImage : string;
  NextImage     : string;
  FirstImage    : string;
  LastImage     : string;
begin
  { If the Requested Record is the First Record, use the disabled images }
  if PreviousID = '' then
  begin
    PreviousImage := 'PriorDis.jpg';
    FirstImage := 'FirstDis.jpg';
  end
  else
  begin
    PreviousImage := 'Prior.jpg';
    FirstImage := 'First.jpg';
  end;

  { If the Requested Record is the Last Record, use the disabled image }
  if NextID = '' then
  begin
    NextImage := 'NextDis.jpg';
    LastImage := 'LastDis.jpg';
  end
  else
  begin
    NextImage := 'Next.jpg';
    LastImage := 'Last.jpg';
  end;

  Result :=
    '<TABLE>'   crlf  
    '  <TR>'   crlf;

  { If the Requested Record is the First Record, use the diabled image and don't
    create an hyper-link for the Button }

  { The Property SpeciesNo holds the current record's values }
  if  SpeciesNo = FirstRecordID then
    Result := Result  
      '    <TD><IMG SRC="/Images/'   FirstImage   '" BORDER="0" ALT="First Record"></TD>'   crlf
  else
    Result := Result  
      '    <TD><A HREF="'   Request.ScriptName   '/ShowRecord?No='   FirstRecordID   '"><IMG SRC="/Images/'   FirstImage   '" BORDER="0" ALT="First Record"></A></TD>'   crlf;

  if PreviousID = '' then
    Result := Result  
      '    <TD><IMG SRC="/Images/'   PreviousImage   '" BORDER="0" ALT="Previous Record"></TD>'   crlf
  else
    Result := Result  
      '    <TD><A HREF="'   Request.ScriptName   '/ShowRecord?No='   PreviousID   '"><IMG SRC="/Images/'   PreviousImage   '" BORDER="0" ALT="Previous Record"></A></TD>'   crlf;

  if NextID = '' then
    Result := Result  
      '    <TD><IMG SRC="/Images/'   NextImage   '" BORDER="0" ALT="Next Record"></TD>'   crlf
  else
    Result := Result  
      '    <TD><A HREF="'   Request.ScriptName   '/ShowRecord?No='   NextID   '"><IMG SRC="/Images/'   NextImage   '" BORDER="0" ALT="Next Record"></A></TD>'   crlf;

  if  SpeciesNo = LastRecordID then
    Result := Result  
      '    <TD><IMG SRC="/Images/'   LastImage   '" BORDER="0" ALT="Last Record"></TD>'   crlf
  else
    Result := Result  
      '    <TD><A HREF="'   Request.ScriptName   '/ShowRecord?No='   LastRecordID   '"><IMG SRC="/Images/'   LastImage   '" BORDER="0" ALT="Last Record"></A></TD>'   crlf;

  Result := Result  
    '  </TR>'   crlf  
    '</TABLE>'   crlf;
end;

The TPageProducer
This is by no means a really good example of the capability of the TPageProducer component, but we do use it here in one of many ways it can be used. This component allows you to use an HTML template (either a physical file or a TStrings property). In the template, we have placeholders for the various fields of our dataset. These place holders are denoted with the # sign. When we read the Content property of the PageProducer (either directly or by assigning it to another variable etc.), the OnHTMLTag event is fired for every placeholder we have in our template.

In this particular example, we'll use a function in our ISAPI that basically returns an HTML "template" . The result of this function is then fed to the TPageProducer component. Lets look at this function before we proceed.

The function GetDisplayTemplate that returns our HTML Template
function TWebModule1.GetDisplayTemplate : string;
begin
  Result :=
    '<A HREF="http://www.matlus.com/scripts/website.dll/Tutorials?DelphiISAPI&DelphiISAPIPreviouNext&7">'  
      '<FONT FACE="Arial">Back to The Tutorial Page</FONT></A><P>'   crlf  
    '<TABLE WIDTH=80% BGCOLOR="BLACK" BORDER="1" BORDERCOLOR="BLACK" CELLSPACING="2" CELLPADDING="2">'   crlf  
    '  <TH COLSPAN="6"><FONT FACE="Arial" COLOR="#9999cc" SIZE="4">Biolife Data with Navigation Capability</FONT></TH>'   crlf  
    '  <TR BGCOLOR="#ffffcc">'   crlf  
    '    <TD><B><FONT FACE="Arial">No</FONT></B></TD>'   crlf  
    '    <TD><FONT FACE="Arial"><#SpeciesNo></FONT></TD>'   crlf  
    '    <TD><B><FONT FACE="Arial">Category</FONT></B></TD>'   crlf  
    '    <TD><FONT FACE="Arial"><#Category></FONT></TD>'   crlf  
    '    <TD><B><FONT FACE="Arial">Common Name</FONT></B></TD>'   crlf  
    '    <TD><FONT FACE="Arial"><#Common_Name></FONT></TD>'   crlf  
    '  </TR>'   crlf  
    '  <TR BGCOLOR="#9999cc">'   crlf  
    '    <TD><B><FONT FACE="Arial">Notes</FONT></B></TD>'   crlf  
    '    <TD COLSPAN="5"><FONT FACE="Arial"><#Notes></FONT></TD>'   crlf  
    '  </TR>'   crlf  
    '  <TR BGCOLOR="#ffffcc">'   crlf  
    '    <TD><B><FONT FACE="Arial">Image</FONT></B></TD>'   crlf  
    '    <TD COLSPAN="5" ALIGN="CENTER"><#Graphic></TD>'   crlf  
    '  </TR>'   crlf  
    '</TABLE>'   crlf;
end;

All we're really doing here is defining the display format of our data in HTML terms. I personally prefer keeping such information inside my applications for such uses, but it may be advantageous to have this as an HTML (.htm file) template. This will give you, or your client the ability to change the format of data presentation simply by modifying the physical HTML file using Homesite or any other such tool. The effects of the change in the template will be seen without the need to recompile the ISAPI application.
Suggestion.gif With regard to the TPageProducer and the special tags we've used in this template…

I personally prefer using tags with attribues. This not only allows me to define a "tag language" of sorts, but also streamlines the code that processes these tags (since there are fewer tags). For example the tags in the above template would be something like this:

<#fieldvalue name="Species_No">
<#fieldvalue name="Category">
<#fieldvalue name="Common_Name">
<#fieldvalue name="Graphic">

The code that will process these tags in the OnHTMLTag event would look like this:

if TagString = 'fieldvalue' then
  ReplaceText := qryData.FieldByName(TagParams.Values['name']).AsString;

This is not to say that this code is a replacement for what we have here, but this is the kind of technique I prefer to use (that is use of attributes in special tags) since it reduces the number of tags you need to process and therefore streamlines the use of special tags in your code/templates. In time, you'll find you have developed a kind of "language" of your own. Thus maintaining/debugging code becomes a lot simpler.

The TPageProducer component has two properties that are used most often. The HTMLDoc and HTMLFile properties. If you used physical HTML files as templates, then you'd assign the HTMLFile property the path and name of the .htm file. In this case, we need to use the HTMLDoc property. This is a TStrings type property. Since a TStrings object has a property called Text, we can easily assign the result of our GetDisplayTemplate function to the HTMLDoc property like so -
PageProducer1.HTMLDoc.Text := GetDisplayTemplate;.

This is exactly what we do in our ShowSingleRecord function above.

The TPageProducer component has a property called Content. The moment we assign this property to the Response object's Content property (that is, when ever the Content property is accessed) the component starts to parse the HTMLDoc or HTMLFile property. Each time it encounters a token/place holder (), it fires the OnHTMLTag event giving use the following information:

procedure TWebModule1.PageProducer1HTMLTag(Sender: TObject; Tag: TTag;
  const TagString: String; TagParams: TStrings; var ReplaceText: String);
In our template, we have tokens such as: <#SpeciesNo>, <#Category>, <#Common_Name> etc. The TagString parameter passed to us in the OnHTMLTag event will contain these values. So the first time the event is fired, the TagString parameter will contain the value - SpeciesNo minus the parts. We then have the option to change this text to whatever we want. Notice that the ReplaceText parameter is a var parameter. If we set this parameter to the actual value of the field (Species No in this case) then we'll see this value in the generated HTML file in a browser. In this way, we can have "place holders" in our HTML template for values that will be determine at run time.

The OnHTMLTag event in this example looks like this:
procedure TWebModule1.PageProducer1HTMLTag(Sender: TObject; Tag: TTag;
  const TagString: String; TagParams: TStrings; var ReplaceText: String);
begin
{ Since this event will be fired during the same session (thread) that
  called the ShowRecord Action, it is safe to use properties and assign
  the values here. Beware of using global variables. They are not thread safe.
  Properties are thread safe. In this case since we don't need to maintain these
  values across instances, properties will work just fine. }
  if TagString = 'SpeciesNo' then
    ReplaceText := SpeciesNo;
  if TagString = 'Category' then
    ReplaceText := Category;
  if TagString = 'Common_Name' then
    ReplaceText := CommonName;
  if TagString = 'Notes' then
    ReplaceText := Notes;
  { For the Image, create an <IMG> tag }
  if TagString = 'Graphic' then
    ReplaceText := Format('<IMG SRC="%s%s?SpeciesNo=%s" BORDER="0" ALT="%s">',
                     [Request.ScriptName,
                      '/GetImage',
                      SpeciesNo,
                      CommonName]);
end;

The only TagString condition that may need some explanation, is

One condition that may need some explnation
  { For the Image, create an <IMG> tag }
  if TagString = 'Graphic' then
    ReplaceText := Format('<IMG SRC="%s%s?SpeciesNo=%s" BORDER="0" ALT="%s">',
                     [Request.ScriptName,
                      '/GetImage',
                      SpeciesNo,
                      CommonName]);

Our graphic place holder/token, needs to be replace with an image. But can't really replace it with an image at this moment, since the web browser is expecting the content as type Text. This is because, by default, the Response.ContentType property is set to text/html. So what we do instead, is replace the TagString with an HTML <IMG> tag using the ReplaceText var parameter. This image tag should contain the information required by the browser to get the right image. Since the image needs to be extracted from a specific record in our database, we need to give the browser enough information to come back to our ISAPI with the required information to enable us to extract the correct image from our database table. One other improtant thing to know/notice is that the our tokens can not contain spaces. That is, the token<#Common Name> would not be the same as<#Common_Name>. The TagString parameter for this token would result in Common and not Common_Name as we would want it. The reason is that the token can contain Parameters. Parameters are delimited with spaces! This is important to know. In this example, we've not used the Parameters capability of this component. To give you a short example:

We could have a token such as <#FirstName Type=Text Length=15>, then in the OnTag event, we could do something like:

  if TagString='FirstName' then
    ReplaceText := '<INPUT TYPE='   TagParams.Values['Type']   ' MAXLENGTH='   TagParams.Values['Length']   '>';
This would generate an Edit box in the browser that would allow a maximum of Length characters.....but let's not get too confused at this time. Let us continue on with our project.

The ReplaceText parameter would evaluate to something like this:

ReplaceText := '<IMG SRC="/scripts/BiolifeSingle.dll/GetImage?SpeciesNo=90012 BORDER="0" ALT="Clown Triggerfish">';
Which means we need to have an action in our ISAPI with a PathInfo property of GetImage which expects to see a Request.QueryString parameter SpeciesNo=90012. In this action, we'll do what it takes for us to extract the correct image from the database table and send it back to the browser using the Response object. This time, we'll set the Response.ContentType property to image/jpeg so the browser knows to expect an image and not text. The OnAction event for this action looks like this:

The OnAction event of the action with the /GetImage PathInfo
procedure TWebModule1.WebModule1WebActionItem1Action(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  Response.ContentType := 'image/jpeg';
  Response.ContentStream := GetImageStream(Request.QueryFields.Values['SpeciesNo']);
end;

and the GetImageStream function looks like this:

The function GetImageStream
function TWebModule1.GetImageStream(SpeciesNo : string) : TMemoryStream;
var
  MemStrm : TMemoryStream;
  JPEG    : TJPEGImage;
  Bmp     : TBitmap;
begin
  MemStrm := TMemoryStream.Create;
  JPEG := TJPEGImage.Create;
  Bmp := TBitmap.Create;
  try
    with qryImage do
    begin
      { The SQL Statement is assigned at designed time in the SQL Property }
      Params[0].AsInteger := StrToInt(SpeciesNo);
      Open;
      Bmp.Assign(FieldByName('Graphic'));
      Close;
    end;
   JPEG.Assign(Bmp);
   JPEG.SaveToStream(MemStrm);
   MemStrm.Position := 0;
   Result := MemStrm;
  finally
    { Do not Free the Memory Stream ! }
    JPEG.Free;
    Bmp.Free;
  end;
end;

Comments have been Disabled for this post





Leave A Comment

Shiv Kumar
Gainesville, Virginia,
United States
Member Bio Member Skills/Specialization

Bio

close

Specializations

close
Photographer
Landscape
Nature
Portrait
Videographer/Cinematographer
Interview
Landscape
Nature
Portrait
 
Privacy Policy | Terms Of Service | Contact Us | Support | Help/FAQ | News