Introduction
AJAX is a remote scripting technique that allows the browser to call the server without posting the entire page back to the server.
Developing AJAX-style applications is not simple as it requires a good knowledge of JavaScript, DOM, CSS, HTML, XML, SOAP, and related technologies. In addition, it is necessary to have a good grasp of the many subtle, and not-so-subtle, cross-browser differences.
With ASP.NET 2.0 AJAX Server Control Extensions that do all the scripting behind, you can implement AJAX functionality without writing a line of JavaScript. But, as with any high-level abstraction, those controls offer less flexibility than if you were to program them on a raw JavaScript level. This article will show how to send or get any data from the server using JavaScript and the XmlHttpRequest
object.
Here are the business requirements for this project: Let’s say, we have an Ecommerce application from which users are able to buy some products online, and let’s say that purchases are happening very often, so that we have to constantly monitor the process online.
In my previous article, I have already described the implementation of a similar project. In that project, the page that actually did all the calls to the server returned the data within a .NET 2.0 GridView
control through XmlHttpRequest
. After the article was published, I received several comments that this was not obviously the best way of transferring data between the client and the server.
So, the project was rewritten in a way that the page behind returns just the data in XML format. The data is then received and parsed by a JavaScript function.
This makes the size of the chunks of data that are transferred from the server to the client relatively small as the page contains only data and has no any additional code related to ASP.NET controls, and thus the server traffic can be significantly reduced.
Background
To implement AJAX calls, you actually need to have two web pages: the one that is visible to the end user, and the one that actually generates the required content for the first web page. The first page calls the second one through an XmpHttpRequest
object implemented in JavaScript.
Below is a very popular diagram that can be found in many AJAX related articles.
The above diagram does not provide enough details to understand the whole process of getting and presenting data on a web page through AJAX, so, please review the diagram below that shows how the AJAX application should be organized.
The JavaScript should have at least two functions: - the first one should implement a callback and will send an asynchronous request through the XmlHttpRequest
object from the web page requested by the client to another web page that is not visible to the client but does the actual calls to the server to retrieve the requested data. In this sample, the server page will go through all the paths in the Business Logic Layer and the Data Access Layer to the database to get the requested data (or, may be to add or delete data), and then will send a response back to the client web page. The second JavaScript function on the client side will parse the request into HTML and present it to the client.
The third diagram below shows the architecture of the project that is described in this article. The Northwind SQL database, Products table was used at the back end, and the only extra functionality that was added to the project was to simulate the Ecommerce functionality: in the request to get the data from the Products table, the functionality to randomly decrease the numbers in the Quantity field was added.
When quantity becomes less than the Reorder level, the data is shown in red on a web page. After the streaming period is finished, the data is updated to its original state.
Using the code
Client page
The SQL Northwind database was used for this project, the connection string is provided in the web.config file:
<appSettings>
<add key="DSN" value="server=localhost;Integrated Security=SSPI;database=Northwind" />
</appSettings>
In this sample, the client page (Default.aspx) has a control, WebControls\ProductListControl.ascx, which builds the table structure to hold the data in a web page Render
event.
DataSet ds = new DataSet();
ProductsClass.GetProducts(ds);
int rowCount = ds.Tables[0].Rows.Count;
The initial dataset is returned by the ProductClass.GetProducts(ds)
method, and then the table header and a table row to present the data returned in the DataSet
are built. When the table is rendered, it is written with the Write()
method.
output.Write("<SPAN><ProductsList>{0}</ProductsList></SPAN>", html);
Here is the Render
method:
protected override void Render(HtmlTextWriter output)
{
DataSet ds = new DataSet();
ProductsClass.GetProducts(ds);
int rowCount = ds.Tables[0].Rows.Count;
StringBuilder strOutput = new StringBuilder();
#region TABLE BEGIN
strOutput.Length = 0;
strOutput.Append("<TABLE cellspacing=‘0‘ cellpadding=‘1‘ " +
"border=‘0‘ class=‘dtBorder‘ width=‘700‘>\n");
#endregion TABLE BEGIN
#region Column Headers
cssHR = "dtHeaderRow";
cssHT = "dtHeaderText";
strOutput.Append("<TR class=‘").Append(cssHR).Append("‘ >\n");
strOutput.Append("<TD width=‘50‘ nowrap=‘true‘ align=‘left‘ height=‘21‘" +
" class=‘").Append(cssHT).Append("‘>").Append(
"ID").Append("</TD>");
strOutput.Append("<TD width=‘240‘ height=‘21‘ align=‘left‘ " +
"class=‘").Append(cssHT).Append("‘>").Append(
"Product Name").Append(" </TD>");
strOutput.Append("<TD width=‘200‘ height=‘21‘ align=‘light‘ " +
"class=‘").Append(cssHT).Append("‘>").Append(
"Quantity per Unit").Append(" </TD>\n");
strOutput.Append("<TD width=‘80‘ height=‘21‘ align=‘right‘ " +
"class=‘").Append(cssHT).Append("‘>").Append(
"Unit Price").Append("</TD>\n");
strOutput.Append("<TD width=‘80‘ height=‘21‘ align=‘right‘ " +
"class=‘").Append(cssHT).Append("‘>").Append(
"Quantity").Append(" </TD>\n");
strOutput.Append("<TD width=‘80‘ height=‘21‘ align=‘right‘ " +
"class=‘").Append(cssHT).Append("‘>").Append(
"Reorder Level").Append(" </TD>\n");
strOutput.Append("</TR>\n");
#endregion Column Headers
#region DISPLAY PRODUCTS
for (int i = 0; i < rowCount; i++)
{
cssDR = (i%2==0) ? "dtLightRow" : "dtDarkRow";
strOutput.Append("<TR id=‘realtimeDIV").Append(i).Append(
"‘ class=‘").Append(cssDR).Append("‘>\n");
string productId = "";
string productName = "";
string quantityPerUnit = "";
string price = "";
string quantity = "";
string reorderLevel = "";
productId = ds.Tables[0].Rows[i]["ProductId"].ToString();
productName = ds.Tables[0].Rows[i]["ProductName"].ToString();
quantityPerUnit = ds.Tables[0].Rows[i]["QuantityPerUnit"].ToString();
price = ds.Tables[0].Rows[i]["UnitPrice"].ToString();
quantity = ds.Tables[0].Rows[i]["UnitsInStock"].ToString();
reorderLevel = ds.Tables[0].Rows[i]["ReorderLevel"].ToString();
strOutput.Append("<TD id=‘T1").Append("_").Append(i).Append(
"‘ width=‘50‘ height=‘21‘ " +
"align=‘left‘>").Append(productId).Append("</TD>\n");
strOutput.Append("<TD id=‘T2").Append("_").Append(i).Append("‘ width" +
"=‘240‘ height=‘21‘ align=‘left‘>").Append(
productName).Append("</TD>\n");
strOutput.Append("<TD id=‘T3").Append("_").Append(i).Append(
"‘ width=‘200‘ height=‘21‘ align=‘left‘>").Append(
quantityPerUnit).Append("</TD>\n");
strOutput.Append("<TD id=‘T4").Append("_").Append(i).Append(
"‘ width=‘80‘ height=‘21‘ align=‘right‘>").Append(
price).Append("</TD>\n");
strOutput.Append("<TD id=‘T5").Append("_").Append(i).Append(
"‘ width=‘80‘ height=‘21‘ align=‘right‘>").Append(
quantity).Append("</TD>");
strOutput.Append("<TD id=‘T6").Append("_").Append(i).Append(
"‘ width=‘80‘ height=‘21‘ align=‘right‘>").Append(
reorderLevel).Append("</TD>");
strOutput.Append("</TR>\n");
}
#endregion DISPLAY PRODUCTS
#region TABLE END
strOutput.Append("<TR class=‘dtHR‘>\n");
strOutput.Append("<TD colspan=‘11‘ height=‘21‘>\n");
strOutput.Append("</TABLE>\n");
#endregion TABLE END
string html = strOutput.Replace(" ", " ").ToString();
output.Write("<SPAN><ProductsList>{0}</ProductsList></SPAN>", html);
}
Server page
The server page, GetProductList.aspx, has two methods:
GetProductList()
RestoreProductList()
GetProductList()
actually gets the data from the data source. RestoreProductList()
restores the original data values once the AJAX calls are completed.
In this sample, the data are retrieved from an MS SQL Server Northwind database through the Business Logic Layer and the Data Access Layer, which are described below:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using Tirex;
public partial class GetProductsList : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Response.Clear();
Response.ContentType = "text/xml";
string productsList = string.Empty;
string getList = "";
try
{
getList = Request.Params["getList"].ToString();
}
catch { }
if (getList == "0")
productsList = RestoreProductList();
else if (getList == "1")
productsList = GetProductList();
Response.Write(productsList);
}
private static string GetProductList()
{
string productsList = String.Empty;
try
{
DataSet ds = new DataSet();
ProductsClass.UpdateProducts(ds);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(ds.GetXml());
productsList = xmlDoc.InnerXml;
}
catch { }
return productsList;
}
private static string RestoreProductList()
{
string productsList = String.Empty;
try
{
DataSet ds = new DataSet();
ProductsClass.RestoreProductList(ds);
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(ds.GetXml());
productsList = xmlDoc.InnerXml;
}
catch { }
return productsList;
}
}
Another important point is to change the Contenttype
of the page to text/XML. In this case, the page will be returned as XML, and will not have any additional HTML code.
Response.Clear();
Response.ContentType = "text/xml";
The Business Logic Layer is located in the App_Code\BLL subfolder, and contains the ProductsClass
class that has three methods:
GetProducts()
- actually returns all the products for CategoryId=1
from the Products table in the Northwind database.UpdateProducts()
- simulates the behavior of the purchase process - it changes the UnitsInStock
value in Products table for a randomly selected product.RestoreProductlist()
- restores the original UnitsInStock
values once the AJAX calls are completed.
public static void GetProducts(DataSet ds)
{
string sqlString = "SELECT * FROM Products WHERE " +
"CategoryId=1 Order By ProductName ";
SqlHelper.FillDataset(SqlHelper.connection(), CommandType.Text, sqlString,
ds, new string[] { "Products" });
}
public static void UpdateProducts(DataSet ds)
{
Random RandomClass = new Random();
int rNumber = RandomClass.Next(100);
string sqlText = "";
if (rNumber % 2 == 0 && rNumber > 0)
{
sqlText = "UPDATE Products SET UnitsInStock = " +
"UnitsInStock - 5 WHERE (ProductId = " + rNumber +
"OR ProductId = "+ rNumber/2 + ") AND CategoryId = 1";
}
else if (rNumber % 2 > 0 && rNumber > 0)
{
sqlText = "UPDATE Products SET UnitsInStock = " +
"UnitsInStock - 10 WHERE (ProductId = " + rNumber +
"OR ProductId = " + rNumber*2 + ") AND CategoryID = 1";
}
SqlParameter[] paramList = new SqlParameter[]
{
new SqlParameter("@ProductId", 1)
};
SqlHelper.ExecuteNonQuery(SqlHelper.connection(), CommandType.Text,
sqlText, paramList);
string sqlString = "SELECT * FROM Products WHERE CategoryId=1 " +
"Order By ProductName ";
SqlHelper.FillDataset(SqlHelper.connection(), CommandType.Text, sqlString,
ds, new string[] { "Products" });
}
public static void RestoreProductList(DataSet ds)
{
string sqlText = "UPDATE Products SET UnitsInStock = 100, " +
"ReorderLevel = 90 WHERE CategoryId = 1";
SqlParameter[] paramList = new SqlParameter[]
{
new SqlParameter("@ProductId", 1)
};
SqlHelper.ExecuteNonQuery(SqlHelper.connection(), CommandType.Text, sqlText, paramList);
string sqlString = "SELECT * FROM Products WHERE CategoryId=1 Order By ProductName ";
SqlHelper.FillDataset(SqlHelper.connection(), CommandType.Text,
sqlString, ds, new string[] { "Products" });
}
The above methods use the data access methods from the Data Access Layer which is located in the App_Code\DAL subfolder, and contains a class SqlHelper
. The SqlHelper
class was created based on an open source Application Blocks data access classes, and contains a collection of data access methods. It can be successfully used to access data from a SQL Server database.
JavaScript
The code below contains two branches - for IE and Firefox. The code below shows how the new XmlHttpRequest
or ActiveXObject("Microsoft.XMLHTTP")
objects should be created for the IE and Firefox browsers:
if (window.XMLHttpRequest)
{
productsListClient = new XMLHttpRequest();
}
else if (window.ActiveXObject)
{
productsListClient = new ActiveXObject("Microsoft.XMLHTTP");
}
- An instance of the
XmlHttpRequest
is created.
function getProductsList()
{
var txtInterval = document.getElementById("txtInterval");
Minutes = txtInterval.value;
LastAccessDate = new Date();
if (ProductsListInterval != null)
{
try
{
clearInterval(ProductsListInterval);
}
catch (ex)
{
}
}
var chkViewAjax = document.getElementById("chkViewAjax");
if(chkViewAjax.checked == true)
{
AjaxServerUrl = AjaxServerUrlConst + "?getList=1";
}
else
{
AjaxServerUrl = AjaxServerUrlConst + "?getList=0";
}
ProductsListInterval = setInterval(‘ProductsListRecursion()‘, Seconds * 1000);
}
- A server request is sent. A boolean variable,
ProductListFlag
, is created to start the new request only after the previous request is completed. Consider the case when we wait for a response longer than a refreshing interval. In such a case, setting and checking this flag value will help to reduce the number of unnecessary calls to the server. In the parameters of the server URL, the currentDate
parameter is added. The callback happens only when the onreadystatechange
event of the xmlHttpRequest
is true
. If the URL is static, there is a chance that the callback will never be called.
{
try
{
if (!ProductsListFlag)
return;
var currentDate = new Date();
var url=AjaxServerUrl + "&date=" + currentDate;
productsListClient.open("GET", url);
productsListClient.onreadystatechange = getProductsListBack;
ProductsListFlag=false;
productsListClient.send(null);
}
catch(ex)
{
alert(ex.message);
}
}
- The server response is processed. The
XmlHttpRequest
object has a state property. The state property may have four values from 1 to 4 where 4 is a complete state. Another XmlHttpRequest
property, status
, shows the status of the XmlHttpRequest
object which should be equal to 200 in order to proceed. In the next step, we get the enumerated cells from a data table, the XML nodes from the XML document, and put the appropriate node values into the corresponding data cells.
Some formatting is done to show the rows in red if the quantity in the stock value is less than the Reorder level value. All other code in this sample is related to the functionality to stop flashing in the required number of minutes, uncheck the checkbox, and restore the original data values.
function getProductsListBack(response)
{
try
{
if(productsListClient.readyState == COMPLETE &&
productsListClient.status == OK)
{
ProductsListFlag = true;
if (document.all)
{
xmlDocument = new ActiveXObject(‘Microsoft.XMLDOM‘);
xmlDocument.async = false;
xmlDocument.loadXML(productsListClient.responseText);
var productsListNodes = xmlDocument.selectNodes(‘NewDataSet/Products‘);
var rowCount=productsListNodes.length;
for (var i=0; i<rowCount; i++)
{
var T1="T1_"+i;
var T2="T2_"+i;
var T3="T3_"+i;
var T4="T4_"+i;
var T5="T5_"+i;
var T6="T6_"+i;
var tdT1=window.document.getElementById(T1);
var tdT2=window.document.getElementById(T2);
var tdT3=window.document.getElementById(T3);
var tdT4=window.document.getElementById(T4);
var tdT5=window.document.getElementById(T5);
var tdT6=window.document.getElementById(T6);
var tr = tdT1.parentNode;
var xmlElementProductId=
productsListNodes[i].selectSingleNode(‘ProductID‘);
var xmlElementProductName=
productsListNodes[i].selectSingleNode(‘ProductName‘);
var xmlElementQuantityPerUnit=
productsListNodes[i].selectSingleNode(‘QuantityPerUnit‘);
var xmlElementPrice=productsListNodes[i].selectSingleNode(‘UnitPrice‘);
var xmlElementQuantity=
productsListNodes[i].selectSingleNode(‘UnitsInStock‘);
var xmlElementReorderLevel=
productsListNodes[i].selectSingleNode(‘ReorderLevel‘);
tdT5.innerHTML = xmlElementQuantity.text;
var qty = parseInt(xmlElementQuantity.text);
var rol = parseInt(xmlElementReorderLevel.text);
if (qty < rol)
tr.style.color="#FF0000";
else
tr.style.color="#000000";
}
}
else if (document.implementation.createDocument)
{
xmlDocument = new ActiveXObject(‘Microsoft.XMLDOM‘);
xmlDocument.async = false;
xmlDocument.loadXML(productsListClient.responseText);
var productsListNodes = xmlDocument.selectNodes(‘NewDataSet/Products‘);
var rowCount=productsListNodes.length;
for (var i=0; i<rowCount; i++)
{
var T1="T1_"+i;
var T2="T2_"+i;
var T3="T3_"+i;
var T4="T4_"+i;
var T5="T5_"+i;
var T6="T6_"+i;
var tdT1=window.document.getElementById(T1);
var tdT2=window.document.getElementById(T2);
var tdT3=window.document.getElementById(T3);
var tdT4=window.document.getElementById(T4);
var tdT5=window.document.getElementById(T5);
var tdT6=window.document.getElementById(T6);
var tr = tdT1.parentNode;
var xmlElementProductId=
productsListNodes[i].selectSingleNode(‘ProductID‘);
var xmlElementProductName=
productsListNodes[i].selectSingleNode(‘ProductName‘);
var xmlElementQuantityPerUnit=productsListNodes[i].
selectSingleNode(‘QuantityPerUnit‘);
var xmlElementPrice=productsListNodes[i].selectSingleNode(‘UnitPrice‘);
var xmlElementQuantity=
productsListNodes[i].selectSingleNode(‘UnitsInStock‘);
var xmlElementReorderLevel=
productsListNodes[i].selectSingleNode(‘ReorderLevel‘);
tdT5.innerHTML = xmlElementQuantity.textContent;
var qty = parseInt(xmlElementQuantity.textContent);
var rol = parseInt(xmlElementReorderLevel.textContent);
if (qty < rol)
tr.style.color="#FF0000";
else
tr.style.color="#000000";
}
}
var currentDateTime=document.getElementById("TopMessage");
var now = new Date();
currentDateTime.innerHTML=now;
var stopFlag = true;
for (var i=0; i<rowCount; i++)
{
var xmlElementQuantity=productsListNodes[i].selectSingleNode(‘UnitsInStock‘);
var qty = parseInt(xmlElementQuantity.text);
if (qty <100)
stopFlag = false;
}
var chkViewAjax = document.getElementById("chkViewAjax");
if(chkViewAjax.checked == false && stopFlag == true)
{
clearInterval(ProductsListInterval);
return;
}
UpdateSession();
}
}
catch(err)
{
}
}
Points of interest
Using the above technique, it is possible to get, add, edit, delete data without visual postbacks to server. All postbacks are done by a web page that is not visible to the end user, but provides the data to the client web page through AJAX using the XmlHttpRequest
object.
A working sample of this project can be viewed here.
History
- 4th December, 2008: Initial post.