General overview
XMLHTTP works by sending a request to the Web server from the client and returning an XML data island. Depending on the structure of the XML that is received, you can use XSLT or the XML DOM to manipulate it and bind portions of the page to that data. This is an extremely powerful technique.
Note
Microsoft does offer a Web Service behavior for Internet Explorer that makes asynchronous calls to ASP.NET Web services quick and easy. However, this behavior is not supported and it's not the best way to update a page asynchronously. You should use XMLHTTP instead!
In the example I'll work through in this column, I will make three Web service calls to an ASP.NET Web service through XMLHTTP. The Web service will query the Northwind database on the local SQL Server and will return a DataSet to the client in the form of an XML diffgram. I will then use the XML DOM to parse that XML data and dynamically update portions of my page. All of this will be done without a post back.
The Web service
The Web service that I'll use is named DynaProducts. It is a basic ASP.NET Web service that is written in C# and that contains the following three methods.
- GetCategories - Returns a DataSet that contains all categories in the Categories table.
- GetProducts - Returns a DataSet that contains all products of the category that are passed to the method.
- GetProductDetails - Returns a DataSet that contains details on the product whose ProductID is passed to the method.
The HTML page
The first thing that may strike you about this sample is that the page that I'm updating though the ASP.NET Web service is not an ASP.NET page. It's just a regular HTML page. However, I've added a fair amount of client-side JavaScript to the page, and it's that script that makes the calls to the Web service.
Let's look at the first snippet of code from the HTML page.
var objHttp;
var objXmlDoc;
function getDataFromWS(methodName, dataSetName, wsParamValue, wsParamName)
{
// create the XML object
objXmlDoc = new ActiveXObject("Msxml2.DOMDocument");
if (objXmlDoc == null)
{
alert("Unable to create DOM document!");
} else {
// create an XmlHttp instance
objHttp = new ActiveXObject("Microsoft.XMLHTTP");
// Create the SOAP Envelope
strEnvelope = "<soap:Envelope xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" +
" xsd=\"http://www.w3.org/2001/XMLSchema\"" +
" soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
" <soap:Body>" +
" <" + methodName + " xmlns=\"http://jimcoaddins.com/DynaProducts\">" +
" </" + methodName + ">" +
" </soap:Body>" +
"</soap:Envelope>";
// Set up the post
objHttp.onreadystatechange = function(){
// a readyState of 4 means we're ready to use the data returned by XMLHTTP
if (objHttp.readyState == 4)
{
// get the return envelope
var szResponse = objHttp.responseText;
// load the return into an XML data island
objXmlDoc.loadXML(szResponse);
if (objXmlDoc.parseError.errorCode != 0) {
var xmlErr = objXmlDoc.parseError;
alert("You have error " + xmlErr.reason);
} else {
switch(dataSetName)
{
case "CategoriesDS":
processCategory();
break;
case "ProductsDS":
processProducts();
break;
case "ProductDetailDS":
processProductDetails();
break;
}
}
}
}
var szUrl;
szUrl = "http://dadatop/wsXmlHttp/DynaProducts.asmx/" + methodName;
if (wsParamValue != null)
{
szUrl += "?" + wsParamName + "=" + wsParamValue;
}
// send the POST to the Web service
objHttp.open("POST", szUrl, true);
objHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
objHttp.send(strEnvelope);
}
}
This is the largest piece of code from the page, and I want to go over it in detail so you'll understand what's going on.
At the top of this script block, I created two variables:
objHttp and
objXmlDoc. These are the variables I will use for my XMLHTTP object and my XML DOM object. Immediately after that is the function definition for the
getDataFromWS function. This is the function that is responsible for making the client-side call to the Web service. It takes the following four arguments, two of which are optional:
- methodName - The name of the method to call on the Web service.
- dataSetName - The name of the DataSet that is returned by the Web service.
- wsParamValue - The value of the parameter that is passed to Web service if applicable. (Optional)
- wsParamName - The name of the parameter that is passed to Web service if applicable. (Optional)
Let's break the
getDataFromWS function into parts and discuss each one. Here's the first snippet:
// create the XML object
objXmlDoc = new ActiveXObject("Msxml2.DOMDocument");
if (objXmlDoc == null)
{
alert("Unable to create DOM document!");
} else {
// create an XMLHTTP instance
objHttp = new ActiveXObject("Microsoft.XMLHTTP");
This block of code creates the XMLHTTP object and the XML Document object. Next, I begin creating the SOAP envelope.
// Create the SOAP Envelope
strEnvelope = "<soap:Envelope xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" +
" xsd=\"http://www.w3.org/2001/XMLSchema\"" +
" soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
" <soap:Body>" +
" <" + methodName + " xmlns=\"http://jimcoaddins.com/DynaProducts\">" +
" </" + methodName + ">" +
" </soap:Body>" +
"</soap:Envelope>";
In this code, I am assigning the SOAP envelope to a string variable so that I can pass it on to the Web service. It's actually quite easy to discover how to format the SOAP envelope for your Web service. Simply browse to the Web service, and click one of the methods to see a SOAP envelope for that method. For example, here is what I see when browsing to the
GetCategories method of the wsXMLHTTP Web service that I created for this article:
ASP.NET tells you how the SOAP envelope should be formatted for an HTTP POST and an HTTP GET. In the example presented in this article, I will be using HTTP POST.
So far, so good. Now let's look at the next section of code.
// Set up the post
objHttp.onreadystatechange = function(){
// a readyState of 4 means we're ready to use the data returned by XMLHTTP
if (objHttp.readyState == 4)
{
// get the return envelope
var szResponse = objHttp.responseText;
// load the return into an XML data island
objXmlDoc.loadXML(szResponse);
if (objXmlDoc.parseError.errorCode != 0) {
var xmlErr = objXmlDoc.parseError;
alert("You have error " + xmlErr.reason);
}
else
{
switch(dataSetName)
{
case "CategoriesDS":
processCategory();
break;
case "ProductsDS":
processProducts();
break;
case "ProductDetailDS":
processProductDetails();
break;
}
}
When a request is made through XMLHTTP, the XMLHTTP object uses a
readyState property to track the status of the request. When all of the data has been received back from the Web service, the
readyState property changes to a value of
4. The
onreadystatechange property for the XMLHTTP object allows you to set up a callback function that will be called when the
readyState property changes. By ensuring that the data has been received in its entirety, I can keep from acting on that data until I'm ready.
Once all data has been received, I create an XML data island with the response by using the
responseText property. As you likely know, the response from a Web service is in XML format. In this case, I am returning a Microsoft ADO.NET DataSet.
The next section of this code block uses a switch statement to call the appropriate function based on the name of the DataSet that is returned from the Web service. I'll go into the code for those functions in detail a bit later.
Now let's look at the code that actually performs the XMLHTTP request.
var szUrl;
szUrl = "http://dadatop/wsXmlHttp/DynaProducts.asmx/" + methodName;
if (wsParamValue != null)
{
szUrl += "?" + wsParamName + "=" + wsParamValue;
}
// send the POST to the Web service
objHttp.open("POST", szUrl, true);
objHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
objHttp.send(strEnvelope);
The variable
szUrl contains the URL that is used to call the Web service for the sake of clarity. I then have an if statement that tacks on any parameters that are passed as a
QueryString value. In your environment, you may want to add the parameters to the SOAP envelope. Either way will work just fine.
The open method of the XMLHTTP object is called next. I've used the first three arguments for the open method; the method, the URL, and a Boolean value that specifies whether or not the call is asynchronous.
Important If you are making an asynchronous call as I am here, you must set up a callback function through the
onreadystatechanged property.
After the request header for the content-type is set, I send the request as a SOAP envelope using the string variable I populated earlier.
We've now gone over all of the code that makes the XMLHTTP request. Now let's have a look at the code that handles the interface in the browser and that handles the response from the Web service call.
First we'll look at the function that is called when the page first loads.
function getCategories()
{
var func = "getDataFromWS('GetCategories', 'CategoriesDS')";
document.all.lblCategoryDropdown.innerText =
"Please wait while data is retrieved...";
window.setTimeout(func, 1);
}
The first thing that I do in this function is create a variable to store the function signature for
getDataFromWS. I do this because I'm going to call
window.setTimeout at the end of this function to call the
getDataFromWS function. The purpose to this approach is to allow me to display the status to the user while waiting for the Web service call to complete. Notice that I'm changing the innerText of a DIV to display a message indicating that data is being retrieved. I then schedule the
getDataFromWS function through the
window.setTimeout call, and I set it to run in one millisecond.
Processing the Web service response
Remember earlier that I used the
onreadystatechanged property to configure a callback function. Also remember that the callback function contains a switch statement that calls a particular function based on the DataSet name. In this case, our DataSet name is CategoriesDS. Therefore, the
processCategory function will be called from the callback function. Let's have a look at that function to see how to use the XML DOM to parse out the response from the Web service.
function processCategory()
{
// get an XML data island with the category data
objNodeList = objXmlDoc.getElementsByTagName("Categories");
// add default value to the drop-down
document.forms[0].drpCategory.options[0] = new Option("Select a Category", 0);
// walk through the nodeList and populate the drop-down
for (var i = 0; i < objNodeList.length; i++)
{
var dataNodeList;
var textNode;
var valueNode;
dataNodeList = objNodeList[i].childNodes;
valueNode = dataNodeList.item(0);
textNode = dataNodeList.item(1);
document.forms[0].drpCategory.options[i + 1] =
new Option(textNode.text, valueNode.text);
document.all.lblCategoryDropdown.innerText = "Select a Category:";
document.forms[0].drpCategory.style.visibility = "visible";
}
}
Remember that the
getDataFromWS function loaded XML from the response into the
objXmlDoc object. In the
processCategory function, I take that XML and parse through it to populate the Category drop-down.
The first thing that I do is create an
IXMLDOMNodeList object using part of the XML response. The DataSet that I'm returning from the Web service call is returned as a diffgram, and the only portion of that response that I'm really interested in is the data from the DataTable that I've inserted into the DataSet. I can get to that by creating an
IXMLDOMNodeList object from the XML block that contains the DataTable.
If you look at the code for the Web service, you'll see that I create a DataTable that is named Categories and add it to the DataSet. When the XML is returned from the Web service, the DataSet is contained within a <CategoriesDS> block, and each row from the DataTable is contained within separate <Categories> blocks as shown in the XML file below.
The following files are available for download from the Microsoft
Download Center:
Download the GetCategories.xml package now.Download the WSXMLHTTP.exe package now.
For more information about how to download Microsoft Support files, click the following article number to view the article in the Microsoft Knowledge Base:
119591 How to obtain Microsoft support files from online services
Microsoft scanned this file for viruses. Microsoft used the most current virus-detection software that was available on the date that the file was posted. The file is stored on security-enhanced servers that help prevent any unauthorized changes to the file.
To get the XML block that contains that DataTable, I use the following code:
objNodeList = objXmlDoc.getElementsByTagName("Categories");
This returns an
IXMLDOMNodeList object that contains each <Categories> node. I then iterate through that list using a for loop.
// walk through the nodeList and populate the drop-down
for (var i = 0; i < objNodeList.length; i++)
{
var dataNodeList;
var textNode;
var valueNode;
dataNodeList = objNodeList[i].childNodes;
valueNode = dataNodeList.item(0);
textNode = dataNodeList.item(1);
document.forms[0].drpCategory.options[i + 1] =
new Option(textNode.text, valueNode.text);
document.all.lblCategoryDropdown.innerText = "Select a Category:";
document.forms[0].drpCategory.style.visibility = "visible";
}
I already know that each <Categories> node will have two nodes that I need: the <ID> node and the <CategoryName> node. Therefore, the first thing I do is create a new
IXMLDOMNodeList and populate it with the child nodes of the current <Categories> node.
dataNodeList = objNodeList[i].childNodes;
I then use the item method to access both of the nodes that I need to populate my drop-down. The first node contains the
CategoryID field from database, and the second node contains the
CategoryName field from the database. I create a new
Option object, set the text to the
CategoryName, set the value to the
CategoryID, and add it to the
drpCategory drop-down. The code that is used in the remaining functions uses the same method to pull the data needed from the XML response and to populate portions of the page.
Note
Since we're dealing with small amounts of data here, using the DOM is a great way to pull out the data we need. If you were dealing with a large amount of data, you may choose to use XSLT instead.
How to make it all work
Now that I've covered the gritty details of how all of this works, it's time to go over how you can use the included sample files to see it work for yourself.
Deploying the Web service
To deploy the ASP.NET Web service, simply unzip the attached Web service sample to the root of your Web server. You will then need to open the code for DynaProducts.asmx and change the connection string. At the least, you will need to enter the SA password. After you've made that change, recompile the Web service.
Deploying the HTML file
The HTML file contains a variable named
szUrl that contains a URL to the Web service. You'll find this variable in the
getDataFromWS function near the bottom of the function. You will need to change that to the URL for the Web service that you deployed above.
After you've deployed both the Web service and the HTML file, browse to the HTML file. When it loads, the Category drop-down will be populated by the first XMLHTTP request to the Web service. Once that has been populated, select a category to kick off the next XMLHTTP request that populates the Products drop-down. Selecting a product from the Products drop-down will populate a table with data about that product.
Notice that the page does not post back during any of these XMLHTTP requests. That's the beauty of XMLHTTP requests. If I had done this on a large page, the page would have also maintained its scroll position without "blinking" at the user. If you ask me, that's some pretty powerful stuff!
One more thing: in this article, I used XMLHTTP to query a Web service. I could have just as easily used it to make a request for an ASPX page or an ASP page. The possibilities for how you can put this technology to use are endless. I hope you find XMLHTTP useful in your future Web application development.