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

ASP.NET 2.0 中的客户端回调

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (19投票s)

2004年9月8日

6分钟阅读

viewsIcon

284785

在 ASP.NET 2.0 中使用客户端回调

目录

在阅读本文之前,请阅读 Fredrik Normén 关于客户端回调的精彩介绍。这篇简短的教程只是他所解释的技术的一个更贴近实际的示例。

引言

ASP.Net 2.0 的新功能之一是能够从客户端代码调用服务器端代码。虽然这不是什么新鲜事,但现在有一个新的、更简单、更干净的机制可用:客户端回调。总体的想法是有一个类型的钩子,客户端函数(JavaScript)可以使用它来利用强大的服务器端函数。这样做的一个关键好处是缩小了与桌面应用程序之间的差距,使其能够提供更干净、更丰富、响应更快的应用程序。Fredrik Normén 有 一篇很棒的博客文章,解释了它是如何工作的以及如何使用它。这篇简短的教程只是为了展示一个更贴近实际的示例。

兼容性

需要注意的是,客户端回调使用 XmlHttp,据我所知,它只在 IE 中有效。希望解决方案和第三方解决方案不会太远,这将使它成为我们武器库中一个真正强大的武器。

链接的下拉列表

我将要做的例子是一个简单的例子,其中一个下拉列表的值与另一个下拉列表的值相关联。这样的例子包括:根据选择的国家/地区列出州/省,或者列出组织中的部门。我们的例子将使用组织/部门的例子。如果不使用客户端回调之类的方法,我们要么必须转储大量难以维护的 JavaScript 数组并进行大量客户端处理,要么每次下拉列表的值更改时都进行回发。每种解决方案虽然可行,但都有一些严重的缺点。使用 JavaScript 可能会很快变得笨拙,而回发可能会让即使是最熟练的冲浪者也感到困惑几秒钟。

要求

我们的要求很简单
  1. 向用户显示两个下拉列表
  2. 第一个下拉列表包含所有加拿大政府部门
  3. 第二个下拉列表包含所选部门内的所有主要部门
  4. 一个部门可能没有任何部门
  5. 我们不能使用回发,必须只支持 IE,并且必须尽量减少 JavaScript 的使用
没问题!

数据库结构

为了保持我们“简单”的主题,我们的数据库结构非常简单

创建表的 SQL 脚本

      CREATE TABLE dbo.Organization (
          OrganizationId int IDENTITY (1, 1) NOT NULL CONSTRAINT 
             [PK_Organization] PRIMARY KEY CLUSTERED,
          [Name] varchar (255),
          ParentId int NULL CONSTRAINT [FK_Parent_Organization] 
             FOREIGN KEY REFERENCES Organization(OrganizationId)
      ) ON [PRIMARY]
      GO
   
基本上,我们所有的顶层部门的 ParentId 都将是 NULL,而我们的部门的 ParentId 将与其组织 ID 相同。在您的情况下,您可能有多对多关系,或者您的实体可能不在同一个表中,在这种情况下,只需更改我们将要使用的数据访问代码,但客户端回调代码保持不变。

为了使这个示例完整,一些示例数据
insert into Organization
   VALUES ('Agriculture and Agri-Food Canada', NULL)

insert into Organization
   VALUES ('Bank of Canada', NULL)

insert into Organization
   VALUES ('Cadets Canada', NULL)

insert into Organization
   VALUES ('Canada Lands Company Limited', NULL)


insert into Organization
   VALUES ('Canadian Rural Partnership ', 1)

insert into Organization
   VALUES ('Co-operatives Secretariat', 1)

insert into Organization
   VALUES ('Fish and Seafood On-line', 1)

insert into Organization
   VALUES ('Bank Notes', 2)

insert into Organization
   VALUES ('Currency Museum', 2)

insert into Organization
   VALUES ('Old Port of Montréal Corporation Inc.', 3)

insert into Organization
   VALUES ('Parc Downsview Park Inc.', 3)

代码

基础知识

