ASP.NET自定义控件实例教程


ASP.NET自定义控件实例教程 ASP.NET 自定义控件实例教程 2009 年 4 月 ASP.NET自定义控件实例教程 前 言 ASP.NET 为开发人员定义了许多常用的服务器端控件,这些控件拥有完整的生命周期, 可以帮助开发人员完成大部分日常工作,并且,ASP.NET 允许开发人员定义自己的服务器 端控件——用户控件和自定义控件。相比较而言,自定义控件编写起来所做的工作更多一些, 但也更灵活,开发人员可以参与到控件完整的生命周期中,灵活的控制控件的生成,本文档 旨在通过一系列实例使您理解 ASP.NET 自定义控件的含义和开发过程。 事实本文档和我一开始规划的内容相比较,少了自定义控件设计时支持这一部分,这是 我个人也感觉比较遗憾的地方——确实时间不太充裕,那么这份 PDF 文档算是 ASP.NET 自 定义控件实例教程的一个终结。 本教程中的代码可以在 http://www.dev6.cn/archive/2009/01/14/19.aspx 下载 在此要感谢我的老师,是她带领我进入软件开发的行业,并且我从她身上学到了很多软 件开发以外的东西,她对我产生了很大的影响,还要感谢在编写过程中博客园( www.cnblo gs.com)和许多热心网友的大力支持,同时还要感谢我的妻子和我的父母,有了他们的支持 我才得以安心工作。 再次感谢大家。 作者介绍 王峰,专注与 .NET 技术的开发,目前从事软件开发和培训工作。我的 MSN 是 wangfengv@hotmail.com,您可以通过该地址与我联系。 ASP.NET自定义控件实例教程 目 录 ASP.NET 自定义控件介绍..............................................................................................................1 第一天 简单的星级控件 .................................................................................................................2 1. 引言 .............................................................................................................................................2 2. 分析 .............................................................................................................................................2 3. 实现 .............................................................................................................................................2 4. 总结 .............................................................................................................................................6 第二天 带有自定义样式的星级控件 .............................................................................................7 1. 引言 .............................................................................................................................................7 2. 分析 .............................................................................................................................................7 3. 实现 .............................................................................................................................................8 4. 总结 ...........................................................................................................................................12 第三天 使用控件状态的星级控件 ...............................................................................................13 1. 引言 ...........................................................................................................................................13 2. 分析 ...........................................................................................................................................13 3. 实现 ...........................................................................................................................................14 4. 总结 ...........................................................................................................................................18 第四天 折叠面板自定义控件 .......................................................................................................19 1. 引言 ...........................................................................................................................................19 2. 分析 ...........................................................................................................................................19 3. 实现 ...........................................................................................................................................20 4. 总结 ...........................................................................................................................................28 第五天 真正可以评分的星级控件 ...............................................................................................29 1. 引言 ...........................................................................................................................................29 2. 分析 ...........................................................................................................................................29 3. 实现 ...........................................................................................................................................30 4.总结 .............................................................................................................................................36 第六天 可以绑定数据源的星级控件 ...........................................................................................37 1. 引言 ...........................................................................................................................................37 2. 分析 ...........................................................................................................................................38 3. 实现 ...........................................................................................................................................40 4. 总结 ...........................................................................................................................................45 第七天 开发具有丰富特性的列表控件 .......................................................................................47 1. 引言 ...........................................................................................................................................47 2. 分析 ...........................................................................................................................................47 3. 实现简单列表控件 ....................................................................................................................49 4. 实现丰富特性列表控件 ............................................................................................................50 5. 总结 ...........................................................................................................................................58 第八天 可以显示多个条目星级评分的列表控件 .......................................................................59 1. 引言 ...........................................................................................................................................59 2. 分析 ...........................................................................................................................................59 3. 实现 ...........................................................................................................................................62 4. 总结 ...........................................................................................................................................74 ASP.NET自定义控件实例教程 第九天 自定义 GridView..............................................................................................................75 1. 引言 ...........................................................................................................................................75 2. 分析 ...........................................................................................................................................75 3. 实现 ...........................................................................................................................................76 4. 总结 ...........................................................................................................................................85 第十天 实现分页功能的 DataList................................................................................................86 1. 引言 ...........................................................................................................................................86 2. 分析 ...........................................................................................................................................86 3. 实现 ...........................................................................................................................................86 4. 总结 ...........................................................................................................................................94 ASP.NET自定义控件实例教程 第 1 页 ASP.NET 自定义控件介绍 ASP.NET 自定义控件是已编译的服务器端控件,它将用户界面和其他功能都封装到可 复用的包中。自定义控件和标准的 ASP.NET 控件相比,除了它们绑定一个不同的标记前缀, 并且必须进行显式的注册和部署以外并没有什么不同。此外,自定义控件拥有自己的对象模 型,能够触发事件,并支持 Microsoft Visual Studio 的所有设计时特性,诸如属性窗口、可 视化设计器、属性生成器和工具箱。 自定义控件是一个继承自某个控件基类的类,通常使用以下两种方法来创建自定义控 件。如果发现某个现有控件只满足部分需求,关缺少某些关键的特征,则只需从该控件类中 派生出一个新类并根据需要进行扩展。如果现有的服务器控件不能满足需求,可以考虑从 S ystem.Web.UI.Control 类或 System.Web.UI.WebControls.WebControl 类继承,实际上 WebCo ntrol 类继承自 Control 类,添加了某些风格特征。 所有的自定义控件最终展现给用户的仍是由 HTML、CSS 和 JavaScript 混合在一起的页 面,所以在开发自定义控件时需要考虑使用现有的网页技术能否实现,如果在纯 HTML 环 境中无法实现某个特征的话自定义控件同样无法做到。 开发自定义控件时经常会用到的命名空间如下: z System.ComponentModel——提供用于实现组件和控件运行时和设计时行为的类, 经常使用该命名空间中的类设置自定义控件的特性或进行类型转换等工作。 z System.Web.UI——包含 ASP.NET 服务器控件基本的类,Control 类位于该命名空 间下。 z System.Web.UI.WebControls——包含在页面上创建 ASP.NET 服务端控件的类,常 用的 Button、Label、TextBox 类均位于该命名空间下。 z System.Web.UI.HtmlControls——同样包含一系列服务器端控件,与 System.Web.UI. WebControls 不同的是该命名空间下的类直接映射到一些 HTML 标记上,并且这些 类都以 HTM 作为前缀。 ASP.NET自定义控件实例教程 第 2 页 第一天 简单的星级控件 1. 引言 我们经常会在网页上看到使用星形图案表示对某个软件或某篇文章的评价,通常以五个 星形作为最高标准,指定的等级对应使用实心填充,如图 1-1 所示,在学习 ASP.NET 自定 义控件的第一天,我们将开发这样的自定义控件。 图 1-1 星形评分 2. 分析 可以看到这样的一个自定义控件包含两部分:显示的文本和包含两种图案(实心和空心 星形)的图片,为了呈现出这样的结果,最方便的就是将这两部分放到包含一行两列的表格 中。接下来要考虑的就是如何根据评分显示若干实心和空心星形。 看到这个图案的第一个想法可能就是根据评分首先显示实心图形,再判断还需要显示几 个空心星形(用 5 减评分),这样的话需要做多次循环才能实现,但是在 HTML 里有更好 的实现方法。 想一下在使用 CSS 设置背景图片时可以设置 background-repeat 属性,该属性标识背景 图片按哪个方向重复,可以利用这个特性首先显示一个外层的 div,该 div 始终显示 5 个空 心的星形图形,在该层中嵌套另一个 div,内层 div 显示实心星形图案。 background-repeat、background-image、background-color、background-position 构成了设 置背景样式的属性族 既然可以按 X 方向重复星形背景,那么显示指定个数的星形也就有答案了—我们可以 根据得分设置指定层( div)的宽度即可,而且为了方便,将两个星形图案放置到一个图片 文件里,再应用 background-position 显示指定位置星形即可。 根据以上分析,此自定义控件中需要具有两个属性: 属性 描述 Comment 评分项目注释,字符串类型 Score 得分,根据该属性显示相应的实心图形,数字类型 3. 实现 1. 首先创建解决方案,包含 ControlLibrary 类库(定义了自定义控件类)和 Web 网站。 2. 在类库中创建 Image 目录,并放置使用的星形图片 stars.gif,为了能够在网站中引用 程序集中的资源文件,需要将图片的 Build Action(生成动作)属性设置为 Embedded Reso urce(嵌入资源),并且在 AssemblyInfo.cs 中声明要使用的资源文件,如下所示: [assembly: WebResource("ControlLibrary.Image.stars.gif","image/jpg")] ASP.NET自定义控件实例教程 第 3 页 使用了 WebResource(实际上是 WebResourceAttribute 类,为了使用该类需要引入 Syst em.Web.UI 命名空间)定义了使用的资源,该类的构造函数使用了两个参数,第一个参数是 Web 资源的名称,第二个参数是 MIME 类型。Web 资源的名称必须遵循特定规则:命名空 间名称+目录名称 +文件名称,中间用半角点字符分隔。 MIME 类型对于不同的文件有不同 的表示,详细的 MIME 列表可参考相关资料。 资源的名称可以通过.NET Reflector 之类的工具浏览。载入某个程序集后,如果嵌有资 源文件,可以通过 Resources 目录浏览嵌入的资源 3. 在类库中创建自定义控件类 Star.cs,并引入必须的命名空间: using System; using System.ComponentModel; using System.Web.UI; using System.Web.UI.WebControls; namespace ControlLibrary { public class Star : WebControl { } } 4. 接下来定义 Score 和 Comment 属性,将这两个属性存储在 ViewState 中,同时如果 用户没有设计得分的情况下将得分设置为 0,也就是将得分的默认值设置为 0,这里使用到 了 DefaultValutAttribute 类,该类用于设置属性的默认值。 [DefaultValue(0)] public int Score { get { object obj = ViewState["Score"]; return obj == null ? 0 : Convert.ToInt32(obj); } set { ViewState["Score"] = value; } } public string Comment { get { object obj = ViewState["Comment"]; ASP.NET自定义控件实例教程 第 4 页 return obj == null ? string.Empty : Convert.ToString(obj); } set { ViewState["Comment"] = value; } } 由于 HTTP 请求完成之后会断开连接,把属性保存到私有变量中不会满足我们的要求(提 交回发时会重新生成自定义控件类,有可能丢失属性值),所以保存到 ViewState 中, 回发时能够正确恢复属性值 5. 重写 CreateChildControls 方法,该类在 Control 类上定义,此方法用于创建控件层次, 以便为回发和呈现做准备。在该方法中调用了自定义 CreateControlHierarchy 方法以创建控 件层次: protected override void CreateChildControls() { base.CreateChildControls(); CreateControlHierarchy(); } 6. 在 CreateControlHierarchy 方法中创建一个一行两列的表格,并分别调用 CreateCom ment 方法和 CreateStarts 方法创建注释和星形图按: protected virtual void CreateControlHierarchy() { Table table = new Table(); TableRow row = new TableRow(); table.Rows.Add(row); TableCell comment = new TableCell(); CreateComment(comment); row.Cells.Add(comment); TableCell stars = new TableCell(); CreateStars(stars); row.Cells.Add(stars); this.Controls.Add(table); } 7. 实现 CreateComment 方法,该方法接收一个 TableCell 参数当做显示文本的容器,直 ASP.NET自定义控件实例教程 第 5 页 接设置单元格的文本为注释字符串: private void CreateComment(TableCell cell) { cell.Text = Comment; } 8. 实现 CreateStarts 方法,该方法亦接收一个 TableCell 当做参数,首先取得图片资源 文件的路径,并在单元格里创建嵌套的层(在服务器端控件中 Panel 类与 div 相对应)并根 据得分设设置其相应属性: private void CreateStars(TableCell cell) { string starPath = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.Image.stars.gif"); Panel panBg = new Panel(); panBg.Style.Add(HtmlTextWriterStyle.Width, "80px"); panBg.Style.Add(HtmlTextWriterStyle.Height, "16px"); panBg.Style.Add(HtmlTextWriterStyle.TextAlign, "left"); panBg.Style.Add(HtmlTextWriterStyle.Overflow, "hidden"); panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panBg.Style.Add("background-position", "0px -32px"); panBg.Style.Add("background-repeat", "repeat-x"); cell.Controls.Add(panBg); Panel panCur = new Panel(); string width=Score*16+"px"; panCur.Style.Add(HtmlTextWriterStyle.Width, width); panCur.Style.Add(HtmlTextWriterStyle.Height, "16px"); panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panCur.Style.Add("background-position", "0px 0px"); panCur.Style.Add("background-repeat", "repeat-x"); panBg.Controls.Add(panCur); } 9. 重写父类的 Render 方法,在该方法中将服务器控件的内容传递给 HtmlTextWriter 对 象以在客户端呈现内容,在该方法中调用了 PrepareControlForRender 方法: protected override void Render(HtmlTextWriter writer) { PrepareControlForReader(); base.Render(writer); } ASP.NET自定义控件实例教程 第 6 页 10. 实现 PrepareControlForRender 方法,该方法用于在呈现前进行其他样式的设置,在 本控件中,只是简单的设置了表格的 CellSpacing 和 CellPadding 属性: private void PrepareControlForReader() { if (this.Controls.Count < 1) return; Table table = (Table)this.Controls[0]; table.CellSpacing = 0; table.CellPadding = 0; } 11. 在网站中添加 ControlLibrary 类库的引用, 并在 Default.aspx 页面中加入自定义控件 的声明: <%@ Register Assembly=”ControlLibrary” Namespace=”ControlLibrary” TagPrefix =”cc” %> 12. 在页面相应位置定义自定义控件: 13. 预览效果。 在多个页面使用自定义控件在每一个页均需要加入声明,有一个更好的替代方法是在 web.config 文件中声明自定义控件,在 配置节中加入 4. 总结 本次任务里我们通过继承 WebControl 类创建了一个很简单的星级自定义控件,随着学 习的加深您将逐渐意识到该控件实际上包含了自定义控件开发的基本步骤——创建自控件、 设置样式、进行呈现,通过把相应的动封装为不同的方法使代码看起来更清晰,并且掌握了 如何在自定义控件中使用资源文件,这在自定义控件使用大量样式或者脚本时尤其有用。最 后讲解了在开发完控件后如何在页面中使用。 ASP.NET自定义控件实例教程 第 7 页 第二天 带有自定义样式的星级控件 1. 引言 在上个任务里,通过继承 WebControl 类创建了一个简单的星级控件,并且也可以设置 字体边框等相关样式,但是需求马上又来了,如果我们想加入自定义的样式,例如希望文本 可以自由显示到图案左边或下边,如下图所示,对于这样的要求怎么办? 图 2-1 文字显示到下方的星级控件 2. 分析 看到上方的图形,很容易想到为第一个任务里的 Star 类添加一个属性,标识是在左面 还是下面显示文本,这的确能够解决现有的问题,但如果过一阵子希望能够设置标签文本的 颜色怎么办,很容易,再添加一个属性,具有敏锐眼光的读者一定想到,这不是解决问题的 最好方法,因为可能会陆续提出新的样式的要求,那么比较合理的解决方法是单独添加一个 样式属性,该属性持有一个样式集合,通过修改这个样式集合可以随时添加新的外观特征, 最后再呈现控件的时候只需要使用第一个单元格应用该样式就可以了。 在.NET 中,System.Web.UI.Style 类封装了 Web 服务器外观的属性,例如可以设置背景 色、前景色或边框等, TableStyle 类和 TableItemStyle 类均扩展了 Style 类,从它们的名字就 可以想到 TableStyle 类用于设置表格控样的样式,而 TableItemStyle 类用于设置 TableRow 或 TableCell 的样式,它们之间的关系如图 2-2 所示: 图 2-2 样式类关系 对于本次任务来说,我们要设置某个单元格的样式,并且添加了自定义特性,因此选择 继承 TableItemStyle 类即可。 确定为 Star 类添加一个扩展了 TableItemStyle 类的属性后,样式的问题解决了,接下来 要考虑的就是如何保存样式如性,像上次任务里直接保存到 ViewState 中吗,还是有更好的 选择。 很显然,ViewState 不能直接保存某个对象,需要进行序列化操作后才可以进行,默认 情况下使用的是 BinaryFormatter 作为对象的序列化器,该类会把对象所有的成员全部序列 化,而不管是公有的或私有的,这样做的话可能导致效率不是很高,为了自己控制属性的保 ASP.NET自定义控件实例教程 第 8 页 存,需要继承 IStateManager 接口,该接口定义任何类为支持服务器控件的视图状态管理而 必须实现的属性和方法,其定义的成员有: 成员 描述 IsTrackingViewState 属性 获取一个值,批示服务器控件是否正在跟踪其视图状态更改 LoadViewState 方法 加载服务器控件以前保存的视图状态 SaveViewState 方法 将服务器控件的视图状态保存到 Object TrackViewState 方法 指示服务器控跟踪其视图状态更改 根据以上分析,为了实现需求需要定义一个继承自 TableItemStyle 并实现 IStateManage r 接口的样式类 TextItemStyle,在该类中增加 DisplayTextAtBottom 属性定义是否在底部显示 注册文本,并在上次任务中开发的 Star 类里增加类型为 TextItemStyle 的属性以设置样式。 3. 实现 1. 在上次任务解决方案的 ControlLibrary 类库中添加 TextItemStyle 类,该类继承 Table ItemStyle 类并实现了 IStateManager 接口: public class TextItemStyle:TableItemStyle,IStateManager { } 2. 在该类中定义布尔类型的 DisplayTextAtBottom 属性,使用一个私有变量保存该属性 设置;当该属性修改时需要通知 Star 类中的样式属性以应用新的样式,所以需要使用 Notif yParentProperty 属性修饰: private bool _displayTextAtBottom; [NotifyParentProperty(true)] public bool DisplayTextAtBottom { get { return _displayTextAtBottom; } set { _displayTextAtBottom = value; } } 3. 实现 IStateManager 中的 IsTrackingViewState 属性和 TrackViewState 方法,由于在这 两个成员里不需要额外的动作,所以只需要简单的访问父类成员即可: bool IStateManager.IsTrackingViewState { get { return base.IsTrackingViewState; ASP.NET自定义控件实例教程 第 9 页 } } void IStateManager.TrackViewState() { base.TrackViewState(); } 4. 实现 IStateManager 接口 SaveViewState 方法以保存自定义属性: object IStateManager.SaveViewState() { Pair p = new Pair(); p.First=base.SaveViewState(); p.Second = _displayTextAtBottom; return p; } 这里使用了 System.Web.UI.Pair 类用于保存两个相关的对象,与此类似的还是 System. Web.UI.Triplet 类,用于保存三个相关的对象。需要保存更多的数据时,可以考虑定义 一个数组。 5. 实现 IStateManager 接口 LoadViewState 方法, 从保存的视图数据中恢复必要的设置: void IStateManager.LoadViewState(object savedState) { if (savedState != null) { Pair p = (Pair)savedState; base.LoadViewState(p.First); _displayTextAtBottom = Convert.ToBoolean(p.Second); } } 可以看到 TextItemStyle 中对接口的实现是通过接口名称 .成员名称实现的,即所谓的显 式继承,采用这种继承方向,不能直接通过类调用接口方法,而必须将类转换为接口类 型后方可调用。 6. 在 Start 类中添加样式属性,并调用接口方法进行视图操作: private TextItemStyle _textStyle; [PersistenceMode(PersistenceMode.InnerProperty)] ASP.NET自定义控件实例教程 第 10 页 public TextItemStyle TextStyle { get { if (_textStyle == null) _textStyle = new TextItemStyle(); if (IsTrackingViewState) ((IStateManager)_textStyle).TrackViewState(); return _textStyle; } set { _textStyle = value; } } 该属性使用 PersitenceMode 标识,该类只是通知 Visual Studio 2005,使用 aspx 源文件 中的一个嵌入标记来永久存储该风格的内容。 7. 修改 CreateControlHierarchy 方法,根据样式属性设置决定是否创建新行: protected virtual void CreateControlHierarchy() { Table table = new Table(); TableRow row = new TableRow(); table.Rows.Add(row); TableCell stars = new TableCell(); CreateStars(stars); TableCell comment = new TableCell(); CreateComment(comment); if (TextStyle.DisplayTextAtBottom) { row.Cells.Add(stars); TableRow text = new TableRow(); text.Cells.Add(comment); table.Rows.Add(text); } else { row.Cells.Add(comment); ASP.NET自定义控件实例教程 第 11 页 row.Cells.Add(stars); } this.Controls.Add(table); } 8. 最后修改呈现方法 PrepareControlForRender,将样式应用到文字单元格上: private void PrepareControlForReader() { if (this.Controls.Count < 1) return; Table table = (Table)this.Controls[0]; table.CellSpacing = 0; table.CellPadding = 0; TableCell cell = null; if (TextStyle.DisplayTextAtBottom) { cell = table.Rows[1].Cells[0]; } else { cell = table.Rows[0].Cells[0]; } cell.ApplyStyle(TextStyle); } 9. 在网站中声明并定义控件: 浏览运行结果。 虽然我们在 TextItemStyle 类中定义了保存和读取视图状态的方法,但是在回发时能够 正常工作吗,尝试在页面的 PageLoad 方法里设置样式的背景色为红色: if (!IsPostBack) star.TextStyle.BackColor=System.Drawing.Color.Red; 接下来在页面中添加一个服务器端按钮,浏览页面并点击提交按钮,会出现怎样的结 果?可以看到红色背景丢失了,这是由于虽然我们定义了样式类属性保存的方法,但它还没 有真正的参与到页面视图读写过程中,为此,需要重写 Start 类的 SaveViewState 和 LoadVi ASP.NET自定义控件实例教程 第 12 页 ewState 方法,指定如何将数据保存到视图状态中,以及如何从视图状态中恢复。 protected override object SaveViewState() { Pair p = new Pair(); p.First=base.SaveViewState(); p.Second = ((IStateManager)TextStyle).SaveViewState(); return p; } protected override void LoadViewState(object savedState) { if (savedState != null) { Pair p = (Pair)savedState; base.LoadViewState(p.First); ((IStateManager)TextStyle).LoadViewState(p.Second); } } 编译解决方案后再次预览页面,并点击提交按钮,可以看到.NET 已经帮助我们正确的 从视图状态中恢复数据了。 4. 总结 本次任务中为星形控件增加了自定义样式,并自定义视图操作状态以更高效的保存和读 取相关数据。在定义属性时候使用了 NotifyParentProperty 特性和 PersistenceMode 特性分别 用来在属性发生更改时通知父属性和将属性使用嵌入标记来保存。可能您会突然想到,如果 用户将页面视图状态禁止后会产生什么样的结果,某些属性还能正确设置吗,下一次任务里 我们将讨论这个问题。 ASP.NET自定义控件实例教程 第 13 页 第三天 使用控件状态的星级控件 1. 引言 正如在前两个任务中所看到的,我们使用视图( ViewState)保存自定义控件属性, Vie wState 实际上是一个 StateBag 对象,开发人员使用键 /值的方法向视图中保存或读取设置, 最终发送给用户的 HTML 页面中会包含一个隐藏域,该隐藏域中保存了经过序列化后的值。 如果过分使用视图的话,会导致页面急剧增大,虽然现在网络带宽已经不是限制条件,但这 仍然是一个不太好的设计,因此开发人员有时会禁用视图状态。 对于自定义控件来说,如果禁用视图状态可能导致控件不能够正常工作,读者可以使用 第一次任务里开发的星级控件,禁用页面的视图状态(设置 Page 指令 EnableViewState 属性 为 false)并在在页面的加载( Page_Load)事件里输入如下代码以设置分数: protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) star.Score = 4; } 并在 ASPX 页面上放置一个服务器端按钮以引起回发事件,预览该页面并点击按钮, 会发生什么情况?由于提交按钮引起回发 IsPostBack 属性为 true,即不会执行分数设置操作, 并且因为禁用了视图状态,在页面生命周期里运行时无法从视图状态中恢复分数,所以会导 致没有分数: 图 3-1 禁用了视图状态的自定义控件 2. 分析 在.NET1.X 版本里,视图状态被作为一个整体,要不允许要么禁止,对于控件开发人员 来说非常不方便,与控件相关的数据放置在 ViewState 里,一旦被禁用后,可能就会出问题。 比较幸运的是,对于这种情况,微软及时做出了反应。在 ASP.NET2.0 里,出现了一个新的 概念——控件状态。 控件状态实际上是一种特殊的视图状态,它仍然保存在客户端的隐藏域中,但是它并不 会受视图状态启用/禁用的影响,也就是说,即使将 ViewState 禁用,运行时仍然能正确的恢 复在控件状态中保存的数据。 为了使用控件状态,仅仅需要做额外的几个工作: 1. 向页面注册使用控件状态 2. 在控件状态保存事件( Control 类的 SaveControlState 方法)中保存相关数据 3. 在控件状态读取事件( Control 类的 LoadControlState 方法)中读取保存的数据 需要说明的一点是,正因为控件状态始终都会发送到客户端,所以将大量数据保存到控 ASP.NET自定义控件实例教程 第 14 页 件状态中显然不是一件太好的事件,始终应该只保存影响控件使用的关键的核心的数据。 为了能够使页面视图状态被禁止后控件仍然能够正常使用,将星级控件的得分保存在控 件状态中。 3. 实现 为了组织文件方便,创建一个新的使用控件状态的类。 1. 在自定义控件解决方案的 ControlLibrary 类库中添加 StateStar 类,像第一次任务那样 定义属性和创建控件层次并呈现: using System; using System.ComponentModel; using System.Web.UI; using System.Web.UI.WebControls; namespace ControlLibrary { public class StateStar : WebControl { private int _score; [DefaultValue(0)] public int Score { get { return _score; } set { _score = value; } } public string Comment { get { object obj = ViewState["Comment"]; return obj == null ? string.Empty : Convert.ToString(obj); } set { ViewState["Comment"] = value; ASP.NET自定义控件实例教程 第 15 页 } } protected override void CreateChildControls() { base.CreateChildControls(); CreateControlHierarchy(); } protected virtual void CreateControlHierarchy() { Table table = new Table(); TableRow row = new TableRow(); table.Rows.Add(row); TableCell comment = new TableCell(); CreateComment(comment); row.Cells.Add(comment); TableCell stars = new TableCell(); CreateStars(stars); row.Cells.Add(stars); this.Controls.Add(table); } /// /// 向单元格中创建注释标签 /// /// 单元格对象 private void CreateComment(TableCell cell) { Label lbl = new Label(); lbl.Text = Comment; cell.Controls.Add(lbl); } /// /// 向单元格中创建星形图案 /// /// ASP.NET自定义控件实例教程 第 16 页 private void CreateStars(TableCell cell) { string starPath = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.Image.stars.gif"); Panel panBg = new Panel(); panBg.Style.Add(HtmlTextWriterStyle.Width, "80px"); panBg.Style.Add(HtmlTextWriterStyle.Height, "16px"); panBg.Style.Add(HtmlTextWriterStyle.TextAlign, "left"); panBg.Style.Add(HtmlTextWriterStyle.Overflow, "hidden"); panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panBg.Style.Add("background-position", "0px -32px"); panBg.Style.Add("background-repeat", "repeat-x"); cell.Controls.Add(panBg); Panel panCur = new Panel(); string width = Score * 16 + "px"; panCur.Style.Add(HtmlTextWriterStyle.Width, width); panCur.Style.Add(HtmlTextWriterStyle.Height, "16px"); panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage,starPath ); panCur.Style.Add("background-position", "0px 0px"); panCur.Style.Add("background-repeat", "repeat-x"); panBg.Controls.Add(panCur); } protected override void Render(HtmlTextWriter writer) { PrepareControlForReader(); base.Render(writer); } } } 不要被这一大堆没有注释的代码吓倒,这只是最简单的星级控件的实现,并且为了方便 某些读者不需要查询以前的资料就可以快速编辑一个控件。 2. 为了使用控件状态,向页面进行注册,而且由于在回发事件的过程中,控件状态的 注册无法在请求之间进行传递,因此使用控件状态的自定义服务器控件必须对每个请求进行 注册,所以重写 OnInit 方法: ASP.NET自定义控件实例教程 第 17 页 protected override void OnInit(EventArgs e) { base.OnInit(e); Page.RegisterRequiresControlState(this); } 3. 重写 SaveControlState 方法,该方法保存自页回发到服务器后发生的任何服务器控件 状态更改,该方法的返回值标识了服务器控件的当前状态,因此只需要将星级控件的得分保 存到返回的 object 中即可(为了保持一致,仍然需要调用父类的实现): protected override object SaveControlState() { object[] o = new object[2]; o[0]=base.SaveControlState(); o[1] = _score; return o; } 4. 重写 LoadControlState 方法,该方法从保存的控件状态中恢复数据,它有一个参数表 示保存的数据,该参数是 object 类型,需要根据保存时使用的类型进行正确的转换: protected override void LoadControlState(object savedState) { if (savedState != null) { object[] o = (object[])savedState; base.LoadControlState(o[0]); _score = Convert.ToInt32(o[1]); } } 5. 完成以后步骤后在 Web 网站中创建一个 ASPX 页面声明并定义 StateStar 控件,将视 图禁用并添加一个引起回发的服务器端按钮,像一开始那样在页面加载事件里设置得分,再 次预览页面并点击按钮,可以看到,得分仍然能够正确的显示出来: 图 3-2 使用控件状态的自定义控件 ASP.NET自定义控件实例教程 第 18 页 4. 总结 在本次任务里,我们使用控件状态保存了自定义控件的得分,使得在页面视图被禁用时 自定义控件仍能正常用行。为了使用控件状态,调用了 Page.RegisterRequiresControlState 方 法,并重调了 SaveControlState 和 LoadControlState 方法,最后需要再次说明的是控件状态 只应该用来保存关键的数字。 到今天为止,我们已经掌握了开发简单的自定义控件的方法,并且围绕一个星级控件讨 论了它的组织、呈现、自定义样式、控件状态和如何使用特性进行定义。在下一次任务里, 我们将一起开发一个具有一些挑战性的复杂控件。 ASP.NET自定义控件实例教程 第 19 页 第四天 折叠面板自定义控件 1. 引言 在前几次任务里我们开发一个星级控件并逐渐为其增加一新的特性,在本次任务里,我 们将开发一个较复杂的自定义控件,该自定义控件需要实现折叠面板的功能。用户可以向面 板控件中自由添加控件,该控件呈现出来后会根据用户设置决定是否显示折叠按钮,如果允 许则用户可以点击按钮展开/折叠按钮以显示或隐藏面板,并且可以在服务器端捕捉到展开 / 折叠事件以进行更多的控制,该控件运行效果图如下: 图 4-1 自定义面板控件 2. 分析 我们在确定该控件最终能够使用 HTML 呈现出来之后,接下来要考虑的是为该自定义 控件选择一个合适的基类。从要实现的功能来看,该控件分为两部分,一部分是包含展开 / 折叠按钮的标题行,另一部分是包含用户放置按钮的容器,很显然这个容器我们可以使用 P anel 类,为了保留用户直接定义 Panel 中子标记的特性,使自定义控件直接继承自 Panel 类, 根据设置决定是否显示标题行,并且根据面板状态显示恰当的展开或折叠图标。 由于 Panel 可以看作一个容器控件,那么当页面上使用多个该控件时会产生什么样的结 果,容器中的子控件还能保证是唯一的吗?可以尝试编写一个继承自 Panel 类的自定义控件 CustomerControl 并向其控件集合( Controls 属性)中添加一个 ID 为 txt 的文本框,当在页面 上放置两个这样的自定义控件时会生成了两个文本框(毫无疑问,因为是两个自定义控件), 但是它们的 id 属性均为 txt,类似于以下代码: 为了避免这种情况发生,只需要使 CustomerControl 类实现 INamingContainer 接口,再 次预览页面,将会发现生成的文本框已经具有唯一的 id 了: 因此,我们的自定义控件也需要实现 INamingContainer 接口,以确保所有子控件 ID 属 性唯一。正如读者所看到的,只需要标记实现 INamingContainer 接口而不需要编写任何额外 的方法,这也就是所谓的“标记接口”,与此类似的还有 ISerializable 等。 为了使折叠控件更灵活,允许开发人员设置是否可以展开 /折叠控件,同时编写属性使 得用户可以不使用服务器端提交方式,仅在客户端执行展开 /折叠操作。还需要即使在用户 禁止视图状态的情况下仍然能够记住控件的展开 /折叠状态,因此需要使用控件状态保存状 ASP.NET自定义控件实例教程 第 20 页 态设置,最后提供一些额外的属性帮助设置标题的样式。 由此得出,该自定义控件应具有以下属性 属性 描述 EnableDropDown 是否允许展开/白叠 EnableClientScript 是否允许使用客户端脚本执行展开/折叠动作 ShowExpanded 初始状态是否被展开 Caption 标题 CaptionBackColor 标题背景色 CaptionForeColor 标题前景色 为了能够当用户展开或折叠时引发服务器端事件,可以简单的使用服务器端图片按钮控 件显示展开/折叠图标,用户在处理该服务器端事件时可能需要知晓当前面板的状态(展开 或折叠,可以简单的使用布尔型变量标识),需要通常事件参数的某个属性来标识,但是现 有的事件参数类不适合完成此项功能,所以我们同样要编写自己的事件类,并且定义面板状 态属性。 .NET 中的事件基于委托,在 ASP.NET 中可以使用 EventHandler 委托(与此相关还定义 了泛型委托)定义事件,事件参数包含了与事件有关的数据。有关委托和事件及泛型的 知识请参阅相关书籍。 最后需要考虑的是如果使用提交引发服务器端事件,如何将该事件暴露给开发人员处 理。实际上对于这种需求有多种实现方式: z 实现 IPostBackEventHandler 接口以处理回发事件。 z 将子控件事件作为顶层事件公开。 z 使用冒泡法将事件沿包含层次向上传播到合适的位置引发。 在本次任务中我们将使用第二种方式,另外两种方式将在以后的任务中介绍。为了将子 控件事件作为顶层事件公开,需要经过以下几个步骤: 1. 为自定义控件定义事件 2. 为了确保事件在引发时已经被订阅编写辅助方法检查事件是否为空(null) 3. 编写子控件事件处理程序,根据需要生成事件参数引发事件(调用第 2 步中辅助方 法)并执行其他的操作。 以上是对面板控件的分析,接下来我们将按照分析的结果实现该控件。 3. 实现 3.1 在解决方案 ControlLibrary 类库中添加 ExtendPanel 类,并根据分析定义相关属性: public class ExtendPanel : Panel, INamingContainer { [Themeable(false)] public bool EnableDropDown { get ASP.NET自定义控件实例教程 第 21 页 { object o = ViewState["EnableDropDown"]; if (o == null) return false; return (bool)o; } set { ViewState["EnableDropDown"] = value; } } // 是否使用客户端脚本 [Themeable(false)] public bool EnableClientScript { get { object o = ViewState["EnableClientScript"]; if (o == null) return false; return (bool)o; } set { ViewState["EnableClientScript"] = value; } } // 是否被展开 [Themeable(false)] public bool ShowExpanded { get { object o = ViewState["ShowExpanded"]; if (o == null) return true; return (bool)o; } set { ViewState["ShowExpanded"] = value; } } //Panel 标题 [Themeable(false)] public string Caption { ASP.NET自定义控件实例教程 第 22 页 get { object o = ViewState["Caption"]; if (o == null) return "Panel"; return (string)o; } set { ViewState["Caption"] = value; } } //标题背景颜色 public Color CaptionBackColor { get { object o = ViewState["CaptionBackColor"]; if (o == null) return Color.SkyBlue; return (Color)o; } set { ViewState["CaptionBackColor"] = value; } } //标题前景颜色 public Color CaptionForeColor { get { object o = ViewState["CaptionForeColor"]; if (o == null) return Color.White; return (Color)o; } set { ViewState["CaptionForeColor"] = value; } } } 对于 EnableDropdown 等属性在主题中定义不会影响到控件的呈现样式,所以这些属性 使用 Themable 特性进行定义,该特性指定属性不受到主题和控件外观的影响。 3.2 接下来定义布尔型私有变量用于标识当前面板是展开状态还是折叠状态,为了避免 视图状态的影响,将该属性值存储在控件状态中: private bool _panelDisplayed; protected override void OnInit(EventArgs e) ASP.NET自定义控件实例教程 第 23 页 { base.OnInit(e); Page.RegisterRequiresControlState(this);//注册控件状态 } protected override object SaveControlState() { Pair p = new Pair(); p.First = base.SaveControlState(); p.Second = _panelDisplayed; return p; } protected override void LoadControlState(object savedState) { if (savedState == null) return; Pair p = (Pair)savedState; base.LoadControlState(p.First); _panelDisplayed = (bool)p.Second; } 3.3 重写 CreateChildControls 方法,根据允许下拉( EnableDropDown)属性设置决定是 否显示标题栏: protected override void CreateChildControls() { if (EnableDropDown)//如果允许下拉,则创建标题条 { base.CreateChildControls(); CreateControlHierarchy(); } else { base.CreateChildControls();//如果不允许下拉,则显示原有控件 } } 3.4 在 CreateChildControls 方法中调用了 CreateControlHierarchy 方法用于创建子控件层 次,该方法首先执行了两部分操作,创建表格并添加第一行显示标题和操作图标;将原 Pan el 容器中的控件加入到表格的第二行中,同时将表格添加到控件集合中并清空原有控件: ASP.NET自定义控件实例教程 第 24 页 protected virtual void CreateControlHierarchy() { Table t = new Table(); TableRow row1 = new TableRow(); t.Rows.Add(row1); TableCell cell1 = new TableCell(); row1.Cells.Add(cell1); cell1.Text = " " + Caption; TableCell cell2 = new TableCell(); row1.Cells.Add(cell2); cell2.HorizontalAlign = HorizontalAlign.Right; TableRow row2 = new TableRow(); t.Rows.Add(row2); TableCell body = new TableCell(); body.ID = "Body"; row2.Cells.Add(body); body.ColumnSpan = 2; Control[] rg = new Control[Controls.Count]; Controls.CopyTo(rg, 0); foreach (Control ctl in rg) body.Controls.Add(ctl); Controls.Clear(); Controls.Add(t); 由于服务器端控件均是引用类型,所以不能直接将控件添加到某个控件集合中,而必须 调用 Controls.CopyTo 将原有控件复制到目标控件数组中, 否则在调用 Controls.Clear 方法时 仍然会将控件移除而导致错误的运行结果。 3.5 接下来在该方法中判断是否使用客户端展开 /折叠动作(EnableClientScript)属性, 如果该属性为 false,即表明要执行服务器提交动作,因此向标题行单元格中添加服务器端 图片按钮,并处理该控件点击事件(稍后会实现该事件处理程序): WebControl img; if (!EnableClientScript) { img = new ImageButton(); ((ImageButton)img).Click += new ImageClickEventHandler(OnClick); cell2.Controls.Add(img); } ASP.NET自定义控件实例教程 第 25 页 3.6 如果在客户端实现展开 /折叠动作则则向页面注册客户端脚本并附加到图标的点击 事件上: else { img = new System.Web.UI.WebControls.Image(); img.ID = "Icon"; cell2.Controls.Add(img); // 添加样式 img.Attributes["onmouseover"] = "this.style.cursor = \"hand\";"; img.Attributes["onmouseout"] = "this.style.cursor = \"\";"; img.Attributes["onclick"] = "__toggle()"; if (!Page.ClientScript.IsClientScriptBlockRegistered("__toggle")) { string js = BuildScript(body); Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "__toggle", js, true); } } 在以上代码段中调用了 BuildScript 方法用于生成 JavaScript 脚本, 以下是该方法实现 (当 然可以使用资源文件实现): private string BuildScript(TableCell body) { StringBuilder sb = new StringBuilder(); sb.AppendLine("function __toggle() {"); sb.AppendFormat(" var body = document.getElementById(\"{0}\");\r\n", body.ClientID); sb.AppendLine(" var display = body.style.display;"); sb.AppendLine(" if (display == \"\") {"); sb.AppendLine(" body.style.display = \"none\";"); sb.AppendLine(" } else {"); sb.AppendLine(" body.style.display = \"\";"); sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } 3.7 在 CreateControlHierarchy 方法的最后根据当前面板状态显示或隐藏面板内容并显 示相应的按钮图片,调用了两个方法: ShowChildControls(_panelDisplayed); ShowImageButton(_panelDisplayed); } ASP.NET自定义控件实例教程 第 26 页 3.8 实现 ShowChildControls 方法以显示或隐藏面板内容: private void ShowChildControls(bool display) { if (Controls.Count != 1) return; Table t = (Table)Controls[0]; TableRow r = t.Rows[1]; TableCell body = r.Cells[0]; body.Style["display"] = (display ? "" : "none"); } 3.9 在 ControlLibrary 类库中添加 Image 目录,将 collapse.bmp 和 expand.bmp 图片放置 到该目录中,设置生成动作为嵌入并在 AssemblyInfo.cs 中添加资源文件的注册,实现 Show ImageButton 根据面板状态显示恰当的图标: private void ShowImageButton(bool display) { if (Controls.Count != 1) return; Table t = (Table)Controls[0]; TableRow r = t.Rows[0]; TableCell icon = r.Cells[1]; //设置相应图片 System.Web.UI.WebControls.Image img = (System.Web.UI.WebControls.Image)icon.Controls[0]; string imageName = "ControlLibrary.Image.expand.bmp"; if (display && !EnableClientScript) imageName = "ControlLibrary.Image.collapse.bmp"; img.ImageUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(), imageName); } 3.10 接下来进行服务器端点击事件的处理, 首先在 ExtendPanel 类中使用泛型委托声明 事件,该泛型委托意味着使用 PanelClickEventArgs 作为事件参数类型,并且该类必须继承 自 EventArgs 类: public event EventHandler PanelClick; 定义 PanelClientEventArgs 事件参数类并添加 BeingClosed 属性标识面板状态: public class PanelClickEventArgs : EventArgs { public bool BeingClosed { ASP.NET自定义控件实例教程 第 27 页 get; set; } } 3.11 根据自控件事件的分析,在 ExtendPanel 类中定义辅助方法检查事件是否已被订 阅: protected virtual void OnPanelClick(PanelClickEventArgs args) { if (PanelClick != null) PanelClick(this, args); } 3.12 触发服务器端图片按钮点击事件时,在事件处理方法 OnClick 中生成事件参数类 后调用 OnPanelClick 方法,并且更新面板和图标显示,如此订阅的事件处理程序就可以参 与到图片按钮点击过程中: private void OnClick(object sender, ImageClickEventArgs e) { PanelClickEventArgs args = new PanelClickEventArgs(); args.BeingClosed = _panelDisplayed; OnPanelClick(args); //更新显示状态 _panelDisplayed = !_panelDisplayed; ShowChildControls(_panelDisplayed); ShowImageButton(_panelDisplayed); } 3.13 最后重写 Render 方法呈现控件,该方法调用 PreparentControlForRendering 方法将 控件的样式应用的创建的表格上并根据属性设置标题的前景色和背景色: protected override void Render(HtmlTextWriter writer) { PrepareControlForRendering(); base.Render(writer); } protected virtual void PrepareControlForRendering() { if (Controls.Count != 1) return; // 应用样式 Table t = (Table)Controls[0]; t.CopyBaseAttributes(this); ASP.NET自定义控件实例教程 第 28 页 if (ControlStyleCreated) t.ApplyStyle(ControlStyle); t.CellPadding = 1; t.CellSpacing = 0; //设置标题样式 TableRow row1 = t.Rows[0]; row1.BackColor = CaptionBackColor; row1.ForeColor = CaptionForeColor; } 3.14 在解决方案的 Web 网站中创建测试页,声明并定义自定义面板控件,测试运行结 果。 4. 总结 在本次任务里我们创建了一个可以折叠/展开的自定义面板控件,应用 EventHandler 泛 型委托和自定义事件类将图片的点击事件公开为面板的顶层事件,这样使用者就可以订阅该 事件进行自定义处理。该面板控件两个比较重要的属性是 EnableDropDown 和 EnableClient Script 属性,根据前者决定是否需要显示附加的标题栏,后者决定是生成 JavaScript 脚本以 在客户端进行展开 /折叠操作,还是使用图片按钮处理服务器点击事件。如果点击事件提交 到服务器端处理,则使用自定义事件类保存面板状态并由图片按钮的点击事件引发面板顶层 事件,使用户能够自定义点击事件的处理。 在下次任务里,我们将介绍事件处理的另外一种方式——使用 IPostEventHandler 接口, 在原有星级控件的基础上增加评分的功能,允许用户自由选择评分并引发服务器端事件,以 能够得到用户选择的分数。 ASP.NET自定义控件实例教程 第 29 页 第五天 真正可以评分的星级控件 1. 引言 在前几次任务里开发的星级控件仅适用于静态展示,例如标明某个软件的受欢迎度,但 是实际上很多网站还希望能够由用户对某一信息进行评分,最终计算出该信息的受欢迎程 度,使数据更为客观和可信,由此需要在原有的星级控件上加以改进,使用户能够动态评分, 实际的效果图看起来如下图所示: 图中第一行是经过评分后控件的状态,开发人员处理了评分事件并在页面输出了选择的 分数;图中第二行显示了另外一种评分状态——鼠标移动到了星形图案上,此时使用红色的 星形提示用户。 2. 分析 对于该控件我们要在原有控件的基础上加入两个特性: 1. 加入鼠标悬浮指示,当鼠标悬停时显示出用户选择的分数。 2. 在鼠标点击时能够回发到服务器并将相应事件暴露出来由开发人员处理。 对于第一个需求,可以处理 JavaScript 中的鼠标事件,当触发 onmouseover 事件时判断 当前鼠标悬停在第几个星形图案,接着显示红色星形图案。显示图案时同样有一些技巧,在 原有显示星形图案的层( div)中嵌套层,并将该层的背景设置为红色星形图案,在页面加 载的时候该层不应该被显示出来(设置宽度为 0 即可),并且在鼠标悬停时设置该层的宽度, 我们只需要注意能够使该层将背景层覆盖即可。 同样要考虑的是,在鼠标移出的时候需要将该层隐藏起来,那么只需要处理 onmouseo ut 事件,将层的宽度再次设置为 0 即达到了隐藏层的目的。 对于第二个需求,首先要在自定义控件中暴露一个公开事件使开发人员能够订阅该事 件,接下来就是在客户端产生一个回发脚本,使得在点击(JavaScript 中的 onclick 事件触发) 时执行此回发脚本提交到服务器即可。 为了产生回发脚本,使自定义控件实现 IPostBackEventHandler 接口,该接口定义了 AS P.NET 服务器控件为处理回发事件而必须实现的方法。 在提交到服务器之后,自定义控件调用公开的事件,并且需要星形图案替换为黄色背景 标识用户评分,那么我们同样可以在背景层中再加入一个层,使用和第一种需求相同的算法 将背景层覆盖就可以了。 需要确认的是,在用户评分之后就不允许再次评分了(将用户当前鼠标悬停选择评分的 层隐藏起来即可)。 ASP.NET自定义控件实例教程 第 30 页 最后要考虑的问题是,自定义控件中使用了服务器端控件作为容器显示图片,为了避免 页面上放置多个自定义控件发生问题,需要保证此时各服务器端控件生成的客户端编号唯 一,您一定已经想到了解决方法,就是实现 INamingContainer 接口。 3. 实现 3.1 在 ControlSolution 解决方案的 ControlLibrary 类库中创建继承自 WebControl 的 Post Start 类,并使其实现 IPostBackEventHandler 和 INamingContainer 接口: using System; using System.ComponentModel; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; namespace ControlLibrary { public class PostStar : WebControl,IPostBackEventHandler,INamingContainer { } } 3.2 为自定义控件添加得分和注释属性: [DefaultValue(0)] public int Score { get { object obj = ViewState["Score"]; return obj == null ? 0 : Convert.ToInt32(obj); } internal set { ViewState["Score"] = value; } } public string Comment { get { object obj = ViewState["Comment"]; return obj == null ? string.Empty : Convert.ToString(obj); } ASP.NET自定义控件实例教程 第 31 页 set { ViewState["Comment"] = value; } } 3.3 重写 CreateChildControls 方法创建子控件层次,在该方法中调用了 CreateControlHi erarchy 方法: protected override void CreateChildControls() { base.CreateChildControls(); CreateControlHierarchy(); } 3.4 编写 CreateControlHierarchy 方法,在该方法中首先在子控件集合中创建了一个一行 两列的表格,接下来调用 CreateComment 方法在第一页中输出注释,然后调用了 CreateStar t 方法创建星形图案: protected virtual void CreateControlHierarchy() { Table table = new Table(); TableRow row = new TableRow(); table.Rows.Add(row); TableCell comment = new TableCell(); CreateComment(comment); row.Cells.Add(comment); TableCell stars = new TableCell(); CreateStars(stars); row.Cells.Add(stars); this.Controls.Add(table); } 3.5 实现 CreateComment 方法,简单的将注释文本赋值给单元格的 Text 属性: private void CreateComment(TableCell cell) { cell.Text = Comment; } 3.6 编写创建星形图案的 CreateStars 方法,在该方法中调用了 RegisterCSS 方法向页面 注册使用的 CSS 样式表文件(该文件作为资源文件发布),接下来分别调用了 CreateBackP anel、CreateCurrentPanel、和 CreateChangePanel 方法用于创建背景层、标识选中以后的层和 表示当前鼠标悬停的层,而且还调用了 CreateList 方法以创建列表,最后按照层次结构将层 ASP.NET自定义控件实例教程 第 32 页 和列表组织起来(使用 Panel 控件表示层): private void CreateStars(TableCell cell) { RegisterCSS(); string starPath = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.Image.stars.gif"); Panel panBg = CreateBackPanel(starPath); cell.Controls.Add(panBg); Panel panCur = CreateCurrentPanel(starPath); Panel panChange =CreateChangePanel(starPath); HtmlGenericControl ul = CreateList(); panBg.Controls.Add(ul); panBg.Controls.Add(panCur); panCur.Controls.Add(panChange); } 3.7 实现 RegisterCSS 方法,取得样式表资源文件并使用 HtmlLink 类注册样式表: private void RegisterCSS() { string css = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.CSS.star.css"); HtmlLink link = new HtmlLink(); link.Href = css; link.Attributes.Add("rel", "stylesheet"); link.Attributes.Add("type", "text/css"); Page.Header.Controls.Add(link); } 3.8 编写 CreateBackPanel、CreateCurrentPanel、CreateChangePanel 和 CreateList 方法实 现: private Panel CreateBackPanel(string starPath) { Panel panBg = new Panel(); panBg.ID = "divBg"; panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); ASP.NET自定义控件实例教程 第 33 页 panBg.CssClass = "stars"; return panBg; } private Panel CreateCurrentPanel(string starPath) { Panel panCur = new Panel(); panCur.ID = "divCur"; panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panCur.CssClass = "current"; return panCur; } private Panel CreateChangePanel(string starPath) { Panel panChange = new Panel(); panChange.ID = "divChange"; panChange.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panChange.CssClass = "change"; return panChange; } private HtmlGenericControl CreateList() { HtmlGenericControl ul = new HtmlGenericControl("ul"); ul.ID = "ulist"; ul.Attributes.Add("class", "ulist"); for (int i = 0; i < 5; i++) { HtmlGenericControl li = new HtmlGenericControl("li"); li.Attributes.Add("value", (i + 1).ToString()); ul.Controls.Add(li); } ASP.NET自定义控件实例教程 第 34 页 return ul; } 3.10 重写 Render 方法呈现控件,该方法调用了 PrepareControlForReader 方法: protected override void Render(HtmlTextWriter writer) { PrepareControlForRender(); base.Render(writer); } 3.11 实现 PrepareControlForRender,在该方法中按照控件层次取出列表中的列表项并注 册 JavaScript 事件: private void PrepareControlForRender() { if (this.Controls.Count < 1) return; Table table = (Table)this.Controls[0]; table.CellSpacing = 0; table.CellPadding = 0; TableCell cell = table.Rows[0].Cells[1]; Panel panCur = (Panel)cell.Controls[0].Controls[1]; Panel panChange = (Panel)panCur.Controls[0]; HtmlGenericControl ul = (HtmlGenericControl)cell.Controls[0].Controls[0]; for (int i = 0; i < ul.Controls.Count; i++) { HtmlGenericControl li = (HtmlGenericControl)ul.Controls[i]; li.Attributes.Add("onmouseover", "document.getElementById('" + panChange.ClientID + "').style.width='" + 16 * (i + 1) + "px';"); li.Attributes.Add("onmouseout", "document.getElementById('" + panChange.ClientID + "').style.width='0px';"); li.Attributes.Add("onclick", Page.ClientScript.GetPostBackClientHyperlink(this, (i + 1).ToString())); } } 可以看到在列表项的点击事件(onclick)中触发了服务器回发,这里是通过 Page.Clien tScript.GetPostBackClientHyperlink 方法完成的,该方法传递两个参数,第一个参数引用引起 回发的服务器控件,第二个参数标识回发时传递的参数,这里传递了星形图案的索引。 ASP.NET自定义控件实例教程 第 35 页 为什么不在创建列表的同时注册 JavaScript 脚本,这是由于在 CreateChildControls 方法 执行时,各服务器端控件的 ClientID 尚未生成,此时注册的脚本在客户端操作时会发生 错误。 3.12 为了能够触发事件,首先编写事件参数类 StarEventArgs,该类定义了 Score 属性 保存得分: public class StarEventArgs : EventArgs { public int Score { get; set; } } 3.13 在 PostStar 中声明事件属性,注意这里和上次任务不同,将事件声明为私有字段 并通过属性暴露出来(相比以前的用法更推荐使用这种用法): private event EventHandler _postScore; public event EventHandler PostScore { add { _postScore += value; } remove { _postScore -= value; } } 3.14 编写 OnPostScore 方法调用事件并从控件层次中取得相应的层以固定得分显示: private void OnPostScore(object sender, StarEventArgs e) { if (_postScore != null) _postScore(sender, e); TableCell cell = ((Table)this.Controls[0]).Rows[0].Cells[1]; Panel panCur = (Panel)cell.Controls[0].Controls[1]; panCur.Style.Add("width", e.Score * 16 + "px"); Panel panChange = (Panel)panCur.Controls[0]; panChange.Style.Add("display", "none"); HtmlGenericControl ul = (HtmlGenericControl)cell.Controls[0].Controls[0]; ul.Style.Add("display", "none"); ASP.NET自定义控件实例教程 第 36 页 } 3.15 实现 IPostBackEventHandler 接口的 RaisePostBackEvent 方法,在客户端引起服务 器回发后会执行此方法并传递相应的参数,我们只需要在此方法中确保已创建了所有的子控 件后引发相应的事件以设置属性即可: public void RaisePostBackEvent(string args) { if (!string.IsNullOrEmpty(args)) { EnsureChildControls(); int score = Convert.ToInt32(args); StarEventArgs e = new StarEventArgs(); e.Score = score; OnPostScore(this, e); } } 3.16 在 Web 网站中创建测试页面,注册并声明自定义控件: <%@ Register TagPrefix="cc" Assembly="ControlLibrary" Namespace=”ControlLibrary” %> 附录:样式表文件: .stars {width: 80px; height: 16px; text-align: left; overflow: hidden; position: relative; background: url(stars.gif) 0px -32px repeat-x;} .stars .ulist { list-style: none; position: absolute; bottom:0px; margin:0px; padding: 0; } .stars .ulist li { display: inline; float: left; width: 16px; height: 16px; cursor: pointer; overflow: hidden; } .stars .current {width: 0px; height: 16px; background: url(stars.gif) 0px 0px repeat-x;} .stars .change {width: 0px; height: 16px; background: url(stars.gif) 0px -16px repeat-x;} 4.总结 本次任务里我们创建了一个可以由用户评分的自定义控件,PostStart 类继承了 IPostEve ntHandler 接口以引发服务器提交,通过引入该接口,我们可以根据需要使任何一个控件引 起服务器提交——只需要注册相应的 JavaScript 事件即可;同时为了防止页面使用多个评分 控件时出现错误,实现了 INamingContainer 接口。在接下来的任务里,将介绍自定义数据绑 定控件的开发方法。 ASP.NET自定义控件实例教程 第 37 页 第六天 可以绑定数据源的星级控件 1. 引言 以上几个任务里,我们创建了一些简单但很实用的自定义控件,但是它们只能按照固定 的设置进行呈现,缺少一些关键的特征——数据绑定和有时为了更灵活的控制以支持模版设 置。在 ASP.NET 数据绑定控件分为三种: z 简单数据绑定:简单数据绑定将一个对象与某个控件的属性绑定在一起。数据源只 是绑定单个数据项,而不是绑定一个数据项列表。简单数据绑定使用数据绑定表达 式完成,数据绑定表达式是用<%#...%>封装的任何可执行代码。 z 列表控件:列表控件是通过一个固定不变的用户界面显示一个数据项列表的控件。 常见的列表控件包含 RadioButtonList 控件、CheckBoxList 控件和 ASP.NET2.0 中新 引入的 BulletedList 控件。 z 复杂数据绑定:复杂数据绑定控件通常是显示一组数据项的组合控件,它们有着灵 活的呈现机制,例如 GridView 控件就是一个复杂数据绑定控件。 为了使星级控件在使用时能够通过某个数据源显示数据,需要使该控件拥有数据绑定的 能力,使用时数据绑定方法代码看起来可能如下所示: private void BindData() { DataTable table = new DataTable(); DataColumn col = new DataColumn("Comment",typeof(string)); table.Columns.Add(col); col = new DataColumn("Score",typeof(int)); table.Columns.Add(col); DataRow row = table.NewRow(); row[0] = "Vista"; row[1] = 3; table.Rows.Add(row); table.AcceptChanges(); star.DataSource = table; star.DataTextField = "Comment"; star.DataValueField = "Score"; star.DataBind(); } ASP.NET自定义控件实例教程 第 38 页 2. 分析 在开始列表控件之前再来讨论一下简单数据绑定,前几次开发的星级控件就是一个简单 数据绑定控件,我们直接可以为他的某个属性使用数据绑定表达式赋值,例如在 StartTest.a spx 中编写如下代码使用自定义控件: 在页面中预览——很不巧,当前日期并没有显示在页面上,这是由于在页面中定义的任 何数据绑定表达式,只有在调用 DataBound 方法之后才会进行计算。我们有多种选择,既 可以调用页面对象( Page)的 DataBind 方法,也可以调用具体控件上的 DataBind 方法,实 际上,如果调用页面对象上的 DataBind 方法,它将递归的调用页面中定义的所有控件上的 DataBind 方法。这就意味着,如果页面上使用了多个数据绑定表达式,最好还是通过调用 P age.DataBind 方法执行数据绑定。 如果想了解在数据绑定时底层究竟执行了什么操作,可以打开 ASP.NET 的调试功能, 并改变临时文件目录,修改 web.config 中 complcation 配置节如下所示: …… …… 以上配置启动了调试并将临时目录设置为 C:\Web(如果不设置 tempDirectory 属性临时 文件将放置在默认%Windows%\Microsoft.NET\Framework\%Version%\Temporary ASP.NET Files 目录下)。在浏览器预览 StarTest.aspx 页面后可以在临时目录中发现 StarTest 两个分 部类定义,其中一个定义了后置代码,另一个用于生成 ASPX 页面。在生成 ASPX 页面的 类文件中可以找到@__BuildControlstar 方法(实际上还有一系列的@__BuildControlXXX 方 法,XXX 对应于控件的 ID),在该方法中包含如下代码: @__ctrl.DataBinding += new System.EventHandler(this.@__DataBindingstar); 相应的,可以在此文件中找到@__DataBindingstar 方法,该方法如下所示: public void @__DataBindingstar(object sender, System.EventArgs e) { ControlLibrary.Star dataBindingExpressionBuilderTarget; System.Web.UI.Page Container; dataBindingExpressionBuilderTarget = ((ControlLibrary.Star)(sender)); Container = ((System.Web.UI.Page)(dataBindingExpressionBuilderTarget.BindingContainer) ); #line 13 "E:\Documents and Settings\holywolf\My Documents\Books\教学\自 定义控件\ControlSolution\Web\StarTest.aspx" dataBindingExpressionBuilderTarget.Comment = System.Convert.ToString(DateTime.Now, System.Globalization.CultureInfo.CurrentCulture); #line default ASP.NET自定义控件实例教程 第 39 页 #line hidden } 如果数据绑定表达式不匹配期望的类型,则通常会得到一个编译错误,然而,如果期望 的类型是 string,则解析器通过 Convert.ToString 方法实现标准转换。 接下来讨论如何使星级控件拥有数据绑定的能力,首先我们来观查 ASP.NET2.0 中数据 绑定类层次结构: 可以看到,所有类都继承自 BaseDataBoundControl 类,该类是 ASP.NET2.0 数据绑定控 件的基类。BasedataBoundControl 类上定义了如何进行数据绑定和如何验证所绑定的数据的 方式,并且该类上还定义了两个数据源属性:用于可枚举数据的 DataSource 属性和用于数 据源控件的 DataSourceID 属性。 DataSource 属性接受一个实现了 IEnumerable 接口(例如集合)或 IListSource 接口(例 如 DataTable)的对象,设置该属性后,必须调用该控件或页面的 DataBind 方法才会真正的 将数据填充的控件中。 DataBoundControl 类继承自 BaseDataBoundControl 类,该类中提供了一个重要的方法 P erformDataBinding 方法,在编写派生自 DataBoundControl 类的自定义控件时,需要编写此 方法实现以加载数据,该方法定义如下: protected virtual void PerformDataBinding(IEnumerable data) { …… …… } 在创建自定义数据绑定控件时,一般需要处理以下内容: z 定义相关的 DataXXXField 指定绑定到数据源映射 z 添加数据项属性并手动管理视图状态以获得更高的效率 z 重载 PerformDataBinding 方法从数据源中读取数据并缓存到数据项中 z 根据数据项中保存的值正确的呈现控件 根据以上分析,可以在修改原有星级控件,使之继承自 DataBoundControl 以获得从数 据源读取数据的能力。 ASP.NET自定义控件实例教程 第 40 页 3. 实现 3.1 向解决方案中 ControlLibrary 类库中添加 StarDataItem 类作为保存数据的数据项, 为了能够管理视图状态,该类实现了 IStateManager 接口: using System; using System.Web.UI; namespace ControlLibrary { public class StarDataItem:IStateManager { } } 3.2 在该类中添加得分和注释属性并添加构造函数: public string Comment { get; set; } public int Score { get; set; } public StarDataItem() { Comment = string.Empty; Score = 0; } public StarDataItem(string comment, int score) { Comment = comment; Score = score; } 3.3 实现 IStateManager 接口相关方法以管理视图状态: private bool _mark; public bool IsTrackingViewState { ASP.NET自定义控件实例教程 第 41 页 get { return _mark; } } public object SaveViewState() { Pair p = new Pair(Comment, Score); return p; } public void LoadViewState(object savedState) { if (savedState != null) { Pair p = (Pair)savedState; Comment = Convert.ToString(p.First); Score = Convert.ToInt32(p.Second); } } public void TrackViewState() { _mark = true; } 3.4 添加 DataBoundStar 类,该类继承了 DataBoundControl 类: using System; using System.Collections; using System.Web.UI; using System.Web.UI.WebControls; namespace ControlLibrary { public class DataBoundStar : DataBoundControl { } } 3.5 在该类中定义数据项属性( StarDataItem 类型): private StarDataItem _data; ASP.NET自定义控件实例教程 第 42 页 public StarDataItem DataItem { get { if (_data == null) _data = new StarDataItem(); if (IsTrackingViewState) _data.TrackViewState(); return _data; } } 3.6 为类增加 DataTextField 和 DataValueField 属性定义与数据源的映射: public virtual string DataValueField { get { object o = ViewState["DataValueField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataValueField"] = value; } } public virtual string DataTextField { get { object o = ViewState["DataTextField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataTextField"] = value; } } 3.7 重写 PerformDataBinding 方法,根据映射从数据源中读取数据填充到数据项中: protected override void PerformDataBinding(IEnumerable data) { ASP.NET自定义控件实例教程 第 43 页 if (data == null) return; IEnumerator e = data.GetEnumerator(); e.MoveNext(); if (!string.IsNullOrEmpty(DataTextField)) DataItem.Comment = Convert.ToString(DataBinder.GetPropertyValue(e.Current, DataTextField)); if (!string.IsNullOrEmpty(DataValueField)) DataItem.Score = Convert.ToInt32(DataBinder.GetPropertyValue(e.Current, DataValueField)); } 该方法接收 IEnumerable 类型的参数用于迭带访问数据源中的数据,在读取数据源中的 数据时使用了 DataBinder 类,该类上有一个实用的 GetPropertyValue 方法,用于根据属性反 射的读取数据源中的值,此处我们使用该方法并传递了 DataTextField 和 DataValueField 以 读取注释和评分。 3.8 重写 CreateChildControls 方法, 调用 CreateControlHierarchy 方法以创建子控件层次: protected override void CreateChildControls() { base.CreateChildControls(); CreateControlHierarchy(); } protected virtual void CreateControlHierarchy() { Table table = new Table(); TableRow row = new TableRow(); table.Rows.Add(row); TableCell comment = new TableCell(); CreateComment(comment); row.Cells.Add(comment); TableCell stars = new TableCell(); CreateStars(stars); row.Cells.Add(stars); this.Controls.Add(table); } ASP.NET自定义控件实例教程 第 44 页 3.9 如上所示在创建子控件时调用了 CreateComment 和 CreateStars 方法,用于创建注释 和星形图案,并且相关数据均由 DataItem 属性读取,以下是这两个方法的实现: private void CreateComment(TableCell cell) { Label lbl = new Label(); lbl.Text = DataItem.Comment; cell.Controls.Add(lbl); } private void CreateStars(TableCell cell) { string starPath = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.Image.stars.gif"); Panel panBg = new Panel(); panBg.Style.Add(HtmlTextWriterStyle.Width, "80px"); panBg.Style.Add(HtmlTextWriterStyle.Height, "16px"); panBg.Style.Add(HtmlTextWriterStyle.TextAlign, "left"); panBg.Style.Add(HtmlTextWriterStyle.Overflow, "hidden"); panBg.Style.Add(HtmlTextWriterStyle.Position, "relative"); panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panBg.Style.Add("background-position", "0px -32px"); panBg.Style.Add("background-repeat", "repeat-x"); cell.Controls.Add(panBg); Panel panCur = new Panel(); string width = DataItem.Score * 16 + "px"; panCur.Style.Add(HtmlTextWriterStyle.Width, width); panCur.Style.Add(HtmlTextWriterStyle.Height, "16px"); panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panCur.Style.Add("background-position", "0px 0px"); panCur.Style.Add("background-repeat", "repeat-x"); panBg.Controls.Add(panCur); } 3.10 重写 SaveViewState 和 LoadViewState 方法将 DataItem 数据项保存到视图状态中并 能够正确的恢复: protected override object SaveViewState() { object o= base.SaveViewState(); Pair p = new Pair(); ASP.NET自定义控件实例教程 第 45 页 p.First = o; p.Second = DataItem.SaveViewState(); return p; } protected override void LoadViewState(object savedState) { if (savedState != null) { Pair p = (Pair)savedState; base.LoadViewState(p.First); DataItem.LoadViewState(p.Second); } } 3.11 最后重写 Render 方法呈现控件: protected override void Render(HtmlTextWriter writer) { PrepareControlForReader(); base.Render(writer); } private void PrepareControlForReader() { if (this.Controls.Count < 1) return; Table table = (Table)this.Controls[0]; table.CellSpacing = 0; table.CellPadding = 0; } 3.12 在 Web 网站中添加页面,声明并定义自定义控件: 3.13 编写类似于本次任务开始的代码以执行数据绑定,并在浏览器预览结果。 4. 总结 本次任务里我们再次为星级控件增加了新的特性——数据绑定,为了实现这个目的,自 定义控件继承了 DataBoundControl 类,实现了其中的一个重要方法 PerformDataBinding,通 ASP.NET自定义控件实例教程 第 46 页 过该方法的 IEnumerable 参数迭代访问数据源,并调用 DataBinder.GetPropertyValue 方法获 取数据源映射的值,最后为了更合理的保存数据,定义了 StarDataItem 类作为自定义控件的 属性。在下次任务里,我们将一起开发一个列表控件。 ASP.NET自定义控件实例教程 第 47 页 第七天 开发具有丰富特性的列表控件 1. 引言 在上次任务里,我们为星级控件增加了数据绑定的特性,但是在实际运用中还会产生更 多的需求,例如用户可能希望创建一个课程列表(如图 1): 或者在数据项比较多的时候,能够手动控制数据的排列方式(图 2) 本次任务中,我们将一起开发这样的控件。 2. 分析 以上两个图例中显示的都是列表控件,在 ASP.NET2.0 中 ListControl 类是列表控件的父 类,通过上次任务的分析可以了解 CheckBoxList、RadioButtonList 和 DropDownList 等控件 均继承自 ListControl 类,这些列表控件都是对于每一个数据项重复的应用一个样式,全如 C heckBoxList 对于每个列表项显示一个复选框,而 RadioButtonList 对于每个列表项显示一个 单元框。实际上,列表控件中的每一个列表项都是 ListItem 类型的, 而且为了显示一个列表, 列表控件常常拥有每一个元素都是 ListItem 类型的集合, 也就是我们经常用到的 Items 属性, 该属性在 ListControl 类上定义, ListControl 类还拥有许多其他非常有用的属性: 属性 描述 AppendDataBoundItems 获取或设置一个值,指示是否在绑定数据之前清除列表项 DataTextField 获取或设置为列表项提供文本内容的数据源字段 DataTextFormatString 获取或设置格式化字符串,该字符串用来控制如何显示绑定到列表控件 的数据 ASP.NET自定义控件实例教程 第 48 页 DataValueField 获取或设置为各列表项提供值的数据源字段 SelectedIndex 获取或设置列表中选定项的最低序号索引 SelectedItem 获取列表控件中索引最小的选定项 SelectedValue 获取列表控件中选定项的值,或选择列表控件中包含指定值的项 在以上属性中,AppendDataBoundItems 属性是.NET2.0 中新增的,该属性在列表中已经 手动定义某些项目后再执行数据绑定时特别有用。例如下拉列表中,可能希望为用户添加“请 选择”项目,那么就可以使用标记定义该列表项,并将 AppendDataBoundIt ems 属性设置为 true,在该下拉列表再次执行数据绑定时会保留“请选择”项目。 对于本次任务的第一个需求,只需要使其继承自 ListControl 类,并且在呈现时将每一 个数据项输出为一个超链接即可。但是该列表控件有一些局限,它只能采用 ListItem 类作为 数据项对象类型,如果希望修改或添加列表项属性时,此类列表控件就无能为力了,为了实 现这种目的(例如第二个需求),需要做一些额外的工作。 接下来考虑第二个需求,需要在数据绑定的基础上为自定义控件增加按照指定的方向呈 现数据项的能力,这有点像 DataList 控件,可以通过设置 RepeatDirection、RepeatColumns 等属性实现上述功能。对于此类问题, .NET 为我们提供了 IRepeatInfoUser 接口,该接口包 含了以下成员的声明: 成员 描述 HasFooter 属性 获取一个值,指示列表控件是否包含脚注部分 HasHeader 属性 获取一个值,指示列表控件是否包含标题节 HasSeparators 属性 获取一个值,指示列表控件是否包含列表项之间的分隔符 RepeatedItemCount 属性 获取列表控件中的项数 GetItemStyle 方法 检索列表控件中指定索引位置的指定项类型的样式 RenderItem 用指定的信息呈现列表中的项 为了实现新特性,使自定义控件实现 IRepeatInfoUser 接口,重要的是该接口中的 Rend erItem 方法,每个绑定的数据项都自动调用此方法,此方法签名如下所示: void RenderItem ( ListItemType itemType, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer ) 其参数说明如下: 参数 描述 itemType ListItemType 枚举值之一,该枚举指定列表控件中项的类型 repeatIndex 指定列表控件中项的位置的序号索引 repeatInfo RepeatInfo 类型对象,表示用于呈现列表中的项的信息 writer System.Web.UI.HtmlTextWriter 类实例,表示用于在客户端呈现 HTML 内 容的输出流 最后如果控件想要支持特殊的布局功能,还需要使用特殊的代码重写自定义控件的 Re nder 方法,在该方法中使用 RepeatInfo 类的 RenderPrpeater 方法呈现控件,以下是该方法的 定义: ASP.NET自定义控件实例教程 第 49 页 public void RenderRepeater ( HtmlTextWriter writer, IRepeatInfoUser user, Style controlStyle, WebControl baseControl ) 其参数说明如下: 参数 描述 writer System.Web.UI.HtmlTextWriter 类实例,表示用于在客户端呈现 HTML 内 容的输出流 user 一个实现了 IRepeatInfoUser 接口的对象,表示要呈现的控件 controlStyle 表示显示项所采用的样式 baseControl 复制基属性的控件 根据以上分析,编写两个自定义控件实现数据项的输出。 3. 实现简单列表控件 3.1 在解决方案的 ControlLibrary 类库中创建 SimpleHyperLinkList 类,并引入必要命名 空间: using System.Web.UI; using System.Web.UI.WebControls; namespace ControlLibrary { [ToolboxData("<{0}:SimpleHyperLinkList runat=\"server\">")] public class SimpleHyperLinkList : ListControl { } } 3.2 重写控件的 Render 方法,在呈现时迭代访问数据项以呈现多个链接控件,此处调 用了 RenderControl 方法,该方法在 Control 类上定义,用于将服务器控件的内容输入到所提 供的 HtmlTextWriter 对象中: protected override void Render(HtmlTextWriter writer) { HyperLink link = new HyperLink(); for (int i = 0; i < Items.Count; i++) { link.ApplyStyle(ControlStyle); link.Text = Items[i].Text; link.NavigateUrl = Items[i].Value; ASP.NET自定义控件实例教程 第 50 页 link.RenderControl(writer); writer.Write("
"); } } 这个类的代码很简单,唯一比较有趣的地方是类使用了 ToolboxData 特性(对应 Syste m.Web.UI.ToolboxDataAttribute 类),该特性指定了将控件从可视化设计器的工具箱中拖放 到设计页面上时默认生成的标记,在需要为自定义控件指定属性的初始值时该特性很有用。 3.3 在 ASPX 页面中声明并定义控件,浏览运行结果。 4. 实现丰富特性列表控件 4.1 在 ControlLibrary 类库中创建 HyperLinkItem 实体类,用于保存最终生成的每一个链 接的文本、注释和地址字符串: namespace ControlLibrary { public class HyperLinkItem { public HyperLinkItem() { Text = string.Empty; Tooltip = string.Empty; Url = string.Empty; } public HyperLinkItem(string text, string tooltip, string url) { Text = text; Tooltip = tooltip; Url = url; } public string Text { get; set; } public string Tooltip { get; set; } ASP.NET自定义控件实例教程 第 51 页 public string Url { get; set; } } } 4.2 在类库中创建 HyperLinkItemCollection 集合类,该类继承了 Collection 泛型集合类, 以确保该集合中只参添加 HyperLinkItem 类型对象,并且为了手动管理视图状态,该类实现 了 IStateManager 接口: using System.Collections.ObjectModel; using System.Web.UI; namespace ControlLibrary { public class HyperLinkItemCollection:Collection,IStateManager { private bool marked; public HyperLinkItemCollection() { marked = false; } public bool IsTrackingViewState { get { return this.marked; } } public void TrackViewState() { marked = true; } public void LoadViewState(object state) { if (state == null) return; ASP.NET自定义控件实例教程 第 52 页 Clear(); Triplet trip = (Triplet)state; string[] rgTooltip = (string[])trip.First; string[] rgText = (string[])trip.Second; string[] rgUrl = (string[])trip.Third; for (int i = 0; i < rgUrl.Length; i++) { Add(new HyperLinkItem(rgText[i], rgTooltip[i], rgUrl[i])); } } public object SaveViewState() { int num = Count; object[] rgTooltip = new string[num]; object[] rgText = new string[num]; object[] rgUrl = new string[num]; for (int i = 0; i < num; i++) { rgTooltip[i] = Items[i].Tooltip; rgText[i] = Items[i].Text; rgUrl[i] = Items[i].Url; } return new Triplet(rgTooltip, rgText, rgUrl); } } } 以上代码稍微有些复杂的就是视图状态的保存了读取,为了保证将某一个 HyperLinkIt em 的三个属性全部保存至视图状态中, 定义了三个 object 数组最终保存到 Triplet 对象中 (读 取时仍遵循此规则)。 4.3 向类库中添加 HyperLinkList 类,为了使该类具有更丰富的展示方式,仍然继承自 D ataBindControl 类并实现 IRepeatInfoUser 接口: using System.Collections; using System.Web.UI; using System.Web.UI.WebControls; namespace ControlLibrary ASP.NET自定义控件实例教程 第 53 页 { [ToolboxData("<{0}:HyperLinkList runat=\"server\">")] public class HyperLinkList:DataBoundControl,IRepeatInfoUser { } } 4.4 定义控件内部使用的重复项属性和暴露出来的集合属性,在 HyperLinkList 类中编 写如下代码: private HyperLinkItemCollection items; private HyperLink controlToRepeat; private HyperLink ControlToRepeat { get { if (controlToRepeat == null) { controlToRepeat = new HyperLink(); } return controlToRepeat; } } public virtual HyperLinkItemCollection Items { get { if (items == null) items = new HyperLinkItemCollection(); if (base.IsTrackingViewState) items.TrackViewState(); return items; } } 4.5 定义数据源映射属性 DataTextField、DataValueField 和 DataTooltipField 以从数据源 中读取相应的数据填充重复项: public virtual string DataTextField { get { object o = ViewState["DataTextField"]; ASP.NET自定义控件实例教程 第 54 页 return o == null ? string.Empty : (string)o; } set { ViewState["DataTextField"] = value; } } public virtual string DataTooltipField { get { object o = ViewState["DataTooltipField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataTooltipField"] = value; } } public virtual string DataUrlField { get { object o = ViewState["DataUrlField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataUrlField"] = value; } } 4.6 重写 PerformDataBinding 方法根据映射从数据源读取数据: protected override void PerformDataBinding(IEnumerable data) { base.PerformDataBinding(data); string urlField = DataUrlField; string textField = DataTextField; string tooltipField = DataTooltipField; ASP.NET自定义控件实例教程 第 55 页 if (data != null) { foreach (object o in data) { HyperLinkItem item = new HyperLinkItem(); item.Url = DataBinder.GetPropertyValue(o, DataUrlField,null); item.Text = DataBinder.GetPropertyValue(o, DataTextField,null); item.Tooltip = DataBinder.GetPropertyValue(o, DataTooltipField,null); Items.Add(item); } } } 4.7 重写 SaveViewState 和 LoadViewState 方法保存或读取数据项集合: protected override object SaveViewState() { object o= base.SaveViewState(); Pair p = new Pair(o, Items.SaveViewState()); return p; } protected override void LoadViewState(object savedState) { if (savedState==null) return; Pair p = (Pair)savedState; base.LoadViewState(p.First); Items.LoadViewState(p.Second); } 4.8 定义 RepeatDirection、RepeatColumns 和 RepeatLayout 属性供 RepeatInfo 对象使用 以控制重复项的呈现: public virtual RepeatDirection RepeatDirection { get { object o = ViewState["RepeatDirection"]; ASP.NET自定义控件实例教程 第 56 页 return o == null ? RepeatDirection.Vertical : (RepeatDirection)o; } set { ViewState["RepeatDirection"] = value; } } public virtual int RepeatColumns { get { object o = ViewState["RepeatColumns"]; return o == null ? 0 : (int)o; } set { ViewState["RepeatColumns"] = value; } } public virtual RepeatLayout RepeatLayout { get { object o = ViewState["RepeatLayout"]; return o == null ? 0 : (RepeatLayout)o; } set { ViewState["RepeatLayout"] = value; } } 4.9 实现 IRepeatInfoUser 接口相关成员,此处为了方便,并没有增加新的特性: bool IRepeatInfoUser.HasFooter { get { return false; } } ASP.NET自定义控件实例教程 第 57 页 bool IRepeatInfoUser.HasHeader { get { return false; } } bool IRepeatInfoUser.HasSeparators { get { return false; } } Style IRepeatInfoUser.GetItemStyle(ListItemType type, int repeatIndex) { return null; } 4.10 实现 IRepeatInfoUser 接口的 RepeatedItemCount 属性,返回当前输出项的数目: int IRepeatInfoUser.RepeatedItemCount { get { return this.Items.Count; } } 4.11 实现 IRepeatInfoUser 接口的 RenderItem 方法以呈现数据项,该方法调用了 Contro lToRepeat 私有属性以得到一个超链接: void IRepeatInfoUser.RenderItem(ListItemType type, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer) { HyperLink link = ControlToRepeat; int i = repeatIndex; link.ID = i.ToString(); link.Text = Items[i].Text; link.NavigateUrl = Items[i].Url; link.ToolTip = Items[i].Tooltip; link.RenderControl(writer); ASP.NET自定义控件实例教程 第 58 页 } 4.12 最后重写 Render 方法呈现控件,根据分析,这里使用 RepeatInfo 类: 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); } } 4.13 创建测试 ASPX 页面,声明并定义自定义控件,预览运行结果。 5. 总结 本次任务里,我们编写了一个继承自 ListControl 类的简单列表控件,在此基础上,我 们再次开发了一个继承自 DataBindControl 类,并且为了实现丰富的布局功能,还实现了 IR epeatInfoUser 类,在呈现方法中,通过 RepeatInfo 类按照定义格式输入数据项。在下一个任 务里,我们将开始一个稍复杂的看起来和 GridView 有些相似的组合数据绑定控件以显示一 系列星级评分的列表。 ASP.NET自定义控件实例教程 第 59 页 第八天 可以显示多个条目星级评分的列表控件 1. 引言 前几次任务里我们开发的星级控件只能显示一个条目的评分,在现实生活中,经常会遇 到需要向用户展示一系列数据的评分状态,例如下图所示: 本次任务里,我们将一起开发这样一个控件。 2. 分析 通过上图可以看到,该自定义控件是一系列数据评分等级的列表,很显然需要作为一个 数据绑定控件来实现才可以灵活的显示多条数据,并且在该列表上方显示了标题和当前的日 期,为了允许用户灵活的定义标题和二级标题(当前日期),有必要引入模版的概念,由用 户编辑模版,最终按照模版内容显示。如此看来再使用 DataBoundControl 作为自定义控件 的基类就不太适合了,因为我们要在该控件中包含多个子控件,那么我们应该选择哪个类作 为基类? 回忆一下第六天的任务中数据绑定控件的类关系图,其中有一个继承自 DataBoudContr ol 类的 CompositeDataBoundControl 类,该类是.NET Framework 2.0 中新增的一个类,用作 绑定到数据源中的数据服务器控件的基类,该定义定义如下: public abstract class CompositeDataBoundControl : DataBoundControl, INamingContainer 可以看到,该在只是在继承 DataBoundControl 类的基础上实现了 INamingContainer,这 意味着该类所包含的子控件都会生成唯一的 ID 属性。 但是 CompositeDataBoundControl 是如何实现数据绑定的呢,换句话说,如果某一个页 面包含了一个复杂数据绑定控件,在某一个服务器端控件引起回发后,如何确保数据绑定控 件能正确的被填充呢?按照设计,ASP.NET 中的数据绑定组合控件只能从数据绑定中获取 数据,并且不会缓存任何绑定的数据,因此,需要提供一个特殊的方法来处理回发事件。 再次回顾一下 DataBoundControl 这个类,我们来分析一下数据是如何被显示出来的。 在 DataBoundControl 类上重载了 BaseDataBoundControl 类上定义的 PerformSelect 方法,该 ASP.NET自定义控件实例教程 第 60 页 方法如下所示: protected override void PerformSelect() { if (this.DataSourceID.Length == 0) { this.OnDataBinding(EventArgs.Empty); } DataSourceView data = this.GetData(); this._arguments = this.CreateDataSourceSelectArguments(); this._ignoreDataSourceViewChanged = true; base.RequiresDataBinding = false; this.MarkAsDataBound(); data.Select(this._arguments, new DataSourceViewSelectCallback(this.OnDataSourceViewSelectCallback)); } 该方法的关键在于不论数据源是通过 DataSource 属性指定还是通过 DataSourceID 属性 指定,最终都是通过数据源视图对象( DataSourceView)来获取数据,在获取数据源视图之 后调用了该对象的 Select 方法,以获取可枚举的数据集合 (不论数据源是以何种方式提供), 该方法还接收多个输入参数和回调函数。 到现在为止,我们已经可以通过为控件设置数据源数据并且通过数据源视图找到要读取 的数据,那么接下来要考虑的是,如果访问这个可绑定的数据集合以将其显示到页面上? 答案是在数据源视图上处理 Select 语句操作结果的回调函数最后会调用一个可重载的 受保护方法 PerformDataBinding 方法,该方法定义如下: protected internal virtual void PerformDataBinding ( IEnumerable data ) PerformDataBinding 方法接收一个 IEnumerable 类型的对象以访问返回的数据列表,我 们可以利用该对象迭代访问数据并根据需要创建控件的内部结构最终呈现给用户。 对于数据绑定组合控件,和独自处理数据绑定的控件不同,它不将数据保存到视图中, 而是交由各个子控件处理,此时,就不能通过简单的重写 CreateChildControls 方法实现了, 通过.NET Reflection 工具观察 CompositeDataBindControl 类上该方法的实现: protected internal override void CreateChildControls() { this.Controls.Clear(); object obj2 = this.ViewState["_!ItemCount"]; if ((obj2 == null) && base.RequiresDataBinding) { this.EnsureDataBound(); } if ((obj2 != null) && (((int)obj2) != -1)) { DummyDataSource dataSource = new DummyDataSource((int)obj2); ASP.NET自定义控件实例教程 第 61 页 this.CreateChildControls(dataSource, false); base.ClearChildViewState(); } } CreateChildControls 方法有两种工作模式:绑定模式和非绑定模式。 在绑定模式下,将会正常创建控件树,具体来说会在执行 PerformDataBinding 方法中调 用重载的 CreateChildControls 方法,以手动的实现数据控件层次的创建; protected internal override void PerformDataBinding(IEnumerable data) { base.PerformDataBinding(data); this.Controls.Clear(); base.ClearChildViewState(); this.TrackViewState(); int num = this.CreateChildControls(data, true); base.ChildControlsCreated = true; this.ViewState["_!ItemCount"] = num; } 而在非绑定模式中,将从 CreateChildControls 方法中调用带有两个参数的重载: DummyDataSource dataSource = new DummyDataSource((int) obj2); this.CreateChildControls(dataSource, false); base.ClearChildViewState(); 在这种情况下,传递给 CreateChildControls 方法的第二个参数为 false,这表示不会向控 件层次添加任何数据。ASP.NET 回发机制确保每一个子控件正确的从视图中恢复自己的值。 接下来观察在 CompositeDataBindControl 类上定义的 CreateChildControls 方法的重载: protected abstract int CreateChildControls ( IEnumerable dataSource, bool dataBinding ) 该方法的参数说明如下: 参数 说明 dataSource IEnumerable 类型,包含要绑定到控件的值 dataBinding 如果为 true ,则指示在数据绑定期间调用 CreateChildControls(IEnumerable,Boolean) 那么我们需要做的就是继承 CompositeDataBindControl 类,并实现传递两个参数的 Cre ateChildControls 方法,在该方法中创建子控件层次。另外,在该自定义控件中,我们还定 义了一个控件项集合属性以持有相关数据。 为了实现模版,需要定义一个模版属性,使用 TemplateContainer 特性声明命名容器的 类型,在创建模版后该模版将包含在该容器中,此外还需要使用 PersistenceMode 特性指出 在主页中如何以声明的方式永久存储一个控件属性,该属性可以取以下值: ASP.NET自定义控件实例教程 第 62 页 属性 描述 Attribute 在最终的标记中,该属性永久存储为一个已编码的 HTML 特性 EncodedInnerDefaultProperty 该属性存储为该控件的内部文本。该属性的值是编码的 HTML,只能 将字符串赋给该属性 InnerDefaultProperty 永久存储为控件中的内部文本的属性并且是元素的默认属性,只有一个 属性能够指定为默认属性 InnerProperty 该属性永久存储为控件中的一个嵌入标记,这是使用模版和风格的复杂 对象常用的一个属性。 模版属性示例如下所示: private ITemplate _titleTemplate; [PersistenceMode(PersistenceMode.InnerProperty)] [TemplateContainer(typeof(TitleTemplateContainer))] public ITemplate TitleTemplate { get { return _titleTemplate; } set { _titleTemplate = value; } } 3. 实现 3.1 在解决方案 ControlLibrary 类库中创建 BarChartItem 类用于表示控件项: using System; using System.Collections.ObjectModel; using System.Web.UI; using System.Web.UI.WebControls; public class BarChartItem:TableRow { public BarChartItem(BarChartItemType itemType) { ItemType = itemType; } public BarChartItemType ItemType { get; set; ASP.NET自定义控件实例教程 第 63 页 } public object DataItem { get; set; } } 3.2 在 BarChartItem 类中使用了 BarChartItemType 枚举以标识当前项的类型(类似于 G ridView 中的标题、交替行等),编写该枚举实现: public enum BarChartItemType { Title, //标题 SubTitle, //二级标题 Item, //条形图项 Footer //脚注项 } 3.3 定义该控件项的集合类 BarChartItemCollection: public class BarChartItemCollection : Collection { } 3.4 为了能够在创建项和绑定数据后暴露出相应的事件中使用户能够访问数据项信息, 编写自定义事件参数类: public class BarChartItemEventArgs : EventArgs { private BarChartItem _item; public BarChartItemEventArgs(BarChartItem item) { _item = item; } public BarChartItem Item { get { return _item; } } } 3.5 定义模版容器类 public class TitleTemplateContainer : WebControl, INamingContainer ASP.NET自定义控件实例教程 第 64 页 { private BarChart _parent; public TitleTemplateContainer(BarChart parent) { _parent = parent; } public string Title { get { return _parent.Title; } } public string SubTitle { get { return _parent.SubTitle; } } public BarChart BarChart { get { return _parent; } } } 3.6 在 ControlLibrary 类库中创建 BarChar 类并定义私有变量,该类继承自 CompositeD ataBoundControl 类: public class BarChart : CompositeDataBoundControl { public event EventHandler BarChartItemCreated; public event EventHandler BarChartItemDataBound; private BarChartItemCollection _items; private ITemplate _titleTemplate; private TitleTemplateContainer _titleTemplateContainer; private TableItemStyle _titleStyle; private TableItemStyle _subtitleStyle; ASP.NET自定义控件实例教程 第 65 页 private TableItemStyle _labelStyle; } 3.7 定义相关属性: public string DataTextField { get { object o = ViewState["DataTextField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataTextField"] = value; } } public string DataValueField { get { object o = ViewState["DataValueField"]; return o == null ? string.Empty : (string)o; } set { ViewState["DataValueField"] = value; } } public BarChartItemCollection Items { get { if (_items == null) { _items = new BarChartItemCollection(); } return _items; } } [Browsable(false)] [PersistenceMode(PersistenceMode.InnerProperty)] ASP.NET自定义控件实例教程 第 66 页 [TemplateContainer(typeof(TitleTemplateContainer))] public ITemplate TitleTemplate { get { return _titleTemplate; } set { _titleTemplate = value; } } public string Title { get { object o = ViewState["Title"]; return o == null ? string.Empty : (string)o; } set { ViewState["Title"] = value; } } public string SubTitle { get { object o = ViewState["SubTitle"]; return o == null ? string.Empty : (string)o; } set { ViewState["SubTitle"] = value; } } [PersistenceMode(PersistenceMode.InnerProperty)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [NotifyParentProperty(true)] [Description("设置标题样式")] public virtual TableItemStyle TitleStyle ASP.NET自定义控件实例教程 第 67 页 { get { if (_titleStyle == null) { _titleStyle = new TableItemStyle(); } if (IsTrackingViewState) ((IStateManager)_titleStyle).TrackViewState(); return _titleStyle; } } [PersistenceMode(PersistenceMode.InnerProperty)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [NotifyParentProperty(true)] [Description("设置子标题样式")] public virtual TableItemStyle SubTitleStyle { get { if (_subtitleStyle == null) { _subtitleStyle = new TableItemStyle(); } if (IsTrackingViewState) ((IStateManager)_subtitleStyle).TrackViewState(); return _subtitleStyle; } } [PersistenceMode(PersistenceMode.InnerProperty)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [NotifyParentProperty(true)] [Description("设置标签样式")] public virtual TableItemStyle LabelStyle { get { if (_labelStyle == null) ASP.NET自定义控件实例教程 第 68 页 { _labelStyle = new TableItemStyle(); // Can initialize the style HERE } if (IsTrackingViewState) ((IStateManager)_labelStyle).TrackViewState(); return _labelStyle; } } 3.8 定义事件响应方法: protected virtual void OnBarChartCreated(BarChartItemEventArgs e) { if (BarChartItemCreated != null) BarChartItemCreated(this, e); } protected virtual void OnBarChartDataBound(BarChartItemEventArgs e) { if (BarChartItemDataBound != null) BarChartItemDataBound(this, e); } 3.9 编写创建子控件层次方法: protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding) { return CreateControlHierarchy(dataSource, dataBinding); } private int CreateControlHierarchy(IEnumerable dataSource, bool dataBinding) { if (dataSource == null) { RenderEmptyControl(); return 0; } Table t = new Table(); Controls.Add(t); //创建标题 CreateTitle(t); ASP.NET自定义控件实例教程 第 69 页 //创子标题 CreateSubTitle(t); //创建进度条 int totalItems = CreateAllItems(t, dataSource, dataBinding); return totalItems; } 3.10 编写创建控件方法: private void RenderEmptyControl() { LiteralControl lbl = new LiteralControl("无数据"); Controls.Add(lbl); } private void CreateTitle(Table t) { BarChartItem item = new BarChartItem(BarChartItemType.Title); t.Rows.Add(item); TableCell cell = new TableCell(); item.Cells.Add(cell); cell.ColumnSpan = 2; if (TitleTemplate != null) { _titleTemplateContainer = new TitleTemplateContainer(this); TitleTemplate.InstantiateIn(_titleTemplateContainer); cell.Controls.Add(_titleTemplateContainer); } else { cell.Text = Title; } item.DataBind(); } private void CreateSubTitle(Table t) { BarChartItem item = new BarChartItem(BarChartItemType.SubTitle); ASP.NET自定义控件实例教程 第 70 页 t.Rows.Add(item); TableCell cell = new TableCell(); item.Cells.Add(cell); cell.ColumnSpan = 2; cell.Text = SubTitle; } private int CreateAllItems(Table t, IEnumerable data, bool useDataSource) { int itemCount = 0; Items.Clear(); foreach (object o in data) { BarChartItemType itemType = BarChartItemType.Item; BarChartItem item = CreateBarChartItem(t, itemType, o, useDataSource); _items.Add(item); itemCount++; } return itemCount; } private BarChartItem CreateBarChartItem(Table t, BarChartItemType itemType, object dataItem, bool useDataSource) { BarChartItem item = new BarChartItem(itemType); TableCell labelCell = CreateLabelCell(item); TableCell valueCell = CreateScoreCell(item); BarChartItemEventArgs argsCreated = new BarChartItemEventArgs(item); OnBarChartCreated(argsCreated); t.Rows.Add(item); if (useDataSource) ASP.NET自定义控件实例教程 第 71 页 { item.DataItem = dataItem; BindLabelCell(labelCell, dataItem); BindValueCell(valueCell, dataItem); BarChartItemEventArgs argsData = new BarChartItemEventArgs(item); OnBarChartDataBound(argsData); } return item; } private TableCell CreateLabelCell(BarChartItem item) { TableCell cell = new TableCell(); item.Cells.Add(cell); return cell; } private TableCell CreateScoreCell(BarChartItem item) { TableCell cell = new TableCell(); item.Cells.Add(cell); string starPath = Page.ClientScript.GetWebResourceUrl(this.GetType(), "ControlLibrary.Image.stars.gif"); Panel panBg = new Panel(); panBg.Style.Add(HtmlTextWriterStyle.Width, "80px"); panBg.Style.Add(HtmlTextWriterStyle.Height, "16px"); panBg.Style.Add(HtmlTextWriterStyle.TextAlign, "left"); panBg.Style.Add(HtmlTextWriterStyle.Overflow, "hidden"); panBg.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panBg.Style.Add("background-position", "0px -32px"); panBg.Style.Add("background-repeat", "repeat-x"); cell.Controls.Add(panBg); Panel panCur = new Panel(); ASP.NET自定义控件实例教程 第 72 页 panCur.Style.Add(HtmlTextWriterStyle.Height, "16px"); panCur.Style.Add(HtmlTextWriterStyle.BackgroundImage, starPath); panCur.Style.Add("background-position", "0px 0px"); panCur.Style.Add("background-repeat", "repeat-x"); panBg.Controls.Add(panCur); return cell; } private void BindLabelCell(TableCell cell, object dataItem) { if (!string.IsNullOrEmpty(DataTextField)) { string txt = Convert.ToString(DataBinder.GetPropertyValue(dataItem, DataTextField)); cell.Text = txt; } } /// /// 绑定显示的值 /// /// /// private void BindValueCell(TableCell cell, object dataItem) { Panel panCur = (Panel)cell.Controls[0].Controls[0]; object o = null; if (!String.IsNullOrEmpty(DataValueField)) o = DataBinder.GetPropertyValue(dataItem, DataValueField); else return; int score = Convert.ToInt32(o); string width = score * 16 + "px"; panCur.Style.Add(HtmlTextWriterStyle.Width, width); } protected override void Render(HtmlTextWriter writer) { PrepareControlForRender(); ASP.NET自定义控件实例教程 第 73 页 base.Render(writer); } public void PrepareControlForRender() { if (Controls.Count != 1) return; Table t = (Table)Controls[0]; t.CopyBaseAttributes(this); if (ControlStyleCreated) t.ApplyStyle(ControlStyle); t.Rows[0].Cells[0].MergeStyle(TitleStyle); t.Rows[1].Cells[0].MergeStyle(SubTitleStyle); // Apply style to the labels that render team names for (int i = 2; i < Items.Count; i++) { // Style team labels t.Rows[i].Cells[0].ApplyStyle(LabelStyle); } } 3.10 在页面中声明并定义自定义控件 <%# Container.Title %> (<%# DateTime.Now.ToString() %>) 3.11 编写后置代码,预览结果: protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) this.BindData(); } private void BindData() { DataTable table = new DataTable(); ASP.NET自定义控件实例教程 第 74 页 DataColumn col = new DataColumn("Comment", typeof(string)); table.Columns.Add(col); col = new DataColumn("Score", typeof(int)); table.Columns.Add(col); Random ran=new Random(); for (int i = 0; i < 10; i++) { DataRow row = table.NewRow(); int num= ran.Next(0, 6); row[0] = "数据 "+i+" , "+num+" 星"; row[1] = num; table.Rows.Add(row); } table.AcceptChanges(); bar.DataSource = table; bar.DataTextField = "Comment"; bar.DataValueField = "Score"; bar.DataBind(); } 4. 总结 复杂绑据绑定控件可以通过扩展 CompositeDataBindControl 类实现,该类有一个特殊的 CreateChildControls 方法实现数据的绑定和恢复,在下一次任务里,我们将尝试扩展 DataLi st 和 GridView 以实现自定义分页控件。 ASP.NET自定义控件实例教程 第 75 页 第九天 自定义 GridView 1. 引言 ASP.NET 2.0 里为我们提供了全新的 GridView 控件,它在 DataGrid 基础之上增加了许 多新的特性,例如不编写一行代码就可以和数据源控件结合起来实现数据的展示并且分页, 但是这种分页效率较低,这是从数据库一次读取所有的数据再进行分页,如果数据量较少则 可以实现快速开发,但是假如数据库中存放大量数据,这种操作性能显得就比较低了,所以 我们一般自己编写数据读取方法,在 PageIndexChanging 事件中绑定新页,这时会遇到一个 问题,如果我们仅读取一页数据时 GridView 不能正确计算出共有多少页,也就无法正确呈 现出分页按钮,因此需要考虑对其进行扩展。另外 DataList 控件提供了灵活的模版设置以显 示记录内容,但是它有一个最大的弱点就是不支持分页,我们同样试图扩展 DataList 以增加 分页的特性。 2. 分析 之所以要将 GridView 和 DataList 放在一起考虑,是因为不论哪一个数据绑定控件生成 分页链接列表时需要执行相似的操作。 GridView 控件本身支持分页,所以在开发自定义表 格控件时,只需要加入相应的按钮服器端控件,将 CommandName 属性设置为 Page,并设 置 CommandArgument 属性为特定值,即可由 GridView 捕捉到页面更改事件,为了避免在 代码中出现“魔法字符”,定义了常量类保存使用的字符串常量。但是这种方法对于 DataL ist 却不适用,因为 DataList 不能接收到客户端的回发事件,这也是 DataList 类和 GridView 类的一个区别—DataList 类没有实现 IPostBackEventHandler 接口。为了能够使 DataList 接收 客户端回发并触发分页事件,需要使自定义 DataList 实现 IPostBackEventHandler 接口,并 使用自定义事件参数类在触发事件时传递页码信息。 现在自定义 GridView 和 DataList 控件均可以实现分页了,为了使两者有一个统一的分 页外观,定义分页基类实现分页功能,并且针对 DataList 分页,继承分页基类并设置分页按 钮的回发脚本。最后为了能够把分页按钮作为一个整体添加到表格中,使之继承自 TableCe ll(表格中的单元格)。 ASP.NET自定义控件实例教程 第 76 页 3. 实现 3.1 创建保存命令字符串的常量类 Constant public class Constant { public const string FIRST_PAGE = "First"; public const string PREV_PAGE = "Prev"; public const string NEXT_PAGE = "Next"; public const string LAST_PAGE = "Last"; public const string PAGE_ARGUMENT = "Page"; public const char ARGUMENT_SPLITTER = '$'; } 分页按钮中的 CommandArgument 属性设置如下: CommandArgument 描述 First 移动到第一页 Last 移动到最后一页 Prev 移动到上一页 Next 移动到下一页 一个数字 移动到某个特定页 3.2 定义 DataPager 类以生成分页链接,该类继承自 TableCell 类,并且定义了私有变量 以计算页码(PagerSettings 类是.NET 预定义类,存储了有关分页设置的信息) public class DataPager : TableCell { private int _pageIndex; private int _recordCount; private int _pageSize; private int _pageCount; private PagerSettings _settings; } 3.3 定义构造函数,接收当前页索引、总记录数、页大小信息并根据这些数据计算出总 页码,接着调用 GeneratePage 方法生成分页 public DataPager(PagerSettings setting, int pageIndex, int recordCount, int pageSize) { _settings = setting; _pageIndex = pageIndex; _recordCount = recordCount; _pageSize = pageSize; _pageCount = _recordCount % _pageSize == 0 ? _recordCount / _pageSize : _recordCount / _pageSize + 1; ASP.NET自定义控件实例教程 第 77 页 //生成分页 GeneratePage(); } 3.3 编写 GeneratePage 方法,根据 PagerSetting 设置生成带有数字或不带有数字的分页 链接: private void GeneratePage() { if (_settings.Mode == PagerButtons.NextPrevious || _settings.Mode == PagerButtons.NextPreviousFirstLast) { GeneratePrevNextPage(); } else if (_settings.Mode == PagerButtons.Numeric || _settings.Mode == PagerButtons.NumericFirstLast) { GenerateNumericPage(); } } 3.4 编写生成两类分页链接的方法: private void GeneratePrevNextPage() { GeneratePage(false); } private void GenerateNumericPage() { GeneratePage(true); } 3.5 生成分页时均调用了 GeneratePage 方法,该方法创建了一系列的 LinkButton 并设置 了 CommandArgument 和 CommandName 属性,以下是该方法的实现: private void GeneratePage(bool generateNumber) { this.Controls.Add(new LiteralControl("  ")); if (_recordCount > 0) { this.Controls.Add(new LiteralControl("共 ")); this.Controls.Add(new LiteralControl(_recordCount.ToString())); this.Controls.Add(new LiteralControl("  条记录   每页  ")); this.Controls.Add(new LiteralControl(_pageSize.ToString())); ASP.NET自定义控件实例教程 第 78 页 this.Controls.Add(new LiteralControl(" 条记录  ")); } this.Controls.Add(new LiteralControl((_pageIndex + 1).ToString())); this.Controls.Add(new LiteralControl("/")); this.Controls.Add(new LiteralControl(_pageCount.ToString())); this.Controls.Add(new LiteralControl("    ")); LinkButton btnFrist = new LinkButton(); LinkButton btnPrev = new LinkButton(); LinkButton btnNext = new LinkButton(); LinkButton btnLast = new LinkButton(); if (!String.IsNullOrEmpty(_settings.FirstPageImageUrl)) { btnFrist.Text = ""; } else { btnFrist.Text = _settings.FirstPageText; } btnFrist.CommandName = Constant.PAGE_ARGUMENT; btnFrist.CommandArgument = Constant.FIRST_PAGE; btnFrist.Font.Underline = false; if (!String.IsNullOrEmpty(_settings.PreviousPageImageUrl)) { btnPrev.Text = ""; } else { btnPrev.Text = _settings.PreviousPageText; } btnPrev.CommandName = Constant.PAGE_ARGUMENT; btnPrev.CommandArgument = Constant.PREV_PAGE; btnPrev.Font.Underline = false; if (!String.IsNullOrEmpty(_settings.NextPageImageUrl)) { btnNext.Text = ""; } ASP.NET自定义控件实例教程 第 79 页 else { btnNext.Text = _settings.NextPageText; } btnNext.CommandName = Constant.PAGE_ARGUMENT; btnNext.CommandArgument = Constant.NEXT_PAGE; btnNext.Font.Underline = false; if (!String.IsNullOrEmpty(_settings.LastPageImageUrl)) { btnLast.Text = ""; } else { btnLast.Text = _settings.LastPageText; } btnLast.CommandName = Constant.PAGE_ARGUMENT; btnLast.CommandArgument = Constant.LAST_PAGE; btnLast.Font.Underline = false; if (this._pageIndex <= 0) { btnFrist.Enabled = btnPrev.Enabled = false; } else { btnFrist.Enabled = btnPrev.Enabled = true; } this.Controls.Add(btnFrist); this.Controls.Add(new LiteralControl("  ")); this.Controls.Add(btnPrev); this.Controls.Add(new LiteralControl("  ")); if (generateNumber)//是否生成数字页码 { // 当前页左边显示的数字分页按钮的数量 int rightCount = (int)(_settings.PageButtonCount / 2); // 当前页右边显示的数字分页按钮的数量 int leftCount = _settings.PageButtonCount % 2 == 0 ? rightCount - 1 : rightCount; for (int i = 0; i < _pageCount; i++) { ASP.NET自定义控件实例教程 第 80 页 if (_pageCount > _settings.PageButtonCount) { if (i < _pageIndex - leftCount && _pageCount - 1 - i > _settings.PageButtonCount - 1) { continue; } else if (i > _pageIndex + rightCount && i > _settings.PageButtonCount - 1) { continue; } } if (i == _pageIndex) { this.Controls.Add(new LiteralControl("" + (i + 1).ToString() + "")); } else { LinkButton lb = new LinkButton(); lb.Text = (i + 1).ToString(); lb.CommandName = Constant.PAGE_ARGUMENT; lb.CommandArgument = (i + 1).ToString(); this.Controls.Add(lb); } this.Controls.Add(new LiteralControl("  ")); } } if (this._pageIndex >= _pageCount - 1) { btnNext.Enabled = btnLast.Enabled = false; } else { btnNext.Enabled = btnLast.Enabled = true; } this.Controls.Add(btnNext); this.Controls.Add(new LiteralControl("  ")); this.Controls.Add(btnLast); ASP.NET自定义控件实例教程 第 81 页 this.Controls.Add(new LiteralControl("  ")); } 3.6 创建自定义 GridView 类,该类继承自 ASP.NET 中默认的 GridView(System.Web. UI.WebControls.GridView): [ToolboxData(@"<{0}:GridView runat='server'>")] public class GridView : System.Web.UI.WebControls.GridView { } 3.7 定义 GridView 属性,新增了 RecordCound 存储总记录数,并且重写了 PageIndex 和 PageCount 属性以存储当前页索引和总页数,此外还定义了 MouseOverBackgroundColor 属性以实现当鼠标移到数据行上时背景变色。 public int RecordCount { get { object o = ViewState["RecordCount"]; return o == null ? 0 : Convert.ToInt32(o); } set { ViewState["RecordCount"] = value; } } public override int PageIndex { get { object o = ViewState["PageIndex"]; return o == null ? 0 : Convert.ToInt32(o); } set { ViewState["PageIndex"] = value; } } public override int PageCount { get { ASP.NET自定义控件实例教程 第 82 页 int pageCount = RecordCount % PageSize == 0 ? RecordCount / PageSize : RecordCount / PageSize + 1; return pageCount; } } public Color MouseOverBackgroundColor { get { object o = ViewState["MouseOverBackgroundColor"]; return o == null ? Color.Silver : (Color)o; } set { ViewState["MouseOverBackgroundColor"] = value; } } 3.8 重写 OnRowCreated 方法,在创建数据行时首先增加背景变色 JavaScript,接下来根 据分页信息创建页码链接单元格添加到表格中。 protected override void OnRowCreated(GridViewRowEventArgs e) { if (e.Row.RowType == DataControlRowType.DataRow) { e.Row.Attributes.Add("onmouseover", "bgcolor=this.style.backgroundColor;this.style.backgroundColor='" + ColorTranslator.ToHtml(MouseOverBackgroundColor) + "';"); e.Row.Attributes.Add("onmouseout", "this.style.backgroundColor=bgcolor;"); } if (e.Row.RowType == DataControlRowType.Pager) { e.Row.Controls.Clear(); TableCell tc = new DataPager(PagerSettings, PageIndex, RecordCount, PageSize); tc.ColumnSpan = this.Columns.Count; e.Row.Controls.Add(tc); } base.OnRowCreated(e); ASP.NET自定义控件实例教程 第 83 页 } 3.9 为了实现在删除最后一行数据时能够滚动到上一页,重写了 OnRowDeleted 方法: protected override void OnRowDeleting(GridViewDeleteEventArgs e) { if (this.Rows.Count == 1) this.PageIndex = this.PageIndex - 1 >= 0 ? this.PageIndex - 1 : 0; base.OnRowDeleting(e); } 3.10 在网站中声明并定义自定义 GridView: 3.11 编写后置代码以绑定到数据源: protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) this.BindData(); } private void BindData() { UserQuery query=null; ASP.NET自定义控件实例教程 第 84 页 object o = ViewState["Query"]; if (o == null) query = new UserQuery(); else query = (UserQuery)o; int total=0; IList users = Users.GetUsers(query, gdvData.PageIndex, gdvData.PageSize, out total); gdvData.RecordCount = total; gdvData.DataSource = users; gdvData.DataBind(); } 3.12 编写 PageIndexChanging 和 RowDeleting 事件处理程序: protected void gdvData_PageIndexChanging(object sender, GridViewPageEventArgs e) { gdvData.PageIndex = e.NewPageIndex; this.BindData(); } protected void gdvData_RowDeleting(object sender, GridViewDeleteEventArgs e) { int id=Convert.ToInt32(gdvData.DataKeys[e.RowIndex].Value); Users.DeleteUser(id); this.BindData(); } 3.13 在浏览器中预览页面(此代码中的 Users 和 User 类均未给出,读者可尝试自行编 写,其中 Users 类相当于一个逻辑类实现了用户的查询和删除,User 类代表用户实体)。 ASP.NET自定义控件实例教程 第 85 页 4. 总结 本次任务中创建了可以自动实现分页的 GridView 控件,把生成分页链接的操作单独提 取出来创建了 DataPager 类,在此类中使用 LinkButton 并设置了 CommandName 和 Comman dArgument 以实现换页。下一次任务我们将在此类的基础上为 DataList 添加分页特性。 ASP.NET自定义控件实例教程 第 86 页 第十天 实现分页功能的 DataList 1. 引言 在 ASP.NET 中 DataList 可以实现数据展示,我们可以通过定制其模版实现丰富的格式, 但是美中不足的时 DataList 默认情况下不支持分页, 我们当然可以编写一个用户控件以实现 分页功能,但是这种方案仍然不是很好,我们希望像使用普通 ASP.NET 服务器端控件一样, 只需要放置一个 DataList 并设置分页样式就可以输出分页链接。 在上次任务中我们创建了 DataPager 类将创建分页的操作从 GridView 分离出来,本次 任务将尝试重用 DataPager 类为 DataList 增加分页特性。 2. 分析 开发自定义 GridView 控件时,可以通过向控件中加入具有特定 CommandName 的按钮 实现分页,但是对于 DataList 却不适用,因为 DataList 不能接收到客户端的回发事件,这也 是 DataList 类和 GridView 类的一个区别—DataList 类没有实现 IPostBackEventHandler 接口。 为了能够使 DataList 接收客户端回发并触发分页事件,需要使自定义 DataList 实现 IPostBac kEventHandler 接口,并使用自定义事件参数类在触发事件时传递页码信息。 IPostBackEventHandler 接口定义了 ASP.NET 服务器控件为处理回发事件而必须实现的 方法,它的成员只有一个方法: void RaisePostBackEvent (string eventArgument) 该方法由类实现时,使服务器控件能够处理将窗体发送到服务器时引发的事件。 接下来需要考虑如何在客户端引起回发事件,即怎样生成回发脚本。这里使用到了 Cli entScriptManager 类,该类作为 Page 类的一个属性 ClientScript 出现,通过调用该类的 GetP ostBackClientHyperlink 方法生成客户端脚本以引起回发,该方法有两个形式的重载: z GetPostBackClientHyperlink (Control, String) 获取一个引用,并在其开头附加 javascript:,可以在客户端事件中使用该引用,并将该 引用与指定的事件参数一起使用,以便回发到指定控件的服务器。 z GetPostBackClientHyperlink (Control, String, Boolean) 获取一个引用,并在其开头附加 javascript:,该引用可用于在客户端事件中回发到指定 控件的服务器,回发时使用指定的事件参数和一个指示是否为事件验证注册该回发的布尔 值。 其中第一个参数指明了处理回发的服务器控件,第二个参数代表传递给服务器控件的参 数,第三个参数代表是否验证注册回发事件。 接下来编写实现代码。 3. 实现 3.1 创建 DataListPager 类,该类继承自 DataPager 类实现了为分页链接添加回发脚本操 作: ASP.NET自定义控件实例教程 第 87 页 public class DataListPager : DataPager { public DataListPager(PagerSettings setting, int pageIndex, int recordCount, int pageSize, DataList dal) : base(setting, pageIndex, recordCount, pageSize) { LinkButton btn = null; int _pageCount = recordCount % pageSize == 0 ? recordCount / pageSize : recordCount / pageSize + 1; int index; foreach (Control control in this.Controls) { if (control is LinkButton) { btn = (LinkButton)control; if (btn.Enabled) { string argvalue = btn.CommandArgument; bool isInt = int.TryParse(argvalue, out index); string arg = string.Empty; if (isInt) { index--; } else { switch (argvalue) { case Constant.FIRST_PAGE: index = 0; break; case Constant.PREV_PAGE: index = pageIndex - 1 < 0 ? 0 : pageIndex - 1; break; case Constant.NEXT_PAGE: index = pageIndex + 1 > _pageCount - 1 ? _pageCount - 1 : pageIndex + 1; break; case Constant.LAST_PAGE: index = _pageCount - 1; break; } } ASP.NET自定义控件实例教程 第 88 页 arg = Constant.PAGE_ARGUMENT + Constant.ARGUMENT_SPLITTER + index; btn.Attributes.Add(HtmlTextWriterAttribute.Href.ToString(), dal.Page.ClientScript.GetPostBackClientHyperlink(dal, arg)); } } } } } 3.2 创建 DataList 类,继承自默认的 DataList 类并实现 IPostBackEventHandler 接口: [ToolboxData(@"<{0}:DataList runat='server'>")] [ParseChildren(true)] [PersistChildren(false)] public class DataList : System.Web.UI.WebControls.DataList, IPostBackEventHandler { } 3.3 定义 DataList 属性,保存分页设置信息: public int RecordCount { get { object o = ViewState["RecordCount"]; return o == null ? 0 : Convert.ToInt32(o); } set { ViewState["RecordCount"] = value; } } public virtual int PageIndex { get { object o = ViewState["PageIndex"]; return o == null ? 0 : Convert.ToInt32(o); } ASP.NET自定义控件实例教程 第 89 页 set { ViewState["PageIndex"] = value; } } [DefaultValue(10)] public virtual int PageSize { get { object o = ViewState["PageSize"]; return o == null ? 10 : Convert.ToInt32(o); } set { ViewState["PageSize"] = value; } } private PagerSettings _settings; [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [PersistenceMode(PersistenceMode.InnerProperty)] public PagerSettings PagerSettings { get { if (_settings == null) _settings = new PagerSettings(); if (base.IsTrackingViewState) ((IStateManager)_settings).TrackViewState(); return this._settings; } } public bool EnablePaging { get { object o = ViewState["EnablePaging"]; ASP.NET自定义控件实例教程 第 90 页 return o == null ? false : Convert.ToBoolean(o); } set { ViewState["EnablePaging"] = value; } } 3.4 创建自定义事件类,保存新页码: public class DataListPageEventArgs : EventArgs { public int NewPageIndex { get; set; } } 3.5 在 DataList 类中创建事件和辅助方法: public event EventHandler PageIndexChanging; protected virtual void OnPageIndexChanging(object sender, DataListPageEventArgs e) { if (PageIndexChanging != null) { PageIndexChanging(sender, e); } } 3.6 编写 RenderContent 方法,该方法是实现了分页的核心操作,通过实例化 DataListP ager 类的实例,将触发回发操作的链接加入到当前 DataList 中: protected override void RenderContents(HtmlTextWriter writer) { this.RenderBeginTag(writer); if (EnablePaging) { Table table = new Table(); TableRow row = new TableRow(); table.Rows.Add(row); TableCell cell = new TableCell(); row.RenderBeginTag(writer); cell.RenderBeginTag(writer); ASP.NET自定义控件实例教程 第 91 页 base.RenderContents(writer); cell.RenderEndTag(writer); row.RenderEndTag(writer); TableRow rowpager = new TableRow(); //计算页链接并加入到单元格中 DataListPager pager = new DataListPager(PagerSettings, PageIndex, RecordCount, PageSize, this); rowpager.Cells.Add(pager); rowpager.RenderBeginTag(writer); pager.RenderControl(writer); rowpager.RenderEndTag(writer); } else { base.RenderContents(writer); } this.RenderEndTag(writer); } 3.7 编写 IPostEventHandler 接口中 RaisePostBackEvent 方法的实现,判断当前事件参数 并触发换页事件: public void RaisePostBackEvent(string eventArgument) { string[] args = eventArgument.Split(Constant.ARGUMENT_SPLITTER); if (args == null || args.Length < 1) return; string name = args[0]; if (name == Constant.PAGE_ARGUMENT) { int index = 0; string argvalue = args[1]; bool isInt = int.TryParse(argvalue, out index); ASP.NET自定义控件实例教程 第 92 页 if (isInt) { DataListPageEventArgs arg = new DataListPageEventArgs(); arg.NewPageIndex = index; OnPageIndexChanging(this, arg); } } } 3.8 重写 SaveViewState 和 LoadViewState 方法定义保存和读取视图状态方法: protected override object SaveViewState() { object o = base.SaveViewState(); object osetting = ((IStateManager)PagerSettings).SaveViewState(); Pair p = new Pair(o, osetting); return p; } protected override void LoadViewState(object savedState) { if (savedState != null) { Pair p = (Pair)savedState; base.LoadViewState(p.First); ((IStateManager)PagerSettings).LoadViewState(p.Second); } else { base.LoadViewState(null); } } 3.9 在网站中创建测试页,声明并定义自定义 DataList:
3.10 在后置代码中编写 BindData 方法加载数据,并且在页面不是回发时调用此方法显 示数据: protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) this.BindData(); } private void BindData() { DataTable table = new DataTable(); DataColumn col = new DataColumn("Id"); table.Columns.Add(col); col = new DataColumn("Name"); table.Columns.Add(col); for (int i = dalData.PageIndex * dalData.PageSize; i < dalData.PageIndex * dalData.PageSize+dalData.PageSize; i++) { DataRow row = table.NewRow(); row[0] = i; row[1] = "人员 " + i; table.Rows.Add(row); } dalData.DataSource = table; dalData.RecordCount = 30; dalData.DataBind(); } 3.11 编写 DataList 的切换页事件,将新的页码索引赋值给 DataList 并执行数据绑定: protected void dalData_PageIndexChanging(object sender, DataListPageEventArgs e) { dalData.PageIndex = e.NewPageIndex; this.BindData(); } ASP.NET自定义控件实例教程 第 94 页 3.12 在浏览器中预览效果: 4. 总结 在本次任务中,通过为 LinkButton 加入了 JavaScript 脚本使得在客户端点击时可以引起 回发操作,这是通过 ClientScriptManager 类的 GetPostBackClientHyperlink 方法实现的。引 起提交后,为了能够在服务器端处理回发事件,在自定义 DataList 控件中实现了 IPostBack EventHandler 接口并在实现方法中调用了自定义页切换事件,使开发人员能够根据新页码进 行数据绑定。可以看到,现在 DataList 和 GridView 都已经实现分页了,但是从某种意义上 来说,这个解决方案还不够优雅(经常被 Java 程序员拿来说事的一个词-_-!!),您可以自 行加以改良。
还剩97页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

316774722

贡献于2011-05-19

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf