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

使用 AJAX 和 ASP.NET 改善 Web 应用程序用户体验

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.74/5 (16投票s)

2005年9月20日

CPOL

9分钟阅读

viewsIcon

155122

downloadIcon

429

本文解释了 AJAX,并展示了一个使用 ASP.NET 的示例,演示了如何使用它来改善 Web 应用程序的用户体验。

引言

作为 Web 开发人员,我们经常希望能够为用户提供桌面应用程序的丰富性和响应性。随着 Web 应用程序的快速发展,并取代其桌面应用程序,用户自然会期望我们开发的 Web 应用程序提供更好的用户体验。

当我们选择一个国家/地区,然后根据该国家/地区填充州下拉列表时,我们都一定讨厌页面刷新。还有其他场景,例如当我们在网站上输入了错误的票证 ID,并等待漫长的提交渲染到我们的浏览器,却发现我们输入了错误的号码,可能是打字错误。这种依赖的下拉列表和网站上的服务器端验证很快就会让用户感到沮丧,并迫使他们去寻找其他无需提交页面就能完成所有操作的网站,就像桌面应用程序一样。随着企业之间在网络上销售产品和吸引潜在客户的激烈竞争,用户体验开始扮演主要角色。

今天,我们将通过一个典型的场景,演示 Web 应用程序如何通过一组协同工作的现有技术(称为 AJAX)提供更好的用户体验。好的,我们首先定义 AJAX

AJAX – 定义

AJAX 代表 Asynchronous JavaScript and XML(异步 JavaScript 和 XML)。AJAX 是 Adaptive Path 创造的一个术语,它不是一项新技术。它是各种技术协同工作的组合,旨在为 Web 用户提供丰富的用户体验。AJAX 使用

  • 使用标准技术(即 HTML)的表示层,此外还使用浏览器的 DOM 进行动态表示和交互。
  • 浏览器的 DOM 功能,用于层之间的数据交换。
  • 使用 XMLHTTPRequest 在层之间交换信息。
  • 客户端脚本 [最好是 JavaScript] 以使这一切协同工作。

现在让我们尝试剖析 AJAX 这个术语。

A – 异步

这意味着此模型不同于与传统 Web 应用程序相关的请求-响应模型。在传统 Web 应用程序中,从请求发送到服务器到响应呈现到浏览器之间经过的时间无法实质性地利用。用户与应用程序的各个层同步工作,并在提交/刷新期间有空闲时间。在 AJAX 编程模型中,我们可以与业务逻辑和数据库层进行通信,而用户仍然可以使用浏览器执行其他受控操作 [这取决于我们允许用户在此期间执行什么操作]。总而言之,就用户而言,这使得后端层和表示层解耦。我们可以称之为表面上的异步。

JAX – JavaScript 和 XML

这意味着从业务逻辑和数据库层接收到的响应将使用 XML 表示,XML 的操作可以使用 JavaScript 完成。JavaScript 也将用于网页上的动态呈现。XML 只是首选的通信方式,返回的响应甚至可以是客户端脚本可以理解的文本。

上图显示了 XMLHttpRequest 对象如何承担 Web 服务器和表示层之间提供通信层的责任,从而实现 Web 服务器和表示层之间的异步通信。

AJAX 带来了我们可以利用的各种优势,以改善我们网站的用户体验,但同时,它也有一些缺点。让我们来看看这两者

优点

  • 更少的数据 – 更少的流量 - 由于我们在大多数情况下使用 AJAX 仅以 XML 形式进行数据交换,因此通过网络发送的数据量可以减少。将其与传统的视图-提交-等待-视图方法进行比较,在传统方法中,表示数据必须作为响应的一部分发送到浏览器。您可以想象此处生成的网络流量的差异。
  • 摆脱页面刷新 - 每次从业务逻辑层需要数据时,无需刷新浏览器页面。一个典型的例子是国家-州依赖下拉列表。当无需刷新页面即可填充州下拉列表时,用户体验会更好。

