ASP.NET 2.0 和数据绑定控件:新的角度,新的做法(二)

翻译|其它|编辑:郝浩|2006-04-05 13:25:00.000|阅读 1644 次

概述:

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>

HeadlineList 示例控件

正如前面提到的那样,ListControl 是 ASP.NET 1.x 和 2.0 中所有列表控件的基类。非常令人愉快的是,可以用一种非常平滑的方式将在此为 ASP.NET 2.0 编写的 HeadlineList 控件向后移植到 ASP.NET 1.x。出于某种原因,当需要生成标题列表时,人们的大脑中涌现的第一个想法往往是使用 Repeater。的确,Repeater 会使这一工作变得非常简单。

<asp:Repeater runat="server">
   <HeaderTemplate>
      <table>
   </HeaderTemplate>
   <ItemTemplate>
      <tr><td>
      <%# DataBinder.Eval(Container.DataItem, "Title") %>
      <hr>
      <%# DataBinder.Eval(Container.DataItem, "Abstract") %>
      </td></tr>
   </ItemTemplate>
   <FooterTemplate>
      </table>
   </FooterTemplate>
</asp:Repeater>

这段代码有什么问题?或者更准确地说,这段代码中有哪些可以改进的地方?

注:在 ASP.NET 2.0 中,您可以将 DataBinder.Eval(Container.DataItem, field) 替换为一个较短的表达式,该表达式受益于 Page 类上的一个新的公共方法 — Eval。这一新的表达式类似于 Eval(field)。在内部,Eval 调用 DataBinder 类上的静态 Eval 方法,并且确定要使用的正确绑定上下文。

字段的名称在 ASPX 页中硬编码。可以实现可重用性,但只能通过剪切和粘贴实现。您所添加的用于使 Repeater 的行为更加丰富多彩的代码越多,对该解决方案及其跨越页和项目的可重用性的危害就越大。如果标题列表控件恰恰是您需要的东西,则请改而尝试以下方法。

public class HeadlineList : ListControl, IRepeatInfoUser
{
  :
}

ListControl 是列表控件的基类(它位于与 CheckBoxListDropDownList 和类似控件相同的系列中);IRepeatInfoUser 是上述大多数控件加以实现以便用水平或垂直方式在列和行中呈现的几乎不为人所知的界面。请注意,ListControlIRepeatInfoUser 还存在于 ASP.NET 1.x 中,并且以几乎与 2.0 相同的方式工作。

列表控件是围绕一个要重复的控件生成的;该控件(或控件图)是一个类属性,并且在加载时实例化以节省一些 CPU 时间。以下为私有 ControlToRepeat 属性的实现。

private Label _controlToRepeat;
private Label ControlToRepeat
{
   get
   {
      if (_controlToRepeat == null)
      {
         _controlToRepeat = new Label();
         _controlToRepeat.EnableViewState = false;
         Controls.Add(_controlToRepeat);
      }
      return _controlToRepeat;
   }
} 

在该示例中,要重复的控件(标题)是一个在首次读取时实例化的 LabelHeadlineList 控件还应当向用户提供通过多种属性(如 RepeatLayoutRepeatColumnsRepeatDirection)影响外观的方式。很多标准列表控件上都定义了这些属性,因此开发人员不应该对它们感到陌生。它们的实现是类似的,并且看起来像下面的代码。

public virtual RepeatDirection RepeatDirection
{
   get
   {
      object o = ViewState["RepeatDirection"];
      if (o != null)
         return (RepeatDirection) o;
      return RepeatDirection.Vertical;
   }
   set
   {
      ViewState["RepeatDirection"] = value;
   }
}

为完成 HeadlineList 控件而需要编写的另一段代码以呈现为中心。IRepeatInfoUser 接口对您可以用来控制呈现过程的各种属性进行计数。这方面的属性示例有 HasHeaderHasFooterHasSeparator 布尔型属性。您可以像实现其他任何普通属性一样实现这些属性,并且根据需要在 RenderItem 接口方法中使用它们。

