Programmer to Programmer (TM)
www.asptoday.com
keyword search

ADSI/CDO (9)
ASP Tricks (67)
BackOffice (27)
Components (48)
Data Access (76)
Miscellaneous (10)
Non-MS ASP (6)
Scripting (55)
Security/Admin (30)
Site Design (20)
Site server (9)
XML (27)
free email updates

ASPTODAY Diary
S M T W T F S
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
Links
Author Page
About ASPToday
Friday, December 24, 1999 
By Geoffrey Pennington
By Geoffrey Pennington
ASP Tricks
 
enter the discussion

"Echo Back" - Screen Persistence of a Browser's Forms 

Picture this: you have filled out a form in your browser and have clicked "Submit". The server finds an error in your entries and asks you to try again, but the screen no longer shows what you typed. Perhaps you can use the "Back" button to retrieve your data, but perhaps you need to start again with a blank form.

Technology changes, but data entry screens remain a constant feature of many systems. In mainframe and client/server systems the screen generally doesn’t change when the user presses "Update". There is a confirmation (or error) message, of course, and perhaps the options change, but the screen itself, complete with the just-submitted data, is still there. This "screen persistence" provides one more chance to review the entries and a chance to immediately correct mistakes. Systems in the stateless Web environment often behave differently from this. You enter data, click Submit, and the next, different, screen you see tells you whether it worked. There is no chance for a final review and no easy way to fix mistakes. In this article, I present a simple technique I call "echo back" which corrects this deficiency.

Note that a different approach to a similar issue has already been presented in ASPToday by Derek Hodgkins in his article on Dynamic ASP/JavaScript for Data Refresh. The difference between his way and mine is that I do everything server-side, and he does a lot of the work on the client. Take a look at both techniques and see what works best for you.

The Sample Application

Our sample application is a form to update the Store table in the ever-popular Pubs database that comes with SQL Server. Here is the form with some typical entries. (Note that you need to provide some arguments with the URL. To setup a new store you need ?Process=New . To modify an existing store you need ?Process=Edit&stor_id=<ID> where <ID> is the stor_id of the store you will edit.)

Here is the form again, after server-side validation has detected a problem.

We have an error message, but we also have the same form back again, complete with the data we entered. All we need to do is to fix the mistake and resubmit.

How the Code Works

Now, let’s go over the code. I think things will be clearer if we start with how the form elements are displayed and work backwards to the overall structure of the program, sort of a bottom-up approach. I won't go over the whole program, just the parts needed in the "echo". The full program is in this article's support material. Also, to run the application, you will need to set up some stored procedures, which are likewise in the article’s support materials.

Here is a subroutine called ShowText. It takes four arguments and writes out an HTML text input element. I like using a subroutine for this purpose because it simplifies the body of the HTML. One of the arguments, strValue, is used for the VALUE attribute.

'----------------------------------------------------------------------
' Purpose:  Handles the display of a text field from a recordset. 
'
' Inputs: strName   - the name used by the HTML form
'         strValue  - the value to display
'         intSize   - the HTML display size
'         intMax    - the maximum character length accepted by the HTML
'----------------------------------------------------------------------
Sub ShowText(strName, strValue, intSize, intMax)
  Response.Write "<INPUT TYPE=Text NAME=" & strName & _
                 " ID=" & strName                   & _
                 " onFocus=select()"                & _
                 " SIZE=" & intSize                 & _
                 " MAXLENGTH=" & intMax             & _
                 " VALUE=" & chr(34) & Server.HTMLEncode(strValue) & _
             chr(34) & ">"
End Sub

ShowText is quite straightforward, but I would like to point out that Server.HTMLEncode solves the problems caused by quotes embedded in the string.

In this form, ShowText is called six times, placed to make the input elements appear in the appropriate places.

