第14章 用户控件

    技术2022-05-11  49

    第14章 用户控件   Asp.net控件集是广泛而引人注目的。它包含封装了基本 HTML标记的控件和提供丰富的高层模型的控件,如日历、目录树(TreeView)和数据控件。当然,即便是最好的控件集也无法满足所有开发人员的需求。有时,你总是希望有更深层的认识、能够修改和建立你自己的用户接口组件。 在 .NET中,你可以将自己的控件通过两种方式插入到网页表单窗体中: l         用户控件:用户控件是页面的一段,它包含了静态 HTML代码和服务器控件。其优点在于一旦你创建了一个用户控件,你就可以在同一个应用的多个页面中重用。你甚至可以添加自己的属性、事件和方法。 l         自定义服务器控件:自定义服务器控件是编译过的类,它能够自动生成 HTML代码。与用户控件不一样的是,服务器控件通常是预先编译为DLL文件,而用户控件则象网页窗体页那样声明为一个文件的文件。基于你服务控件的编码,你可以从已有的网页控件组合和继承外观和行为来生成控件内容,并且对其进行扩展,或者通过实例化和配置委托控件组来构建接口。 在这一章中,首先会研究第一种情况 —用户控件。用户控件是Web站点中标准化在所有页面中的重复内容的重要方法。举个例来说,设想你希望在多个不同页面中输入地址信息时提供一致的方法。为了解决这个问题,你可以创建一个地址用户控件。这个控件组合了文本框和少量相关的验证器,然后你就可以添加此地址控件到任何网页窗体和程序中,而不是将其仅作为一个单一的对象。 当你需要构建和重用站点头部( Header)、尾脚(Footer),以及导航帮助时,用户控件也是一个很好的选择。(主控页面通过提供标准化网页布局的方法来组合控件,将在第15章介绍)。在所有这些例子中,当你想要重复使用那些代码时,你能够避免对用户控件代码的完全复制粘贴。如果你一定要这么做,当你在修改、调试或者增强控件时,你会遇到严重的问题。因为用户接口代码的多份拷贝将分散到你的站点之中,你将不得不做艰巨而毫无价值的工作:跟踪每一份拷贝并且重复你的修改。显然,用户控件提供了更加优雅的、面向对象的方法。 用户控件基础(User Control Basics) 用户控件(后缀名为 .ascx)文件与ASP.NET网页窗体(后缀名为.aspx)文件相似。就象网页窗体一样,用户控件由用户接口部分和控制标记组成,而且可以使用嵌入脚本或者.cs代码后置文件。用户控件能够包含网页所能包含的任何东西,包括静态HTML内容和ASP.NET控件,它们也作为页面对象(Page Object)接收同样的事件(如Load和PreRender),也能够通过属性(如Application,Session,Request 和Response)来展示ASP.NET内建对象。 用户控件与网页页面的关键区别是: l         用户控件开始于控件指令而不是页面指令。 l         用户控件的文件后缀是 .ascx,而不是.aspx,它的后置代码文件继承于System.Web.UI.UserControl类.事实上,UserControl类和Page类都继承于同一个TemplateControl类,所有它们能够共享很多相同的方法和事件。 l         用户控件不能被客户端直接访问,(如果客户端直接访问, ASP.NET将产生“不能使用该文件类型”错误信息)。相反,用户控件可以嵌入到其它网页页面中。 创建一个简单的用户控件(Creating a Simple User Control) 在 Visual Studio中创建一个用户控件,选择站点->添加新项目,然后选择用户控件模板(Web User Control template)。 下面是一个可能最简单的用户控件,它仅仅包含静态 HTML。这个用户控件描绘了一个页眉条。 <%@ Control Language="C#" AutoEventWireup="true" CodeFile="Header.ascx.cs" Inherits="Header" %> <table width="100%" border="0" bgcolor="blue"> <tr> <td><font size="6" color="yellow"><b> User Control Test Page</b></font> </td> </tr> <tr> <td align="right"><font size="3" color="white"><b> An Apress Creation . 2004</b></font>    </td> </tr> </table> 可能你已经注意到,控件指令标识了后置代码类。当然,简单头部控件不要求任何自定义代码,你可以保持这个类的内容为空: public partial class Header : System.Web.UI.UserControl {} 当与 ASP.NET网页窗体一起时,用户控件是一个局部类,因为它是由ASP.NET产生的分离的部分合并而成的。自动产生的部分具有适用于在设计时添加的所有控件的成员变量。 现在来测试控件,你需要将控件放置到网页窗体中。首先,你需要使用注册指令来告诉 ASP.NET页你将使用它,如下所示: <%@ Register TagPrefix="apress" TagName="Header" Src="Header.ascx" %> 这一行通过使用 Src属性指令了包含用户控件的源文件,它也定义了标记前缀和标记名,它们将用于声明页面中一个新控件。ASP.NET服务器控件也是同样的方法,具有<asp:…>前缀来声明控件(如: <asp:TextBox>),你可以使用自定义的标记前缀来帮助区分创建的控件。下面这个例子使用了标记前缀apress和标记名Header。 全部标签显示在这个页面中: <%@ Page Language="C#" AutoEventWireup="true" CodeFile="HeaderTest.aspx.cs" Inherits="HeaderTest" %> <%@ Register TagPrefix="apress" TagName="Header" Src="Header.ascx" %> <html> <head> <title>HeaderHost</title> </head> <body> <form id="Form1" method="post" runat="server"> <apress:Header id="Header1" runat="server"></apress:Header> </form> </body> </html> 作为一个空的最小值,当你添加一个用户控件到页面时,你必须指定一个唯一的 ID,并且必须指明它在服务器运行,象ASP.NET控件一样。图14-1显示了有一个自定义头部的示例页面。 图 14-1 测试页眉用户控件 在 Visual Studio中,你不需要手工编写代码来注册指令,一旦你创建了用户控件,只需要在Solution Explorer中选择.ascx文件,然后将其拖动到网页页面中的绘制区域。Visual Studio将自动添加注册指令和用户控件标记实例。 头部控件可能是最简单的用户控件示例,但它却显示了现实的优点。仔细想一想,当你必须将头部代码手工复制到所有的 ASP.NET页面中,而且之后你还必须改变标题(title),添加联系方式链接,或者其它的事情,你需要再次修改和上传所有的页面。但使用分离的用户控件,你只需要更新一个文件。最大的好处是,你可以将任何HTML组合、用户控件、服务器控件放置到一个ASP.NET网页窗体中。 将网页转换为用户控(Converting a Page to a User Control) 有时,开发用户控件的最简便的方法是,先将它放置到一个网页中,测试它本身,然后将网页转换为一个用户控件。即便你不采用这个方法,你也可能需要析取一个网页中的用户接口的一部分,并将其重用到多个地方。 总的来说,这个过程是个直接的剪切粘贴操作。但是,你需要注意几个要点: l         移除所有的 <html>,<body>和<form>标签,这些标签只能在网页中出现一次,因此你不能将它们添加到用户控件中(否则可能会在一个网页中多处出现这些标签)。 l         如果有一个页面指令,将它更改为控件指令,同时移除控件指令不支持的属性,如 AspCompat,Buffer,ClientTarget,CodePage,Culture,EnableSessionState,EnableViewStateMac,ErrorPage,LCID,ResponseEncoding,Trace,TraceMode和Transaction。 l         如果你没有使用后置代码模型,你必须在控件指令中使用 ClassName属性包含一个类名。这样,页面能够强制地使用控件,允许访问你添加到控件中的属性和方法。 l         将文件后缀名由 .aspx改为.ascx 向用户控件中添加代码(Adding Code to a User Control) 前面的控件并没有包含任何代码,但是,它却提供了一个简单的方法来重用静态的网页用户接口块。在多数情况下,你需要添加一些代码到你的控件中,或者处理事件,或者添加客户能够访问的功能。就象网页窗体一样,你能够直接在 .ascx文件中的<script>中添加代码,或者使用分离的.cs后置代码文件。 处理事件(Handling Events) 为了更好理解工作原理 ,下一个示例创建了一个简单的TimeDisplay用户控件,这个控件具有一些事件逻辑,它封装了一个链接按钮控件(LinkButton)。只要点击链接,在链接中显示的时间就会更新。时间也会在控件首次加载时刷新。 下面的用户控件了使用 <script>内嵌代码方式。 <%@ Control Language="C#" ClassName="TimeDisplay" %> <asp:LinkButton runat="server" ID="lnkTime" OnClick="lnkTime_Click"/> <script language="C#" runat="server"> protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) RefreshTime(); } protected void lnkTime_Click (object sender, EventArgs e) { RefreshTime(); } public void RefreshTime() { lnkTime.Text = DateTime.Now.ToLongTimeString(); } </script> 注意,事件 lnkTime_Click处理器调用RefreshTime()方法。因为这个方法是公开的,所以在网页窗体上的代码会通过调用方法来触发标签刷新程序。另一个重要细节是控件指令中的ClassName属性,它指明了代码为会编译为为用户控件TimeDisplay,并且具有脚本块中定义的方法。 图 14-2显示了这个控件的效果

    图 14-2 处理自身事件的用户控件   如果将代码分解到单独的 .ascx和.cs后置代码文件中会更好一点。在这种情况下,控件指令必须指明Src属性,属性值为用户控件源代码文件或者带有编译过的类名的继承属性。创建的用户控件通常使用继承属性,如下所示: <%@ Control Language="c#" AutoEventWireup="true" CodeFile="TimeDisplay.ascx.cs" Inherits="TimeDisplay" %> <asp:LinkButton id="lnkTime" runat="server" OnClick="lnkTime_Click" /> 相应的后置代码类 : public partial class TimeDisplay : System.Web.UI.UserControl { protected void Page_Load(object sender, System.EventArgs e) { if (!Page.IsPostBack) RefreshTime(); } protected void lnkTime_Click(object sender, System.EventArgs e) { RefreshTime(); } public void RefreshTime() { lnkTime.Text = DateTime.Now.ToLongTimeString(); } } 注意:在这个例子中,用户控件接收和处理 Page.Load事件,这个事件和事件处理器从Page.Load事件中完全分离,以便于网页窗体进行响应(虽然它们都是页面创建时同一个事件的结果)。这使得向用户控件中添加初始代码更为方便。 添加属性(Adding Properties) 目前,用户控件 TimeDisplay仅仅允许与所在的页面进行交互,在网页窗体代码中你所能做的只是调用RefreshTime()来更新显示。为了使用户控件更加灵活,可用性更强,开发人员通常会增加属性。 接下来的示例修改了 TimeDisplay控件,添加了一个公共Format属性。这个属性接收标准.NET格式串,用以配置日期显示格式。使用RefreshTime()方法刷新时考虑了这个信息。 public class TimeDisplay : System.Web.UI.UserControl { protected void Page_Load(object sender, System.EventArgs e) { if (!Page.IsPostBack) RefreshTime(); } private string format; public string Format { get { return format; } set { format = value; } } protected void lnkTime_Click(object sender, System.EventArgs e) { RefreshTime(); } public void RefreshTime() { if (format == "") { lnkTime.Text = DateTime.Now.ToLongTimeString(); } else { // This will throw an exception for invalid format strings, // which is acceptable. lnkTime.Text = DateTime.Now.ToString(format); } } } 在这个页面里,你可以有两种不同的选择。一种是你可以在你的代码中通过操作控件对象在某个位置设置 Format属性。例如: TimeDisplay1.Format = "dddd, dd MMMM yyyy HH:mm:ss tt (GMT z)"; 另一种是初始化时,通过设置控件标记里的值来配置用户控件,例如: <apress:TimeDisplay id="TimeDisplay1" Format="dddd, dd MMMM yyyy HH:mm:ss tt (GMT z)" runat="server" /> <hr /> <apress:TimeDisplay id="TimeDisplay2" runat="server" /> 在这个示例中,创建了两种版本的 TimeDisplay控件。一个版本用默认格式显示日期,另一个版本用自定义格式显示日期。图14-3显示了浏览效果。

    图 14-3 动态用户控件的两个实例 注意:如果你使用简单的属性类型,如 int, DateTime, Float等,你可以在页面里声明控件时使用字符串设置值设置它们,Asp.net会自动转换字符串为类里定义的属性类型。从技术上讲,asp.net使用一个类型转换器---一对象的特别类型通常用于在数据类型与串表示间转换,详见第28章。 当你向控件中添加属性时,理解事件顺序就变得尤为重要。页面基本上按如下顺序进行初始化: 1、 请求页面。 2、 创建用户控件。如果变量有默认值,或者在类构造函数里执行了初始化时,此时开始起作用。 3、 用户控件标记( control tag)里设置的属性起用。 4、 执行页面 Page.Load(加载)事件,潜在地初始化用户控件。 5、 执行用户控件里的 Page.Load事件,用户控件被潜在地初始化。 一旦你理解了这个顺序,你就会意识到你不需要在用户控件的 Page.Load事件中执行用户控件初始化,否则可能会覆盖客户端的设置。 使用自定义对象(Using Custom Objects) 许多用户控件使用高层控件模型,将通用视图的细节进行抽象。例如,你需要输入地址信息,你可能将几个文本控件组成更高层的地址输入控件。当你对这种控件进行建模时,你需要使用更复杂的数据,而不是单纯的字符串和数字。通常情况下,你希望创建一个自定义类,用于在网页和你的用户控件间通信。 为了描述这个思想,下一个示例开发了一个 LinkTable控件,它在一个格式化的表格中渲染一组超级链接。图14-4显示了LinkTable控件。

    图 14-4 显示链接表的用户控件   为了支持这个控件,你需要一个自定义类来定义每个链接所需的信息: public class LinkTableItem { private string text; public string Text { get { return text; } set { text = value; } } private string url; public string Url { get { return url; } set { url = value; } } // Default constructor. public LinkTableItem() {} public LinkTableItem(string text, string url) { this.text = text; this.url = url; } } 这个类可以被扩展来包含其它细节,如显示控件相邻的图标。 TinkTable简单地在每个项目都使用了相同图标。 接下来,考虑 LinkTable的后置代码类。它为每个在表中要显示的链接定义了Title属性(便于设置标题)和项目集(接收LinkTableItem对象数组)。 public partial class LinkTable : System.Web.UI.UserControl { public string Title { get { return lblTitle.Text; } set { lblTitle.Text = value; } } private LinkTableItem[] items; public LinkTableItem[] Items { get { return items; } set { Items = value; // Refresh the grid. listContent.DataSource = items; listContent.DataBind(); } } } 控件使用数据绑定来渲染大多数自身的接口,无论何时设置或者更改了项目属性, LinkTable控件中的DataList自动重新绑定到项目集。DataList包含了一个简单的模板,它为每个链接显示超级链接控件,控件旁边显示一个醒目的图标。 <%@ Control Language="c#" AutoEventWireup="false" Codebehind="LinkTable.ascx.cs" Inherits="LinkTable" %> <table border="1" width="100%" cellspacing="0" cellpadding="2" height="43"> <tr> <td width="100%" height="1"> <asp:Label id="lblTitle" runat="server" ForeColor="#C00000" Font-Bold="True" Font-Names="Verdana" Font-Size="Small"> [Title Goes Here]</asp:Label> </td> </tr> <tr> <td width="100%" height="1"> <asp:DataList id="listContent" runat="server"> <ItemTemplate> <img height="23" src="exclaim.gif" width="25" align="absMiddle" border="0"> <asp:HyperLink id="HyperLink1" NavigateUrl='<%# DataBinder.Eval(Container.DataItem, "Url") %>' Font-Names="Verdana" Font-Size="XX-Small" ForeColor="#0000cd" Text='<%# DataBinder.Eval(Container.DataItem, "Text") %>' runat="server"> </asp:HyperLink> </ItemTemplate> </asp:DataList> </td> </tr> </table> 最后,有一段典型的网页代码,用于定义链接列表并且通过绑定到 LinkTable用户控件来显示它。 protected void Page_Load(object sender, System.EventArgs e) { // Set the title. LinkTable1.Title = "A List of Links"; // Set the hyperlinked item list. LinkTableItem[] items = new LinkTableItem[3]; items[0] = new LinkTableItem("Test Item 1", "http://www.apress.com"); items[1] = new LinkTableItem("Test Item 2", "http://www.apress.com"); items[2] = new LinkTableItem("Test Item 3", "http://www.apress.com"); LinkTable1.Items = items; } 一旦对它进行了配置,网页代码就不再需要与这个控件进行交互。当用户点击其中一个链接时,用户会被带到新的目标,而不需要额外的代码。设计 LinkTable的另一个方法是唤起服务器端的点击事件,这在下一节中进行介绍。 添加事件(Adding Events) 用户控件和网页间通信的另一种方法是通过事件的方式。使用方法和属性,用户控件对网页代码产生的改变进行响应。使用事件,情况就相反了,用户控件将动作通知给网页,网页代码进行响应。 一般地,当你创建用户需要交互的用户控件时,你会在事件上下很多功夫。当用户采用了一个确定的动作,比如单击按钮或者从列表中选择一个选项,用户控件截获一个网页控制事件,然后唤起一个新的、更高层的事件来通知给网页。 第一个版本的 LinkTable控件具有了一些功能,但它没有使用事件。相反,它简单地创建了请求链接。为描述事件是怎样使用的,下一个示例修改了LinkTable,以便于当项目被点击时通知用户,网页就可以根据点击的项目决定采用什么动作。 设计的第一步是定义事件。记住,定义事件必须使用代表事件的关键词来描述事件签名。 .NET标准事件明确要求每个事件必须使用两个参数,第一个参数提供对发送事件的控件的引用,第二个参数合并其它的额外信息。额外信息被封装到一个自定义的EventArgs对象中,EventArgs是从类System.EventArgs类继承而来。如果事件不需要额外信息,你可以使用不带任何额外信息的普通的Sysem.EventArgs对象。Asp.net中的许多事件,如Page.Load和Button.Click都使用的这种模式。 在 LinkTable示例中,将被点击的链接的信息进行转换是有意义的。为了支持这种设计,你可以创建如下的EventArgs对象,它添加了只读的属性来响应LinkTableItem对象。 public class LinkTableEventArgs : EventArgs { private LinkTableItem selectedItem; public LinkTableItem SelectedItem { get { return selectedItem; } } private bool cancel = false; public bool Cancel { get { return cancel; } set { cancel = value; } } public LinkTableEventArgs(LinkTableItem item) { selectedItem = item; } } 你会注意到, LinkTableEventArgs定义了两个属性,即SelectItem属性和Cancel属性。前者允许用户获得项目被点击的信息,后者使用户可以阻止LinkTable导向一个新的页面。你可能会设置Cancel属性的一个原因是,如果你想对页面代码中的事件进行进行响应,并且自己处理重新导向。例如,你希望在服务器端的<iframe>里显示目标链接,或者使用它为<img>标签设置内容,而不是导向一个新的页面。 下一步,你需要创建新的代理( delegate),用来描述LinkClicked事件签名,下面显示了这种情况: public delegate void LinkClickedEventHandler(object sender, LinkTableEventArgs e); 为了截获服务器点击,需要使用 LinkButton代替HyperLink控件,因为只有LinkButton才能唤起服务器端事件。HyperLink只是简单地作为一个锚点进行渲染,当点击发生时,将用户直接导向目标。如下的模板可能你会用到: <ItemTemplate> <img height="23" src="exclaim.gif" width="25" align="absMiddle" border="0"> <asp:LinkButton id="HyperLink1" Font-Names="Verdana" Font-Size="XX-Small" ForeColor="#0000cd" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "Text") %>' CommandArgument='<%# DataBinder.Eval(Container.DataItem, "Url") %>'> </asp:LinkButton> </ItemTemplate> 这样,你就可以截获服务器端点击事件,并且作为 LinkClicked事件对页面进行导航。下面是示例代码: protected void listContent_ItemCommand(object source, System.Web.UI.WebControls.DataListCommandEventArgs e) { if (LinkClicked != null) { // Get the HyperLink object that was clicked. LinkButton link = (LinkButton)e.Item.Controls[1]; // Construct the event arguments. LinkTableItem item = new LinkTableItem(link.Text, link.CommandArgument); LinkTableEventArgs args = new LinkTableEventArgs(item); // Fire the event. LinkClicked(this, args); // Navigate to the link if the event recipient didn't // cancel the operation. if (!args.Cancel) { Response.Redirect(item.Url); } } }   值得注意的是,当你唤起一个事件时,你必须首先检验事件变量是不是包含了空引用。如果有空引用,那意味着没有已注册的事件处理器(当然,有可能是控件还没有创建)。此时释放事件可能会产生一个空引用异常。如果事件变量不为空,你可以通过使用名字和传递正确的事件参数来释放事件。 使用事件并不象标准的 asp.net 控件那样简单,问题在于用户控件并没有提供关于设计时(design-time)支持(第27章会看到的自定义控件,提供了设计时支持)。因此,你不能在设计时使用属性窗口来包装事件处理器。相反,你需要在其中编写事件处理器代码。 下面的示例就是一个有请求的信号的事件处理器的代码(由 LinkClickEventHandler定义)。 Protected void LinkClicked(object sender, LinkTableEventArgs e) { lblInfo.Text = “You clicked ‘”+e.SelectedItem.Text + “ ‘ but this page chose not to direct you to ‘” + e.SelectedItem.Url + “’.”; e.Cancel = true; } 你有两种方法来包装( wire up)事件处理器,一种是在Page.Load事件处理器中使用代理代码来手动进行: LinkTable1.LinkClicked += new LinkClickedEventHandler(LinkClicked); 另一种方法是,你可以在控件标签中进行。即仅需要在事件事前加上前缀,下面给出了例子: <apress:LinkTable ID = “LinkTable1” runat = “server” OnlinkClicked = “LinkClicked” /> 下图是点击链接时显示的结果。

    图 14-5 释放事件的用户控件   内部网页控件揭秘(Exposing the Inner Web Control) 有一点必须必须记住的是,用户控件的委托控件只能被用户控件自身访问。换句话说,就是放置了用户控件的网页能够接收事件,设置属性,或者调用这些控件的方法。例如,在 TimeDisplay用户控件中,网页不能够访问LinkButton控件。 通常情况下,这个行为是你所期望的。这意味着用户控件能够通过通过添加公共属性来公开特定的细节,而不需要给予网页自由控制的能力,防止影响其它内容和潜在地引入无效或者有冲突的改变。例如,你想使网页有能力调整 LinkButton控件的前景色,会可能会添加ForeColor属性到你的用户控件中,代码如下: public Color ForeColor { get { return lnkTime.ForeColor; } set { lnkTime.ForeColor = value; } } 为了改变网页代码中的前景色,你需要添加如下代码: TimeDisplay1.ForeColor = System.Drawing.Color.Green; 这个例子映射 LinkTime.ForeColor属性到用户控件的ForeColor属性上。这个技巧通常是最好的办法,但是如果你需要公开(expose)大量的属性,它将变得很冗长。例如,你的控件可能要渲染一个表,而且你可能希望让用户配置每个表单元的格式。 在这种情况下,公开完整的控件对象就非常有意义。下面的例子为 TimeDisplay用户控件公开了了lnkTime控件: public LinkButton InnerLink {     Get { return lnkTime;} } 注意,你需要只读属性 ,因为对于网页来说,用其它的东西来代替控件是不可能的. 下面的代码显示了在所属页面上怎样设置前景色。 TimeDisplay1.InnerLink.ForeColor = System.Drawing.Color.Green; 记住,当你使用这个练习的时候,你公开了内部控件的所有细节,这意味着网页能够调用方法和从该控件接收事件。这个方法带来了无限的灵活性,但是它减少了代码的重用性,同时,也增强了网页与你的控件的内部执行的细节的紧密联系。因此,如果不中断正在使用的网页,你要修改或者增强用户控件的可能性就减小了。作为一条通用的规则,创建专门的方法、事件和属性来公开你需要的功能,而不是开放一个后门来创建混乱的工作区,相对而言是更好的办法。 动态加载用户控件(Danamically Loading User Controls) 到目前为止,你已经明白怎么添加服务器控件到网页上,即通过注册用户控件类型,并且添加对应的标签来实现。你也可以动态地创建用户控件,换句话说,你可以使用少量的代码就可以很快地创建它们。 这个技术与你常用的动态地添加网页控件的技术相似 (详见第3章)。对于普通的控件,你会做如下事情: l         当 Page.Load事件释放时,添加用户控件。这样,用户控件就可以存储它的状态,接收postback事件。 l         使用容器控件和占位符( PlaceHolder)控件来确保用户控件处于正确的位置。 l         通过设置 ID属性,为用户控件指定一个唯一的名字。当你用Page.FindControl()方法来使用它时,你可以使用这个信息来为控件接收参数。 这里,还有一个关键。你不能直接创建控件对象,正和普通控件差不多。原因是用户控件并不是完全基于代码的,它们还需要在 .ascx文件中定义的控制标签。为了使用用户控件,ASP.NET需要处理这个文件,并且初始化相应的子控件对象。 为了执行这一步,你需要调用 Page.LoadControl()方法。当你调用LoadControl()方法时,你传递了.ascx用户控件标记文件的文件名。LoadControl()返回用户控件对象,你可以将其添加到网页,并且转换为特定的类类型来访问基于特定的控件的功能。 下面的例子动态地加载了 TimeDisplay用户控件,并且使用PlaceHolder控件将其添加到了网页中。 TimeDisplay ctrl = (TimeDisplay)Page.LoadControl(“TimeDisplay.ascx”); PlaceHolder1.Controls.Add(ctrl); 尽管这有点过于详细,但当用于连接用户控件时,动态加载是一个非常有效的方法。它被广泛地运用于创建高度可配置的接口框架( Portal Framework)。 接口框架(Portal Framework) 创建完整的接口框架尽管有点公式化,你可以从一个简单的示例中看到最重要的原则。图 14-6显示了展示了这一点。它包含了一个面板,面板内有三个控件:DropDownList,Label,PlaceHolder。

    Figure 14-6. A panel for holding user controls   当用户从下拉列表( drop-down list)中选择一个项目时,网页回传(post back),动态地加载相应的用户控件,并且将其插入到PlaceHolder中。图14-7显示了结果。

    Figure 14-7. A dynamically loaded user control 加载选定控件的代码如下: protected void Page_Load(object sender, System EventArgs e) { // Remember that the control must be loaded in the Page.Load event handler. // The DropDownList.SelectedIndexChanged event fires too late. string ctrlName = listControls.SelectedItem.Value; if (ctrlName.EndsWith(".ascx")) { placeHolder.Controls.Add(Page.LoadControl(ctrlName)); } lbl.Text = "Loaded..." + ctrlName; } 这个示例演示了大量引人注目的特征。首先,由于 PlaceHolder存储在格式化的容器中,自动加载的用户控件请求容器的字体、背景颜色等等。(除非它们已经显性地定义过字体和颜色)。 当 Page.Load事件释放时,由于你加载了这些控件,控件对象就能够处理它们自己的事件。你可以对此进行试验,通过加载TimeDisplay用户控件,然后点击链接来刷新时间。 注意,由于 TimeDisplay控件是在网页回传(post back)之后才会加载,所以,直到你点击链接时才会显示时间,在这之前,它会使用普通的控件名文本来替代。你可以通过很多方法来解决这个问题,包括在控件加载时在网页上调用RefreshTime()方法。一个更好的办法是为所有用户控件定义一个接口来定义一些基本的方法,比如InitializeControl()。这样,你就可以正常地初始化任何控件。大多数接口框架使用接口来提供这种标准化的类型。 通过扩展这个示例来提供完整的可配置的网页并不难。你所要做的就是创建更多的面板,并且在页面上组织它们(可能使用表格和其它面板来群组它们)。这看起来是一项艰巨的任务,但是你可以写一些通用的代码来处理网页上的所有面板,这样效率就相当高。一种方式是创建一个加载其它用户控件的用户控件,另一个方法使用自定义方法来处理。如下所示,为 3个面板加载了用户控件: protected void Page_Load(object sender, System.EventArgs e) { LoadControls(div1); LoadControls(div2); LoadControls(div3); } private void LoadControls(Control container) { DropDownList list = null; PlaceHolder ph = null; Label lbl = null; // Find the controls for this panel. foreach (Control ctrl in container.Controls) { if (ctrl is DropDownList) { list = (DropDownList)ctrl; } else if (ctrl is PlaceHolder) { ph = (PlaceHolder)ctrl; } else if (ctrl is Label) { lbl = (Label)ctrl; } } // Load the dynamic content into this panel. string ctrlName = list.SelectedItem.Value; if (ctrlName.EndsWith(".ascx")) { ph.Controls.Add(Page.LoadControl(ctrlName)); } lbl.Text = "Loaded..." + ctrlName; } 图 14-8显示了运行结果

    图14-8 具有多用户控件的动态网页

    使用这种技术来构建完整的网页接口框架是可能的,但在实现前它还有个重要的工作要做。创建这个框架是冗长的、费时的任务,在第 31章,你将了解到,Web parts, 它是一个本地的ASP.NET解决方案,构建网页接口而不需要被迫做多余的工作。Web parts部分构建于用户控件之上。 局部页面缓存(Partial Page Caching) 在第 11章,你学习了如何缓存一个网页,即通过添加OutputCache指令到.aspx页面中来实现。这种类型的缓存,叫输出缓存(output caching),缓存了页面已渲染的HTML版本,能够在下一次访问时自动重用,而不需要执行页面程序。 响应缓存的一个缺点是,它是基于全部或者全不( all-or nothing)基础的。如果你需要渲染页面中一的部分,它就无法满足需要。例如,你可能想缓存一个表,该表填充了来自于数据源的记录,这样就可以减少访问数据库服务器的次数,但你仍需要刷新网页的其它部分。此时,用户控件能够满足你的需要,因为它们能够缓存自己的输出。这个特征就叫局部缓存(partial caching)或段缓存(fragment caching)。它的工作方式与输出缓存是一样的。唯一的不同之处是你可以添加OutputCache指令到用户控件,而不是到网页中。 为了测试这个特征,添加下面一行到用户控件 .ascx中,如TimeDisplay: <%@ OutputCache Duration = “10” VaryByParam = “None” %> 现在,在宿主页面,你会看到显示的时间在 10秒种内不会发生改变。刷新页面也没有任何变化。VaryByParam参数与其添加网页中的含义是一样的,即当URL中的参数字符串发生改变时,它允许你产生和缓存新的HTML输出。 同时,你可以通过添加下面的属性到用户控件类的声明中来启用缓存: [PartialCaching(10)] Public class MyUserControl:UserControl { … } 当使用段缓存时要注意:当用户控件缓存时,用户控件本质上是一段静态 HTML块。因此,用户控件对象在网页中就无法访问。取而代之的是,ASP.NET根据创建用户控件的方式,实例化了一个或更多普通的对象类型。如果用户控件是通过声明的方式创建的(即添加用户标签到网页中),就会添加一个StaticPartialCachingControl对象。如果用户控件是通过程序创建的(即使用LoadControl()方法),就会添加一个PartialCachingControl对象。ASP.NET放置对象到用户控件在网页控件结构中的逻辑位置(如果页面没有被缓存)。但是,这些对象仅仅是一个占位符,它们并不允许你通过用户控件的属性和方法来与进行交互。如果你不能保证缓存起作用,你应当在试图使用用户控件对象前,对是否为空引用进行测试。 VarByControl 如果用户控件包含了输入控件,就不容易缓存。如果在输入控件中的内容影响了用户控件显示的缓存内容,就会出现问题。使用普通的缓存,你就会使用用户控件的相同拷贝,无论用户在输入控件中输入了什么内容都不会起起作用。(在网页中存在相似的问题,因此对包含了输入控件的网页进行缓存没有多少意义。) VarByControl属性就是用来解决这个问题的。 VaryByControl使用点分字符串的控件名,用于以非常相似的方法来改变缓存的内容,VaryByParameter为查询字符串值改变缓存的内容。 例如,考虑下面的控件: <%@ Control Language="C#" AutoEventWireup="true" CodeFile="VaryByControl.ascx.cs" Inherits="VaryByControl" %> <asp:DropDownList id="lstMode" runat="server" Width="187px"> <asp:ListItem>Large</asp:ListItem> <asp:ListItem>Small</asp:ListItem> <asp:ListItem>Medium</asp:ListItem> </asp:DropDownList> <br /> <asp:button ID="Button1" text="Submit" OnClick="SubmitBtn_Click" runat=server/> <br /><br /> Control generated at:<br /> <asp:label id="TimeMsg" runat="server" /> 点击按钮时,它以三种格式中的一种显示当前数据。 protected void Page_Load(object sender, EventArgs e) { switch (lstMode.SelectedIndex) { case 0: TimeMsg.Font.Size = FontUnit.Large; break; case 1: TimeMsg.Font.Size = FontUnit.Small; break; case 2: TimeMsg.Font.Size = FontUnit.Medium; break; } TimeMsg.Text = DateTime.Now.ToString("F"); } 要保存这个页面的一个缓存备份,这样是不够的,因为显示的格式的变化依赖于在 lstMode控件中的选择(图14-9)

    (图 14-9 控件的选择改变内容) 可以使用 VaryByControl属性来处理这个问题,并且指向这个控件: <%@ OutputCache Duration="30" VaryByControl="lstMode" %> 对这个示例进行测试,你将看到每个选择都会有不同的数据显示。 共享缓存的控件(Sharing Cached Controls) 如果在 10个不同的页面中使用相同的用户控件,ASP.NET将缓存那个控件的10个不同的版本。这使得每个页面在缓存用户控件之前,第一次执行时自定义用户控件。但是,多数情况下,你会发现你会在多个页面中重用相同的用户控件,你并不需要特定页面的自定义。在这种情况下,你可以通过告诉ASP.NET来共享缓存的控件的拷贝。 ASP.NET通过 OutputCache指令的Shared属性来启用这个功能。Shared属性仅仅在你将指令应用到用户控件时才起作用,而不是应用到窗体。示例如下: <%@ OutputCache Duration="10" VaryByParam="None" Shared="True" %> 你可以为用户控件添加 PartialCaching属性到类的声明中使用相同的请求。 [PartialCaching(10, null, null, null, true)] public class MyUserControl : UserControl { ... } 此时,空参数代表 VaryByParameter,VaryByControl和VaryByCustom。 总结 本章中,你学习了如何创建简单的和有点复杂的用户控件。也看到如何动态加载控件和进行缓存。尽管用户控件非常容易创建,但它们并不能解决每个自定义控件带来的挑战。事实上,用户控件的使用范围非常有限(要在不同应用之间进行共享并不是容易的事情),它们也被限制设计时支持(如,你不能在属性窗口附加事件处理器)。用户控件也缺乏高级的功能,它们并不适于快速渲染 HTML和JavaScript。为了改善这种现状,你可以使用自定义控件,这样创建起来就比较复杂一点。第27章介绍了自定义控件。 注意,尽管服务器控件比用户控件要强大,但是本章中介绍的大多数概念在应用于服务器控件和用户控件中的方式是一样的。例如,你可以创建包含了属性和方法的服务器控件,使用自定义对象,释放事件,使用子控件。  

    最新回复(0)