public void RenderItem(ListItemType itemType, int repeatIndex, 
RepeatInfo repeatInfo, HtmlTextWriter writer)
{
   string format = "<b>{0}</b><hr style='solid 1px black'>{1}";
   Label lbl = ControlToRepeat;
   int i = repeatIndex;
   lbl.ID = i.ToString();
   string text = String.Format(format, Items[i].Text, Items[i].Value);
   lbl.Text = text;
   lbl.RenderControl(writer);
}

RenderItem 对向页提供的输出承担最终的责任。它获得要重复的控件,并且将其呈现到标记中。RenderItem 是从 Render 中调用的。

protected override void Render(HtmlTextWriter writer)
{
   if (Items.Count >0)
   {
      RepeatInfo ri = new RepeatInfo();
      Style controlStyle = (base.ControlStyleCreated 
                                  ? base.ControlStyle : null);
      ri.RepeatColumns = RepeatColumns;
      ri.RepeatDirection = RepeatDirection;
      ri.RepeatLayout = RepeatLayout;
      ri.RenderRepeater(writer, this, controlStyle, this);
   }
}

RepeatInfo 是一个 Helper 对象,它经过专门设计,以便通过重复现有的控件图来生成新控件。以上就是所需的全部代码。让我们准备一个示例页,并测试该控件。

<expo:headlinelist id="HeadlineList1" runat="server" 
       repeatlayout="Table" repeatdirection="Vertical" repeatcolumns="2" 
       datatextfield="LastName" datavaluefield="Notes" />

图 2 显示了该控件的工作方式。



图 2. HeadlineList 数据绑定控件

该控件在设计时工作正常,并且不需要插入其他任何代码。然而,这段代码的最令人愉快的边界效应并非免费的设计时支持。对我来说,它简直太美妙了,因为它能够使用 ADO.NET 数据源对象(例如,DataTableDataSet)和数据源组件(如 SqlDataSource)。您可以取走这段代码,将其编译为 ASP.NET 1.x 项目,而它就可以使用基于 IEnumerable 的数据源。如果将这段代码引入到 ASP.NET 2.0 项目中,则它无须更改就同样可以使用数据源对象。

这一事实的意义是什么?

在 ASP.NET 1.x 中,ListControl 类是一个令人愉快的例外 — 但仍然是一个例外。在 ASP.NET 2.0 中,您可以使用类似的简单但有效的方法来生成任何数据绑定控件。在这样做的时候,您可以利用合并了大部分复杂性并且将大多数已知的最佳做法硬编码的新基类。

管理自定义集合

ListControl 是一个过于专用的类,它以不受您控制的固定方式执行数据绑定 — 除非您重写诸如 PerformSelectOnDataBindingPerformDataBinding 之类的方法。它还提供了预定义的 Items 集合属性。让我们在 ASP.NET 2.0 中的更低级别处理数据绑定,并且设计具有下列功能的 ButtonList 控件:

   •使用自定义集合类来保留组成项
   •用自定义方式管理视图状态

ButtonList 控件是另一个为每个绑定数据项输出按钮的列表控件。您可以让它从 ListControl 继承;而且,您可以获得 HeadlineList 的源代码,将 Label 替换为 Button,而它仍然应当正常工作。这一次,我将采用一种不同的方法来说明 DataBoundControl 的行为。为简单起见,我仍将跳过 IRepeatInfoUser 接口。

public class ButtonList : System.Web.UI.WebControls.DataBoundControl
{
   :
}

标题和命令名称表现了每个按钮的性质。该信息是通过几个自定义属性(如 DataTextFieldDataCommandField)从绑定数据源中获得的。您可以容易地添加类似的属性,以提供数据绑定工具提示,甚至提供 URL。

public virtual string DataCommandField
{
   get
   {
      object o = ViewState["DataCommandField"];
      if (o == null)
         return "";
      return (string)o;
   }
   set { ViewState["DataCommandField"] = value; }
}

所发现的有关每个绑定按钮的所有信息都被填充到一个通过 Items 属性公开的自定义对象集合中。(请注意,Items 只是该属性的标准、惯用而任意的名称。)

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public virtual ButtonItemCollection Items
{
   get
   {
      if (_items == null)
      {
         _items = new ButtonItemCollection();
         if (base.IsTrackingViewState)
            _items.TrackViewState();
      }
      return _items;
   }
}

Items 集合是自定义 ButtonItemCollection 类的实例 — ButtonItem 对象的集合。ButtonItem 类只是存储了有关绑定按钮的关键信息 — TextCommandName 属性,外加几个构造函数以及 ToString 方法。ButtonItem 类是作为普通列表控件的 ListItem 类的对等物。下面是一个示例。

public class ButtonItem
{
   private string _text;
   private string _command;

   public ButtonItem(string text, string command) {
      _text = text;
      _command = command;
   }
   public string Text {
      get {return _text;}
      set {_text = value;}
   }
   public string CommandName {
      get { return _command; }
      set { _command = value; }
   }
   public override string ToString() {
      return "Button [" + Text + "]";
   }
}

现在,如何创建 ButtonItem 对象的集合呢?在 ASP.NET 1.x 中,您必须生成一个从 CollectionBase 继承的自定义集合类,并且起码重写几个方法。然而,自定义集合只是围绕 ArrayList 对象的包装而已,在访问速度方面并没有任何真正的优势。实际上,仍然需要进行转换。.NET 2.0 中的泛型提供了真正的转折点。要生成 ButtonItem 对象集合,您需要以下代码:

public class ButtonItemCollection : Collection <ButtonItem>
{
}

并且,它的性能也会更好,因为编译器在幕后完成了某些工作。ButtonList 控件只需要两个被重写的方法:RenderPerformDataBindingRender 假定 Items 集合被填充;因此,它只是进行迭代并输出标记代码。