<table border="0" width="100%">
    <tr>
      <td align="left">ID</td>
      <td align="left"><%ShowText "stor_id", stor_id, 4, 4 %></td>
    </tr>
    <tr>
      <td align="left">Name</td>
      <td align="left"><%ShowText "stor_name", stor_name, 40, 40 %></td>
    </tr>
    <tr>
      <td align="left">Address</td>
      <td align="left"><%ShowText "stor_address", stor_address, 40, 40 %></td>
    </tr>
    <tr>
      <td align="left">City: </td>
      <td align="left"><%ShowText "city", city, 20, 20 %></td>
    </tr>
    <tr>
      <td align="left">State: </td>
      <td align="left"><%ShowText "state", state, 2, 2%></td>
    </tr>
    <tr>
      <td align="left">Zip Code: </td>
             <td align="left"><%ShowText "zip", zip, 5, 5 %></td>
    </tr>
</table>

Look at the variables in the position corresponding to strValue: stor_id, stor_name, stor_address, city, state, and zip . Where are they set? From either of two subroutines shown below. SetFromForm uses entries submitted from the form, and SetFromRS uses data from a recordset.

'********************************************************************
'Purpose:  Reset the form from values previously entered.  Intended for use
'          when providing user feedback.
'********************************************************************
SUB SetFromForm
  stor_id       = Request.Form("stor_id")
  stor_name     = Request.Form("stor_name")
  stor_address  = Request.Form("stor_address")
  city          = Request.Form("city")
  state         = Request.Form("state")
  zip           = Request.Form("zip")
END SUB

'********************************************************************
'Purpose:  Use a recordset to set the variables to display in the form 
'********************************************************************
SUB SetFromRS
  stor_id       = rsForm("stor_id")
  stor_name     = rsForm("stor_name")
  stor_address  = rsForm("stor_address")
  city          = rsForm("city")
  state         = rsForm("state")
  zip           = rsForm("zip")
END SUB

The decision of which subroutine to use is made in the following SELECT CASE statement:

SELECT CASE Request("Process")
CASE "New" 
  'Display a blank form
  strTitle = "New Store Setup"
  
CASE "Setup" 
  DIM intNewID
  'User submitted the form; insert the data into the DB
  SetFromForm
 strStatus = EditForm
  IF strStatus = "" THEN
    'Update the DB; set the display variables; redisplay their input 
    'and let them know it worked.
    strStatus = InsertForm
    intNewID  = Cmd.Parameters("RETURN_VALUE")
    IF Len(Trim(strStatus)) = 0 THEN
      strFeedBack = "The store has been entered into the database.<BR>"
    ELSE
      strFeedBack = strStatus
    END IF
  ELSE
    'Set the display variables; redisplay their input and let them 
    'know what the problem is.
    strFeedBack = strStatus
  END IF  
  strTitle = "New Store Setup Feedback"
  
CASE "Edit" 
  RetrieveForm Request("stor_id")
  SetFromRS  
 strTitle = "Edit an Existing Store"
    
CASE "Update" 
  'Update a previously entered form; we get here following an "Edit" request
  SetFromForm
 strStatus = EditForm
  IF strStatus = "" THEN
    strStatus = UpdateForm (stor_id)
    IF Len(Trim(strStatus)) = 0 THEN
      strFeedBack = "The store has been updated.<BR>"
    ELSE
      strFeedBack = strStatus
    END IF
  ELSE
    strFeedBack = strStatus
  END IF
  strTitle = "Store Update Feedback"

CASE "Delete"
       'Delete a previously entered form; we get here from following 
       'an "Edit" request
  SetFromForm 
       strStatus = DeleteForm (Request("stor_id"))
       IF strStatus = "" THEN
             'All's well that deletes well.
             strFeedback = "<B>The store record has been deleted.</B><BR>"
       ELSE
             'set the display variables; redisplay their input and 
             'let them know what the problem is.
             strFeedback = strStatus
       END IF
    strTitle = "Store Delete Feedback"