缺点

  • 应尽可能减少往返 Web 服务器的次数。频繁地向服务器发送大量数据会迅速耗尽网络带宽,而用户可能并未意识到。
  • 禁用浏览器上的客户端脚本会导致脚本停止执行。因此,使用 AJAX 的应用程序应能够向用户提供替代功能。

现在让我们来看看示例应用程序

示例应用程序

员工-区域 [Northwind 数据库] 依赖下拉示例

此应用程序使用一对“员工-区域”下拉列表。当从“员工”下拉列表中选择一名员工时,相应的区域将加载到“区域”下拉列表中。这种依赖下拉列表的用法在应用程序中非常常见。传统上,我们一直使用“员工”下拉列表的 OnChange 服务器端事件提交页面,并在服务器端事件中填充“区域”下拉列表。这会导致页面刷新,并且根据表单的大小,可能需要很长时间才能重新加载。整个演示代码必须再次呈现给浏览器,此外还要维护视图状态并用其先前的值填充表单字段。在此示例中,我们将看到如何使用 AJAX 执行相同的操作。使用 AJAX,我们将消除页面刷新,并了解我们在网络流量上节省了多少。

我们将使用随 SQL Server 安装附带的著名 Northwind 示例数据库。我们考虑三个表:Employees、EmployeeTerritories 和 Territories,它们的结构并相互关联,如下图所示:

Northwind 数据库表

EmployeeTerritoryAjaxClient.aspx

EmployeeTerritoryAjaxClient.aspx 是加载到浏览器的页面。最初,当页面加载时,“员工”下拉列表在 Page_Load 服务器端事件中填充。该页面最初将如下图所示

EmployeeTerritoryAjaxClient.aspx

“员工”下拉列表的 OnChange 事件调用 populateTerritory() 客户端 JavaScript 函数。我们将在本文的后续部分中与其它辅助函数一起查看此函数。现在让我们看一下 EmployeeTerritoryAjaxClient.aspx 页面的演示代码。

 <form id="frmEmployee" method="post" runat="server">
   <table border="0" cellpadding="5" cellspacing="0" bgcolor="#ccffff" width="280">
    <tr>
     <th colspan="2">
      AJAX Drop Down Demo</th>
    </tr>
    <tr>
     <td>
      <asp:Label id="lblEmployee" runat="server">Select Employee</asp:Label>
     </td>
     <td>
      <asp:DropDownList id="ddEmployee" Runat="server"></asp:DropDownList>
     </td>
    </tr>
    <tr>
     <td>
      <asp:Label id="lblTerritory" runat="server">Select Territory</asp:Label>
     </td>
     <td>
      <asp:DropDownList id="ddTerritory" runat="server">
       <asp:listitem Value="">--Select--</asp:listitem>
      </asp:DropDownList>
     </td>
    </tr>
    <tr>
     <td colspan="2" align="center">
       <asp:Label id="lblStatus" runat="server" class="statustext"></asp:Label>
     </td>
    </tr>
   </table>
 </form>

现在,我们将查看此页面的代码隐藏 C# 代码。

EmployeeTerritoryAjaxClient.aspx.cs

  private void Page_Load(object sender, System.EventArgs e)
  {
   if(!IsPostBack)
   {
    string connString = ConfigurationSettings.AppSettings["CONN_STRING"];
    //Connect to the Northwind Database


    SqlConnection connection = new SqlConnection(connString);
    connection.Open();



    //Build the SQL Query

    string SQL = "SELECT EmployeeID, TitleOfCourtesy + ' ' + LastName + ', ' + 
                  FirstName as EmpName FROM Employees";
    SqlDataAdapter da = new SqlDataAdapter(SQL, connection);
    DataSet ds = new DataSet();
    da.Fill(ds);

    //Create a DataView with the DefaultView property of the DataSet table

    DataView dv = new DataView();
    dv = ds.Tables[0].DefaultView;

    //Call BindEmployee by passing the created DataView object

    BindEmployee(dv);
   }
  }

BindEmployee 方法

  private void BindEmployee(DataView dv)
  {
   try
   {
    if(dv != null) //If DataView is not null

    {
     ddEmployee.DataSource = dv;
     //Text to display in Employee Dropdown

     ddEmployee.DataTextField = "EmpName";
     //Value to be associated in Employee Dropdown

     ddEmployee.DataValueField = "EmployeeID";
     ddEmployee.DataBind();
     ListItem item = new ListItem("--Select--", "");
     ddEmployee.Items.Insert(0, item);
    }
    else
    {
     throw new Exception("Dataview is null.");
    }
   }
   catch(Exception ex)
   {
    Response.Write("Exception: " + ex.Message);
   }
  }

InitializeComponent 方法

  private void InitializeComponent()
  {    
   this.Load += new System.EventHandler(this.Page_Load);
   //Important: Add this statement below to invoke the populateTerritory() JavaScript

   this.ddEmployee.Attributes.Add("OnChange", "return populateTerritory();");
  }

Page_Load 代码直截了当。建立与数据库的连接,并通过 SqlDataAdapter 将 Employees 表中的记录填充到数据集中。数据集 Employees 表的 DefaultView 作为参数传递给 BindEmployee() 方法。BindEmployee() 填充页面上的 Employee 下拉列表。

这里需要注意的一点是 InitializeComponent() 方法的以下代码

ddEmployee.Attributes.Add("OnChange", "return populateTerritory()"); 

我们在此处将 OnChange 事件添加到 Employee 下拉列表。此 OnChange 事件调用 populateTerritory() JavaScript 函数,该函数又调用其他辅助函数。接下来我们将介绍这些 JavaScript 函数。

JavaScript 函数

   <script language="javascript">
   var request;
   var response;
   var Territory = document.getElementById("ddTerritory");
   var status = document.getElementById("lblStatus");
   function populateTerritory()
   {
    var Employee = document.getElementById("ddEmployee");
    if(Employee.options[Employee.selectedIndex].value != '')
    //Check if the selectedItem is not "--Select--"

    {
     return SendRequest(Employee.options[Employee.selectedIndex].value);
    }
    else
    {
     clearSelect(Territory);//Clear the Territory dropdown

     status.innerText = "";//Blank the status text label

    }
   }
   function InitializeRequest()
   {
    try
    {
     request = new ActiveXObject("Microsoft.XMLHTTP");
     //Try creating an XMLHTTP Object

    }
    catch(Ex)
    {
     try
     {
      //First failure, try again creating an XMLHTTP Object

      request = new ActiveXObject("Microsoft.XMLHTTP");
     }
     catch(Ex)
     {
        //Else assign null to request


    request = null;
     }
    }
    if(!request&&typeof XMLHttpRequest != 'undefined')
    {
     request = new XMLHttpRequest();
    }
   }
   function SendRequest(ID)
   {
    status.innerText = "Loading.....";//Set the status to "Loading....."

    InitializeRequest();//Call InitializeRequest to set request object

    
    //Create the url to send the request to

    var url = "EmployeeTerritoryAjaxServer.aspx?EmployeeID="+ID;


    //Delegate ProcessRequest to onreadystatechange property so 

    //it gets called for every change in readyState value

    request.onreadystatechange = ProcessRequest;

    request.open("GET", url, true);//Open a GET request to the URL

    request.send(null);//Send the request with a null body.

   }
   function ProcessRequest()
   {
    if(request.readyState == 4)
    //If the readyState is in the "Ready" state

    {
     if(request.status == 200)
     //If the returned status code was 200. 

     //Everything was OK.

     {
      if(request.responseText != "")
      //If responseText is not blank

      {
       //Call the populateList fucntion

       populateList(request.responseText);
       //Set the status to "Territories Loaded"

       status.innerText = "Territories Loaded";
      }
      else
      {
       //Set the status to "None Found"

       status.innerText = "None Found";
       //Call clearSelect function

       clearSelect(Territory);
      }
     }
    }
    return true;//return

   }
   function populateList(response)
   {
    //Create the XMLDOM object

    var xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
    xmlDoc.async = false;
    //Load the responseText into the XMLDOM document

    xmlDoc.loadXML(response);
    var opt;
    //Create the EmployeeTerritories element

    var TerritoriesElem = 
      xmlDoc.getElementsByTagName("EmployeeTerritories"); 

    //Create the TERRITORIES element

    var TerritoryElem = 
      TerritoriesElem[0].getElementsByTagName("TERRITORIES");

    clearSelect(Territory);
    //Clear the dropdown before filling it with new values



    if(TerritoriesElem.length > 0)
    //If there are one or more TERRITORIES nodes

    {
    for (var i = 0; i < TerritoryElem.length; i++)
    //Loop through the XML TERRITORIES nodes

    {
     //Create a TextNode

     var textNode = document.createTextNode(
           TerritoryElem[i].getAttribute("TERRITORYDESCRIPTION"));
     //Call appendToSelect to append the text 

     //elements to the Territory dropdown

     appendToSelect(Territory, 
       TerritoryElem[i].getAttribute("TERRITORYID"), textNode);
    }
    }
   }
   function appendToSelect(select, value, content)
   {
    var opt;
    //Create an Element of type option

    opt = document.createElement("option");
    //Set the option's value

    opt.value = value;
    //Attach the text content to the option

    opt.appendChild(content);
    //Append the option to the referenced [Territory] select box

    select.appendChild(opt);
   }
   function clearSelect(select)
   {
    //Set the select box's length to 1 

    //so only "--Select--" is available in the selection on calling this function.

     select.options.length = 1;
    //You may want to write your own clearSelect logic

   }
   </script>

有三个 JavaScript 全局变量:requeststatestatusrequest 将包含 XMLHttpRequest 对象。statestatus 将分别包含 ddTerritory 下拉列表和 lblStatus 对象。现在,让我们看看选择员工时执行流程如何进行

  • populateTerritory() 函数被调用。此函数又通过传递 ddEmployee 下拉列表的 selectedIndex 值来有条件地调用 SendRequest() 函数。
  • SendRequest() 函数调用 InitializeRequest,后者创建一个名为 requestXMLHttpRequest 对象。
  • SendRequest() 还执行以下操作
    • 它将 ProcessRequest() 函数委托给请求对象的 onreadystatechange 事件。
    • 它向 EmployeeTerritoryAjaxServer.aspx 页面 URL 打开一个 GET 请求。
    • 它向页面发送一个带有 null 参数的请求。如果您希望将页面值作为 POST 请求的一部分发送,则可以使用此参数发送这些值。
    • ProcessRequest() 函数对我们特别重要。我们使用此函数确定请求的当前状态,并相应地将功能分配给其他函数。请求对象有以下五种可能的 readyState

      readyState 值

      状态

      0 未初始化
      1 加载中
      2 Loaded (加载)
      3 交互式
      4 Completed
    • ProcessRequest() 函数调用 populateList() 函数,并从 EmployeeTerritoryAjaxServer.aspx 页面获取响应。此响应是一个 XML 流,使用 DOM 解析器进行解析,并调用自描述的 appendToSelect() 函数。

现在让我们看一下 EmployeeTerritoryAjaxServer.aspx 页面。此页面实质上只包含 Page 属性标签,没有 HTML。我们以 "text/xml" 的内容类型从页面返回响应。

这是 EmployeeTerritoryAjaxServer.aspx 页面的代码隐藏 C# 代码