protected override void Render(HtmlTextWriter writer)
{
   for(int i=0; i< } btn.RenderControl(writer); btn.CommandName="item.CommandName;" btn.Text="item.Text;" Button(); btn="new" Button item="Items[i];" ButtonItem { i++)>

Items 集合为什么如此重要?它可以帮助您获得两个结果。首先,您可以用手动添加的项填充该列表控件。其次,一旦在视图状态中持久保存该集合,您就可以在回发时重新生成该控件的用户界面,而无须绑定到数据。在进行数据绑定时,Items 集合是在何处以及由谁填充的呢?这需要用到 PerformDataBinding。该方法获得一个可枚举的数据列表(无论原始数据源是什么)并使用它来填充 Items 集合。

protected override void PerformDataBinding(IEnumerable dataSource)
{
   base.PerformDataBinding(dataSource);
   string textField = DataTextField;
   string commandField = DataCommandField;

   if (dataSource != null) {
   foreach (object o in dataSource)
   {
      ButtonItem item = new ButtonItem();
      item.Text = DataBinder.GetPropertyValue(o, textField, null);
      item.CommandName = DataBinder.GetPropertyValue(o, 
                                             DataCommandField, null);
      Items.Add(item);
   } 
   }
}

每当需要进行数据绑定时,该方法都能够确保 Items 集合被填充。在回发时会发生什么?在这种情况下,必须根据视图状态重新构建 Items 集合。您可以通过 IStateManager 接口上的方法赋予自定义集合类这一能力。以下为该接口的关键方法:

public void LoadViewState(object state)
{
   if (state != null) {
      Pair p = (Pair) state;
      Clear();
      string[] rgText = (string[])p.First;
      string[] rgCommand = (string[])p.Second;

      for (int i = 0; i < rgText.Length; i++)
         Add(new ButtonItem(rgText[i], rgCommand[i]));
   }
}

public object SaveViewState()
{
   int numOfItems = Count;
   object[] rgText = new string[numOfItems];
   object[] rgCommand = new string[numOfItems];

   for (int i = 0; i < numOfItems; i++) {
      rgText[i] = this[i].Text;
      rgCommand[i] = this[i].CommandName;
   }

   return new Pair(rgText, rgCommand);
}

该类使用一个 Pair 对象(一种经过优化的 2 位置数组)将自身序列化为视图状态。您需要创建两个对象数组,以便保留每个按钮的文本和命令名称。这两个数组随后被成对打包并插入到该视图状态中。当还原该视图状态时,会将该数组对拆包,并且使用先前存储的信息重新填充 Items 集合。使用该方法要比使 ButtonItem 类可序列化更可取,因为传统的二进制格式化程序的性能(在空间和时间这两个方面)更差。

然而,向集合中添加视图状态支持还不够。还必须增强 ButtonList 控件以利用集合的序列化功能。您可以重写控件类上的 LoadViewStateSaveViewState

protected override void LoadViewState(object savedState)
{
   if (savedState != null) {
      Pair p = (Pair) savedState;
      base.LoadViewState(p.First);
      Items.LoadViewState(p.Second);
   }
   else
      base.LoadViewState(null);
}

protected override object SaveViewState()
{
   object baseState = base.SaveViewState();
   object itemState = Items.SaveViewState();
   if ((baseState == null) && (itemState == null))
      return null;
   return new Pair(baseState, itemState);
}

控件的视图状态由两个元素组成:默认控件的视图状态以及 Items 集合。这两个对象被打包到 Pair 对象中。除了 Pair 对象以外,您还可以使用 Triplet 对象(包含三个对象的数组),或者使用 PairTriplet 对组成任意数量的对象。

以这种方式设计的自定义集合还可以在设计时满足需要。Visual Studio 2005 中嵌入的默认集合编辑器可以识别该集合并弹出如图 3 所示的对话框。



图 3. 设计时的 ButtonList Items 集合

值得说明的是,在 ASP.NET 2.0 中,某些数据绑定控件使您可以将数据绑定项与以编程方式通过 Items 集合添加的项分开。布尔型的 AppendDataBoundItems 属性用于控制该控件的编程接口的这一方面。该属性在 ListControl(而非 DataBoundControl)上定义,并且默认为 false。

关于复合控件的一点讨论

CompositeDataBoundControl 类是生成复合控件(我相信当您想到数据绑定控件时,您所指的就是这些控件)的起点。复合控件必须能够:

   •充当命名容器。
   •通过 CreateChildControls 方法创建它自己的用户界面。
   •实现特定的逻辑,以便在回发之后还原它的子元素层次结构。

最后一点在 Nikhil Kothari 的著作中进行了良好的阐述,并且在 ASP.NET 1.x 的所有内置控件中都得到了实现。如果您迄今为止尚未完全了解该概念,则好消息是您现在可以彻底忘掉有关它的所有内容。一切内容现在都在 CompositeDataBoundControl 类中硬编码。您需要关心的主要方面是设计您的控件的子控件。您可以通过重写按以下方式定义的新方法来完成该工作:

protected abstract int CreateChildControls(
      IEnumerable dataSource, bool dataBinding);

CompositeDataBoundControlDataBoundControl 继承,因此,本文中陈述的有关集合、绑定和视图状态的大部分内容也适用于复合控件。

小结

数据绑定和数据绑定控件代表着 ASP.NET 1.x 中的一项巨大突破,但是有一些要点没有得到解释,并且有几个问题有待回答。Nikhil Kothari 的著作为所有开发人员提供了完美的权威的指南。ASP.NET 2.0 将该书的一些最佳做法(大部分已经在 ASP.NET 1.x 的幕后得以实现)转换为可重用的类以及新的数据绑定控件对象模型。

本文重点讨论了在从 ASP.NET 1.x 升级到 ASP.NET 2.0 时进行的主要更改,并且通过几个实际示例概述了它们影响开发的方式。接下来,我们将需要关注 ASP.NET 2.0 控件开发中的样式和主题。但是,那也许会成为在不久的将来问世的另一篇文章的中心议题。请继续关注我们的工作。


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com


为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP