This article focuses both on solving the problem of sending data using HTTP POST, and on the
definition of a state that is associated with an HTML page.
This article covers the following topics:
• Explanation of the problems associated with HTTP POSTs
• How to fix the problems related to HTTP POSTs
• Illustration of a workflow architecture
• Association of state with a handler
Problem You want to use Ajax to solve some of the problems associated with submitting forms the traditional
way with POST.
Theory When implementing a workflow such as buying an airplane ticket, you use HTML forms. An
HTML form has a number of UI elements, such as a text box, a list box, or a combo box, that
users can use to enter information. When they’re content with the information entered, they
press a button to submit the data to the server. The server receives the data and generates
some kind of result.
In previous articles, an HTTP POST was used when data was sent to the server. None of the
examples used the HTTP POST in the context of an HTML form. When an HTTP POST is used in
the context of an HTML form, the server processes the form and generates some data that
then becomes the next HTML page.
From a content-processing perspective, the difference in processing between an HTTP
GET and POST is dramatic.
The client could call the URL multiple
times, and the content would be generated each time. An HTTP POST is different in that the
client has to send state. Based on that state, the server does some processing and generates
some content. The state that the client has to send is not optional.
A browser helps the client by caching the state sent previously, and when requested, sends the
state again with an appropriate dialog box. The problem with
sending the state again is that you might execute the process to buy a plane ticket again, thus
buying two.
Imagine a situation in which you’re buying a plane ticket and you want to experiment
with different times and airports.
Yet
you would probably reply that in fact, this is an incorrect assertion, as the dialog box rarely
appears. The reason why the dialog box rarely appears is because you’re guided and told which
buttons you can and cannot press. To go back a step in the workflow process, you would not
press the Browser Back button. Next time, look closely at a typical HTML-based workflow, and
you’ll notice how information is repeated on multiple screens and how the workflow guides
the user. It is very clever, and users are typically not aware of being manipulated into thinking
in certain directions.
Solution Web application frameworks have gone to great extents to solve the HTTP POST problem. You
need to look no further than the ASP.NET framework to see the complexity involved in making
sure that the Browser Back or Refresh buttons don’t cause a mountain of problems with respect
to the server-side state. ASP.NET is not to be blamed, because its creators were trying to fit
a dynamic architecture into an old static Web infrastructure.
The following points sum up the advantage to using this approach:
• The data is sent to the server only once, and only if the user presses the Form Submit
button.
• The server side stores an application state that you can use HTTP GET to call whenever
a particular HTML page is called. Therefore, you can use the Browser Back and Browser
Forth buttons to move between pages without corrupting the server-side state or the
client-side HTML page.
Converting the POST to Use the XMLHttpRequest Object
The solution to the HTTP POST problem is simple, but it requires the ability to use the
XMLHttpRequest object. In this section, you’ll learn how to convert the original application to
use the new architecture. The original and new HTML pages will be illustrated at a code level.
The idea of this section is to illustrate the conversion.
The Original HTML Form
The original HTML form used an HTTP POST, and is illustrated as follows:
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/ajaxrest articles/architecture/forms/Posted.ashx"
method="POST">
<input type="text" name="example" />
<input type="submit" value="Submit" />
</form>
</body>
</html>
The bold form attributes show that when the Submit button is pressed, the URL
/ajaxrest articles/architecture/forms/Posted.ashx is called using a POST. From a Representational
State Transfer (REST) perspective, the URL only accepts a single verb, POST. This is the
heart of the problem in that you cannot retrieve the page using a GET, which is the default
HTTP verb used by a browser.
The Converted HTML Form
The converted HTML form uses the same HTML constructs, but it delegates the POST to an
XMLHttpRequest and retrieves the next page using a GET. The following code shows the converted
HTML form:
<html>
<head>
<title>Title</title>
</head>
<script language="JavaScript" src="/scripts/jaxson/common.js"></script>
<script language="JavaScript" src="/scripts/jaxson/communications.js"></script>
<script language="JavaScript" src="/scripts/jaxson/commonmorphing.js"></script>
<script language="JavaScript" type="text/javascript">
var representations = { };
function OnSubmit() {
var obj = RepresentationManager.iterateHtml.get( representations,
document.getElementById( "form"));
var stringToSend = Ops.serializeCGI( obj);
var asynchronous = FactoryHttp.getAsynchronous();
asynchronous.settings = {
onComplete : function(xmlhttp) {
location.href = document.getElementById( "form").action;
}
}
var obj = new Object();
obj.data = stringToSend;
obj.length = obj.data.length;
obj.mimetype = "application/x-www-form-urlencoded";
asynchronous.post( document.getElementById( "form").action, obj);
}
</script>
<body>
<form id="form"
action="/ajaxrest articles/architecture/forms/AjaxPosted.ashx"
method="POST">
<input type="text" name="example" />
<input type="button" value="Submit" onclick="OnSubmit()"/>
</form>
<div id="output"></div>
<div id="error"></div>
</body>
</html>
The important modified parts of the HTML page are shown in bold. The overall change
has been to convert the input type of submit to button, and have the button implement the
click event. The click event calls OnSubmit and is responsible for doing a POST and GET.
When OnSubmit is called, the function RepresentationManager.iterateHTML.get is called,
which is used to extract the state in the HTML form elements. Normally, when using the previous
HTML form-submit technique, the browser manages the extraction of the state from the
HTML form. It is more complicated to perform a custom extraction, but it does give the added
benefit of being able to extract the state from other HTML elements, such as div or span elements.
You need to think of a GET method call as a general state extraction that is assigned to
a JavaScript object.
When the JavaScript object that contains the state has been created, you need to convert
the object into a computer graphics interface (CGI)-encoded query string. The function
Ops.serializeCGI carries out the conversion. Again, you need to perform a custom serialization,
but you have the added flexibility of serializing to a CGI-encoded query string,
a persisted JavaScript Object Notation (JSON), or even an XML string.
Then when you have the CGI query string, you use the remaining code in OnSubmit to POST
the data to the server. Notice that the POST URL used is from the HTML form. Once the POST has
been completed in the implementation of the onComplete method, the location.href is
assigned the URL that was POSTed to.
Here is where you might get confused. Why first POST and then GET the same URL? It might
seem more efficient to perform a single POST or GET. The reason for executing the two verbs is due to browser history. The POST is executed by the XMLHttpRequest object and thus not part of
the browser history. The POST is used to create a state on the server, and then the browser executes
the GET, so that the page is recorded in the browser history. As a result, the browser has
two GETs in the history, instead of a GET and a POST, as was the case in the original HTML form
example. When you have two GETs in the history, you don’t need to send state to the server, as
the state is retrieved.
The client still calls the same URL, but the functionality of the server URL has changed. In
the modified HTML form example, the server has to react to a POST and a GET. However, the
server must associate a state with the request, which was not necessary in the case of the original
HTML form. In the original HTML form, the state was generated with every POST.
Associating a state with the request is not that difficult and only requires the use of the
Web application-provided session mechanism. You have to change the server-side code so
that the information generated by the POST is stored in the session and retrieved when the GET
is called. The following code shows an extremely simple implementation of the original serverside
code:
public void ProcessRequest(HttpContext ctx) {
ctx.Response.ContentType = "text/html";
ctx.Response.Write("<HTML><BODY>You wrote <b>" +
ctx.Request["example"] + "</b></BODY></HTML>");
}
Here’s the modified server code:
public void ProcessRequest(HttpContext ctx) {
ctx.Response.ContentType = "text/html";
if( ctx.Request.HttpMethod == "POST") {
ctx.Session.Add( "example", ctx.Request[ "example"]);
ctx.Response.Write("<HTML><BODY>You wrote <b>" +
ctx.Request["example"] + "</b></BODY></HTML>");
}
else if( ctx.Request.HttpMethod == "GET") {
ctx.Response.Write("<HTML><BODY>You wrote <b>" +
ctx.Session["example"] + "</b></BODY></HTML>");
}
}
The example is coded using ASP.NET, but even if you’re not an ASP.NET programmer, you
should be able to follow the explanation. In the original implementation of ProcessRequest, it was
expected that you call the method using a POST. To generate the content, you extract the example
variable using the method ctx.Request. As the original implementation of ProcessRequest is
defined, there is no memory of having been called previously. The generated output is dependent
on the parameters sent in the POST. The original code is considered unsafe, because a POST is
assumed. If a GET is executed, then problems will occur, because an inconsistent state will be
defined.
In the modified source code, you first test the HTTP verb that is being called (ctx.Request.
HttpMethod). If an HTTP POST is called, then the generated content is like the original HTML
form example, and the state is saved to the session (ctx.Session). If an HTTP GET is called,
then the generated content is similar to the POST, except that the state is retrieved from the
session.
POSTing Forms and REST
In the modified example, the URL uses /ajaxrest articles/architecture/forms/AjaxPosted.ashx
and uses a session. The problem with this approach is that HTTP cookies are used to determine
what content is generated. The solution is used to illustrate that you can relatively
easily modify an existing application that uses a POST into one that uses Ajax-combined POST
and GET.
Next, you modify the POST and GET combination to use no session variables. Or, to put it
more succinctly, it’s OK to use session variables as long as they don’t use cookies. For example,
in the ASP.NET architecture, it is possible to have the session variables modify the URL to
include an identifier that cross-references to a session variable.
Having the infrastructure modify the URL makes it possible to articlemark the URL and reference
it at some later point in time. However, you’ll run into a problem if you use session
variables that time out. A timed-out session variable, even if it doesn’t use cookies, is problematic,
because users might have articlemarked the URL and found out later that they cannot
reference it. It is possible to just set the session variable to time out a very long time from now,
but this doesn’t solve the problem of the state eventually disappearing. In the case of this
article, the last thing you want to happen is the disappearance of state.
Therefore, you need to re-architect the server to use a cache instead of session variables.
The advantage of the cache is that it gives you the ability to control when a piece of state
remains and is deleted. The next problem is a bit more complicated. In the modified example
of the HTML form, the page PostAjax.ashx responded to either a GET or a POST. When the GET
was called, the state of the HTML page and the HTML page itself were combined in one step.
From Article 5, you know that combining the HTML page with its state is wrong. You want to
be able to load the state as aWeb service call. This means two URLs are necessary. The “Supporting
HTML Pages with Relative URLs” section in Article 5 shows you how to manage the
two URLs.
• /workflow/page1/1234: This URL is used to download the HTML page that is displayed
in the browser. The URL supports only the GET verb, as it is used to download the HTML
content from the server.
• /services/workflow/page1/1234: This URL supports both the POST and GET verbs and is the
state associated with the HTML page URL. The state URL is created using the techniques
explained in the section “Supporting HTML Pages with Relative URLs” in Article 5.
Both example URLs are appended with the number 1234, which represents the unique
cached data identifier used when loading the state.
In the state and HTML page solution, the state created by one HTML page is delegated to
another page. Look back at the code of the modified HTML form example. The state of the
HTML page is associated with the PostAjax.ashx page. With the state and HTML page solution,
the state needs to be associated with the page, and thus the implementation of the modified
HTML form example changes slightly to the following:
<html>
<head>
<title>Title</title>
</head>
<script language="JavaScript" src="/scripts/jaxson/common.js"></script>
<script language="JavaScript" src="/scripts/jaxson/communications.js"></script>
<script language="JavaScript" src="/scripts/jaxson/commonmorphing.js"></script>
<script language="JavaScript" type="text/javascript">
var representations = { };
function OnSubmit() {
var obj = RepresentationManager.iterateHtml.get( representations,
document.getElementById( "form"));
var stringToSend = Ops.serializeCGI( obj);
var asynchronous = FactoryHttp.getAsynchronous();
asynchronous.settings = {
onComplete : function(xmlhttp) {
location.href = document.getElementById( "form").action;
}
}
var obj = new Object();
obj.data = stringToSend;
obj.length = obj.data.length;
obj.mimetype = "application/x-www-form-urlencoded";
asynchronous.post(URLEngine.serviceURL(), obj);
}
URLEngine.serviceURL = function() {
return "/services/" + this.requestChunks.slice( 0,
this.requestChunks.length - 1).join( "/");
}
function Initialize() {
var asynchronous = FactoryHttp.getAsynchronous();
asynchronous.settings = {
onComplete : function(xmlhttp) {
var obj = Ops.serializeFromCGI( xmlhttp.responseText);
RepresentationManager.iterateHTML.set( representations,
document.getElementById( "form"), obj);
}
}
asynchronous.get(URLEngine.serviceURL());
}
</script>
<body onload="Initialize()">
<form id="form"
action="/ajaxrest articles/architecture/forms/AjaxPosted.ashx"
method="POST">
<input type="text" name="example" />
<input type="button" value="Submit" onclick="OnSubmit()"/>
</form>
<div id="output"></div>
<div id="error"></div>
</body>
</html>
The bold code shows the additional pieces necessary to implement the state and HTML
page solution. As explained in Article 5, when the page has been loaded completely, the
body.onload event is triggered and calls the function Initialize. Calling Initialize results in
the state being retrieved using the URL dynamically computed using the function URLEngine.
servicesURL. When the state has been retrieved, it is deserialized and assigned to the HTML
page using the methods serializeFromCGI and iterateHTML.set.
The implementation of OnSubmit stays the same, with aminor modification being the
URL where the state is being POSTed. The POST URL must be the same as the one used to
retrieve the data in the Initialize function. The next page that loads after the state stays the
same; you can modify it to your liking.
The URL that determines the next page warrants a little discussion. In the case of the
HTML code, the loaded URL is /ajaxrest articles/architecture/forms/AjaxPosted.ashx. The
URL is not appended with a cache identifier, meaning that the URL is not associated with any
state. In the modified HTML form example, a cookie defines the state that is associated with
the URL. Since you’re not using cookies, the URL has no state. If this is your desired effect,
then you can leave the code as is.
However, this probably is not the desired effect, so you need to associate the cache
identifier with the URL. Therefore, the URL must be /ajax articles/architecture/forms/1234/
AjaxPosted.ashx or something along those lines. The important bit is that the unique cache
identifier is included in the URL. However, the URL is hard-coded, so you need to modify it
dynamically, much like the approach illustrated in Article 5. In a nutshell, you need to define
groupings of URLs. For example, one workflow might have the URLs /workflow/app-name/page1,
/workflow/app-name/page2, and so on. Each of the URLs would be associated with a cache
identifier. Thus, whenever you navigate one of the URLs, you’ll be navigating the cached data
associated with the workflow application.
Remember the following points:
• Using the XMLHttpRequest POST and GET combination avoids the dreaded HTTP “Post
data again” dialog box. You can rest easy that your clients won’t be purchasing the same
item twice.
• By having POST and GET as separate steps, you can optimize the efficiency of the Web
application, because the data associated with the GET can be cached.
• You don’t need to make huge changes to your Web application to take advantage of the
separate POST and GET, as illustrated in the modified HTML form example.
• In a complete state navigation implementation, use a two-URL approach, where one
URL defines the HTML page, and the second URL represents the state Web service
called by the HTML page.
• In a complete state navigation implementation, the URLs for the state and the next
page need to be algorithmically definable.
• You can combine this solution with the data-validation articles illustrated in Article 3.
• To associate cached data with a URL, you should not use cookies, but instead use
unique cached identifiers in the URL. This allows you to articlemark an HTML page and
its associated state for later reference.
|