EmployeeTerritoryAjaxServer.aspx.cs

  private void Page_Load(object sender, System.EventArgs e)
  {
   string ID = Request["EmployeeID"];
   
   if(ValidateID(ID))
   {
    if(ID.Trim() != "" && ID != null)
    {
     try
     {
      SqlConnection conn = new 
        SqlConnection(ConfigurationSettings.AppSettings["CONN_STRING"]);
      //Connect to the Northwind Database

      conn.Open();
      SqlCommand command = 
        new SqlCommand("SELECT TERRITORYID, RTrim(TERRITORYDESCRIPTION) as" + 
            "TERRITORYDESCRIPTION FROM TERRITORIES WHERE TERRITORYID IN" + 
            "(SELECT TERRITORYID FROM EMPLOYEETERRITORIES WHERE EMPLOYEEID=" 
            + ID + ") for xml auto", conn);
      //Execute the SqlCommand Object

      SqlDataReader dr = command.ExecuteReader();
      System.Text.StringBuilder sbXML = 
             new System.Text.StringBuilder();
      if(dr.HasRows)
      {
       while(dr.Read())
       {
        sbXML.Append(dr.GetString(0).Trim());
       }
       //Set the ContentType of the Response Object to "text/xml"

       Response.ContentType = "text/xml";
       //Response.Write the XML generated 

       //by appending <EmployeeTerritories> tag on 

       //both ends of the XML string to create a documentElement

       Response.Write("<EmployeeTerritories>" + 
             sbXML.ToString() + "</EmployeeTerritories>");
      }
      else
      {
       //Response.Write an empty string so your client script 

       //understands no response was returned

       Response.Write("");
      }
     }
     catch
     {
      //Response.Write an empty string so your client script 

      //understands no response was returned

      Response.Write("");
     }
    }
    else
    {
     //Response.Write an empty string so your client script 

     //understands no response was returned

     Response.Write("");
    }
   }
  }

Page_Load 事件

Page_Load 事件是直截了当的。这里需要注意的一点是,我使用 SQL Server 2000 的 FOR XML AUTO 选项将查询结果作为 XML 获取。我在此结果 XML 周围附加一个 <Territories> 标签,并将响应发送回调用方。如果您使用的是不支持自动 XML 生成的不同数据源,则可以构建自己的 XML 节点并写入响应。

ValidateID 方法

  private bool ValidateID(string ID)
  {
   for(int Count=0;Count<ID.Length;Count++)
   {
    if (!System.Char.IsNumber(ID, Count))
    //Check if the input ID value is numeric

    {
     return false;
    }
   }
   return true;
  }

ValidateID() 方法验证调用方发送的 ID,并返回一个布尔值,说明其是否有效。

执行示例代码

在运行示例应用程序之前,您需要解压缩后在 AJAX___ASPNET 文件夹中创建 AJAXDropDown 文件夹的虚拟目录。此外,请确保您有一个 SQL Server 2000 实例可用,并且其中有 Northwind 数据库。在 Visual Studio 中打开解决方案,并修改 web.config 文件中的以下元素。

<add key="CONN_STRING" value="data source=<your data source here >;
     Database=northwind;User=<user name>;PWD=<password>"/>

运行应用程序之前请先构建它。

尝试选择一个员工,您将看到“区域”下拉列表填充了相应的区域,如下图所示

摘要

在本文中,我们讨论了 AJAX 以及如何在 Web 应用程序中使用它来改善用户体验。我们还看到,通过将响应作为 XML 而不是整个渲染 HTML 代码发送,我们如何节省了通过网络传输的数据量。有各种各样的应用程序可以通过使用 AJAX 受益。同时,我们应该根据安全、环境、性能等各种因素来决定何时使用此概念。

我要感谢我的同事 Rashmi Ramachandra 为本文中您之前看到的那些图表所做的贡献。

希望这能为您带来有趣的阅读体验。您可以将您的评论发送至 srinivas.alagarsamy@gmail.com。谢谢!

© . All rights reserved.