用户控件
在 .NET 里,可以通过两种方式把自己的控件插入到 Web 窗体框架中:
- 用户控件:它是一小段页面,可以包括静态 HTML 代码和 Web 服务器控件。用户控件的好处是一旦创建了它,就可以在同一个 Web 应用程序的多个页面重用它。用户控件可以加入自己的属性,事件和方法。
- 自定义服务器控件:它是被编译的类,它通过编程生成自己的 HTML 。服务器控件总是预编译到 DLL 程序集。根据你编写服务器控件的方式,可以从零开始呈现它的内容,继承一个现有的服务器控件的外观和行为并扩展它的功能,或者通过实例化和配置一组组合控件来创建界面。
用户控件基础
用户控件由一个含有控件标签的界面部分(.ascx 文件)以及嵌入脚本或一个在后台的 cs 文件组成。用户控件几乎可以包括所有的内容(HTML,ASP.NET 控件),还可接收 Page 对象的事件(如 Load 和 PreRender),并通过属性公开一组相同的 ASP.NET 固有的对象(如 Application、Session、Request、Response)。
用户控件和网页之间的主要区别如下:
- 用户控件以 Control 指令而不是 Page 指令开头。
- 用户控件使用的扩展名是 .ascx 而不是 .aspx 。
- 用户控件后台代码从 System.Web.UI.UserControl 类继承。(其实 UserControl 类和 Page 类 继承自同一个 TemplateControl 类,这就是他们共享这么多共同方法和事件的原因)
- 用户控件不能被客户端浏览器直接请求,用户控件需要嵌入到其他网页里。
创建简单的用户控件
用户控件是一个分部类。它会和 ASP.NET 自动生成的独立部分合并。要测试用户控件,必须把它放入一个 Web 窗体上。通过 Register 指令告诉 ASP.NET 你要使用一个用户控件:
<%@ Register Src="Header.ascx" TagName="Header" TagPrefix="apress" %>
src 指定了用户控件的源文件;TagPrefix 特性指定了页面上声明新控件的标签前缀;TagName 特性指定了页面上用户控件的标签名。
下面是示例完整的用户控件代码和页面代码:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="Header.ascx.cs" Inherits="Chapter15_Header" %>
<table width="100%" border="0" style="background-color: Blue">
<tr>
<td align="center">
<b style="color:White; font-size:60px">User Control Test Page</b>
</td>
</tr>
<tr>
<td align="right">
<b style="color:White">An Apress Creation 2008</b>
</td>
</tr>
</table>
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="HeaderTest.aspx.cs" Inherits="Chapter15_HeaderTest" %>
<%@ Register Src="Header.ascx" TagName="Header" TagPrefix="apress" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>HeaderHost</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<apress:Header ID="Header1" runat="server" />
</div>
</form>
</body>
</html>
把页面转换为用户控件
其实,开发用户控件最快捷的方式是把它先放到一个网页里,测试后再把它转换为一个用户控件。即使不采用这种开发方式,你仍然可能需要把页面的用户界面的某部分提取出来并在多个地方重用。
大体上,这就是一个剪切 - 粘贴 的操作,不过应该注意以下几点:
- 删除所有 <html>、<head>、<body>、<form> 标签。(在一个页面里这些标签只能出现一次)
- 将页面的 Page 指令更改为 Control 指令,并去除 Control 指令不支持的那些特性。
- 如果没有使用代码隐藏模型,记住在 Control 指令中包含 ClassName 特性(这样控件就是强类型的,可以访问到控件的属性和方法)。如果正在使用代码隐藏模型,就需要修改代码隐藏类以便它从 UserControl 而不是 Page 继承。
- 把文件扩展名从 .aspx 更改为 .ascx
处理事件
下面这个示例创建一个简单的 TimeDisplay 用户控件,它有几个事件处理逻辑,这个用户控件封装了一个 LinkButton 控件:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="TimeDisplay.ascx.cs" Inherits="Chapter15_TimeDisplay" %>
<asp:LinkButton ID="lnkTime" runat="server" OnClick="lnkTime_Click"></asp:LinkButton>
public partial class Chapter15_TimeDisplay : System.Web.UI.UserControl
{
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();
}
}
添加属性
目前你能在 Web 窗体里做的只是调用 RefreshTime() 这个公共的方法来更新显示。为了让用户控件更具灵活性和可重用性,开发人员通常会为用户控件添加属性。修改后用户控件代码如下(新增 Format 属性、修改了 RefreshTime 方法):
public string Format { get; set; }
public void RefreshTime()
{
if (Format == null)
{
lnkTime.Text = DateTime.Now.ToLongTimeString();
}
else
{
lnkTime.Text = DateTime.Now.ToString(Format);
}
}
Web 页面的代码如下:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="TimeDisplayHost.aspx.cs"
Inherits="Chapter15_TimeDisplayHost" %>
<%@ Register Src="~/Chapter15/TimeDisplay.ascx" TagPrefix="apress" TagName="TimeDisplay" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<apress:TimeDisplay ID="TimeDisplay1" runat="server" Format="dddd,dd MMMM yyyy HH:mm:ss tt (GMT z)" />
<hr />
<apress:TimeDisplay ID="TimeDisplay2" runat="server" />
</div>
</form>
</body>
</html>
public partial class Chapter15_TimeDisplayHost : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Page.IsPostBack)
{
TimeDisplay2.Format = "dddd,dd MMMM yyyy HH:mm:ss tt (GMT z)";
}
}
}
给用户控件添加属性时,理解页面事件发生的顺序就变的很重要了,一般按如下顺序初始化页面:
- 请求页面。
- 创建用户控件。如果变量有默认值,或者在类的构造函数里执行了初始化,那么此时会用到它们。
- 如果在用户标签里设置了任意属性,会用到它们。
- 执行页面的 Page.Load 事件,准备初始化用户控件。
- 执行用户控件的 Page.Load 事件,准备初始化用户控件。
理解了这个顺序后你会明白,不应该在 用户控件的 Page.Load 事件里执行用户控件初始化,因为它可能会覆盖客户端指定的设置。
使用自定义对象
很多用户控件是为通过更高层控件模型对通用场景细节进行抽象而设计的(例如地址信息,你可能会组合几个文本框到一个更高层次的 AddressInout 控件)。为这类控件建模的时候,需要使用比单独的字符串和数值更复杂的数据。通常,你会创建一个自定义类,它是为网页和用户控件之间的通信而特别设计的。
为了说明这一思想,下面的示例开发了一个 LinkTable 控件,它在一个格式化表里呈现一组超链接:
/// <summary>
/// 为了支持用户控件,使用这个自定义类定义每个链接所需的信息
/// </summary>
public class LinkTableItem
{
public string Text { get; set; }
public string Url { get; set; }
public LinkTableItem() { }
public LinkTableItem(string text, string url)
{
this.Text = text;
this.Url = url;
}
}
接下来考虑 LinkTable 用户控件的代码隐藏类。它定义了 Title 属性,还定义了 Items 集合用来接受 LinkTableItem 对象数组:
public partial class Chapter15_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;
this.gridLinkList.DataSource = items;
this.gridLinkList.DataBind();
}
}
}
控件自身使用数据绑定呈现它大部分用户界面。每当 Items 属性被设置或变更时,LinkTable 里的 GridView 就会重新绑定到条目集合:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="LinkTable.ascx.cs" Inherits="Chapter15_LinkTable" %>
<table border="1" cellpadding="2">
<tr>
<td>
<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>
<asp:GridView ID="gridLinkList" runat="server" AutoGenerateColumns="false" ShowHeader="false"
GridLines="None">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<img height="23" src="exclaim.gif" alt="Menu Item" style="vertical-align: middle" />
<asp:HyperLink ID="lnk" runat="server" NavigateUrl='<%# DataBinder.Eval(Container.DataItem,"Url") %>'
Font-Names="Verdana" Font-Size="XX-Small" ForeColor="#0000cd">
<%1: # DataBinder.Eval(Container.DataItem,"Text")%>
</asp:HyperLink>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
</td>
</tr>
</table>
最后,这是一个典型的网页代码,用它定义一个链接列表,然后将列表绑定到 LinkTable 用户控件来显示它:
public partial class Chapter15_LinkTableTest : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
LinkTable1.Title = "A List of Links";
LinkTableItem[] items = new LinkTableItem[3];
items[0] = new LinkTableItem("Apress", "http://www.apress.com");
items[1] = new LinkTableItem("Microsoft", "http://www.microsoft.com");
items[2] = new LinkTableItem("ProseTech", "http://www.prosetech.com");
LinkTable1.Items = items;
}
}
添加事件
用户控件和网页交互的另一种方式要借助事件。通过方法和属性,用户控件响应网页代码带来的变化。使用事件时,刚好相反,用户控件通知网页发生了某个活动,然后网页代码作出响应。
用户执行某个活动后,比如单击某个按钮或者从列表框里选择了某个选项,用户控件就会截获一个 Web 控件事件并产生一个新的,更高层次的事件通知网页。
定义事件必须使用一个 event 关键字以及一个代表事件签名的委托。.NET 事件标准指定了每个事件必须有2个参数,第一个参数是引发事件的控件的引用,第二个参数包含额外的信息(这些信息包含在一个继承自 System.EventArgs 类的自定义类中)。
下一个示例修改 LinkTable 以便用户单击某项时通知用户,这样网页就可以根据单击的项作出不同反应。在 LinkTable 示例里有必要传递“哪一个链接被单击了”这样的基本信息,为了支持这一设计,我们创建一个自定义的 EventArgs 对象,加入了一个只读属性,返回相应的 LinkTableItem 对象:
public class LinkTableEventArgs:EventArgs
{
private LinkTableItem selectedItem;
public LinkTableItem SelectedItem
{
get { return selectedItem; }
}
public bool Cancel { get; set; }
public LinkTableEventArgs(LinkTableItem item)
{
this.selectedItem = item;
}
}
public delegate void LinkClickedEventHandler(object sender,LinkTableEventArgs e);
接着,LinkTable 类使用 LinkClickedEventHandler 定义一个事件:
public event LinkClickedEventHandler LinkClicked;
为了截获服务器端的单击,需要用 LinkButton 控件替换 HyperLink 控件,因为前者才会引发一个服务器事件,后者只是呈现为一个锚标记:
<ItemTemplate>
<img height="23" src="exclaim.gif" alt="Menu Item" style="vertical-align: middle" />
<asp:LinkButton ID="lnk" runat="server" Font-Names="Verdana" Font-Size="XX-Small"
ForeColor="#0000cd" CommandName="LinkClick"
CommandArgument='<%# DataBinder.Eval(Container.DataItem,"Url") %>'
Text='<%# DataBinder.Eval(Container.DataItem,"Text") %>'>
</asp:LinkButton>
</ItemTemplate>
然后,通过处理 GridView.RowCommand 事件截获服务器端的单击事件,编写把它作为 LinkClicked 事件传送给网页的事件处理程序:
public event LinkClickedEventHandler LinkClicked;
protected void gridLinkList_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (LinkClicked != null)
{
LinkButton link = e.CommandSource as LinkButton;
LinkTableItem item = new LinkTableItem(link.Text, link.CommandArgument);
LinkTableEventArgs args = new LinkTableEventArgs(item);
LinkClicked(this, args);
// 引用类型的传递,修改结果会得以保留
// 因此后续可接着判断 Cancel 的值确定行为
if (!args.Cancel)
{
Response.Redirect(item.Url);
}
}
}
接着在 Web 页面上对这个事件进行注册,由于用户控件没有提供设计时支持,你必须手工编写事件处理程序及进行注册:
protected void LinkClicked(object sender, LinkTableEventArgs e)
{
lblInfo.Text = "You clicked '" + e.SelectedItem.Text
+ "' but this page not to direct you to '" + e.SelectedItem.Url + "'.";
e.Cancel = true;
}
可以在 Page.Load 里注册这个事件:
LinkTable1.LinkClicked += LinkClicked;
也可以在源页面的控件标签里关联(必须加上 On 前缀):
<apress:LinkTable ID="LinkTable1" runat="server" OnLinkClicked="LinkClicked" />
公开内部 Web 控件
用户控件包含的控件只能够被用户控件自身访问。通常这正是你希望的行为,它意味着用户控件可以加入公开特定细节的公有属性而不会让网页任意干预控件内所有的事,否则往往会带来无效或不稳定的变化。
例如,如果想调整 LinkButton 控件的前景色,那可以给用户控件添加 ForeColor 属性:
public Color ForeColor
{
get { return lnkTime.ForeColor; }
set { lnkTime.ForeColor = value; }
}
如果要公开一大堆属性,这个工作就变的很乏味了,这时应考虑公开整个对象(需要使用只读属性,网页不可能用一个其他东西取代控件):
public LinkButton InnerLink
{
get { return lnkTime; }
}
宿主页面设置前景色的代码就变成了:
TimeDisplay.InnerLink.ForeColor = System.Drawing.Color.Green;
公开整个内部控件对象时,网页可以调用控件所有方法可以接收它所有的事件,这种方式带来了无限的灵活性,但同时限制了代码的重用性,它还增大了网页与用户控件当前实现的内部细节紧密耦合的可能性。
作为一个基本规则,创建专门的方法、事件、属性,只公开必要的功能。这总会更好一些,不会为制造混乱提供机会。
动态加载用户控件
除了在页面注册用户控件类型并添加相应的控件标签把用户控件添加到页面上,还可以动态的创建用户控件,需要做如下这些事情:
- 在 Page.Load 事件发生时添加用户控件(这样用户控件可以正确重置它的状态并接收回发事件)。
- 使用容器控件和 PlaceHolder 控件来确保用户控件在你希望的位置结束。
- 设置 ID 属性给用户控件一个唯一的名称。在需要的时候可以借助 Page.FindControl()获取对控件的引用。
- 普通控件可以直接创建,而用户控件不可以直接创建(因为用户控件并非完全基于代码,它们还需要 .ascx 文件里定义的控件标签,ASP.NET 必须处理这个文件并初始化相应的子控件对象)。
- 必须调用 Page.LoadControl()并传递 .ascx 文件名,此方法返回一个 UserControl 对象,可以把它添加到页面上并把它转换为特定类型。
protected void Page_Load(object sender, EventArgs e)
{
TimeDisplay ctrl = Page.LoadControl("TimeDisplay.ascx") as TimeDisplay;
PlaceHolder1.Controls.Add(ctrl);
}
除了一些微不足道的琐碎细节外,和用户控件一起使用时,动态加载是一项非常强大的技术,它常用于创建高度可配置的门户框架。
门户框架
创建一个完整的门户框架需要大量的公式化代码,但是你可以从一个简单的示例中看出最重要的规则。
protected void Page_Load(object sender, EventArgs e)
{
string ctrlName = DropDownList1.SelectedItem.Value;
if (ctrlName.EndsWith(".ascx"))
{
PlaceHolder1.Controls.Add(Page.LoadControl(ctrlName));
}
Label1.Text = "Loaded..." + ctrlName;
}
动态加载用户控件的话,Web 页面还是需要注册用户控件的,勿忘!
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="DynamicUserControl.aspx.cs"
Inherits="Chapter15_DynamicUserControl" %>
<%@ Register Src="~/Chapter15/TimeDisplay.ascx" TagName="TimeDisplay" TagPrefix="apress" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Panel ID="Panel1" runat="server" Width="600" BackColor="Silver">
<asp:DropDownList ID="DropDownList1" runat="server" AutoPostBack="true" Style="margin-right: 23px">
<asp:ListItem Value="(None)">(None)</asp:ListItem>
<asp:ListItem Value="TimeDisplay.ascx">TimeDisplay</asp:ListItem>
</asp:DropDownList>
<br />
<asp:PlaceHolder ID="PlaceHolder1" runat="server"></asp:PlaceHolder>
<br />
<br />
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
</asp:Panel>
</div>
</form>
</body>
</html>
由于列表默认选项是 None,因此 TimeDisplay 控件只在页面至少回发一次后才会被加载。因此它是不会显示时间的。可以通过多种方式解决这一问题,例如加载控件时从 Web 页面调用 RefreshTime():
TimeDisplay time = Page.LoadControl(ctrlName) as TimeDisplay;
time.RefreshTime();
PlaceHolder1.Controls.Add(time);
一个更好的办法是为用户控件创建一个定义特定方法(如 InitializeControl())的接口。这样,你就可以通过一致的方式初始化任意控件了,多数门户框架使用接口提供这种标准化。
局部页面缓存
输出缓存的一个缺点是它工作在要么全有要么全无的模式。如果你需要动态的缓存页面的某些部分,它就不再有效。例如,你会希望缓存一个从数据库获得的记录填充表格,从而减少与数据库间的往返,不过在这同时你还需要获得页面其他部分最新的输出。
如果遇到的是这种情形,用户控件完全能够满足你的要求,因为它们可以缓存自己的输出。这个功能叫部分缓存,或者片段缓存。
把下面这行加入到用户控件的 .ascx 部分,比如 TimeDisplay:
<%@ OutputCache Duration="10" VaryByParam="None" %>
VaryByParam 特性和页面缓存一样,允许在 URL 改变时根据查询字符串参数缓存不同的 HTML 输出。
使用局部缓存时有一点要注意,缓存用户控件后,它本质上变成了一段静态的 HTML 代码,这样,网页代码不可以再访问用户控件对象。
VaryByControl
如果用户控件里有输入控件,就很难使用缓存。如果输入控件的内容会影响用户控件要显示的缓存内容,就会发生问题。无论用户输入了什么,都只能使用同样的用户控件副本(类似的问题也存在于网页,这就是缓存含有输入控件的页面通常没有意义的原因)。
VaryByControl 属性解决了这一问题。VaryByControl 接受一个用分号分隔的控件名称字符串,用于缓存不同的内容(与 VaryByParameter 根据查询字符串值缓存不同的内容相同)。
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="VaryingDate.ascx.cs" Inherits="VaryingDate" %>
<%@ OutputCache Duration="30" VaryByControl="lstMode" %>
<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" 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");
}
运行这个示例你会看到,ASP.NET 确实为列表中的每个选项单独进行了缓存。
共享缓存控件
如果在 10 个不同的页面中使用同一个用户控件,ASP.NET 将会缓存该控件的 10 个独立版本,这样用户控件被缓存前,每个页面第一次执行时都可以自定义用户控件。
不过在很多情况下需要在多个页面上重用相同的用户控件,但不需要任何的自定义。此时,ASP.NET 共享控件的缓存副本可以节省内存。ASP.NET 通过 OutputCache 指令的 Shared 属性启用共享。Shared 属性只在你把它应用到用户控件而不是 Web 窗体的指令时才起作用:
<%@ OutputCache Duration="10" VaryByParam="None" Shared="true" %>
还可以在用户控件的类声明前添加 PartialCaching 特性实现等效的结果:
[PartialCaching(10, null, null, null, true)]
public partial class VaryingDate : System.Web.UI.UserControl
{...}
这里的 null 分别代表:VaryByParameter、VaryByControl、VaryByCustom 。