Dynamic page updates using XMLHTTP (893659)



The information in this article applies to:

  • Microsoft ASP.NET (included with the .NET Framework 1.0)
  • Microsoft ASP.NET (included with the .NET Framework 1.1)

ASP.NET Support Voice column

Dynamic page updates using XMLHTTP

To customize this column to your needs, we want to invite you to submit your ideas about topics that interest you and issues that you want to see addressed in future Knowledge Base articles and Support Voice columns. You can submit your ideas and feedback using the Ask For It form. There's also a link to the form at the bottom of this column.

INTRODUCTION

One of my favorite ways of studying Web application usability is to watch my wife navigate around a Web site. She can make her way around the Internet quite well, but she knows little about the low-level technical aspects (what she calls "the boring stuff") that make it all work.

On a recent evening, I was watching my wife peruse an e-commerce application from one of the big boys. She was drilling down into a product listing by using multiple drop-downs, each one feeding off of the selection made previously. As she clicked an item in each drop-down, the page posted back to get data for the next drop-down. The experience was frustrating for her because her impression was that it was taking a long time due to the post backs.

The frustration level she was experiencing could have been easily alleviated by the developers of the application if they only used XMLHTTP to retrieve the data instead of posting back. That's what this month's column is about. I'll show you how to use XMLHTTP to update a portion of a Web page with data from a Microsoft ASP.NET Web service without doing a post back. This is going to be really cool! Trust me.

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:

envelope.png

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:
DownloadDownload the GetCategories.xml package now.
DownloadDownload 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.
As always, feel free to submit ideas on topics you want addressed in future columns or in the Microsoft Knowledge Base by using the Ask For It form.

Modification Type:MajorLast Reviewed:11/18/2005
Keywords:kbgraphic kbScript kbXML kbhowto KB893659 kbAudITPRO kbAudDeveloper