摘要:在本系列文章中,我们首先讨论使用ASP.NET 2.0构建一个标准ListBox控件的增强版本(EnhancedListBox)。这个控件能够对它的项进行重排序,并且能够实现客户端与服务器端的同步功能。然后,我们把这样的两个控件组合起来创建一个复合控件(ListMover)。

   一、 引言

  构建提供丰富的客户端接口的复杂Web控件经常需要把一些客户端JavaScript代码与控件的服务器端代码集成到一起。然而,在一些情况下,为了达到某种巧妙的效果而把问题搞得过于复杂经常会破坏控件的内部服务器代码与生成的客户端HTML代码之间的数据同步,而当进行页面回寄时这将成为一个问题。在本文中,我将首先构建两个“很酷”的Web控件(都极容易导致这一问题),然后向你展示如何来修改这一“脆弱性”。

  本文中,我们将使用C#+ASP.NET 2.0来定制这些控件,并在后面向你简短介绍如何使之工作在ASP.NET 1.1(或1.0)环境中。

  当前,HTML仍然保持为Web应用程序生成阶段的主要语言。遗憾的是,它所使用的协议是无状态的,所以必须由Web开发者自己来处理这种无状态特点。通过使用一些架构特征,例如回寄机制和ViewState变量,ASP.NET有助于处理这个问题。然而,为了实现某些功能,还需要再作努力,从而借助于回寄事件把Web页面不断向服务器发出请求的各种技术结合起来。

  具体地说,我将分析如何使用JavaScript和DHTML存取在客户端生成的元素。其实,把客户端和服务器功能融合到一起要求使用大量的技巧才能达到最佳用户体验效果,而微软在其ASP.NET校验控件中就实现了这一点。为了提供一种丰富的客户端校验效果,该控件中使用了大量的JavaScript。

  作者注:本文假定你对定制Web控件开发有一个基本了解。因此,我将不再重复Web控件开发的基础内容,例如属性工作原理与风格的添加方式。

   二、 一种常规实现方法

  下面,我想向你展示如何构建一组很酷的控件,它们具有你在商业控件中才能看到的优秀功能。稍后,我将继续展示定制Web控件带给Web编程的完全封装优点。既然你已经了解如何开发定制Web控件,那么你应该知道的一个概念是封装一个控件所有的功能和行为(就象你在一个标准业务对象中所实现的那样)。在学习构建具有复杂行为的控件时,这种封装将极有用处。

  在第一个控件中,我将向你展示如何构建一个称为EnhancedListBox的控件。这个控件将扩展ASP.NET的ListBox控件—添加一个头部和一些重排序按钮。注意,这是一个直接继承自常规ListBox的控件。
之后,我还将向你展示如何构建一个复合控件—ListMover,它将包含两个上面提到的EnhancedListBox控件。这个ListMover控件还包含一些允许你从一个列表到另一个列表中移动项的按钮。

  其实,用常规方法(非面向Web控件的)来实现这种ASP.NET功能也并不困难。首先,你要把一个常规ListBox控件拖动到你的Web表单上并且使用一些数据填充它。

  然后,再添加一个标签用作标题,还有一组按钮用作重排序按钮。捕获这些按钮的服务器端事件是ASP.NET中的标准操作;因此,你只需要使用一种方法来取得当前选定的项并且根据用户点击的按钮从而把它放到该列表中的更高或更低的位置即可。例如,你可能编写如下的代码实现移动列表中的一项。

i_Index = ListBox1.SelectedIndex;
o_Item = ListBox1.SelectedItem;
ListBox1.Items.RemoveAt(this.SelectedIndex);
i_Index--;
if(i_Index < 0) i_Index = 0;
ListBox1.Items.Insert(i_Index, o_Item);

  下面,让我进行简单的分析。首先,我保存了当前的列表中选定项的索引值与当前项。然后,我在当前位置删除该项;之后,在一个较低位置(上一个索引值减1)重新插入该项。这里的逻辑非常简单,那么为什么我还要说明这个问题呢?

  借助于这种常规的ASP.NET编程方法,Web表单上面的重排序按钮将会引发一个实现ListBox中重排序的服务器端事件。这是由一个到服务器的回寄触发的;因此,这个回寄可能是一次“繁重的”往返,具体要信赖于表单上的具体内容及因特网速度。

  然而,因为这一代码实现的是一个标准ASP.NET回寄过程,所以由ASP.NET使用它的ViewState机制来负责状态处理。当再次生成页面时,列表框内容按要求的顺序正确生成。

  当然,你也可以使用与此相同的常规方式在ListMover控件中重新创建这个功能。篇幅所限,我在此省略,只好留待读者您来实现。这个Web表单上包含一对ListBox,还有一些指示从左向右或从右向左移动的按钮。这些按钮的服务器端事件将从一个ListBox中提取选择的项,然后把它添加到另一个列表中;反之亦然。如在刚才的例子中所展示的,ViewState在此能够完好工作以保持这两个ListBox中的项。
三、 目的

  下面,我想向你展示如何把刚才描述的这些例子中所用的单个控件放到一个Web表单上。你可能猜出,我将向你展示如何把这两个例子中的功能封装到它们自己的一个Web控件中。借助于与在常规方法示例中描述的相同的服务器端事件模型,我们可以把所有的行为封装到每一个控件中来实现必要的功能。既然每一个控件都能够控制它自己的状态,那么包含它们的Web表单不必要做任何额外的工作。

  到目前为止,一切顺利。你可能问:“问题在哪里?”很好,假定页面开发者在含有大量内容的页面上使用这两个控件,而且每当发生一次重排序或移动,都需要到服务器端的重回寄时,这显然不是一个高效的Web站点要实现的。这正是使用一些JavaScript的原因。

  在本例中,你要使用JavaScript代码来存取EnhancedListBox控件中ListBox的内容以便在客户端进行重排序。

  在ListMover控件中,JavaScript代码将把项从一个列表移动到另一个列表。其最终结果是一样的,但是不需要进行服务器来回传送,因为不需要触发任何回寄。这样以来,你就可以解决即时响应和不需要回馈的问题。

   四、 问题

  ASP.NET在服务器端生成内容与在客户端生成内容之间有明显的界定。事实上,大部分情况下,这两部分没有关系;因此,问题出现了。其实,一个Web控件只是一个服务器端组件,它负责把HTML生成到浏览器端。的确,标准ASP.NET ListBox控件正是以HTML形式生成一个ListBox(作为一个<select>标签)。

  在<select>标签中的<option>子标签可以使用ListBox控件中的Item属性的内容来创建。Item属性在服务器端被填充,而其内容有助于在生成期间构建适当的HTML。这非常类似于生成一个<input>标签的文本框Web控件,而它的Text属性映射到<input>标签的Value属性。每当触发一个到服务器的页面回寄时,ListBox控件的Item属性都被保存到ViewState中,并且在重新生成页面前从ViewState中进行重建。

  在EnhancedListBox中进行重排序或在服务器端的ListMover中移动项都非常直接,并且允许支持正常的内置的ViewState机制而不需要我们作任何干扰。但是,当你使用客户端JavaScript添加这一能力来实现它们的功能时,它将破坏ViewState。这些控件并不再转回到服务器端,所以Item集合属性永远不会被保存以便在重新生成时被重载。代之的是,直接在HTML级别上存取生成的<select>标签中的<option>项。你可以借助JavaScript代码移动或重排序控件项;但是,当在页面上再次发生回寄时,你猜发生了什么?在移动(或重排序)开始前,控件的列表项就恢复它们的状态。

  我说过,如果功能发生在回寄期间的服务器端,那么,ViewState被保存并且被良好重载,从而使Item集合正确填充。但是,既然你的最终目标是在客户端实现这个功能,那么你就不再需要重新调整Item属性的内容,而是由你依赖的这个属性负责状态存储。现在,你可能会为难了。但是别担心—我有一个解决方案。现在,让我们开始使用必要的客户端脚本代码来开发该控件来实现每一个子控件所需要的功能。然后,我将向你展示如何使它与服务器代码保持重新同步。

   五、 EnhancedListBox控件

  在这个控件中,你要把两部分内容添加到现有ASP.NET ListBox控件。首先,添加一个头部—把一个标签放到一个ListBox的上方。然后,把两个按钮添加到ListBox—分别用于向下和向上重排序。

  注意 为了简单起见,我在后面所有的代码描述中省略所有的属性部分。

  现在,创建一个继承自ListBox控件的新类,如下所示:

using System.Web.UI;
using System.Web.UI.WebControls;
public class EnhancedListBox : ListBox
{}

  如果你编译这部分代码并且把该控件添加到你的工具箱中,那么你将有一个完整功能的ASP.NET ListBox控件副本。我把这个控件作为一个继承控件开发,是因为我想使它具有一个ASP.NET ListBox控件的“占位符”的作用。以后,我再添加其它的属性以实现头部的可见性并支持重排序按钮的打开或关闭。当这些属性全部关闭时,这些控件将在外观与行为上与一个常规ListBox控件一样。然而,你不能使用一个重载的CreateChildControls把控件添加到其上,因为这个函数是用来构建一个控件层次树的。这个ASP.NET ListBox控件被编写为一个生成控件而且直接把它的所有HTML内容绘制到生成引擎;这样以来,你需要在此处“注入”你的内容。你将使用生成控件方法来构建一个标签和两个按钮,并且通过重载Render方法来生成它们。然而,一旦你重载这个方法,你就完全取消了所有的在原始ListBox中的生成内容,而这是不可取的。因此,我想借助于一些小技巧来实现。
六、 代码注入

  我的方案是,以一个标准生成控件方式来绘制这个控件,其中包括table标签以及该标签与我添加的按钮的生成方式等。当我编写生成显示部分时,也就是在我想注入继承的原始的ListBox的地方,我调用了base.Render方法。这将把微软为ListBox控件编写的所有代码注入到我试图绘制的HTML部分(见源码中的列表1)。下列表格1中列出的属性决定了这部分代码的外观与行为。在本文中,我没有列出相应的属性代码,但是你可以在下载源代码中找到。注意,在代码中,你要生成的按钮将导致一个回寄(基于属性ReorderButtonPostback的值);而对接口IPostBackEventHandler的实现将捕获这个回寄。

  表格1:EnhancedListBox属性

属性名 类别
UpButtonCaption Appearance
DownButtonCaption Appearance
ShowReorderButtons Appearance
ShowHeading Appearance
HeadingCaption Appearance
ReorderButtonPostBack Behavior
SideButtonWidth Layout
SideButtonHeight Layout
SideButtonVerticalAlign Layout
ReorderButtonSide Layout
UpButtonStyle Styles
DownButtonStyle Styles
ListBoxStyle Styles
HeadingStyles Styles

  现在,既然该控件已经看上去如你希望的样式(见图1),那么你可以让该按钮多负责一些工作而不是仅引发一个回寄。最终的产品中包含事件处理代码;这部分代码位于接口IPostBackEventHandler的实现中,这样以来事件能够被有选择地向服务器激发,而另一方面开发者也可以在此处加入更多的代码。但是记住,你要使用这些按钮来重排序ListBox中的项,并且希望在不执行回寄的情况下实现这一功能。现在,我们开始分析最有趣的部分。


图1.EnhancedListBox控件让用户重排序一个列表中的项。