一、 增加EnhancedListBox的客户端功能
为了使用客户端代码实现EnhancedListBox中项的重排序,你必须使用JavaScript脚本,并且要把它们依附到EnhancedListBox的两个按钮上。为此,我建议你使用“往后考虑”的方法。就象编写一个老式的ASP以前的Web页面,首先编写一些生成HTML文件的JavaScript。为此,最好的方法是运行该控件,然后观察其源码并把它的HTML代码复制到一个编辑器,再添加JavaScript。列表2(见下载源代码)展示了你需要添加到你的控件中的JavaScript的原始形式。然后,借助于StringBuilder/StringWriter技术(参考源码列表3),该控件构建这部分代码。该JavaScript代码由两部分功能组成:接收一个HTML控件(在本例中是一个<select>控件);使用选择索引并且在列表中上下移动它(基本上与我在本文开始我使用服务器代码向你展示的一样)。现在,你要理解,你把该JavaScript代码添加到Web控件的何处。为了实现在一个Web表单上有多个EnhancedListBox控件的情况下,该JavaScript代码不会被重复复制,你需要使用Page.ClientScript对象的ReGISterClientScriptBlock方法输出它。
要使这个方法起作用,你必须在重载的OnInit事件中调用它(见源码中列表4)。
最后,为使按钮正确工作,你需要把添加的客户端方法依附到其上。在列表1中的代码中,你会看到引用了一个方法RenderButtons。尽管我没有把该代码在此列出(请参考本文相应源码),但是它能够使用我在以前文章中介绍的技术生成按钮。当时,在生成实际HTML标签的之前,标签属性是使用AddAttribute方法以栈式存放的。在此,你使用一样的技术把客户端方法依附到你的按钮。
string s_MoveUp = "MoveItemUp(document.all." +this.ClientID + ");
output.AddAttribute(HtmlTextWriterAttribute.OnClick,s_MoveUp);
记住,MoveItemUp是你已经编写成功的JavaScript函数之一。在生成用于排序的按钮之前,该代码将以堆栈存放这些JavaScript命令。对于向下(down)按钮,你使用一样的技术。注意,我使用ClientId代表该生成后的控件的ID;但是,在这个控件位于一个复合控件内部时,这个属性要考虑使用父控件的名字。
现在,你可以成功地把该控件应用于一个Web表单中。你可以使用与你操作一个标准ListBox控件一样的方式在其上添加一些项。
事实上,这完全是一个投放位置占位符(或ASP.NET ListBox控件)。当你使用重排序按钮时,你将看到列表中的项相应地改变顺序。现在让我们先记下这个问题。如果你把一个按钮拖动到一个Web表单上(不需要为之添加代码)并执行一个回寄,你猜会发生什么呢?完全与我以前描述的一样;任何你使用重排序按钮作的重排序改变都将恢复到在最近一次回寄之前该控件看上去的状态。因此,让我们修改一下这个问题。
首先,我再添加一些JavaScript(源码列表5)。注意,这部分代码被添加到重载的OnInit方法中并且使用StringBuilder/StringWriter技术进行构建;而且,这个JavaScript方法的名字是BuildItemList。这个函数负责构建列表框完整内容的一个字符串描述并且把该串放到要传递到该函数的一个HTML元素的value属性中。你可以把这看作是列表内容的一种串行化。该串行化的输出风格会根据你自己的设计的不同而有所不同。调用这个JavaScript函数需要依附到该按钮上的其它代码。
string s_MoveUp = "MoveItemUp(document.all." + this.ClientID + "); ";
string s_BuildItemList ="BuildItemList(document.all." + this.ClientID +
",document.all.__" + this.ClientID + "); ";
output.AddAttribute(HtmlTextWriterAttribute.Onclick,MoveUp + " " + BuildItemList);
现在,让我们来分析一下你发送到BuildItemList函数的两个参数。第一个参数相应于生成的控件(<select>标签)的ID。第二个参数是另外一个ID,与前一个命名一致,但是前面有一个"__"。这是一个你仍然需要添加到你的Web控件的隐藏的文本框,它将作为一个“串行化”项列表的占位符。我要在OnPreRender事件中注册这个隐藏的文本域。
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if(Page != null)
{
Page.ClientScript.RegisterHiddenField("__" + this.ID, "");
}
}
注意,我已经使用我们的控件的ID来标识隐藏的文本域。
到目前为止,你已经拥有了一个完整功能的Web控件;其中,客户端JavaScript被绑定到其中的两个按钮上。该JavaScript成功地实现在ListBox中的项的重排序并且把其内容串行化为一个字符串;然后,该字符串被存储在一个隐藏的文本域中。所有这些都发生在客户端。如果一个回寄发生,不会发生重排序,因为当重排序时控件的Item服务器属性还没有收到你对它作的任何改变的消息;但是幸运的是,位于隐藏的文本域中的表单的一个串行化快照中发生了这一变化。现在,你有了可以与Item属性一起使用的内容了。那么,接下来,你该如何实现呢?
二、 同步
为了在第一次回寄和所有随后的回寄中实现同步,ASP.net在IPostBackDataHandler接口的实现中提供了一个LoadPostData方法。在每一次回寄时都要调用这个LoadPostData方法;因此,你需要在此做一些工作。
值得一提的是,ASP.NET 2.0修整了一个在1.1版本中被忽视的小地方,然而这一修改能够使你的工作容易许多。ASP.NET ListBox控件已经在两个版本(1.1和2.0)中实现了IPostBackDataHandler接口。但是在2.0版本中,微软使这个接口的方法定义虚拟化(virtual,在VB中称作Overridable)。这意味着,你不必在EnhancedListBox控件中重新实现这个接口;而是,你仅需重载LoadPostData方法。
更重要的是,这也意味着,你可以存取基类实现而不必创建所有已经存在于你的扩展控件中的功能。什么功能呢?这包括微软加于其中的一切:用于处理Item集合,SelectedIndex,SelectedValue和SelectedItem属性,及其它许多执行ListBox控件功能的代码。在ASP.NET 1.1中,你必须在你的派生控件中实现这个接口并且要提供你自己对这两个方法的定义代码,不仅包括你自己的加入的代码而且还要重复微软已经在其控制中所实现的一切。
我猜测,微软有人已经发现了他们的实现中的错误,并且把方法变为virtual的,这样开发者能够存取基类的代码。因此,在源码列表6中向你展示如何实现重载的LoadPostData方法。在这个重载中,你将首先调用基类实现代码;然后,加上你需要的代码以与Item集合同步。
另外,你还可以利用ListBox控件—通过把它编写成一个复合控件。此时,你需要把ListBox中的每一个属性映射到你的EnhancedListBox以便使它成为ListBox控件的一个投放位置点位符。无论使用哪一种方法,或者通过LoadPostData方法的重新创建,你都仍然需要写很多代码。如果我专门为ASP.NET1.1编写这个控件,那么我很可能采取最直接的方案:复合控件方案。
LoadPostData方法使你能够存取寄送到服务器的每一个域,包括你的隐藏文本域(存储在要传递到这个方法的postCollection参数中)。你可以问:为什么需要该隐藏文本域,而不是使用这个参数来存取被回寄的<select>元素呢?现在,我作一下解释。首先,回顾一下典型的ASP时代,当时你使用Request.Form属性来存取页面域。在回寄时,你能够存取一个<select>元素的唯一的部分是选择的项。在该方案中,你需要完整的列表内容(因此,包括隐藏的文本域)。列表6向你展示如何分析该隐藏的文本域的内容并且把Item重新添加到Item集合中。注意,你是怎样调用基类实现的。
最后,在你第一次生成控件时,你必须构建这个隐藏的文本域,以防在任何重排序前发生页面回寄。Render方法的最后一行是:
output.Write("<script language='JavaScript'>BuildItemList(document.all." + this.ClientID +",document.all.__" + this.ClientID +");</script>");
你可以在列表1的最后看到这一点。
现在,你可以使用EnhancedListBox控件来重排序一些项,回寄,并且确保在重新生成页面前,控件的服务器存储与在客户端被改变的客户端存储完全同步。因此,现在让我们使用相同的技术来构建一个复合控件ListMover。