ASP.NET AJAX:有状态还是无状态?






4.49/5 (13投票s)
2007年1月2日
10分钟阅读

77667

386
一篇关于在使用 ASP.NET AJAX 框架时我们可以采取的两种不同设计路径的文章
引言
在本文中,我们将探讨在使用 ASP.NET AJAX(异步 JavaScript 和 XML)框架时可能遇到的两种场景——我们是否需要状态。
状态?
在 ASP.NET 中,我们可以使用和创建在超文本传输协议 (HTTP) 请求中维护其状态的控件。一个使用状态的很好的例子是 GridView
控件。GridView
具有视图和编辑状态,如果您启用了过滤和分页,则具有更多状态。在 ASP.NET 中,控件的状态由 ViewState 属性维护。ViewState 允许我们在 HTTP 请求中维护控件的值,从而提供与桌面应用程序相同的用户界面 (UI) 一致性。
何时需要状态
正如我们前面所指出的,GridView 是一个具有许多状态的控件的良好示例,调用每个特定状态都需要向服务器进行完全回发,这会通过可怕的屏幕闪烁破坏应用程序 UI 的一致性。
随着 ASP.NET AJAX(之前代号为 Atlas)的引入,通常与向服务器进行回发相关的屏幕闪烁通过引入 UpdatePanel
得到了解决。从高层次来看,您可以将 UpdatePanel
视为为您包裹在 UpdatePanel
控件中的内容提供异步更新。
需要注意的是,使用 UpdatePanel
仍然会导致向服务器进行完全回发,就像我们不使用 UpdatePanel
时一样。UpdatePanel
的优点在于它保留了页面事件模型,因此我们可以从包裹在 UpdatePanel 中的控件引发事件,并受益于数据的异步处理。
让我们看看在 UpdatePanel
中使用 GridView
并确定从服务器请求了哪些数据。
图 1:在 UpdatePanel 控件中使用 GridView
<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>
在图 1 中,我们描述了一个位于 UpdatePanels
的 ContentTemplate 元素中的 GridView
控件——这就是我们为了受益于 GridView
内容的异步更新而需要做的一切。
图 1 中有一个我们尚未解释的关键组件,那就是 ScriptManager 控件,它是 ASP.NET AJAX 背后的“大脑”——它决定要下载哪些脚本到客户端,以便 AJAX 功能在客户端浏览器(IE、Safari、Firefox、Mozilla 等)上工作。
如果我们运行图 1,您将看不到屏幕闪烁,然而更重要的是,当我们在数据中分页时,从服务器请求的内容。在图 2 中,我们可以看到客户端在我们的 GridView
控件中查看另一页数据时请求的一些数据。响应的第一行(887 | updatePanel | upGv)告诉我们 UpdatePanel
控件的更新内容的总大小为 887 字节,并且相关联的 UpatePanel
的 ID 是 upGv。当我们继续查看发送回客户端的内容时,我们注意到只有 GridView
的更新 HTML 代码发送到客户端,页面上 UpdatePanel
之外的任何其他对象都没有重新下载。
图 2:客户端为 GridView 控件请求的数据
延迟问题
由于我们向服务器发出异步请求,因此我们需要处理延迟。在 AJAX 中,延迟是我们必须考虑的一个主要因素,当用户发起某些操作时,我们需要向用户告知该请求的进度——请记住,用户体验到的延迟会因服务器所承受的压力而异。
ASP.NET AJAX 组件带有一个 UpdateProgress
控件,它允许我们以友好的方式通知用户请求已成功发起且正在进行中,当请求完成后,我们预定义的消息就会消失。
让我们看一下图 3 中的示例,它与图 1 相同,但添加了一个 UpdateProgress
控件。
图 3:使用 UpdateProgress 控件
<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>
在图 3 中,我们可以看到定义 UpdateProgress 控件非常简单,我们友好的消息位于 UpdateProgress 控件的 ProgressTemplate 元素中——这样我们就有效地解决了由我们的 GridView 引起的异步请求的延迟问题!
多个控件和 UpdatePanel
到目前为止,我们已经解决了在单个 UpdatePanel
控件中使用单个控件的问题,在某些时候,我们将希望将 UpdatePanel
提供的异步行为与多个控件一起使用。
实现此功能非常简单,最佳实践是将您的 UI 逻辑地分解为几个原子功能块,然后为每个功能块分配一个 UpdatePanel
。
让我们看一下图 4,此示例在同一页面上有一个 GridView
和一个 FormView
,每个都有自己的 UpdatePanel
和 UpdateProgress
控件。
图 4:多个 UpdatePanel
<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>
如果您运行图 4 中的代码,您会注意到每个原子功能块都有自己的 UpdateProgress
控件,UpdateProgress
控件允许我们使用 AssociateUpdatePanelID 属性将其显式关联到 UpdatePanel。
条件更新
有时,您希望仅在满足特定条件时更新 UpdatePanel
的内容,ASP.NET AJAX 开箱即用地提供了此功能。我们可以定义 UpdatePanel
的内容将通过更改属性或从控件引发事件来更新。
让我们看一下图 5,它在按钮的 Click 事件引发时更新 UpdatePanel
的内容。
图 5:使用触发器有条件地更新 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();
}
}
当我们不需要状态时
当我们的应用程序不需要维护状态时,我们可以使用基本的 HTML 和 JavaScript 来提供对服务器上代码的异步调用。采用无状态方法的优点是它非常轻量级——我们不再向服务器进行完全回发。
ASP.NET AJAX 提供了一个丰富的框架,用于使用 JavaScript 调用服务器上的代码。我们能够异步调用托管在服务器上的 ASP.NET Web Service 或 WCF Service 方法——所有这些都无需往返服务器!
在我们查看任何代码之前,让我们首先介绍如何访问 Web Service 上的方法。我们已经在本文的状态部分中看到了 ScriptManager 控件的重要性,它变得更好了,因为我们可以定义 ScriptManager
控件的 Services 子元素并添加对服务器上服务的引用。在图 6 中,我们添加了对 People Web Service 的引用,它是一个 ASP.NET Web Service。
图 6:引用 Web Service
<asp:ScriptManager
ID="sm"
runat="server">
<Services>
<asp:ServiceReference
Path="~/WebServices/PeopleWebService.asmx" />
</Services>
</asp:ScriptManager>
如果我们运行此代码,我们将看到一个 URL 的引用,其地址与 Web Service 相同,但是,它将附加一个 /js 标志。此 JavaScript 文件是一个自动生成的代理,用于从客户端的 JavaScript 代码调用 Web Service 的方法。我们不会直接从 Web Service 中获得此功能,我们必须明确定义我们的 Web Service 可以由客户端 JavaScript 调用 - 这通过使用 ScriptService
属性(在 System.Web.Scripts.Services
命名空间中找到)来完成,图 7 展示了这一点。
图 7:允许客户端 JavaScript 调用 Web Service 的方法
[ScriptService]
public class PeopleWebService : System.Web.Services.WebService {
// ....
}
现在我们已经设置好了 Web Service 和 ScriptManager,让我们创建一个简单的 JavaScript 代码,它将从服务器检索当前日期和时间(图 8)。
图 8:异步调用 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>
在图 8 中,当单击按钮输入时,我们调用一个名为 GetServerTime()
的函数,在此函数中,我们调用在 _PeopleWebService.asmx_ 文件中定义的 GetServerTime
方法。在 JavaScript 中,我们像服务是静态定义类型一样访问服务的方法(例如 WebServiceName.NameOfMethod
),我们向此方法传递三个参数——当从服务返回一些数据时要调用的函数,以及当服务超时或发生错误时要执行的操作。OnSuccessCallback
函数接受一个参数 result,result 是我们返回的任何数据类型的变量名——它可以是数组、整数等。在这种情况下,我们返回一个字符串——然后我们在警报框中显示该字符串(图 9 显示了这一点)。
图 9:调用 Web Service 方法的结果
现在我们已经了解了如何调用 Web Service 的方法,我们需要讨论正在通过网络发送到客户端的数据,更重要的是如何发送。当我们调用 Web Service 的方法时,我们使用 JavaScript Object Notation (JSON) 将数据发送回客户端。解释为什么使用 JSON 很简单,它比发送序列化的 XML 更轻量级,而且与 JavaScript 必须解析的 XML 不同——JSON 可以原生执行(只调用安全的代码,我们不想向用户发送恶意脚本!)。
在图 10 中,我们检查在图 7 中调用函数 OnSuccessCallback
时发送到客户端的数据。
注意:我们将在下一个示例中查看一些通过网络发送的更复杂的数据。
图 10:查看使用 JSON 时通过网络发送的数据
目前,我们仅使用 JSON 将简单的字符串数据类型发送回客户端,现在让我们将自定义类型发送回客户端。
当向客户端发送复杂的自定义类型时,我们需要让系统知道我们打算通过脚本向客户端发送哪些类型,为此我们需要向我们的 Web Service 添加另一个属性——GenerateScriptType(typeof(MyType))
,此属性位于 System.Web.Script.Services
命名空间中。在我们继续之前,我们应该看一下 Person Web Service 的代码隐藏文件(图 11)。
图 11: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 personList = new List();
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();
}
}
图 12:Person 类型的定义
using System;
/// <SUMMARY>
/// Person entity
/// </SUMMARY>public class Person {
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; }
}
}
我们对 GetPeople
方法感兴趣,该方法返回一个 Person 类型数组(Person 类型代码如图 12 所示)。对于下一个示例,我们将调用此方法 (GetPeople
),然后遍历项目,输出 HTML 无序列表(图 13)。
图 13:输出人物的 HTML 无序列表
<%@ 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>
因为调用 GetPeople()
方法的结果返回一个数组,所以 result 变量的类型是 Person 类型数组。同样重要的是,我们可以像使用 C# 代码一样访问 Person 类型的属性(例如 result[i].FirstName
)。
为了总结无状态的使用,让我们看看当我们调用 People Web Service 的 GetPeople()
方法时 JSON 对象图的形式(图 14)。
图 14:JSON 对象图
摘要
在本文中,ASP.NET AJAX 的强大和灵活性得到了展现。我们研究了在使用 ASP.NET AJAX 框架时使用有状态和无状态方法的优点/缺点,我们还检查了每种方法通过网络发送的数据。
使用代码
注意:您需要 .NET 2.0、ASP.NET AJAX RC(或 RTM)和 SQL Server Express/2005 才能运行本文中的示例。用于设置数据库的 SQL 脚本以及所有代码文件都包含在 .zip 文件中。