我们的 HTML 将由两个下拉列表 Web 控件组成
   1:  <asp:DropDownList ID="ParentOrganizations" Runat="server"  />
   2:  <asp:DropDownList ID="ChildOrganizations" Runat="Server" />
现在,我们的 Page_Load 将非常基础
   1:   private void Page_Load(object source, EventArgs e) {
   2:     if (!Page.IsPostBack) {
   3:        DataTable dt = GetOrganizations();
   4:        DataView dv = dt.DefaultView;
   5:        dv.RowFilter = "ParentId IS NULL";
   6:        ParentOrganizations.DataSource = dv;
   7:        ParentOrganizations.DataTextField = "Name";
   8:        ParentOrganizations.DataValueField = "OrganizationId";
   9:        ParentOrganizations.DataBind();
  10:        ParentOrganizations.Items.Insert(0, new ListItem("Select", "0"));
  11:     }
  12:  }
我们获取一个数据表(我们将在下一节中查看 GetOrganization() 函数)[第 3 行],获取其默认视图 [第 4 行] 并对其进行过滤,以便我们只获取顶层部门 [第 5 行]。然后我们进行常规的数据绑定操作 [第 6-9 行],并在顶部添加一个选项以提高用户友好性 [第 10 行]

GetOrganizations() 函数是您典型的 DAL 方法
   1:     private DataTable GetOrganizations() {
   2:        DataTable dt = (DataTable)Cache["Organizations"];
   3:        if (dt == null) {
   4:           SqlConnection connection = new SqlConnection(connectionString);
   5:           SqlCommand command = new SqlCommand(
                    "SELECT * FROM Organization ORDER BY Name", connection);
   6:           command.CommandType = CommandType.Text;
   7:   
   8:           SqlDataAdapter da = new SqlDataAdapter(command);
   9:           dt = new DataTable();
  10:           try {
  11:              connection.Open();
  12:              da.Fill(dt);
  13:              Cache.Insert("Organizations", dt, null, 
                        DateTime.Now.AddHours(6), TimeSpan.Zero);
  14:           } finally {
  15:              connection.Dispose();
  16:              command.Dispose();
  17:              da.Dispose();
  18:           }
  19:        }
  20:        return dt;
  21:     }
这里真的没什么特别的。但请注意,我们获取所有部门/子部门,这就是为什么我们在 Page_Load 中过滤 ParentId IS NULL。如果您的实体在两个单独的表中,您可以在此处获取您的顶层实体,并为您的子实体创建一个第二个函数,我们将在稍后使用。

客户端回调

现在我们将进行所有必要的更改,以使此神奇的回调生效。同样,您必须先阅读 Fredrik 的博客,然后才能继续,因为我不会详细介绍。

我们做的第一件事是在我们的 Page_Load 中添加内容,以便挂接和注册我们所有的客户端回调代码

   1:   private void Page_Load(object source, EventArgs e) {
   2:     if (!Page.IsPostBack) {
   3:        DataTable dt = GetOrganizations();
   4:        DataView dv = dt.DefaultView;
   5:        dv.RowFilter = "ParentId IS NULL";
   6:        ParentOrganizations.DataSource = dv;
   7:        ParentOrganizations.DataTextField = "Name";
   8:        ParentOrganizations.DataValueField = "OrganizationId";
   9:        ParentOrganizations.DataBind();
  10:        ParentOrganizations.Items.Insert(0, new ListItem("Select", "0"));
  11:   
  12:        ParentOrganizations.Attributes.Add("onchange", 
           "GetChildren(this.options[this.selectedIndex].value, 'ddl');");
  13:        string callBack = Page.GetCallbackEventReference(this, "arg", 
             "ClientCallback", "context", "ClientCallbackError");
  14:        string clientFunction = "function GetChildren(arg, context){ "
                    + callBack + "; }";
  15:        Page.ClientScript.RegisterClientScriptBlock(this.GetType(), 
                 "GetChildren", clientFunction, true);
  16:     }
  17:  }
[第 12 行],我们向下拉列表添加了一个 onChange JavaScript 事件,以便它调用 GetChildren() Javascript 函数。这是我们服务器端回调函数的代理函数。请注意,第一个 JavaScript 参数将是选定选项的值(换句话说,就是 OrganizationId)。d 然后我们使用 Page 创建实际执行回调的 JavaScript 字符串 [第 13 行]。我们将该字符串包装在我们的代理 GetChildren Javascript 函数中 [第 14 行]。最后,我们将脚本注册到页面 [第 15 行]

接下来,请确保您的页面实现了 ICallbackEventHandler
   1:  public partial class index_aspx : ICallbackEventHandler {
   2:  ...
   3:  }
在我们的代码隐藏中的最后一件事是实现 ICallbackEventHandler.RaiseCallbackEvent,这是处理客户端调用的服务器端函数。
   1:     public string RaiseCallbackEvent(string eventArgument) {
   2:        int parentId;
   3:        if (Int32.TryParse(eventArgument, out parentId)) {
   4:           DataTable dt = GetOrganizations();
   5:           DataView dv = dt.DefaultView;
   6:           dv.RowFilter = "ParentId = " + parentId.ToString();
   7:           StringBuilder sb = new StringBuilder();
   8:           for (int i = 0; i < dv.Count; ++i) {
   9:              sb.Append(dv[i]["OrganizationId"]);
  10:              sb.Append("^");
  11:              sb.Append(dv[i]["Name"]);
  12:              sb.Append("|");
  13:           }
  14:           return sb.ToString();
  15:        }
  16:        return "";
  17:     }
同样,这里真的没什么特别的。我们所做的就是使用作为 eventArgument 传递的 organizationId 来获取具有匹配 ParentId 的所有子部门 [第 6 行]。我们循环遍历匹配的子部门 [第 8 行] 并创建格式为 Id^Name|Id^Name|Id^Name 的字符串 [第 9-12 行]。最后,我们返回我们的字符串 [第 14 行]。如果出现任何错误或未找到记录,将返回一个空字符串。

最后一步是创建我们的 Javascript 函数,它将处理我们服务器端函数的返回。回到我们修改过的 Page_Load,我们说这个函数将被命名为 ClientCallBack
   1:     <script language="Javascript">
   2:     function ClientCallback(result, context){
   3:        var childOrganizations = document.forms[0].elements[
                  '<%=ChildOrganizations.UniqueID%>'];
   4:        if (!childOrganizations){
   5:           return;
   6:        }
   7:        childOrganizations.length = 0;
   8:        if (!result){
   9:           return;
  10:        }
  11:        
  12:        var rows = result.split('|'); 
  13:        for (var i = 0; i < rows.length; ++i){
  14:           var values = rows[i].split('^');
  15:           var option = document.createElement("OPTION");
  16:           option.value = values[0];
  17:           option.innerHTML = values[1];     
  18:           childOrganizations.appendChild(option);
  19:        }
  20:     }
  21:     </script>
我们获取子下拉列表的引用 [第 3 行],清除它可能存在的任何现有值 [第 7 行],并将字符串分割成 id^name 块 [第 12 行]。然后我们循环遍历每个对,并添加一个新选项 [第 15 行],将其与正确的文本和值 [第 16、17 行] 一起添加到我们的子下拉列表中。

观察

我只有几点观察想要传达。这些可能是不正确的,希望有人会纠正我,但是
  1. 请注意,您一次只能将一个字符串参数传递给服务器端函数和从中传出。如果您需要传递更多内容,就像我们出去时一样,您需要以某种方式序列化数据(使用 | 显然是一种非常高效的序列化算法 ;))。
  2. 如果我们在代码隐藏中的 RaiseCallbackEvent 中能够直接将 ChildOrganization 下拉列表绑定到 DataView 而无需任何 JavaScript,那就太好了,但这显然行不通。客户端回调的目的是允许您在不进行回发的情况下执行服务器端处理,而不是重新渲染页面的一部分/全部。
  3. 据我所知,RaiseCallbackEvent 不会在实际请求的同一个实例中发生。如果您在 Page_Load 中设置了一个私有字段,您将无法在 RaiseCallbackEvent 中访问它,除非它是静态的。

