« first  « prev  » next  » last 

Jump to:


A custom webserver to demonstrate AJAX

The first bit of code in this article is a custom webserver, written using the Indy component set, which Delphi users will be familiar with and which is now available for .NET. I've used a DIY webserver, rather than using an existing webserver, because we can use breakpoints to step through the code as requests are made by the client. Using a custom server also cuts down on the implementation-specific issues that crop up with larger web servers.

I've used a simple but large test dataset in this example: a list of all surnames from the US Census. There are more than 80,000 records in the dataset, but the data itself is very simple, having only three fields: "surname_id", "surname" and "prob" (a probability measure which represents the popularity of the name).

1. Architecture


The custom web server serves static HTML, javascript and stylesheet files, as well as a dynamic XML file containing name information as fetched from the database. The web server is only half the story (or maybe even less), since much of the work in an AJAX application is performed by client side javascript; we'll start off by looking at the webserver and examine the client-side stuff later on.

The file "index.html" is a very simple HTML document with a few important "div" and "span" elements thrown in. These elements are given meaningful id attributes so that they can be located and manipulated by JavaScript in-situ within the web browser.

Two JavaScript files are used - one containing a simple XML parser and the other containing application specific code. When the HTML page is loaded a JavaScript method "load()" is called. This method constructs a URL using various parameters (see description of the JavaScript code later) and fetches the XML surnames data from the server. After the XML data is fetched by the client it is parsed using the aforementioned XML parser class and data values are used to populate the HTML.

Figure 1 shows the names list demo page. The list of names is generated by JavaScript code based on XML fetched from our custom webserver.
AJAX demonstration screenshot
Figure 1: AJAX demonstration screenshot

2. The Custom Webserver


Setting up a very simple web server application using Indy is very easy. Simply create the webserver object, set up a few properties (binding IP address and port) and then listen for a "Request" event.

3. Creating the HTTPServer object


public Form1()
{
    InitializeComponent();

    httpServer = new HTTPServer();
    httpServer.OnCommandGet += httpServerCommandGet;
    httpServer.DefaultPort = 80;
    httpServer.Active = true;
}

4. Listening for GET commands


A delegate is used to listen for GET commands as they are received by the server component. Since we're not worried about concurrency or multiple users, it's very easy to create the delegate method to handle the GET command...
protected void httpServerCommandGet(Context AContext,
                                          HTTPRequestInfo ARequestInfo,
                                          HTTPResponseInfo AResponseInfo)
{
    lock (this)
    {
        // Send appropriate content type

        if (ARequestInfo.Document.EndsWith(".css"))
            AResponseInfo.ContentType = "text/css";
        else if (ARequestInfo.Document.EndsWith(".js"))
            AResponseInfo.ContentType = "text/javascript";
        else if (ARequestInfo.Document.EndsWith(".xml"))
            AResponseInfo.ContentType = "text/xml";
        else
            AResponseInfo.ContentType = "text/html";

        // Serve dynamic or static document as appropriate

        if (ARequestInfo.Document == "/data.xml")
        {
            AResponseInfo.ContentText = xmlData(ARequestInfo.params.get_Values("start"),
                             ARequestInfo.params.get_Values("count"),
                             ARequestInfo.params.get_Values("nameStr"));                         
        }
        else
        {
            AResponseInfo.ContentText = serveFile(ARequestInfo.Document);
        }
    }
}


Note that to avoid the method being called twice by concurrent threads (each GET is handled by a different thread) we surround the entire method with a lock. The first if statement sets the appropriate content type for the reponse, after which we return either the contents of a file or some automatically generated XML data.

5. Serving a static file


Serving a static file is very simple. We simply open the file from the "document root" directory and copy its contents into the "ContentText" property of the "AResponseInfo" parameter of our GET deligate. Here's the source code of the "serveFile" method, which locates and reads the contents of a static file.
protected string serveFile(string filename)
{
    string f = "..\\..\\public_html" + filename;
    f = f.Replace("/", "\\");
    FileInfo sourceFile = new FileInfo(f);
    if (!sourceFile.Exists)
    {
        if (filename == "/" || filename == "")
            sourceFile = new FileInfo("..\\..\\public_html\\index.html");
        else
            sourceFile = new FileInfo("..\\..\\public_html\\404.html");
    }

    StreamReader reader = sourceFile.OpenText();

    string s;
    StringBuilder builder = new StringBuilder();

    do
    {
        s = reader.ReadLine();
        builder.AppendLine(s);
    } while (s != null);

    reader.Close();

    return builder.ToString();
}

6. Generating the XML data


