在 .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 。