Programmer to Programmer (TM) |
![]() | ||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||
Links Author Page About ASPToday |
![]() | |
Home | Today's Article | Search | Feedback | Write For Us | Suggest an Article | Advertise |
Friday, December 24, 1999 |
![]() |
By Geoffrey Pennington |
ASP Tricks |
enter the discussion |
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.
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.
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:
ACTION="whatever-is-the-name-of-the-current-form"
). You can use
a server variable for this purpose)
Request.Forms
collection.
ShowText
subroutine to display the variables.
ShowText
writes the data back out to the screen. 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.
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.
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.
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.
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.
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.