The dynamic XML file, which has the alias "data.xml", is generated at run time by the web server using data from our simple names database. The method which creates the XML takes two parameters: "start" and "count" which tell it how many records to select and where to start, along with a nameStr parameter which is used to fetch all records which contain a specified substring.

The XML returned by our dynamic "data.xml" document takes the following format. Within the 'xml' document element are two component parts: the 'meta' element contains values for our start and count variables along with the total number of records; the second part of the document is a list of 'name' elements which contain the names data.
<xml>
  <meta start="0" count="10" total="88799"/>
  <names>
    <name id="1" name="SMITH" prob="1.006" rank="1"/>
    <name id="2" name="JOHNSON" prob="0.81" rank="2"/>
    <name id="3" name="WILLIAMS" prob="0.699" rank="3"/>
    <name id="4" name="JONES" prob="0.621" rank="4"/>
    <name id="5" name="BROWN" prob="0.621" rank="5"/>
    <name id="6" name="DAVIS" prob="0.48" rank="6"/>
    <name id="7" name="MILLER" prob="0.424" rank="7"/>
    <name id="8" name="WILSON" prob="0.339" rank="8"/>
    <name id="9" name="MOORE" prob="0.312" rank="9"/>
    <name id="10" name="TAYLOR" prob="0.311" rank="10"/>
  </names>
</xml>

The method which generates the XML looks like this...
protected string xmlData(string startStr, string countStr, string nameStr)
{
    int start;
    int count;

    // Convert strings to integers or use defaults if this is not possible

    try
    {
        start = Convert.ToInt32(startStr);
    }
    catch
    {
        start = 0;
    }

    try
    {
        count = Convert.ToInt32(countStr);
    }
    catch
    {
        count = 100;
    }

    // Create a data table to store the names data

    namesDataset.SurnamesDataTable ndt = new namesDataset.SurnamesDataTable();

    // Generate the data as appropriate

    if (nameStr == "")
    {
        // Fetch all records

        surnamesTableAdapter.Fill(ndt);
    }
    else
    {
        // Fetch records containing specified substring

        surnamesTableAdapter.FillBySurnamesContaining(ndt, nameStr);
    }
   
    // Create XML document to construct recults

    XmlDocument xml = new XmlDocument();

    // Document element

    xml.LoadXml("<xml/>");

    // Meta element

    XmlElement metaElement = xml.CreateElement("meta");
    xml.DocumentElement.AppendChild(metaElement);

    XmlAttribute attr = xml.CreateAttribute("start");
    attr.Value = start.ToString();
    metaElement.Attributes.Append(attr);

    attr = xml.CreateAttribute("count");
    attr.Value = count.ToString();
    metaElement.Attributes.Append(attr);

    attr = xml.CreateAttribute("total");
    attr.Value = ndt.Rows.Count.ToString();
    metaElement.Attributes.Append(attr);

    // List of names

    XmlElement namesElement = xml.CreateElement("names");
    xml.DocumentElement.AppendChild(namesElement);

    for (int i = 0; (i < count) && ((start + i) < ndt.Rows.Count); i++)
    {
        DataRow r = ndt.Rows[start + i];

        XmlElement siteElement = xml.CreateElement("name");

        attr = xml.CreateAttribute("id");
        attr.Value = r["NameId"].ToString();
        siteElement.Attributes.Append(attr);

        attr = xml.CreateAttribute("name");
        attr.Value = r["Name"].ToString();
        siteElement.Attributes.Append(attr);

        attr = xml.CreateAttribute("prob");
        attr.Value = r["Prob"].ToString();
        siteElement.Attributes.Append(attr);

        attr = xml.CreateAttribute("rank");
        attr.Value = r["Rank"].ToString();
        siteElement.Attributes.Append(attr);

        namesElement.AppendChild(siteElement);
    }

    // return the XML as a string

    return (xml.OuterXml);
}

7. Core HTML file


Now we've had a quick look at how our simple webserver works we can get on with the AJAX proper. The first file to look at is the core HTML file "index.html". In the head element we reference (include) our stylesheet and two JavaScript files.
<head>
  ...
  <title>Logical Genetics - AJAX demonstration</title>
  <!-- Stylesheet file -->
  <link rel="StyleSheet" href="ajax.css" type="text/css"/>
  <!-- Include important JavaScript files -->
  <script src="xmlwalker.js" type="text/javascript"/>
  <script src="ajax.js" type="text/javascript"/>
</head>