CASE ELSE
  strTitle = "Store Maintenance"
  strFeedback = "Type of processing not recognized: <B>" & _
                Request("Process") & "</B>.<BR>"
END SELECT

If Request("Process") is "New", neither SetFromForm nor SetFromRS is called. The variables are left un-initialized as we fall through to the HTML.

If Request("Process") is "Setup", "Update", or "Delete" we use SetFromForm because for each of these we are working with data displayed on the form. SetFromRS is used only in the case of "Edit", the only case where we have retrieved data from the database and have a recordset.

Finally, the form is self-posting, because the action attribute is <%Request.ServerVariables("PATH_INFO")%>. You don't have to use the ServerVariables collection, but doing so gives you one less thing to worry about if you change the file name.

To recap, this time in a top-down manner:

And there you have it. A simple way to make the form either show what was retrieved from the database, or echo back the user input. The entire program is in the download material.

More Difficult Cases: CheckBoxes, Radio Buttons, and Select Lists

Now that you have the idea, let’s look at how checkboxes and radio buttons might be handled. The issue here is that we want to echo the checked/unchecked status of the box/button instead of the value. I have not prepared a complete program but simply present some functions I have used.

'------------------------------------------------------------------------------
' Purpose:  Places a checkbox on a form.
' Input: strName     Name to be given the checkbox.
'        strValue    Value assigned to the checkbox.
'        strCurrent  Current value; used to determine whether the checkbox 
'        is checked.
'------------------------------------------------------------------------------
Sub ShowCB(strName, strValue, strCurrent)
    DIM strChecked
    strChecked = ""

    IF Trim(strValue) = Trim(strCurrent) THEN
      strChecked = "checked"
    END IF
    Response.Write "<INPUT TYPE=checkbox NAME=" & strName & _
                   " VALUE=" & strValue & " " & strChecked & " >"
End Sub

Really, ShowCB isn't much different from ShowText. We pass in some variables and it writes out the HTML. A typical call might be:

ShowCB "cbDelivered", "Y", strDelivered

The first two parameters should be obvious from the fact that they are hard coded and from the comments with the subroutine itself. strDelivered is a little more interesting, but only a little bit more. Like the variables in the sample application, it is set in either SetFromForm or SetFromRS. If the checkbox were checked, then strDelivered/strCurrent will be "Y" and will match the value assigned to the checkbox, and the box will redisplay as checked; otherwise, it won't match, and the box will redisplay as not checked. Simple.

Here is code for a radio button. It is essentially the same thing.

'------------------------------------------------------------------------------
' Purpose:  Places a radio button on a form.
' Input: strName  Name to be given the radio button.
'        strValue Value assigned to the radio button.
'        strSet   Current value; used to determine whether the button is
'                 checked.
'------------------------------------------------------------------------------
Sub ShowRB(strName, strValue, strSet)
    DIM strChecked
    strChecked = ""
    IF Trim(strValue) = Trim(strSet) THEN
      strChecked = "checked"
    END IF
    
    Response.Write "<INPUT TYPE=radio NAME=" & strName  & _
                   " VALUE='" & strValue & "' " & strChecked & " >"
End Sub

If you would like to see another approach to the same subject, check out http://www.powerasp.com/content/code-snippets/default.asp.

Next, what about SELECT lists? The difficulty is that you need a value for each OPTION. Here is code I have used:

'********************************************************************
'Purpose:  Generate a SELECT box from a recordset
'Input: rsOptions   Recordset to provide the OPTIONS.
'       strName     Name to be given the SELECT box.
'       strValue    Name of the recordset field to serve as the OPTION.value
'       strDisplay  Name of the recordset field to serve as the OPTION's
'                   display value
'       strCurrent  Currently selected value
'********************************************************************
SUB SelectBox(rsOptions, strName, strValue, strDisplay, strCurrent)
  strSelect = "selected"

  Response.Write "<SELECT name=" & strName & " id=" & strName & " size=1>"
  DO UNTIL rsOptions.EOF
    IF Trim(rsOptions.Fields(strValue)) = Trim(strCurrent) THEN
      strSelect = "selected"
    ELSE
      strSelect = ""
    END IF
    Response.Write "<OPTION value=" & Trim(rsOptions.Fields(strValue)) & _
                   " " & strSelect & ">"  & _
                   rsOptions.Fields(strDisplay) & "</OPTION>" & vbCRLF
    rsOptions.MoveNext
  LOOP
  Response.Write "</SELECT>"

END SUB

strCurrent is used to identify the currently selected value, much as was done for check boxes and radio buttons. The extra wrinkle here is the recordset passed in to provide the list of values for the <OPTION> tags. This code works but has the disadvantage that the recordset must be re-retrieved each time the page is displayed. You could keep the recordset in a session variable, but that may be an even worse burden on system resources. A better option may be to write out the complete SELECT list as an application variable and use client-side techniques to set the selected value; that, however, is outside the scope of the present article.

Repeating Rows

Finally, you may have seen my article on "Creating an Editable Grid". The data in the grid "echoes" also, but by re-retrieving from the database. I recently needed a variant of the grid where I used a selection screen to determine what rows to show, as discussed in my article on "Form Driven Queries ". When I re-retrieved, the just-edited rows no longer met the retrieval criteria, so the "echo" failed. Fortunately, I remembered the article on "ADO Disconnected Recordsets" by Chris Blexrud and adapted it to my purpose. Let's take a look now at how it works. The sample application is StoreGrid.asp. Again, I will not go over the entire program, but the complete code is in the support material.

BookStore.asp used ordinary variables to store the values to display but that is impractical for a grid, since we don’t know how many rows we will have. Therefore, data from the form will be placed in a disconnected recordset, which we populate as we update the database. Because we always use a recordset, we have no need for the SetFromForm and SetFromRS subroutines, or the decision logic to choose between them.

Here are the key points in StoreGrid.asp, when the form has been submitted from the browser. Create the disconnected recordset, using the following subroutine:

SUB CreateRS
  'Create a client (IIS) side recordset to redisplay the rows even 
  'if they no longer meet the retrieval conditions.
  SET rsGrid = Server.CreateObject("ADODB.RecordSet")
  rsGrid.CursorLocation = adUseClient
  rsGrid.Fields.Append "stor_id",      adChar, 4
  rsGrid.Fields.Append "stor_name",    adVarChar, 40  
  rsGrid.Fields.Append "stor_address", adVarChar, 40
  rsGrid.Fields.Append "city",         adVarChar, 20
  rsGrid.Fields.Append "state",        adChar, 2
  rsGrid.Fields.Append "zip",          adChar, 5
END SUB

Within the HandleUpdates subroutine, create (but do not immediately set) the parameters passed to the stored procedure that actually does the update:

Sub HandleUpdates()
  DIM ctr, strMsg

  Set Cmd = Server.CreateObject("ADODB.Command")
  Cmd.CommandType      = adCmdStoredProc
  Cmd.ActiveConnection = Conn
  Cmd.CommandText      = "proc_update_store"
  
  Cmd.Parameters.Append Cmd.CreateParameter("stor_id",      adChar, adParamInput,4,0)
  Cmd.Parameters.Append Cmd.CreateParameter("stor_name",    adVarChar, adParamInput,40," ")
  Cmd.Parameters.Append Cmd.CreateParameter("stor_address", adVarChar, adParamInput,40," ")
  Cmd.Parameters.Append Cmd.CreateParameter("city",         adVarChar, adParamInput,20,0)
  Cmd.Parameters.Append Cmd.CreateParameter("state",        adChar, adParamInput,2,0)
  Cmd.Parameters.Append Cmd.CreateParameter("zip",          adChar, adParamInput,5,0)
  Cmd.Parameters.Append Cmd.CreateParameter("msg",          adChar, adParamOutput,60," " )

Go into a loop where we load each row from the form into the disconnected recordset.

  ctr = 1
  rsGrid.Open
  DO While Len(Trim(Request("stor_id" & CStr(ctr)))) > 0
  
    'Populate the disconnected recordset.  Does NOT affect the database. 
    rsGrid.AddNew
    rsGrid.Fields("stor_id").value      = Request.Form("stor_id" & CStr(ctr))
    rsGrid.Fields("stor_name").value    = Request.Form("stor_name" & CStr(ctr))
    rsGrid.Fields("stor_address").value = Request.Form("stor_address" & CStr(ctr))
    rsGrid.Fields("city").value         = Request.Form("city" & CStr(ctr))
    rsGrid.Fields("state").value        = Request.Form("state" & CStr(ctr))
    rsGrid.Fields("zip").value          = Request.Form("zip" & CStr(ctr))
    'Does not update the database. Remember, the recordset is disconnected.
    rsGrid.Update

If the "Change" indicator has been set for a given row, call the UpdateRow subroutine that will set the parameters we have already created.

    IF Request("Changed" & CStr(ctr)) = "YES" THEN
      strMsg =  UpdateRow("Update", ctr) 
    END IF
    
    IF Trim(strMsg) <> "OK" THEN 
       ‘The #s set off the message.  Useful if the message is blank!
             Response.Write "#" & strMsg  & "#<BR>" 
             EXIT SUB
    END IF
    
    ctr = ctr + 1
  Loop 

When we are finished with the loop, position ourselves at the top of the disconnected recordset, so it will be ready for the DisplayRows subroutine, and get rid of the Command object we no longer need.

rsGrid.MoveFirst Set Cmd = Nothing END SUB

Finally, when we get around to re-displaying the form, the DisplayRows routine neither knows, nor cares, whether its data comes from the database or the form; either way, it references a recordset named rsGrid. The complete program is in the download material as StoreGrid.asp.

As an aside, this application contains a significant improvement over my original Editable Grid, in that the logic to create the parameters is moved outside of the loop.

A Word About Efficiency

You my have heard that, to make the code more efficient, you need to minimise the number of calls to the parser, i.e. instead of this:

<td><%=var1%></td><td><%=var2%></td>

You should write this:

Response.Write "<td>" & var1 & "</td><td>" & var2 & "</td>"

True enough, the second form is more efficient, and yet in my sample code you saw that I switched between plain HTML and script tags (<% %>) several times. Why?

I sometimes work with non-programmers who are better than I am at page layout. They set up the HTML for the page, and all I need do is insert function calls to display the input fields. Also, I find the first format easier to set up, read, and modify, and thus less prone to errors. My opinion is that the increase in the developer’s efficiency is worth the additional burden on the server. If the server were being pushed to the limit, I would recommend tuning the database and server, and checking for inefficient logic, and using more compiled code before using a style that would hinder my personal productivity.

Conclusion

We have seen here a set of techniques which I have used in several of my applications to provide more natural interface behaviour. The methods may not be suited to all situations, due to the overhead of carting around and resetting the data. Just another item in your toolbox, to be used when needed.

The editors at ASPToday and I had some discussions as to whether this article is detailed enough. On the one hand we were concerned that the discussion is too cursory for some readers to follow. On the other hand, the code is similar to that presented in other articles that were discussed more thoroughly, and more detail here might make the article too long and tedious for more advanced readers. What do you think? Post a note in the discussion area.

Click here to download this article's support material.


RATE THIS ARTICLE

Overall
Poor Excellent
User Level
Beginner Expert
Useful
No! Very
enter the discussion
 



If you would like to contribute to ASPToday, then please get in touch with us by clicking here.

ASPToday is a subsidiary website of WROX Press Ltd. Please visit their website. This article is copyright ©2000 Wrox Press Ltd. All rights reserved.