完整代码

复制粘贴,更改连接字符串并创建 包含数据的 Organization 表 完整的 HTML 源代码是
<%@ Page Language="C#" CompileWith="index.aspx.cs" ClassName="index_aspx" %>
<html>
<head>
   <title>Client CallBack</title>
</head>
<body>
<form runat="server">
   <asp:DropDownList ID="ParentOrganizations" Runat="server"  />
   <asp:DropDownList ID="ChildOrganizations" Runat="Server" />

   <script language="javascript">
   function ClientCallback(result, context){
      var childOrganizations = document.forms[0].elements[
            '<%=ChildOrganizations.UniqueID%>'];
      if (!childOrganizations){
         return;
      }
      childOrganizations.length = 0;
      if (!result){
         return;
      }
      
      var rows = result.split('|'); 
      for (var i = 0; i < rows.length; ++i){
         var values = rows[i].split('^');
         var option = document.createElement("OPTION");
         option.value = values[0];
         option.innerHTML = values[1];     
         childOrganizations.appendChild(option);
      }
   }
   </script>
   function ClientCallbackError(result, context){
      alert(result);
   }
</form>
</body>

</html>
完整的代码文件是
using System;
using System.Data.SqlClient;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.Text;

public partial class index_aspx : ICallbackEventHandler {
   private const string connectionString = @"YOUR CONNECTION STRINGS";

    private void Page_Load(object source, EventArgs e) {
      if (!Page.IsPostBack) {
         DataTable dt = GetOrganizations();
         DataView dv = dt.DefaultView;
         dv.RowFilter = "ParentId IS NULL";
         ParentOrganizations.DataSource = dv;
         ParentOrganizations.DataTextField = "Name";
         ParentOrganizations.DataValueField = "OrganizationId";
         ParentOrganizations.DataBind();
         ParentOrganizations.Items.Insert(0, new ListItem("Select", "0"));

         ParentOrganizations.Attributes.Add("onchange", 
"GetChildren(this.options[this.selectedIndex].value, 'ddl');");         
         string callBack = Page.GetCallbackEventReference(this, "arg", 
"ClientCallback", "context", "ClientCallbackError");
         string clientFunction = "function GetChildren(arg, context){ " 
+ callBack + "; }";
         Page.ClientScript.RegisterClientScriptBlock(this.GetType(), 
"GetChildren", clientFunction, true);
      }
   }

   public string RaiseCallbackEvent(string eventArgument) {
      int parentId;
      if (Int32.TryParse(eventArgument, out parentId)) {
         DataTable dt = GetOrganizations();
         DataView dv = dt.DefaultView;
         dv.RowFilter = "ParentId = " + parentId.ToString();
         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < dv.Count; ++i) {
            sb.Append(dv[i]["OrganizationId"]);
            sb.Append("^");
            sb.Append(dv[i]["Name"]);
            sb.Append("|");
         }
         return sb.ToString();
      }
      return "";
   }

   private DataTable GetOrganizations() {
      DataTable dt = (DataTable)Cache["Organizations"];
      if (dt == null) {
         SqlConnection connection = new SqlConnection(connectionString);
         SqlCommand command = new SqlCommand(
"SELECT * FROM Organization ORDER BY Name", connection);
         command.CommandType = CommandType.Text;

         SqlDataAdapter da = new SqlDataAdapter(command);
         dt = new DataTable();
         try {
            connection.Open();
            da.Fill(dt);
            Cache.Insert("Organizations", dt, null, 
DateTime.Now.AddHours(6), TimeSpan.Zero);
         } finally {
            connection.Dispose();
            command.Dispose();
            da.Dispose();
         }
      }
      return dt;
   }
}
© . All rights reserved.