In the body of the HTML document are various div and span elements which are given id values so that JavaScript code can find them within the document structure and manipulate their contents (generally by adding things) within the client browser. The "debug" element is used to output messages from JavaScript code to aid in debugging.
<body onload="load()">
  ...
  <div id="debug">
  </div>

  <form>
    <p>
      <label for="nameStr">Name contains:</label>
      <input id="nameStr" value=""/>
      <input type="button" id="update" value="Update"
                     onclick="javascript: updateNames();"/>
    </p>
  </form>

  <p>
    Showing records <span id="start">0</span>
    to <span id="finish">0</span>
    of <span id="total">0</span>.
  </p>

  <p>
    <a href="#" onclick="javascript: first();">&laquo; first</a>
    <a href="#" onclick="javascript: prev();">&laquo; prev</a>
    <a href="#" onclick="javascript: next();">next &raquo;</a>
    <a href="#" onclick="javascript: last();">last &raquo;</a>
  </p>

  <div id="output">
    <p>No data to display</p>
  </div>
</body>

8. The "load()" function


Note that a function is called within the document's "onload" event (<body onload="...">smilie. This function, "load()", is responsible for setting up a simple XML fetching and parsing class (developed for this example) and sending the initial GET request for "data.xml"...
function load()
{
  // Replace contents of "output" div with loading message

  document.getElementById("output").innerHTML = "<p>Loading data, please wait...</p>";
 
  // Construct URL for "data.xml"

  url = "/data.xml?count=" + count + "&start=" + start + "&nameStr=" + nameStr;
 
  // Create XmlWalker class to fetch and parse data.xml

  var xmlWalker = new XmlWalker(url);

  // Add callback functions which will be called when named elements

  // are encountered in the XML

  xmlWalker.addCallback('name', enterName, 0);
  xmlWalker.addCallback('meta', enterMeta, 0);

  // "Done" callback function called when parsing done

  xmlWalker.setDoneCallback(parserDone);
 
  // Send GET request

  xmlWalker.fetchAndParseXml();
}

9. Element callback functions


Callback functions are assigned to specific element names within the XML parsing class; these will be called once for every matching element encountered within the XML data. We assign callbacks for the "meta" element and the "name" element.

The callback for the "meta" element parses start, count and record count values from the XML then inserts these values into the document, using the browser's XML DOM functionality to manipulate the document HTML.
function enterMeta(element)
{
  clearDisplay();

  // read the start, count and total values from the XML

  start = element.attributes["start"].value * 1;
  count = element.attributes["count"].value * 1;
  totalRecords = element.attributes["total"].value * 1;
 
  // Place these values within specific span elements in the HTML doc

  document.getElementById("start").innerHTML = start + 1;
  document.getElementById("finish").innerHTML = (start + count);
  document.getElementById("total").innerHTML = totalRecords;
}

The callback assigned to "name" elements calls another function called "displayName" to take data from each name record and append it to the "data" div in the HTML document.
function enterName(element)
{
  displayName(element.attributes["rank"].value,
     element.attributes["name"].value, element.attributes["prob"].value);
}

As the comment in the code listing says, using the "innerHTML" property of HTML elements is frowned upon by many JavaScript developers. We're putting unvalidated string data into a structured document object model and there are no guarantees that things will work properly if we make a mistake when formatting the innerHTML text. To be sure that we won't corrupt our document we should really use DOM functions to add elements, attributes and text data; the second AJAX example in this article does exactly that, but to keep things simple we show the innerHTML technique here.
function displayName(rank, name, prob)
{
  // Note that this is a nasty way to construct the HTML

  // far better to use the DOM properly as shown later

  // in the article

  var html = "";

  html = '<p class="row' + ((rowCount++ % 2) + 1) + '">';
  html += rank + ': ' + name + ' (' + prob + ')';
  html += '</p>';

  document.getElementById("output").innerHTML += html;
}

10. Navigation


The onclick handlers of the "first", "prev", "next" and "last" links on the demo page are hooked up to JavaScript functions with appropriate names. These functions simply alter the values of our "first" and "count" variables, before calling the "load()" function to fetch new data. The page itself is not refreshed during this process, thus the 'A' in AJAX!
function next()
{
  start += count;
 
  if(start > totalRecords - count)
    start = totalRecords - count;
   
  load();
}

function prev()
{
  start -= count;
 
  if(start < 0)
    start = 0;
   
  load();
}

function first()
{
  start = 0;
  load();
}

function last()
{
  start = totalRecords - count;
  load();
}

11. Basic AJAX example: Done!


We've now covered the basics of an AJAX system...
  • The webserver and dynamic XML generation
  • The skeleton HTML document and placeholder div and span elements
  • Javascript functions to fetch and display data

In the remainder of this article we look at how we can use our AJAX system to consume a simple SOAP web service and at some simple code for fetching and parsing XML.

« first  « prev  » next  » last 

Jump to: