65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.49/5 (13投票s)

2007年1月2日

10分钟阅读

viewsIcon

77667

downloadIcon

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,每个都有自己的 UpdatePanelUpdateProgress 控件。

图 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 文件中。

© . All rights reserved.