C#+ASP.NET 2.0定制复合组件之高级篇

80酷酷网    80kuku.com

  asp.net|高级  一、 增加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。

  三、 构建复合控件—ListMover

  这个ListMover控件包含两个EnhancedListBox控件,还有一些按钮用于在两个列表之间来回移动项。借助于这些复合控件构建技术,你可以学习如何创建子控件并且使用一些HTML生成它们,最终的控件看上去如图2所示。对于这个控件,你要注意的是某些事情必须发生的位置。

定制复合组件之高级篇
图2.这个ListMover控件提供了一种标准方式让用户在两个列表间移动项。


  首先,借助于与在以前的控件中相同的技术,你必须把在这个控件中需要的JavaScript代码添加到OnInit事件的重载版本中。列表7显示了你需要的JavaScript代码。如你在上一个控件中所做的一样,你也是使用JavaScript存取一个ListBox(<select>元素)中的元素。而且,我已经编制了函数分别实现把项添加到一个列表,从一个列表中删除项,以及从一个列表中添加或删除所有项。

  我已经进行了功能的分离,而不是创建单个“move”方法;这样以来,我可以实现基于属性设置而使得从一个列表中删除项成为可选的。毫无疑问,这可以使最终的控件更为强壮些,但是我在本文中不再分析这些代码。还应该注意,就象在前面控件中一样,我也添加了一个BuildItemList方法。

  现在,你需要把这一客户端代码依附到复合控件的按钮中。你可以在CreateChildControls方法的最后完成这一点;并且,在此时,完成子控件的初始化和构建控件集合。在此,我仅向你展示相应于一个按钮的代码(另外的按钮代码与此类似,省略)。

string s_AddToLeft = "AddSelectedItemToList(document.all." +
this.lstItemsOnRight.ClientID + ", document.all."
+ this.lstItemsOnLeft.ClientID + ", " +
(this.AllowDuplicatesOnLeft ? "true" : "false") + "); ";
string s_RemoveFromRight = "RemoveSelectedItemFromList(document.all." +
this.lstItemsOnRight.ClientID + "); ";
string s_BuildItemList = "BuildItemList(document.all." +
this.lstItemsOnRight.ClientID + ", document.all.__" + lstItemsOnRight.ClientID + "); " + "BuildItemList(document.all." + this.lstItemsOnLeft.ClientID + ", document.all.__"
+ lstItemsOnLeft.ClientID + "); ";
this.btnAdd.Attributes.Add("onclick", s_AddToLeft
+ " " + s_RemoveFromRight + " " + s_BuildItemList
+ " return false");
  注意,我实现了在以前的控件中同样的工作。我把JavaScript函数调用构建成一字符串并且把它们依附到一个按钮上。主要区别在于,既然这是一个包含其它控件的复合控件,那么你可能使用把代码添加到onclick事件的Attributes.Add方法,这与在一个生成控件中把它放到一个栈上的方法形成对照。还要注意,我把多个功能放到onclick属性中;而且,函数调用的最后返回false以便取消按钮将执行的任何回寄。

  最后,代码将在一个对Render重载的方法中初始化对客户端函数BuildItemList的调用。这看起来很象我在EnhancedListBox控件中向你介绍的那个,在此不再重复。注意,在这个控件中,我注册了两个隐藏的文本域,每一个相应于一个ListBox。

protected override void OnPreRender(EventArgs e)
{
 base.OnPreRender(e);
 if(Page != null)
 {
  Page.ClientScript.RegisterHiddenField("__" + this.lstItemsOnRight.ClientID,
"");
  Page.ClientScript.RegisterHiddenField("__" + this.lstItemsOnLeft.ClientID,
"");
  Page.RegisterRequiresPostBack(this);
 }
}
  现在,你已经构建成功该复合控件,能够提供一些客户端JavaScript,并且把它绑定到按钮上。与以前一样,你可以把它放到一个表单上并且使用它;但是,在你添加同步代码之前,它仍将会遇到你在第一个控件中所遇到的问题—你可以前后移动项,但是一旦你初始化一个回寄(通过表单上的任何其它控件),该控件就会恢复到它回寄之前的状态。

  为了修改这个问题,你要实现你在第一个控件中所做的同样的工作。然而,既然你在开发一个复合控件,而不是扩展一个已经现有的控件,那么你需要实现IPostBackDataHandler接口并且提供LoadPostData和RaisePostDataChangedEvent方法的实现代码。这些实现(见列表7)与前面的控件基本一致,除了你要实现两个EnhancedListBox控件中的项集合的同步而不是只考虑一个控件外。并且与以前一样,你需要确保你保存你的SelectedIndex位置;这样以来,在你完成项集合的同步后你就可以把它们设置回去。还要注意,在第一个控件中,你重载了基控件的LoadPostData方法,因此在某处调用了它的基类。现在,既然你要从头编写一个复合控件,那么就没有基类可调用,而仅需提供你自己的方法实现。

  这个控件的最后版本包含若干新的属性:包括用来决定是否添加到一个列表中的项能够被从另一个列表中删除的属性(如果一个列表将允许出现重复项的话);它还包含可扩展的风格化以实现最大化重用的目的,等等。

  就这些。你已经使用了可用于客户端脚本中的隐藏的文本域来存储列表框的状态。在回寄期间,你使用隐藏文本域的内容来与服务器端项集合重新同步。最终结果是一个漂亮的复合控件—允许你在没有服务器回寄的情况下实现各列表项间的来回移动,而当一个回寄真正发生时仍能够保持这种变化。

  四、 取二者最优

  前面我没有提及的一个细节是为什么我在本文中混合了两个控件。文章一开始,我首先向你展示了一个标准ListBox控件的增强版本,然后把这个增强控件的两个实例应用于ListMover控件,而没有使用两个标准ListBox控件来构建这个ListMover。在本文中,我没有涉及的是ListMover控件的属性部分,它们将负责映射添加到EnhancedListBox控件上的属性。通过这种方式,我就能够从包含两个EnhancedListBox控件的ListMover控件中控制两个EnhancedListBox控件的增强功能。因此,你可以看到,你拥有结合了两个控件的最好的功能—你有了一个ListMover控件,它允许你在两个列表或单个列表的各项之间进行项的移动与重排序。

  其实,这里真正关键的地方在于面向Web控件的ASP.NET开发—完全封装。本文中的EnhancedListBox控件包含实现其目标(对它的项进行重排序)的所有代码。当我把两个这种控件包括在一个ListMover控件中时,我可以使用所有伴随着它们的智能性作为新控件的额外功能,包括每一个控件含有的客户端脚本以及在EnhancedListBox控件中的客户端到服务器的同步功能。因此,这个ListMover控件只需注意其自己的功能。图3展示了和EnhancedListBox控件在一起的ListMover控件,其中重排序按钮处于开状态。

定制复合组件之高级篇
图3.通过组合ListMover和EnchancedListBox控件,你提供给用户的是具有两个列表的一个完整的控件,这样你可以实现在一个列表中或两个列表的各项间进行移动。


  最后,我强烈鼓励你下载本文完整的源码进行研究。既然我编写这些控件,那么我已经多次使用过ListMover,而且EnhancedListBox控件在我的所有工程开发中几乎可以替换标准的ASP.NET ListBox控件。

  五、 小结

  我在本文中向你展示了如何在一个Web控件内保持在服务器和客户端之间的同步,而且这里所使用的技术可以适用于许多类似的情况。尽管ASP.NET 2.0中引入了一个直观的接口用以处理脚本回调,但是,使功能完全位于客户端则是一种最快的解决方案而且能够与ASP.NET 1.1保持兼容。说实在的,我更对ASP.NET 2.0中内置的回调能力感兴趣,请你也一起研究吧。

分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: