Introduction
In this article we are going to look at two possible scenarios we may find ourselves in when using the ASP.NET AJAX (Asynchronous JavaScript and XML) framework – whether we require state or not.
State?
In ASP.NET we can use and create controls which maintain their state over Hypertext Transfer Protocol (HTTP) requests. A good example of a control that uses state is the GridView
control. The GridView
has a view and edit state, and if you have filtering and paging enabled you have even more states. In ASP.NET the state of a control is maintained by the ViewState property. ViewState allows us to maintain the values of our controls over HTTP requests, delivering the same user interface (UI) consistency of a desktop application.
When we require state
As we have identified earlier the GridView is a good example of a control that has many states, invoking each particular state requires a full Postback to the server which breaks the application UI consistency with the dreaded screen flicker.
With the introduction of ASP.NET AJAX (previously codenamed Atlas) the screen flicker commonly associated with a Postback to the server was solved by the introduction of the UpdatePanel
. From a high level view you can think of the UpdatePanel
providing asynchronous updates for the content you have wrapped in the UpdatePanel
control.
Something which should be noted is that the use of the UpdatePanel
still causes a full Postback to the server, just like it would do if we were not using the UpdatePanel
. The beauty of the UpdatePanel
is that it preserves the Page event model, so we can raise events from controls wrapped in the UpdatePanel and benefit from asynchronous processing of data.
Let us take a look at using the GridView
within an UpdatePanel
and identify what data is being requested from the server.
Figure 1: Using a GridView within an UpdatePanel control
<asp:ScriptManager
ID="sm"
runat="server" />
<asp:UpdatePanel
ID="upGv"
runat="server">
<ContentTemplate>
<asp:GridView
ID="gvPeople"
runat="server"
AllowPaging="True"
AllowSorting="True"
AutoGenerateColumns="False"
DataKeyNames="personid"
DataSourceID="odsPeople"
Width="400px">
<Columns>
<asp:CommandField
ShowEditButton="True" />
<asp:CommandField
ShowDeleteButton="true" />
<asp:BoundField
DataField="firstname"
HeaderText="First Name"
SortExpression="firstname" />
<asp:BoundField
DataField="lastname"
HeaderText="Last Name"
SortExpression="lastname" />
</Columns>
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
In Fig. 1 we describe a GridView
control within an UpdatePanels
’ ContentTemplate element – that is all we need to do in order to benefit from the asynchronous update of the GridView
s’ content.
There is a key component of Fig. 1 which we have yet to explain and that is the ScriptManager control, this is the brain behind ASP.NET AJAX – it decides what scripts to download to the client in order for the AJAX functionality to work on the clients’ browser (IE, Safari, Firefox, Mozilla, etc...).
If we run Fig. 1 then you will see no screen flicker, more important, however, is the content which is being requested from the server when we page through our data. In Fig. 2 we can see some of the data that has been requested by the client as we viewed another page of data in our GridView
control. The first line of the response (887 | updatePanel | upGv) tells us that the total size of the updated content for the UpdatePanel
control was 887 bytes and that the ID of the associated UpatePanel
is upGv. As we continue to look at what was sent back to the client, we notice that only the updated HTML code for the GridView
was sent to the client, any other objects on the page outside of the UpdatePanel
were not re-downloaded.
Figure 2: Data requested by the client for GridView control
Latency issues
Because we are issuing asynchronous requests to the server we need to deal with latency. In AJAX latency is a major factor we must take into consideration, when a user initiates something we need to inform the user of the progress of that request – remember that the latency our user will experience will vary depending on the stress our server is under.
The ASP.NET AJAX bits come with an UpdateProgress
control which allows us to inform the user in a friendly manner that the request has been initiated successfully and the request is in progress, when the request is complete the message that we pre-define disappears.
Let us look at the example in Fig. 3 which is the same as Fig. 1, but with an UpdateProgress
control added.
Figure 3: Using the UpdateProgress control
<asp:ScriptManager
ID="sm"
runat="server" />
<asp:UpdatePanel
ID="upGv"
runat="server">
<ContentTemplate>
<asp:GridView
ID="gvPeople"
runat="server"
AllowPaging="True"
AllowSorting="True"
AutoGenerateColumns="False"
DataKeyNames="personid"
DataSourceID="odsPeople"
Width="400px">
<Columns>
<asp:CommandField
ShowEditButton="True" />
<asp:CommandField
ShowDeleteButton="true" />
<asp:BoundField
DataField="firstname"
HeaderText="First Name"
SortExpression="firstname" />
<asp:BoundField
DataField="lastname"
HeaderText="Last Name"
SortExpression="lastname" />
</Columns>
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
<asp:UpdateProgress
ID="upProgGv"
runat="server">
<ProgressTemplate>
<p>Please wait Loading...</p>
</ProgressTemplate>
</asp:UpdateProgress>
In Fig. 3 we can see that defining the UpdateProgress control is very simple, our user friendly message goes within the ProgressTemplate element of our UpdateProgress control – and with that we have effectively addressed the latency issue of the asynchronous requests invoked by our GridView!
Multiple Controls and the UpdatePanel
So far we have addressed the use of a single control within a single UpdatePanel
control, at some point we will want to use the asynchronous behaviour provided by the UpdatePanel
with multiple controls.
Implementing this functionality is very simple, the best practice is to logically break your UI into several atomic pieces of functionality and then assign an UpdatePanel
to each.
Let us take a look at Fig. 4, this example has a GridView
and a FormView
on the same page each with its own UpdatePanel
and UpdateProgress
control.
Figure 4: Multiple UpdatePanel’s
<asp:ScriptManager
ID="sm"
runat="server" />
<asp:UpdatePanel
ID="upGv"
runat="server">
<ContentTemplate>
<asp:GridView
ID="gvPeople"
runat="server"
AllowPaging="True"
AllowSorting="True"
AutoGenerateColumns="False"
DataKeyNames="personid"
DataSourceID="odsPeople"
Width="400px">
<Columns>
<asp:CommandField
ShowEditButton="True" />
<asp:CommandField
ShowDeleteButton="true" />
<asp:BoundField
DataField="firstname"
HeaderText="First Name"
SortExpression="firstname" />
<asp:BoundField
DataField="lastname"
HeaderText="Last Name"
SortExpression="lastname" />
</Columns>
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
<asp:UpdateProgress
ID="upProgGv"
runat="server"
AssociatedUpdatePanelID="upGv">
<ProgressTemplate>
<p>Please wait Loading...</p>
</ProgressTemplate>
</asp:UpdateProgress>
<asp:UpdatePanel
ID="upFv"
runat="server">
<ContentTemplate>
<asp:FormView
ID="fvPerson"
runat="server"
DataSourceID="odsPeople"
DefaultMode="Insert">
<InsertItemTemplate>
<asp:TextBox
ID="txtFirstName"
Text='<%# Bind("firstname") %>'
runat="server" />
<asp:TextBox
ID="txtLastName"
Text='<%# Bind("lastname") %>'
runat="server" />
<asp:LinkButton
ID="lbInsert"
CommandName="Insert"
Text="Add Person"
runat="server" />
<asp:LinkButton
ID="lbCancel"
CommandName="Cancel"
Text="Cancel"
runat="server" />
</InsertItemTemplate>
</asp:FormView>
</ContentTemplate>
</asp:UpdatePanel>
<asp:UpdateProgress
ID="upProgFv"
runat="server"
AssociatedUpdatePanelID="upFv">
<ProgressTemplate>
<p>Adding Person Please wait...</p>
</ProgressTemplate>
</asp:UpdateProgress>
If you run the code in Fig. 4 you will notice that each piece of atomic function has its own UpdateProgress
control, the UpdateProgress
control allows us to explicitly associate it with an UpdatePanel using the AssociateUpdatePanelID property.
Conditional Updates
There will be times when you wish to update the content of an UpdatePanel
only when a specific condition is met, ASP.NET AJAX comes with this functionality out of the box. We can define that an UpdatePanel
s’ content is to be updated by the changing of a property or the raising of an event from a control.
Let us take a look at Fig. 5 which updates the content of the UpdatePanel
when the Click event of a button is raised.
Figure 5: Using triggers to conditionally update the content of an UpdatePanel
<asp:ScriptManager
ID="sm"
runat="server" />
<asp:UpdatePanel
ID="up"
runat="server">
<ContentTemplate>
<asp:TextBox
ID="txtText"
runat="server" />
<asp:Button
ID="btnCopy"
Text="Copy Text"
OnClick="btnCopy_Click"
runat="server" />
<asp:Label
ID="lblText"
runat="server" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger
ControlID="btnCopy"
EventName="Click" />
</Triggers>
</asp:UpdatePanel>
using System;
using System.Web.UI;
public partial class State_ConditionalUpdate : System.Web.UI.Page {
protected void Page_Load(object sender, EventArgs e) {
}
protected void btnCopy_Click(object sender, EventArgs e) {
lblText.Text = txtText.Text.ToString();
}
}
When we don’t require state
When we don’t require state to be maintained by our application we can use basic HTML and JavaScript to provide asynchronous calls to code on our server. The benefit of taking the stateless approach is that it is lightweight – we no longer do a full Postback to the server.
ASP.NET AJAX provides a rich framework for calling code on the server using JavaScript. We have the ability to asynchronously call ASP.NET Web Service or WCF Service methods which are hosted on the server – all without doing a roundtrip to the server!
Before we look at any code let us first cover how we can access methods on a Web Service. We have already seen in the state part of this article how important the ScriptManager control is, well it gets better because we can define a Services child element of the ScriptManager
control and add a reference to services on the server. In Fig. 6 we add a reference to the People Web Service which is an ASP.NET Web Service.
Figure 6: Referencing a Web Service
<asp:ScriptManager
ID="sm"
runat="server">
<Services>
<asp:ServiceReference
Path="~/WebServices/PeopleWebService.asmx" />
</Services>
</asp:ScriptManager>
If we run this code we will see a reference to a URL with the same address as the Web Service, however, it will have a /js flag appended on to it. This JavaScript file is an auto generated proxy for calling the Web Services’ methods from JavaScript code on the client. We don’t get this functionality out of the box with Web Services, we have to explicitly define that our Web Service can be called by client JavaScript - this is done using the ScriptService
attribute (found in the System.Web.Scripts.Services
namespace), Fig. 7 shows this.
Figure 7: Allowing a Web Services’ methods to be called by client JavaScript
[ScriptService]
public class PeopleWebService : System.Web.Services.WebService {
}
Now we have our Web Service and ScriptManager all set up, let us create some JavaScript code that will simply retrieve the current date and time from the server (Fig. 8).
Figure 8: Asynchronously calling a method of a Web Service
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ServerTime.aspx.cs"
Inherits="No_State_ServerTime" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Server Time</title>
<script type="text/javascript">
function GetServerTime() {
PeopleWebService.GetServerTime(OnSuccessCallback, OnServiceTimeout,
OnServiceError);
}
function OnSuccessCallback(result) {
alert(result);
}
function OnServiceTimeout(result) {
alert(result);
}
function OnServiceError(result) {
alert(result);
}
</script>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager
ID="sm"
runat="server">
<Services>
<asp:ServiceReference
Path="~/WebServices/PeopleWebService.asmx" />
</Services>
</asp:ScriptManager>
<input
type="button"
value="Get Server Time"
onclick="GetServerTime()" />
</form>
</body>
</html>
In Fig. 8 we call a function called GetServerTime()
when the button input is clicked, in this function we call the method GetServerTime
that is defined in the PeopleWebService.asmx file. In JavaScript we access the method of the service as if the service were a statically defined type (e.g. WebServiceName.NameOfMethod
), we pass this method three arguments – the function to call when we get some data sent back from the service, and what to do when the service times out or incurs an error. The OnSuccessCallback
function takes a parameter result, result is the variable name for whatever data type we are passed back – it could be an array, integer, etc... In this case we are passed back a string – we then display that string in an alert box (Fig. 9 shows this).
Figure 9: The result of invoking a method of a Web Service
Now we have walked through how a method of a Web Service is invoked we need to talk about what data is being sent over the wire to the client and more importantly how. When we invoke a method of a Web Service we use JavaScript Object Notation (JSON) to send the data back to the client. An explanation of why JSON is used is simple, it is more lightweight than sending serialized XML and unlike XML which JavaScript has to parse – JSON can be natively executed (only call code which is secure, we don’t want to send nasty scripts down the wire to our users!).
In Fig. 10 we inspect the data that is sent to the client when the function OnSuccessCallback
in Fig. 7 is invoked.
Note: We will look at some more complex data sent over the wire in the next example.
Figure 10: Looking at what is sent over the wire when using JSON
At the moment we have only sent a simple string data type back to the client using JSON, now let us send a custom type back to the client.
When sending back complex custom types to the client we need to let the system know what types we intend to send to the client via script, for this we need to add another attribute to our Web Service – GenerateScriptType(typeof(MyType))
, this attribute is located in the System.Web.Script.Services
namespace. Before we move on we should look at the code behind file for the Person Web Service (Fig. 11).
Figure 11: Code behind for the PersonWebService.asmx
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Web;
using System.Web.Configuration;
using System.Web.Script.Services;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace = "http://gbarnett.org/services")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
[GenerateScriptType(typeof(Person))]
public class PeopleWebService : System.Web.Services.WebService {
public static string _conn
= WebConfigurationManager.ConnectionStrings["CodeProjectConn"]
.ConnectionString;
public PeopleWebService() { }
[WebMethod(Description="Gets a list of all the People")]
public Person[] GetPeople() {
List<PERSON> personList = new List<PERSON>();
using (SqlConnection sqlConn = new SqlConnection(_conn))
using (SqlCommand sqlCmd
= new SqlCommand("SELECT PersonId, FirstName, LastName FROM People",
sqlConn)) {
sqlConn.Open();
SqlDataReader sqlRdr = sqlCmd.ExecuteReader();
while (sqlRdr.Read()) {
personList.Add(new Person((int)sqlRdr.GetInt32(0),
sqlRdr.GetString(1) as string,
sqlRdr.GetString(2) as string));
}
}
return personList.ToArray();
}
[WebMethod(Description = "Returns the time on the server")]
public string GetServerTime() {
return DateTime.Now.ToLongDateString() + ", " +
DateTime.Now.ToLongTimeString();
}
}
Figure 12: Definition of Person type
using System;
private int _personId;
private string _firstName;
private string _lastName;
public Person() { }
public Person(int personId, string firstName, string lastName) {
_personId = personId;
_firstName = firstName;
_lastName = lastName;
}
public int PersonId {
get { return _personId; }
}
public string FirstName {
get { return _firstName; }
set { _firstName = value; }
}
public string LastName {
get { return _lastName; }
set { _lastName = value; }
}
}
We are interested in the GetPeople
method which returns an array of type Person (Person type code shown in Fig. 12). For the next example we are going to call this method (GetPeople
) and then iterate through the items outputting a HTML unordered list (Fig. 13).
Figure 13: Outputting a HTML unordered list of People
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="PeopleList.aspx.cs"
Inherits="NoStateViewPeople" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<script type="text/javascript">
function GetPeople() {
PeopleWebService.GetPeople(OnCompleteCallback, OnServiceTimeout,
OnServiceError);
}
function OnCompleteCallback(result) {
for(i = 0; i < result.length; i++) {
$get("people").innerHTML += "<li>" + result[i].LastName + ", " +
result[i].FirstName + "</li>";
}
}
function OnServiceTimeout(result) {
alert(result);
}
function OnServiceError(result) {
alert(result);
}
</script>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager
ID="sm"
runat="server">
<Services>
<asp:ServiceReference
Path="~/WebServices/PeopleWebService.asmx" />
</Services>
</asp:ScriptManager>
<input
type="button"
value="Get People"
onclick="GetPeople()" />
<ul
id="people"></ul>
</form>
</body>
</html>
Because the result of invoking the GetPeople
() method returns an array, the type of the result variable is an array of type Person. Also of significance is that we can access the properties of the Person type as we would do using C# code (e.g. result[i].FirstName
).
To conclude the use of no state let us see the form of the JSON object graph (Fig. 14) when we invoke the GetPeople()
method of the People Web Service.
Figure 14: JSON Object Graph
Summary
Throughout this article the power and flexibility of ASP.NET AJAX has been revealed. We have looked at the benefits/disadvantages of using both state and no state approaches when using the ASP.NET AJAX framework, we also inspected the data which was sent across the wire for each approach.
Using the code
Note: You will need .NET 2.0, ASP.NET AJAX RC (or RTM), and SQL Server Express/2005 to run the examples in this article. The SQL scripts for setting the database up as well as all code files are in the .zip file.