ASP.NET
Integrating jQuery, Web Services, AJAX and ASP.NET
The jQuery library makes it easier to create applications that execute in the browser. By leveraging existing ASP.NET technologies and merging them with jQuery, you can create faster, more responsive applications.
jQuery is a lightweight-only 19KB-open source JavaScript library that simplifies client-side programming, including working with CSS. Because jQuery supports all the major browsers, it also effectively provides a powerful, general-purpose API for executing code in the client. What's especially attractive about jQuery is that it's obviously the work of developers who understand the typical activities that need to be performed on the client and wanted to create an API that would support those activities.
In this article, you'll see how to use jQuery, along with server-side code and ASP.NET for AJAX, to create a typical CRUD business page (see Figure 1). The page will leverage the best of server-side processing, service-oriented architecture (SOA), ASP.NET Validators, AJAX for ASP.NET and jQuery to create the application.
[Click on image for larger view.] |
Figure 1. When the user selects a customer from the ListBox, the customer information is displayed on the page without a postback. When the user clicks the Update button, the data goes back to the server, but the page does not. |
The page, when requested by the user, first displays a list of customers. When the user selects a customer, the page uses the ScriptManager to call a Web Service that returns a Customer object when passed a CustomerId. Client-side code, using jQuery, then moves the object's data to elements on the page. After users make their changes, jQuery is used to retrieve the changes and return an updated Customer object to a Web Service that will update the database. In between, the client-side code generated by ASP.NET Validators will be used to check the data. Throughout this process the page will never post back.
The benefits of combining these technologies are many: more responsive applications, more processing on the client, less processing on the server and reduced network traffic. Everything runs faster and uses fewer resources.
In addition to building this application, you'll also see some of the issues particular to using jQuery in ASP.NET.
You need to download several files to add jQuery support to current versions of Visual Studio; for instance, to get IntelliSense when working with jQuery. The best description of what you need to do to add jQuery to your Visual Studio toolkit is provided by Scott Guthrie here. If you're willing to wait until March, jQuery will be integrated with Visual Studio 2010 out of the box.
Configuring the Site
To use jQuery in a Web site or ASP.NET project, you need to copy into your project the two files that contain the jQuery library and its IntelliSense support. I keep my JavaScript code files in a folder called js, so I created a subfolder named jquery within that folder and put the two files in there.
For any page where you intend to use jQuery, you need to link to the jQuery library from your .ASPX source. If you'll be using jQuery on most of your pages-and you should-then you should add the link to your Master Page.
You have two choices for adding the link. If all of your jQuery code will execute after the browser has processed the open tag of your <form> element, then you can add a ScriptManager to your page and add a reference to the jQuery library file to it (see Figure 2).
[Click on image for larger view.] |
Figure 2. To use the ScriptManager to generate the links to the jQuery library, add a ScriptReference member and set its Path property to the full path (with ~) of the jQuery library file. |
If you'll be executing jQuery code before the <form> tag is reached, then you should add a <script> tag to your <head> element that looks like this:
<script type="text/javascript"
src="js/jquery/jquery-1.3.2.js" />
The easiest way to generate this tag is to just drag the jQuery file from Solution Explorer and drop it inside your <head> element.
After you've made these changes, you can test that you've configured your page correctly by adding this "Hello, World" code to your .ASPX file and displaying the page:
<script type="text/javascript">
$("aspnetForm").ready(function()
{
alert("Hello, world");
}
);
</script>
If you've enabled Forms Authentication and are using your Master Page for your log-in page, you'll now run into one of the few differences between IIS and the Visual Studio development server. The development server uses the ASP.NET runtime to process all file requests, including requests for JavaScript files. Because the ASP.NET runtime honors your security settings, if you've locked the anonymous user out of your Web site, the log-in page won't be able to access your jQuery file; Internet Explorer generates an unhelpful and undebuggable "Runtime Error" message. While it won't be a problem when IIS is hosting your application, to support debugging in the development server you'll need to configure your JavaScript folder to allow access to the anonymous user.
If you use the ScriptManager to add a reference to your jQuery library, you'll lose whatever IntelliSense support Visual Studio provides for jQuery. The only way to get it back is to add a script tag that references the jQuery library to each page where you'll be using jQuery. However, you must add the tag in a way that doesn't cause the page to end up with two script tags downloading the jQuery library. This code, on a content page, does the trick:
<% if False then %>
<script type="text/javascript"
src="js/jquery/jquery-1.3.2.js" />
<% End if %>
Or, you could just add the script tag that links to the jQuery library to every page.
On the Server
The page used for this article is very simple: a ListBox to show all the customers in the Northwind database, a Label to display the Id of the selected customer, a TextBox to display and let the user update the ContactName, and an HTML button to trigger updates. It's not pretty-and a real version of this page would have a lot more TextBoxes-but it will demonstrate the architecture that leverages all three technologies.
Because the user must request the page from the server, you should use standard server-side ASP.NET processing to put as much data in the page as you can before sending it to the user. While using jQuery doesn't require it, I used Entity Framework and an Entity control to fill my ListBox. For this page, I didn't need to do anything in the page's code file.
I did, however, need to create two Web Services: one that, when passed a customer Id, returns a Customer object, called GetCustomer; and a second that, when passed a Customer object, updates the database with the values in the control's properties, called UpdateCustomer. These services could be created using either .ASMX files or Windows Communication Foundation (WCF) classes. Using .ASMX files requires fewer configuration changes than using a WCF service, so I'll use an .ASMX file called NorthwindCustomer.asmx as my example. (See http://tinyurl.com/c825vc and http://tinyurl.com/c4hfe8 for the steps required to implement calling a WCF service from client-side code.) The download for this article includes both .ASMX and WCF versions.
To configure an .ASMX service to support being called from client-side code, you simply need to uncomment the ScriptService attribute on the class that's included by the default. If you're creating a Web site-rather than an ASP.NET project-you'll also need to make sure you enclose the code in your .ASMX code file in a Namespace using the site's name. In addition, you'll need to specify the Namespace in the .ASMX file's inherits attribute.
Regardless of whether you use .ASMX files or WCF classes, you'll need to specify the .ASMX or .SVC file in the ScriptManager's Services collection. With that in place you can move to your client-side code.
Retrieving ASP.NET Controls
When the user selects a customer in the ListBox, the code in the ListBox's click event must call the GetCustomer method on the Web Service. Without jQuery, adding an event handler means tracking down the HTML element generated for the ListBox and adding a new onclick attribute. Among other issues, this makes it impossible to fully separate the processing code from the user interface.
With jQuery the process doesn't require adding anything to the HTML element. Instead, your code retrieves a reference to the element and binds the function to the control. To retrieve a reference to an element you use a jQuery selector enclosed in parentheses. The selectors are a combination of CSS selectors and XPath location paths. This selector finds all elements with an id attribute set to "ListBox1" (All jQuery commands begin with the $; the # sign indicates that only the id attribute is to be searched.):
$("#ListBox1")
Once you've retrieved a reference to an element, you can call jQuery functions that perform operations on the retrieved element. As an example, the bind function attaches an event handler to a control when passed the name of an event and the function to run when the event fires. This example attaches a function that displays "Hello, World" when the user clicks on the ListBox:
$("#ListBox").bind("click", function(e) {
alert("Hello, World");
});
One of the benefits of using the bind method is that attaching an event to a control is now completely divorced from the UI code. You could put this code in a separate JavaScript file rather than in the page and it would still work.
To integrate with ASP.NET you need to deal with the way that ASP.NET generates the id attribute of an element from the Id property of a control. While there are several ways to handle this issue, there are two methods that minimize the work required.
The first method is to use server-side code embedded in the .ASPX file to return the value that will be used in the id attribute of the element created from the control. This value is available through the control's ClientId property. The code below demonstrates the technique:
$("#<%=Me.ListBox1.ClientId%>").bind(...
The alternative solution is to use a different selector that allows you to find controls by specifying an attribute and a value for an attribute. Using this technique you can still explicitly search by the element's id attribute. However, instead of checking the full Id property, you use jQuery's "ends with" operator (also a dollar sign) to check just the final characters of the id attribute. Regardless of the changes that ASP.NET makes to a control's Id, the resulting identifier always ends with the control's server-side Id, prefixed with an underscore. This code also finds the element that corresponds to the control with its Id property set to of ListBox1:
$("element[id$='_ListBox1']").bind(...
In this article, I use the first option-embedding server-side code-only because I'm used to it. In the Microsoft .NET Framework 4 you'll have a third option: the ClientId property is write-enabled so that you can control the control's id attribute.
Retrieving Data
The function called by the ListBox's click event must call the GetCustomer Web Service. Because I specified my service in the ScriptManager, I can call my GetCustomer service by name, passing the current value for the ListBox. When calling the service, I also pass the names of the function to be called when the Web Service returns-CustomerRetrieved in this example-and the one to be called if the Web Service returns a fault, GenericFailure. Within the function bound to the element's click event, the element that fires the even-in this case, the ListBox-is pointed to by the this variable.
The code to bind to the click event of the ListBox and call the Web Service now looks like this:
$("#<%=Me.ListBox1.ClientId%>").bind("click", function(e) {
NorthwindCRM.NorthwindCustomers.GetCustomer(
this.value, CustomerRetrieved, GenericFailure);});
You don't have to use the ScriptManager's functionality to call your Web Service; jQuery includes a post function for calling Web Services. The equivalent call to the GetCustomer Web Service method, processing the XML document that would be returned, would look like this:
$.post("NorthwindCustomers.asmx/GetCustomer", false,
function(xml) { CustomerRetrieved(xml); }, "string");
Personally, I find the ScriptManager functionality easier to use. However, if you're using the ScriptManager and jQuery, then you're incorporating two sets of code to call Web Services into your page. That's a lot of duplicated code. If you're not using the ScriptManager to support other AJAX for ASP.NET functionality-and you haven't put the ScriptManager on your Master Page so that it's included on every page-it makes sense to omit the ScriptManager and use jQuery's post function.
Updating the Page
Within the CustomerRetrieved function, I want to retrieve elements on the page that display the data and update them with the values from the properties on the Customer object. This leads to another feature of jQuery: a selector finds all matching elements. jQuery functions are specifically designed to process all the controls retrieved by the selector. My previous example, for instance, binds to the click event of all elements with the id attribute I provided.
To set the value attribute of all the elements retrieved by a selector, I can pass the data to jQuery's val function. This example finds the CustomerTextBox and sets its value attribute to the Customer object's ContactName property:
function CustomerRetrieved(cust)
{
$('#<%=Me.CustomerNameTextBox.ClientId%>').val(cust.ContactName);
}
However, not all controls generate an element with a value property. For instance, a Label control generates a Span element with the Label's Text inside the tags:
<span id="ctl00_MainPlaceHolder_CompanyIdLabel">
ALFKI
</span>
With the Span element, rather than update the value attribute, I want to update its text property, which is equivalent to the DOM's innerText property. Again, jQuery has a function, called text, that sets the inner text on all the elements retrieved by the selector. This example finds the CompanyIdLabel control and sets its inner text to the customer object's CompanyName property:
$('#<%=Me.CompanyIdLabel.ClientId%>').text( cust.CompanyName );
Updating Server-Side Data
To perform the updates, the HTML Button on the page will call the DoUpdate function, which will pass data back to the UpdateCustomer Web Service. While I could use jQuery's bind function to attach a handler to the Button's click event in this case, I'll just set the Button's onclick attribute, as in standard JavaScript client-side code, to call the DoUpdate function:
<input id="UpdateButton" type="button" value="Update"
onclick="return DoUpdate()" />
Within the DoUpdate function, the code creates an instance of the NorthwindCustomer object and sets its properties from the elements on the page. The start of the function looks like this:
function DoUpdate() {
var cust = new NorthwindCustomer();
cust.CompanyName = $('#<%=Me.CompanyIdLabel.ClientId%>').text();
cust.ContactName = $('#<%=Me.CustomerNameTextBox.ClientId%>').val();
To call the UpdateCustomer Web Service method-again, I'm using ScriptManager functionality-I pass the Customer object required by the method. As before, I also pass the name of the function to call if the method runs to completion and the method to call if the method returns a fault. That code looks like this:
NorthwindCRM.NorthwindCustomers.UpdateCustomer(
cust, CustomerUpdated, GenericFailure);
To extend this page to support insert and delete activities would just require additional buttons, client-side code and Web Services.
Validating Data
The ASP.NET Validator controls automatically generate client-side code, and client-side code can be optionally added to the CustomValidator. It would be useful to call that code from the DoUpdate event and, if one of the Validators fails, skip the update. The good news is that ASP.NET pages add a function called ValidatorValidate to the page that, when passed a client-side Validator, calls the control's client-side code. If the validation fails, the control's ErrorMessage is displayed and the Validator's isvalid property is set to false.
The first step in using the client-side validation code is to find the Validator controls on the page. Each Validator must then be passed to the ValidateValidator function and have its isvalid property checked. If any Validator fails, the function must set a flag to prevent further processing.
While jQuery has an enormous inventory of functions, as well as a flourishing industry creating jQuery add-ons, I don't know of a function that will do this. Fortunately, if jQuery doesn't have a function that will do what you want, you can use jQuery's each function. The each function executes a function of your own once for every element returned by the selector. Within your function, the keyword will point to the current element.
On the client, Validator controls are rendered as Span elements. Placed in the DoUpdate function, the selector in the following code first finds all the Span elements with id attributes that end in "Validator." The code then calls the ValidatorValidate method for each Validator, checks the isvalid property and sets an isValid flag. Finally, the code skips the update if any Validator fails:
var isValid = true;
$("span[id$='Validator']").each(function() {
ValidatorValidate(this);
if (!this.isvalid) { isValid = false }; });
if (isValid) {
..update code..
Managing the Page's Appearance
While the focus in this article is on typical CRUD activities that a business application requires, a great deal of jQuery is designed to support manipulating CSS. It wouldn't be right to not look at least one part of that functionality.
In the DoUpdate function, the UpdateCustomer Web Service returns the string "OK" if it's successful, and an error message if it's not. The CustomerUpdated function is called when the Web Service method moves the message to a label on the page:
function CustomerUpdated(msg) {
if (msg == "OK") {
$('#<%=Me.StatusLabel.ClientId%>').text("Update successful");
}
else {
$('#<%=Me.StatusLabel.ClientId%>').text(msg);
}
}
However, when an error message is displayed, it should be highlighted. This code sets the CSS class attribute on the message to a style that should highlight the error:
else {
$('#<%=Me.StatusLabel.ClientId%>').text(msg);
$('#<%=Me.StatusLabel.ClientId%>').addClass("ErrorStyle");
}
This article has combined numerous technologies: AJAX for ASP.NET, Web Services, server-side programming and, of course, jQuery. The result is a faster, more responsive business application that reduces the load on the server by transferring processing to the client and reducing network traffic. It's a very compelling package, and you should consider using it.