適用于:
Microsoft ASP.NET 2.0
Visual Basic 2005
Visual C# 2005
.NET Frameworks
Visual Web Developer 2005
摘要: Dino Esposito 一直在編寫有關(guān) ASP.NET 控件開發(fā)的系列教程,并在以下第四部分中介紹了如何使用和創(chuàng)建復(fù)合控件。
隨本文提供了 Visual Basic 和 C# 兩種源代碼。請(qǐng)從 此處 下載。
簡(jiǎn)介
復(fù)合控件只不過是普通的 ASP.NET 控件,還不屬于要論及的另一種類型的 ASP.NET 服務(wù)器控件。既然這樣,為什么在各書籍和文檔中總要留出專門的章節(jié)來論述復(fù)合控件呢?ASP.NET 復(fù)合控件有什么特別之處呢?
顧名思義,復(fù)合控件是將多個(gè)其他控件聚集在某單一頂部和單一 API 下的控件。如果某個(gè)自定義控件由一個(gè)標(biāo)簽和一個(gè)文本框組成,就可以說該控件是一個(gè)復(fù)合控件?!皬?fù)合”一詞表明該控件本質(zhì)上是由其他構(gòu)成組件在運(yùn)行時(shí)組合而成。復(fù)合控件所暴露的方法集和屬性集通常(但不是必須)由構(gòu)成組件的方法和屬性提供,并加入一些新成員。復(fù)合控件也可以引發(fā)自定義事件,還可以處理并激起子控件所引起的事件。
復(fù)合控件在 ASP.NET 中如此特別并不是因?yàn)槠溆锌赡艹蔀榉?wù)器控件新類型的代表。更確切的說是因?yàn)樗诔尸F(xiàn)時(shí)獲得了 ASP.NET 運(yùn)行時(shí)的支持。
復(fù)合控件是一個(gè)功能強(qiáng)大的工具,可以生成豐富復(fù)雜的組件,這些組件產(chǎn)生自活動(dòng)對(duì)象的相互作用而不是某些字符串生成器對(duì)象的標(biāo)記輸出。復(fù)合控件以構(gòu)成控件樹的形式呈現(xiàn),每個(gè)構(gòu)成控件都有其自己的生命周期和事件,并且所有構(gòu)成控件都聯(lián)合構(gòu)成一個(gè)全新的 API,并按需要盡可能地抽象化。
在本文中,我將論述復(fù)合控件的內(nèi)部體系結(jié)構(gòu),以闡明它在多種情況下為您帶來的好處。接下來,我將生成一個(gè)復(fù)合列表控件,與我在以前文章中所述控件的功能集相比,此控件的功能集更為豐富。
復(fù)合控件的要點(diǎn)是什么?
前一段時(shí)間,我曾經(jīng)自己嘗試在 ASP.NET. 中研究復(fù)合控件。我從 MSDN 文檔學(xué)習(xí)理論和實(shí)踐知識(shí),并也設(shè)計(jì)出一些不錯(cuò)的控件。但是,只有當(dāng)我有一次在純屬偶然的情況下看到以下示例時(shí),我才真正領(lǐng)悟到復(fù)合控件的要點(diǎn)(和優(yōu)點(diǎn))。設(shè)想一下由兩個(gè)其他控件(Label 和 TextBox)的組合生成的迄今為止最簡(jiǎn)單(也是最常見)的控件。以下介紹了一種編寫這種控件的可行方法。我們將其命名為 LabelTextBox。
public class LabelTextBox :WebControl, INamingContainer
{
public string Text {
get {
object o = ViewState["Text"];
if (o == null)
return String.Empty;
return (string) o;
}
set { ViewState["Text"] = value; }
}
public string Title {
get {
object o = ViewState["Title"];
if (o == null)
return String.Empty;
return (string) o;
}
set { ViewState["Title"] = value; }
}
protected override void CreateChildControls()
{
Controls.Clear();
CreateControlHierarchy();
ClearChildViewState();
}
protected virtual void CreateControlHierarchy()
{
TextBox t = new TextBox();
Label l = new Label();
t.Text = Text;
l.Text = Title;
Controls.Add(l);
Controls.Add(t);
}
}
該控件具備兩個(gè)公共屬性(Text 和 Title)以及一個(gè)呈現(xiàn)引擎。這兩個(gè)屬性保存在視圖狀態(tài)中,并分別表示 TextBox 和 Label 的內(nèi)容。該控件對(duì)于 Render 方法沒有替換方法,并通過 CreateChildControls 替換方法來生成其自己的標(biāo)記。我馬上就會(huì)詳述呈現(xiàn)階段的例行過程。CreateChildControls 的代碼首先清除子控件的集合,然后為當(dāng)前控件輸出的構(gòu)成控件生成控件樹。CreateControlHierarchy 是一種特定于控件的方法,不要求必須標(biāo)記為受保護(hù)和虛擬。但請(qǐng)注意,大多數(shù)自帶復(fù)合控件(例如 DataGrid)只是通過一個(gè)類似的虛擬方法來暴露用于生成控件樹的邏輯。
CreateControlHierarchy 方法會(huì)根據(jù)需要實(shí)例化多個(gè)構(gòu)成組件,然后合成最終輸出。完成之后,各控件將被添加到當(dāng)前控件的 Controls 集合。如果希望控件的輸出結(jié)果是一個(gè) HTML 表,則可以創(chuàng)建一個(gè) Table 控件,并相應(yīng)添加含有各自內(nèi)容的行和單元格。所有行、單元格和所含控件都是最外部表的子項(xiàng)。這時(shí),您只需將 Table 控件添加到 Controls 集合中即可。在上述代碼中,Label 和 TextBox 是 LabelTextBox 控件的直接子項(xiàng)并直接添加到集合中。控件的呈現(xiàn)狀態(tài)和運(yùn)行狀態(tài)都很正常。
單純從性能上看,創(chuàng)建控件的暫態(tài)實(shí)例不如呈現(xiàn)一些純文本的效率高。讓我們考慮一種無需子控件就能編寫上述控件的替代方法。這次讓我們將其命名為 TextBoxLabel。
public class LabelTextBox :WebControl, INamingContainer
{
:
protected override void Render(HtmlTextWriter writer)
{
string markup = String.Format(
"<span>{0}</span><input type=text value='{1}'>",
Title, Text);
writer.Write(markup);
}
}
該控件具備同樣的兩個(gè)屬性(Text 和 Title)并替換了 Render 方法。正如您所看到的那樣,其實(shí)現(xiàn)過程相當(dāng)簡(jiǎn)單并且代碼運(yùn)行速度也略勝一籌。您可以通過在字符串生成器中合成文本并為瀏覽器輸出最終標(biāo)記來取代合成子控件的這種方法。同樣,此時(shí)控件的呈現(xiàn)狀態(tài)良好。但我們真的可以說它的運(yùn)行狀態(tài)也同樣良好嗎?圖 1 顯示了在示例頁中運(yùn)行的兩個(gè)控件。
圖 1:使用不同呈現(xiàn)引擎的相似控件
在頁面中啟用跟蹤功能并重新運(yùn)行。當(dāng)頁面顯示在瀏覽器中時(shí),將其向下滾動(dòng)并查看控件樹。它將如下所示:
圖 2:由兩個(gè)控件生成的控件樹
復(fù)合控件由構(gòu)成組件的活動(dòng)實(shí)例組成。ASP.NET 運(yùn)行時(shí)會(huì)發(fā)現(xiàn)這些子控件,并可以在處理已發(fā)布數(shù)據(jù)時(shí)同它們進(jìn)行直接通信。其結(jié)果是,子控件可以自己處理視圖狀態(tài)并自動(dòng)激起事件。
對(duì)于基于標(biāo)記合成的控件,情況則不同。如圖中所示,該控件是一個(gè)帶有空 Controls 集合的代碼基本單位。如果標(biāo)記在頁面中注入交互元素(文本框、按鈕、下拉式菜單),則 ASP.NET 在不涉及控件本身的情況下無法處理回發(fā)數(shù)據(jù)及事件。
嘗試在兩個(gè)文本框中輸入一些文本并單擊圖 1 中的“刷新”按鈕,這樣就可以發(fā)生一個(gè)回發(fā)。第一個(gè)控件(即復(fù)合控件)在經(jīng)過回發(fā)后會(huì)正確保留所分配的文本。使用 Render 方法的第二個(gè)控件在經(jīng)過回發(fā)后會(huì)丟失新文本。為什么會(huì)這樣呢?其中兼有兩個(gè)原因。
第一個(gè)原因是,在上述標(biāo)記中我沒有為 <input> 標(biāo)記命名。這樣,它的內(nèi)容就不會(huì)回發(fā)。請(qǐng)注意,必須使用 name 屬性來為元素命名。讓我們對(duì) Render 方法做如下修改。
protected override void Render(HtmlTextWriter writer)
{
string markup = String.Format(
"<span>{0}</span><input type=text value='{1}' name='{2}'>",
Title, Text, ClientID);
writer.Write(markup);
}
注入客戶端頁面的 <input> 元素現(xiàn)在與服務(wù)器控件使用相同的 ID。頁面回發(fā)時(shí),ASP.NET 運(yùn)行時(shí)可發(fā)現(xiàn)一個(gè)與已發(fā)布字段的 ID 相匹配的服務(wù)器控件。但它并不知道如何處理該控件。要使 ASP.NET 將所有的客戶端更改都應(yīng)用于服務(wù)器控件,該控件必須實(shí)現(xiàn) IPostBackDataHandler 接口。
包含 TextBox 的復(fù)合控件無需擔(dān)心回發(fā)問題,因?yàn)樗度氲目丶?huì)使用 ASP.NET 自動(dòng)解決該問題。呈現(xiàn) TextBox 的控件需要與 ASP.NET 進(jìn)行交互,以確??梢哉_處理回發(fā)值并正常引發(fā)事件。以下代碼表明了如何擴(kuò)展 TextBoxLabel 控件以使其完全支持回發(fā)。
bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
string currentText = Text;
string postedText = postCollection[postDataKey];
if (!currentText.Equals(postedText, StringComparison.Ordinal))
{
Text = postedText;
return true;
}
return false;
}
void IPostBackDataHandler.RaisePostDataChangedEvent()
{
return;
}
復(fù)合控件的常見方案
復(fù)合控件是適合用于構(gòu)建復(fù)雜組件的工具,在復(fù)合控件中,多個(gè)子控件聚合到一起,并在彼此之間以及與外部之間進(jìn)行交互。呈現(xiàn)控件則只用于只讀式控件聚合,其輸出不包括交互元素(例如下拉框或文本框)。
如果您對(duì)事件處理和回發(fā)數(shù)據(jù)感興趣,我強(qiáng)烈建議您選擇復(fù)合控件。如果使用子控件,則生成復(fù)雜的控件樹會(huì)更加輕松,而且最終結(jié)果也更清晰簡(jiǎn)潔。此外,只有需要提供附加功能時(shí)才需要處理回發(fā)接口。
呈現(xiàn)控件不但需要實(shí)現(xiàn)附加接口,還要將含有屬性值的標(biāo)記靜態(tài)部分縫合到一起。
復(fù)合控件的優(yōu)點(diǎn)還表現(xiàn)在可以呈現(xiàn)多個(gè)同類項(xiàng),這與在 DataGrid 控件中的情況類似。將每個(gè)構(gòu)成項(xiàng)作為活動(dòng)對(duì)象啟用使您可以引發(fā)創(chuàng)建事件并以編程方式訪問它們的屬性。在 ASP.NET 2.0 中,對(duì)于要完全實(shí)現(xiàn)實(shí)際的數(shù)據(jù)綁定復(fù)合控件(上述控件只是隨便的舉例)所需的樣板代碼,絕大部分都隱藏在新基類的折疊部分中:CompositeDataBoundControl。
復(fù)合控件的呈現(xiàn)引擎
在深入探討 ASP.NET 2.0 編碼技術(shù)之前,讓我們回顧一下復(fù)合控件的內(nèi)部例行過程。我們提到過,復(fù)合控件的呈現(xiàn)是集中圍繞 CreateChildControls 方法進(jìn)行的,該方法從 Control 基類繼承而來。您可能會(huì)認(rèn)為,要使服務(wù)器控件呈現(xiàn)其內(nèi)容,替換 Render 方法是必不可少的一步。正如我們先前所看到的,如果 CreateChildControls 被替換,則并不總是需要執(zhí)行這一步。但是,何時(shí)在控件調(diào)用棧中調(diào)用 CreateChildControls 呢?
如圖中所示,在頁面第一次顯示時(shí),會(huì)在預(yù)呈現(xiàn)階段調(diào)用 CreateChildControls。
圖 3:在預(yù)呈現(xiàn)階段調(diào)用 CreateChildControls
特別是,請(qǐng)求處理代碼(在 Page 類中)在將 PreRender 事件引發(fā)至頁面和每個(gè)子控件之前會(huì)直接調(diào)用 EnsureChildControls。換言之,如果控件樹還未完全生成,則不會(huì)呈現(xiàn)任何控件。
以下代碼段例示了 EnsureChildControls(在 Control 基礎(chǔ)上定義的另一種方法)的偽代碼。
protected virtual void EnsureChildControls()
{
if (!ChildControlsCreated)
{
try {
CreateChildControls();
}
finally {
ChildControlsCreated = true;
}
}
}
此方法可能會(huì)在頁面和控件的生命周期內(nèi)反復(fù)調(diào)用。為避免控件重復(fù),ChildControlsCreated 屬性被設(shè)為 true。如果此屬性返回 true,則該方法會(huì)立即退出。
當(dāng)頁面回發(fā)時(shí),ChildControlsCreated 會(huì)在周期前期調(diào)用。如圖 4 所示,它在已發(fā)布數(shù)據(jù)處理階段調(diào)用。
圖 4:發(fā)生回發(fā)時(shí)在已發(fā)布數(shù)據(jù)處理階段調(diào)用
當(dāng) ASP.NET 頁面開始處理從客戶端發(fā)布的數(shù)據(jù)時(shí),它會(huì)嘗試查找一個(gè)其 ID 與已發(fā)布字段的名稱相匹配的服務(wù)器控件。在執(zhí)行此步驟期間,頁面代碼會(huì)調(diào)用 Control 類中的 FindControl 方法。反之,該方法需要確保在進(jìn)行操作之前控件樹已完全生成,因此它調(diào)用 EnsureChildControls 并按需要生成控件層次結(jié)構(gòu)。
那么要在 CreateChildControls 方法內(nèi)部執(zhí)行的代碼是怎樣的呢?盡管沒有正式的指南可供遵循,但通常認(rèn)為 CreateChildControls 至少必須完成以下任務(wù):清除 Controls 集合,生成控件樹,并清除子控件的視圖狀態(tài)。并不嚴(yán)格要求必須從 CreateChildControls 方法內(nèi)部設(shè)置 ChildControlsCreated 屬性。實(shí)際上,ASP.NET 頁面框架始終通過 EnsureChildControls(此方法可自動(dòng)設(shè)置布爾標(biāo)記)來調(diào)用 CreateChildControls。
用于解決設(shè)計(jì)時(shí)問題的 CompositeControl
隨 ASP.NET 2.0 一同提供了一個(gè)名為 CompositeControl 的基類。因此,新的非數(shù)據(jù)綁定復(fù)合控件應(yīng)該從該類派生而不是從 WebControl 派生。在開發(fā)控件方面,CompositeControl 的用法變動(dòng)不大。您仍然需要替換 CreateChildControls 并按先前所述方式編碼。那么 CompositeControl 的作用是什么?讓我們先從其原型著手:
public class CompositeControl :WebControl,
INamingContainer,
ICompositeControlDesignerAccessor
使用該類就無需再用 INamingContainer 裝飾控件,但這實(shí)際上并不是很重要,因?yàn)榻涌谥皇且粋€(gè)標(biāo)記并且不包含任何方法。更為重要的是,該類實(shí)現(xiàn)了一個(gè)名為 ICompositeControlDesignerAccessor 的全新接口。
public interface ICompositeControlDesignerAccessor
{
void RecreateChildControls();
}
此接口由復(fù)合控件的標(biāo)準(zhǔn)設(shè)計(jì)器用于在設(shè)計(jì)時(shí)重建控件樹。以下是 CompositeControl 中方法的默認(rèn)實(shí)現(xiàn)過程。
void ICompositeControlDesignerAccessor.RecreateChildControls()
{
base.ChildControlsCreated = false;
EnsureChildControls();
}
簡(jiǎn)言之,如果您從 CompositeControl 派生復(fù)合控件,就不會(huì)遇到設(shè)計(jì)時(shí)的故障,而且無需采用技巧和妙計(jì)就可以使控件在運(yùn)行時(shí)和設(shè)計(jì)時(shí)都能正常運(yùn)行。
要充分理解此接口的重要性,可試以寄存某 LabelTextBox 復(fù)合控件的示例頁為例,并將其轉(zhuǎn)換為設(shè)計(jì)模式??丶谶\(yùn)行時(shí)工作正常,但在設(shè)計(jì)時(shí)卻不可見。
圖 5:只有復(fù)合控件從 CompositeControl 派生才對(duì)它們進(jìn)行特殊的設(shè)計(jì)時(shí)處理
如果只是用 CompositeControl 替換 WebControl,則控件在運(yùn)行時(shí)仍然保持正常工作,而在設(shè)計(jì)時(shí)也會(huì)運(yùn)行良好。
圖 6:在設(shè)計(jì)時(shí)運(yùn)行良好的復(fù)合控件
生成數(shù)據(jù)綁定復(fù)合控件
大多數(shù)復(fù)雜的服務(wù)器控件都已綁定數(shù)據(jù)(也可能已經(jīng)模板化),并且由各種子控件構(gòu)成。這些控件保留了一個(gè)構(gòu)成項(xiàng)(通常為表的行或單元格)的列表。該列表在經(jīng)過回發(fā)后會(huì)保存在視圖狀態(tài)中,并且從綁定數(shù)據(jù)生成或從視圖狀態(tài)重建。該控件還在視圖狀態(tài)中保存其構(gòu)成項(xiàng)的數(shù)量,以便在頁面中其他控件引起回發(fā)時(shí)可以正確重建表結(jié)構(gòu)。我將用 DataGrid 控件舉例說明。
DataGrid 由一列行構(gòu)成,每一行都代表綁定數(shù)據(jù)源中的一個(gè)記錄。每個(gè)網(wǎng)格行都通過一個(gè) DataGridRow 對(duì)象(從 TableRow 派生的一個(gè)類)表示。在各網(wǎng)格行創(chuàng)建完成并被添加到最終網(wǎng)格表時(shí),諸如 ItemCreated 和 ItemDataBound 之類的相應(yīng)事件將被引發(fā)至頁面。當(dāng)通過數(shù)據(jù)綁定創(chuàng)建 DataGrid 時(shí),其行數(shù)由綁定項(xiàng)數(shù)和頁面大小決定。如果帶有 DataGrid 的頁面回發(fā)會(huì)怎樣?
這種情況下,如果是由 DataGrid 自身引起的回發(fā)(例如,用戶單擊以進(jìn)行排序或標(biāo)頁),則新頁面會(huì)再次通過數(shù)據(jù)綁定來呈現(xiàn) DataGrid。這是顯而易見的,因?yàn)?DataGrid 需要刷新數(shù)據(jù)進(jìn)行顯示。如果是主頁回發(fā),則情況就不同了,因?yàn)閱螕袅隧撁嫔系牧硪粋€(gè)控件(例如某按鈕)。這種情況下,DataGrid 不綁定到數(shù)據(jù)并且必須從視圖狀態(tài)進(jìn)行重建。(如果禁用了視圖狀態(tài),就是另外一種情況了,這時(shí)只能通過數(shù)據(jù)綁定顯示網(wǎng)格。)
數(shù)據(jù)源不保存在視圖狀態(tài)中。作為復(fù)合控件,DataGrid 包含子控件,其中每個(gè)子控件都將自己的狀態(tài)保存到視圖狀態(tài)并從視圖狀態(tài)恢復(fù)。DataGrid 只需跟蹤在所有行和所包含控件從視圖狀態(tài)恢復(fù)之前它所必須重復(fù)執(zhí)行的次數(shù)。此次數(shù)與所顯示綁定項(xiàng)的數(shù)量一致,并且必須作為控件狀態(tài)的一部分存儲(chǔ)到視圖狀態(tài)中。在 ASP.NET 1.x 中,您必須自己學(xué)習(xí)并實(shí)現(xiàn)此模式。在 ASP.NET 2.0 中,從新類 CompositeDataBoundControl 派生您的復(fù)合控件就可以了。
讓我們嘗試使用一種顯示可擴(kuò)展數(shù)據(jù)綁定新聞標(biāo)題行的網(wǎng)格類控件。在此過程中,我們將再度使用在前文中論及的 Headline 控件。
public class HeadlineListEx :CompositeDataBoundControl
{
:
}
HeadlineListEx 控件包含了一個(gè)收集了所有綁定數(shù)據(jù)項(xiàng)的 Items 集合屬性。該集合為公共集合,并且可在與多數(shù)列表控件一起運(yùn)行時(shí)通過編程方式填充。對(duì)典型數(shù)據(jù)綁定的支持是通過一對(duì)屬性(DataTextField 和 DataTitleField)實(shí)現(xiàn)的。這兩個(gè)屬性表明了數(shù)據(jù)源中將用于填充新聞標(biāo)題和文本的字段。Items 集合被保存到視圖狀態(tài)中。
要將 HeadlineListEx 控件轉(zhuǎn)換為真正的復(fù)合控件,您首先需要從 CompositeDataBoundControl 將其派生出來,然后再替換 CreateChildControls。有意思的是,你會(huì)注意到 CreateChildControls 是重載方法。
override int CreateChildControls()
override int CreateChildControls(IEnumerable data, bool dataBinding)
第一個(gè)重載方法替換了在 Control 類中定義的方法。第二個(gè)重載方法是每個(gè)復(fù)合控件都必須替換的一種抽象方法。實(shí)際上,復(fù)合控件的開發(fā)工作簡(jiǎn)化為兩大主要任務(wù):
? 替換 CreateChildControls。
? 實(shí)現(xiàn) Rows 集合屬性以跟蹤控件的所有構(gòu)成項(xiàng)。
Rows 屬性不同于 Items,因?yàn)樗槐4嬖谝晥D狀態(tài)中,且具有與請(qǐng)求相同的生存期,并引用幫助程序?qū)ο蠖皇墙壎〝?shù)據(jù)項(xiàng)。
public virtual HeadlineRowCollection Rows
{
get
{
if (_rows == null)
_rows = new HeadlineRowCollection();
return _rows;
}
}
Rows 集合在控件生成時(shí)填充。讓我們看一下 CreateChildControls 的替換方法。該方法采用了兩個(gè)參數(shù):綁定項(xiàng)和一個(gè)布爾標(biāo)記,其中布爾標(biāo)記用于指明該控件是通過數(shù)據(jù)綁定創(chuàng)建還是通過視圖狀態(tài)創(chuàng)建。(請(qǐng)注意示例程序文件中的程序員注釋使用的是英文,本文中將其譯為中文是為了便于參考。)
override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
if (dataBinding)
{
string textField = DataTextField;
string titleField = DataTitleField;
if (dataSource != null)
{
foreach (object o in dataSource)
{
HeadlineItem elem = new HeadlineItem();
elem.Text = DataBinder.GetPropertyValue(o, textField, null);
elem.Title = DataBinder.GetPropertyValue(o, titleField, null);
Items.Add(elem);
}
}
}
// 開始生成控件層次結(jié)構(gòu)
Table t = new Table();
Controls.Add(t);
Rows.Clear();
int itemCount = 0;
foreach(HeadlineItem item in Items)
{
HeadlineRowType type = HeadlineRowType.Simple;
HeadlineRow row = CreateHeadlineRow(t, type,
item, itemCount, dataBinding);
_rows.Add(row);
itemCount++;
}
return itemCount;
}
在數(shù)據(jù)綁定的情況下,首先要填充 Items 集合。遍歷綁定集合,提取數(shù)據(jù),然后填充 HeadlineItem 類的新建實(shí)例。接下來,遍歷 Items 集合(該集合中可能包含以編程方式添加的附加項(xiàng)),并在控件中創(chuàng)建行。
HeadlineRow CreateHeadlineRow(Table t, HeadlineRowType rowType,
HeadlineItem dataItem, int index, bool dataBinding)
{
// 為最外部表創(chuàng)建新行
HeadlineRow row = new HeadlineRow(rowType);
// 為子控件創(chuàng)建單元格
TableCell cell = new TableCell();
row.Cells.Add(cell);
Headline item = new Headline();
cell.Controls.Add(item);
// 此時(shí)引發(fā) HeadlineRowCreated 事件
// 將此行添加到所創(chuàng)建的 HTML 表
t.Rows.Add(row);
// 處理數(shù)據(jù)對(duì)象綁定
if (dataBinding)
{
row.DataItem = dataItem;
Headline ctl = (Headline) cell.Controls[0];
ctl.Text = dataItem.Text;
ctl.Title = dataItem.Title;
// 此時(shí)引發(fā) HeadlineRowDataBound 事件
}
return row;
}
CreateHeadlineRow 方法會(huì)創(chuàng)建并返回 HeadlineRow 類(從 TableRow 派生而來)的一個(gè)實(shí)例。在這種情況下,此行會(huì)包含一個(gè)由 Headline 控件填充的單元格。在其他情況下,您可以更改此部分代碼以根據(jù)需要添加多個(gè)單元格并相應(yīng)填充內(nèi)容。
重要的是,要將所需完成的任務(wù)分為兩個(gè)不同的步驟:創(chuàng)建和數(shù)據(jù)綁定。首先,創(chuàng)建行的布局,引發(fā)行創(chuàng)建事件(如果有),并最后將其添加到父表中。接下來,如果要將控件綁定到數(shù)據(jù),則設(shè)置對(duì)綁定數(shù)據(jù)敏感的子控件屬性。完成操作后,則引發(fā)一個(gè)行數(shù)據(jù)綁定事件(如果有)。
請(qǐng)注意,該模式更準(zhǔn)確描述了 ASP.NET 自帶復(fù)合控件的內(nèi)部體系結(jié)構(gòu)。
可以使用以下代碼來引發(fā)事件。
HeadlineRowEventArgs e = new HeadlineRowEventArgs();
e.DataItem = dataItem;
e.RowIndex = index;
e.RowType = rowType;
e.Item = row;
OnHeadlineRowDataBound(e);
請(qǐng)注意,只在要引發(fā)數(shù)據(jù)綁定事件時(shí)才設(shè)置 DataItem 屬性。事件數(shù)據(jù)結(jié)構(gòu)被任意設(shè)置為以下形式。如果您認(rèn)為有必要,盡可以對(duì)其進(jìn)行更改。
public class HeadlineRowEventArgs :EventArgs
{
public HeadlineItem DataItem;
public HeadlineRowType RowType;
public int RowIndex;
public HeadlineRow Item;
}
若要實(shí)際引發(fā)一個(gè)事件,通常的做法是使用一個(gè)如下定義的受保護(hù)方法。
protected virtual void OnHeadlineRowDataBound(HeadlineRowEventArgs e)
{
if (HeadlineRowDataBound != null)
HeadlineRowDataBound(this, e);
}
若要聲明此事件,可在 ASP.NET 2.0 中使用新的一般事件處理程序委托。
public event EventHandler<HeadlineRowEventArgs> HeadlineRowDataBound;
在示例頁中,一切均照常執(zhí)行。您可在控件標(biāo)記上定義處理程序并將某方法寫入代碼文件。示例如下。
<cc1:HeadlineListEx runat="server" ID="HeadlineListEx1"
DataTextField="notes" DataTitleField="lastname"
DataSourceID="MySource" OnHeadlineRowDataBound="HeadlineRowCreated" />
HeadlineRowCreated 事件處理程序的代碼顯示如下。
protected void HeadlineRowCreated(object sender, HeadlineRowEventArgs e)
{
if (e.DataItem.Title.Contains("Doe"))
e.Item.BackColor = Color.Red;
}
圖 7:運(yùn)行中的 HeadlineListEx 控件
通過掛接數(shù)據(jù)綁定事件,所有含有 Doe 的項(xiàng)都將以紅色背景呈現(xiàn)。
結(jié)論
復(fù)合控件是通過將其他控件聚合在某一公用 API 頂下創(chuàng)建而成的控件。復(fù)合控件將保留自己子控件的活動(dòng)實(shí)例,并且不僅限于呈現(xiàn)這些實(shí)例。通過檢查頁面跟蹤輸出中的控件樹部分,您就可以很容易看到這一點(diǎn)。使用復(fù)合控件可以帶來幾點(diǎn)好處,例如可以簡(jiǎn)化對(duì)事件和回發(fā)的處理。在 ASP.NET 1.x 中生成復(fù)雜的數(shù)據(jù)綁定控件有點(diǎn)棘手,需要您深入了解一些實(shí)現(xiàn)細(xì)節(jié)。在引進(jìn) CompositeDataBoundControl 基類的情況下,這種復(fù)雜性在 ASP.NET 中基本可以迎刃而解。最后,如果在 ASP.NET 2.0 中需要非數(shù)據(jù)綁定的復(fù)合控件,則可以使用 CompositeControl 基類。對(duì)于數(shù)據(jù)綁定復(fù)合控件,則可以改為考慮 CompositeDataBoundControl。無論是哪種情況,您都必須提供一個(gè) CreateChildControls 的有效替換方法,這是所有復(fù)合控件的核心,用于創(chuàng)建子控件層次結(jié)構(gòu)。
Microsoft ASP.NET 2.0
Visual Basic 2005
Visual C# 2005
.NET Frameworks
Visual Web Developer 2005
摘要: Dino Esposito 一直在編寫有關(guān) ASP.NET 控件開發(fā)的系列教程,并在以下第四部分中介紹了如何使用和創(chuàng)建復(fù)合控件。
隨本文提供了 Visual Basic 和 C# 兩種源代碼。請(qǐng)從 此處 下載。
簡(jiǎn)介
復(fù)合控件只不過是普通的 ASP.NET 控件,還不屬于要論及的另一種類型的 ASP.NET 服務(wù)器控件。既然這樣,為什么在各書籍和文檔中總要留出專門的章節(jié)來論述復(fù)合控件呢?ASP.NET 復(fù)合控件有什么特別之處呢?
顧名思義,復(fù)合控件是將多個(gè)其他控件聚集在某單一頂部和單一 API 下的控件。如果某個(gè)自定義控件由一個(gè)標(biāo)簽和一個(gè)文本框組成,就可以說該控件是一個(gè)復(fù)合控件?!皬?fù)合”一詞表明該控件本質(zhì)上是由其他構(gòu)成組件在運(yùn)行時(shí)組合而成。復(fù)合控件所暴露的方法集和屬性集通常(但不是必須)由構(gòu)成組件的方法和屬性提供,并加入一些新成員。復(fù)合控件也可以引發(fā)自定義事件,還可以處理并激起子控件所引起的事件。
復(fù)合控件在 ASP.NET 中如此特別并不是因?yàn)槠溆锌赡艹蔀榉?wù)器控件新類型的代表。更確切的說是因?yàn)樗诔尸F(xiàn)時(shí)獲得了 ASP.NET 運(yùn)行時(shí)的支持。
復(fù)合控件是一個(gè)功能強(qiáng)大的工具,可以生成豐富復(fù)雜的組件,這些組件產(chǎn)生自活動(dòng)對(duì)象的相互作用而不是某些字符串生成器對(duì)象的標(biāo)記輸出。復(fù)合控件以構(gòu)成控件樹的形式呈現(xiàn),每個(gè)構(gòu)成控件都有其自己的生命周期和事件,并且所有構(gòu)成控件都聯(lián)合構(gòu)成一個(gè)全新的 API,并按需要盡可能地抽象化。
在本文中,我將論述復(fù)合控件的內(nèi)部體系結(jié)構(gòu),以闡明它在多種情況下為您帶來的好處。接下來,我將生成一個(gè)復(fù)合列表控件,與我在以前文章中所述控件的功能集相比,此控件的功能集更為豐富。
復(fù)合控件的要點(diǎn)是什么?
前一段時(shí)間,我曾經(jīng)自己嘗試在 ASP.NET. 中研究復(fù)合控件。我從 MSDN 文檔學(xué)習(xí)理論和實(shí)踐知識(shí),并也設(shè)計(jì)出一些不錯(cuò)的控件。但是,只有當(dāng)我有一次在純屬偶然的情況下看到以下示例時(shí),我才真正領(lǐng)悟到復(fù)合控件的要點(diǎn)(和優(yōu)點(diǎn))。設(shè)想一下由兩個(gè)其他控件(Label 和 TextBox)的組合生成的迄今為止最簡(jiǎn)單(也是最常見)的控件。以下介紹了一種編寫這種控件的可行方法。我們將其命名為 LabelTextBox。
public class LabelTextBox :WebControl, INamingContainer
{
public string Text {
get {
object o = ViewState["Text"];
if (o == null)
return String.Empty;
return (string) o;
}
set { ViewState["Text"] = value; }
}
public string Title {
get {
object o = ViewState["Title"];
if (o == null)
return String.Empty;
return (string) o;
}
set { ViewState["Title"] = value; }
}
protected override void CreateChildControls()
{
Controls.Clear();
CreateControlHierarchy();
ClearChildViewState();
}
protected virtual void CreateControlHierarchy()
{
TextBox t = new TextBox();
Label l = new Label();
t.Text = Text;
l.Text = Title;
Controls.Add(l);
Controls.Add(t);
}
}
該控件具備兩個(gè)公共屬性(Text 和 Title)以及一個(gè)呈現(xiàn)引擎。這兩個(gè)屬性保存在視圖狀態(tài)中,并分別表示 TextBox 和 Label 的內(nèi)容。該控件對(duì)于 Render 方法沒有替換方法,并通過 CreateChildControls 替換方法來生成其自己的標(biāo)記。我馬上就會(huì)詳述呈現(xiàn)階段的例行過程。CreateChildControls 的代碼首先清除子控件的集合,然后為當(dāng)前控件輸出的構(gòu)成控件生成控件樹。CreateControlHierarchy 是一種特定于控件的方法,不要求必須標(biāo)記為受保護(hù)和虛擬。但請(qǐng)注意,大多數(shù)自帶復(fù)合控件(例如 DataGrid)只是通過一個(gè)類似的虛擬方法來暴露用于生成控件樹的邏輯。
CreateControlHierarchy 方法會(huì)根據(jù)需要實(shí)例化多個(gè)構(gòu)成組件,然后合成最終輸出。完成之后,各控件將被添加到當(dāng)前控件的 Controls 集合。如果希望控件的輸出結(jié)果是一個(gè) HTML 表,則可以創(chuàng)建一個(gè) Table 控件,并相應(yīng)添加含有各自內(nèi)容的行和單元格。所有行、單元格和所含控件都是最外部表的子項(xiàng)。這時(shí),您只需將 Table 控件添加到 Controls 集合中即可。在上述代碼中,Label 和 TextBox 是 LabelTextBox 控件的直接子項(xiàng)并直接添加到集合中。控件的呈現(xiàn)狀態(tài)和運(yùn)行狀態(tài)都很正常。
單純從性能上看,創(chuàng)建控件的暫態(tài)實(shí)例不如呈現(xiàn)一些純文本的效率高。讓我們考慮一種無需子控件就能編寫上述控件的替代方法。這次讓我們將其命名為 TextBoxLabel。
public class LabelTextBox :WebControl, INamingContainer
{
:
protected override void Render(HtmlTextWriter writer)
{
string markup = String.Format(
"<span>{0}</span><input type=text value='{1}'>",
Title, Text);
writer.Write(markup);
}
}
該控件具備同樣的兩個(gè)屬性(Text 和 Title)并替換了 Render 方法。正如您所看到的那樣,其實(shí)現(xiàn)過程相當(dāng)簡(jiǎn)單并且代碼運(yùn)行速度也略勝一籌。您可以通過在字符串生成器中合成文本并為瀏覽器輸出最終標(biāo)記來取代合成子控件的這種方法。同樣,此時(shí)控件的呈現(xiàn)狀態(tài)良好。但我們真的可以說它的運(yùn)行狀態(tài)也同樣良好嗎?圖 1 顯示了在示例頁中運(yùn)行的兩個(gè)控件。
圖 1:使用不同呈現(xiàn)引擎的相似控件
在頁面中啟用跟蹤功能并重新運(yùn)行。當(dāng)頁面顯示在瀏覽器中時(shí),將其向下滾動(dòng)并查看控件樹。它將如下所示:
圖 2:由兩個(gè)控件生成的控件樹
復(fù)合控件由構(gòu)成組件的活動(dòng)實(shí)例組成。ASP.NET 運(yùn)行時(shí)會(huì)發(fā)現(xiàn)這些子控件,并可以在處理已發(fā)布數(shù)據(jù)時(shí)同它們進(jìn)行直接通信。其結(jié)果是,子控件可以自己處理視圖狀態(tài)并自動(dòng)激起事件。
對(duì)于基于標(biāo)記合成的控件,情況則不同。如圖中所示,該控件是一個(gè)帶有空 Controls 集合的代碼基本單位。如果標(biāo)記在頁面中注入交互元素(文本框、按鈕、下拉式菜單),則 ASP.NET 在不涉及控件本身的情況下無法處理回發(fā)數(shù)據(jù)及事件。
嘗試在兩個(gè)文本框中輸入一些文本并單擊圖 1 中的“刷新”按鈕,這樣就可以發(fā)生一個(gè)回發(fā)。第一個(gè)控件(即復(fù)合控件)在經(jīng)過回發(fā)后會(huì)正確保留所分配的文本。使用 Render 方法的第二個(gè)控件在經(jīng)過回發(fā)后會(huì)丟失新文本。為什么會(huì)這樣呢?其中兼有兩個(gè)原因。
第一個(gè)原因是,在上述標(biāo)記中我沒有為 <input> 標(biāo)記命名。這樣,它的內(nèi)容就不會(huì)回發(fā)。請(qǐng)注意,必須使用 name 屬性來為元素命名。讓我們對(duì) Render 方法做如下修改。
protected override void Render(HtmlTextWriter writer)
{
string markup = String.Format(
"<span>{0}</span><input type=text value='{1}' name='{2}'>",
Title, Text, ClientID);
writer.Write(markup);
}
注入客戶端頁面的 <input> 元素現(xiàn)在與服務(wù)器控件使用相同的 ID。頁面回發(fā)時(shí),ASP.NET 運(yùn)行時(shí)可發(fā)現(xiàn)一個(gè)與已發(fā)布字段的 ID 相匹配的服務(wù)器控件。但它并不知道如何處理該控件。要使 ASP.NET 將所有的客戶端更改都應(yīng)用于服務(wù)器控件,該控件必須實(shí)現(xiàn) IPostBackDataHandler 接口。
包含 TextBox 的復(fù)合控件無需擔(dān)心回發(fā)問題,因?yàn)樗度氲目丶?huì)使用 ASP.NET 自動(dòng)解決該問題。呈現(xiàn) TextBox 的控件需要與 ASP.NET 進(jìn)行交互,以確??梢哉_處理回發(fā)值并正常引發(fā)事件。以下代碼表明了如何擴(kuò)展 TextBoxLabel 控件以使其完全支持回發(fā)。
bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
string currentText = Text;
string postedText = postCollection[postDataKey];
if (!currentText.Equals(postedText, StringComparison.Ordinal))
{
Text = postedText;
return true;
}
return false;
}
void IPostBackDataHandler.RaisePostDataChangedEvent()
{
return;
}
復(fù)合控件的常見方案
復(fù)合控件是適合用于構(gòu)建復(fù)雜組件的工具,在復(fù)合控件中,多個(gè)子控件聚合到一起,并在彼此之間以及與外部之間進(jìn)行交互。呈現(xiàn)控件則只用于只讀式控件聚合,其輸出不包括交互元素(例如下拉框或文本框)。
如果您對(duì)事件處理和回發(fā)數(shù)據(jù)感興趣,我強(qiáng)烈建議您選擇復(fù)合控件。如果使用子控件,則生成復(fù)雜的控件樹會(huì)更加輕松,而且最終結(jié)果也更清晰簡(jiǎn)潔。此外,只有需要提供附加功能時(shí)才需要處理回發(fā)接口。
呈現(xiàn)控件不但需要實(shí)現(xiàn)附加接口,還要將含有屬性值的標(biāo)記靜態(tài)部分縫合到一起。
復(fù)合控件的優(yōu)點(diǎn)還表現(xiàn)在可以呈現(xiàn)多個(gè)同類項(xiàng),這與在 DataGrid 控件中的情況類似。將每個(gè)構(gòu)成項(xiàng)作為活動(dòng)對(duì)象啟用使您可以引發(fā)創(chuàng)建事件并以編程方式訪問它們的屬性。在 ASP.NET 2.0 中,對(duì)于要完全實(shí)現(xiàn)實(shí)際的數(shù)據(jù)綁定復(fù)合控件(上述控件只是隨便的舉例)所需的樣板代碼,絕大部分都隱藏在新基類的折疊部分中:CompositeDataBoundControl。
復(fù)合控件的呈現(xiàn)引擎
在深入探討 ASP.NET 2.0 編碼技術(shù)之前,讓我們回顧一下復(fù)合控件的內(nèi)部例行過程。我們提到過,復(fù)合控件的呈現(xiàn)是集中圍繞 CreateChildControls 方法進(jìn)行的,該方法從 Control 基類繼承而來。您可能會(huì)認(rèn)為,要使服務(wù)器控件呈現(xiàn)其內(nèi)容,替換 Render 方法是必不可少的一步。正如我們先前所看到的,如果 CreateChildControls 被替換,則并不總是需要執(zhí)行這一步。但是,何時(shí)在控件調(diào)用棧中調(diào)用 CreateChildControls 呢?
如圖中所示,在頁面第一次顯示時(shí),會(huì)在預(yù)呈現(xiàn)階段調(diào)用 CreateChildControls。
圖 3:在預(yù)呈現(xiàn)階段調(diào)用 CreateChildControls
特別是,請(qǐng)求處理代碼(在 Page 類中)在將 PreRender 事件引發(fā)至頁面和每個(gè)子控件之前會(huì)直接調(diào)用 EnsureChildControls。換言之,如果控件樹還未完全生成,則不會(huì)呈現(xiàn)任何控件。
以下代碼段例示了 EnsureChildControls(在 Control 基礎(chǔ)上定義的另一種方法)的偽代碼。
protected virtual void EnsureChildControls()
{
if (!ChildControlsCreated)
{
try {
CreateChildControls();
}
finally {
ChildControlsCreated = true;
}
}
}
此方法可能會(huì)在頁面和控件的生命周期內(nèi)反復(fù)調(diào)用。為避免控件重復(fù),ChildControlsCreated 屬性被設(shè)為 true。如果此屬性返回 true,則該方法會(huì)立即退出。
當(dāng)頁面回發(fā)時(shí),ChildControlsCreated 會(huì)在周期前期調(diào)用。如圖 4 所示,它在已發(fā)布數(shù)據(jù)處理階段調(diào)用。
圖 4:發(fā)生回發(fā)時(shí)在已發(fā)布數(shù)據(jù)處理階段調(diào)用
當(dāng) ASP.NET 頁面開始處理從客戶端發(fā)布的數(shù)據(jù)時(shí),它會(huì)嘗試查找一個(gè)其 ID 與已發(fā)布字段的名稱相匹配的服務(wù)器控件。在執(zhí)行此步驟期間,頁面代碼會(huì)調(diào)用 Control 類中的 FindControl 方法。反之,該方法需要確保在進(jìn)行操作之前控件樹已完全生成,因此它調(diào)用 EnsureChildControls 并按需要生成控件層次結(jié)構(gòu)。
那么要在 CreateChildControls 方法內(nèi)部執(zhí)行的代碼是怎樣的呢?盡管沒有正式的指南可供遵循,但通常認(rèn)為 CreateChildControls 至少必須完成以下任務(wù):清除 Controls 集合,生成控件樹,并清除子控件的視圖狀態(tài)。并不嚴(yán)格要求必須從 CreateChildControls 方法內(nèi)部設(shè)置 ChildControlsCreated 屬性。實(shí)際上,ASP.NET 頁面框架始終通過 EnsureChildControls(此方法可自動(dòng)設(shè)置布爾標(biāo)記)來調(diào)用 CreateChildControls。
用于解決設(shè)計(jì)時(shí)問題的 CompositeControl
隨 ASP.NET 2.0 一同提供了一個(gè)名為 CompositeControl 的基類。因此,新的非數(shù)據(jù)綁定復(fù)合控件應(yīng)該從該類派生而不是從 WebControl 派生。在開發(fā)控件方面,CompositeControl 的用法變動(dòng)不大。您仍然需要替換 CreateChildControls 并按先前所述方式編碼。那么 CompositeControl 的作用是什么?讓我們先從其原型著手:
public class CompositeControl :WebControl,
INamingContainer,
ICompositeControlDesignerAccessor
使用該類就無需再用 INamingContainer 裝飾控件,但這實(shí)際上并不是很重要,因?yàn)榻涌谥皇且粋€(gè)標(biāo)記并且不包含任何方法。更為重要的是,該類實(shí)現(xiàn)了一個(gè)名為 ICompositeControlDesignerAccessor 的全新接口。
public interface ICompositeControlDesignerAccessor
{
void RecreateChildControls();
}
此接口由復(fù)合控件的標(biāo)準(zhǔn)設(shè)計(jì)器用于在設(shè)計(jì)時(shí)重建控件樹。以下是 CompositeControl 中方法的默認(rèn)實(shí)現(xiàn)過程。
void ICompositeControlDesignerAccessor.RecreateChildControls()
{
base.ChildControlsCreated = false;
EnsureChildControls();
}
簡(jiǎn)言之,如果您從 CompositeControl 派生復(fù)合控件,就不會(huì)遇到設(shè)計(jì)時(shí)的故障,而且無需采用技巧和妙計(jì)就可以使控件在運(yùn)行時(shí)和設(shè)計(jì)時(shí)都能正常運(yùn)行。
要充分理解此接口的重要性,可試以寄存某 LabelTextBox 復(fù)合控件的示例頁為例,并將其轉(zhuǎn)換為設(shè)計(jì)模式??丶谶\(yùn)行時(shí)工作正常,但在設(shè)計(jì)時(shí)卻不可見。
圖 5:只有復(fù)合控件從 CompositeControl 派生才對(duì)它們進(jìn)行特殊的設(shè)計(jì)時(shí)處理
如果只是用 CompositeControl 替換 WebControl,則控件在運(yùn)行時(shí)仍然保持正常工作,而在設(shè)計(jì)時(shí)也會(huì)運(yùn)行良好。
圖 6:在設(shè)計(jì)時(shí)運(yùn)行良好的復(fù)合控件
生成數(shù)據(jù)綁定復(fù)合控件
大多數(shù)復(fù)雜的服務(wù)器控件都已綁定數(shù)據(jù)(也可能已經(jīng)模板化),并且由各種子控件構(gòu)成。這些控件保留了一個(gè)構(gòu)成項(xiàng)(通常為表的行或單元格)的列表。該列表在經(jīng)過回發(fā)后會(huì)保存在視圖狀態(tài)中,并且從綁定數(shù)據(jù)生成或從視圖狀態(tài)重建。該控件還在視圖狀態(tài)中保存其構(gòu)成項(xiàng)的數(shù)量,以便在頁面中其他控件引起回發(fā)時(shí)可以正確重建表結(jié)構(gòu)。我將用 DataGrid 控件舉例說明。
DataGrid 由一列行構(gòu)成,每一行都代表綁定數(shù)據(jù)源中的一個(gè)記錄。每個(gè)網(wǎng)格行都通過一個(gè) DataGridRow 對(duì)象(從 TableRow 派生的一個(gè)類)表示。在各網(wǎng)格行創(chuàng)建完成并被添加到最終網(wǎng)格表時(shí),諸如 ItemCreated 和 ItemDataBound 之類的相應(yīng)事件將被引發(fā)至頁面。當(dāng)通過數(shù)據(jù)綁定創(chuàng)建 DataGrid 時(shí),其行數(shù)由綁定項(xiàng)數(shù)和頁面大小決定。如果帶有 DataGrid 的頁面回發(fā)會(huì)怎樣?
這種情況下,如果是由 DataGrid 自身引起的回發(fā)(例如,用戶單擊以進(jìn)行排序或標(biāo)頁),則新頁面會(huì)再次通過數(shù)據(jù)綁定來呈現(xiàn) DataGrid。這是顯而易見的,因?yàn)?DataGrid 需要刷新數(shù)據(jù)進(jìn)行顯示。如果是主頁回發(fā),則情況就不同了,因?yàn)閱螕袅隧撁嫔系牧硪粋€(gè)控件(例如某按鈕)。這種情況下,DataGrid 不綁定到數(shù)據(jù)并且必須從視圖狀態(tài)進(jìn)行重建。(如果禁用了視圖狀態(tài),就是另外一種情況了,這時(shí)只能通過數(shù)據(jù)綁定顯示網(wǎng)格。)
數(shù)據(jù)源不保存在視圖狀態(tài)中。作為復(fù)合控件,DataGrid 包含子控件,其中每個(gè)子控件都將自己的狀態(tài)保存到視圖狀態(tài)并從視圖狀態(tài)恢復(fù)。DataGrid 只需跟蹤在所有行和所包含控件從視圖狀態(tài)恢復(fù)之前它所必須重復(fù)執(zhí)行的次數(shù)。此次數(shù)與所顯示綁定項(xiàng)的數(shù)量一致,并且必須作為控件狀態(tài)的一部分存儲(chǔ)到視圖狀態(tài)中。在 ASP.NET 1.x 中,您必須自己學(xué)習(xí)并實(shí)現(xiàn)此模式。在 ASP.NET 2.0 中,從新類 CompositeDataBoundControl 派生您的復(fù)合控件就可以了。
讓我們嘗試使用一種顯示可擴(kuò)展數(shù)據(jù)綁定新聞標(biāo)題行的網(wǎng)格類控件。在此過程中,我們將再度使用在前文中論及的 Headline 控件。
public class HeadlineListEx :CompositeDataBoundControl
{
:
}
HeadlineListEx 控件包含了一個(gè)收集了所有綁定數(shù)據(jù)項(xiàng)的 Items 集合屬性。該集合為公共集合,并且可在與多數(shù)列表控件一起運(yùn)行時(shí)通過編程方式填充。對(duì)典型數(shù)據(jù)綁定的支持是通過一對(duì)屬性(DataTextField 和 DataTitleField)實(shí)現(xiàn)的。這兩個(gè)屬性表明了數(shù)據(jù)源中將用于填充新聞標(biāo)題和文本的字段。Items 集合被保存到視圖狀態(tài)中。
要將 HeadlineListEx 控件轉(zhuǎn)換為真正的復(fù)合控件,您首先需要從 CompositeDataBoundControl 將其派生出來,然后再替換 CreateChildControls。有意思的是,你會(huì)注意到 CreateChildControls 是重載方法。
override int CreateChildControls()
override int CreateChildControls(IEnumerable data, bool dataBinding)
第一個(gè)重載方法替換了在 Control 類中定義的方法。第二個(gè)重載方法是每個(gè)復(fù)合控件都必須替換的一種抽象方法。實(shí)際上,復(fù)合控件的開發(fā)工作簡(jiǎn)化為兩大主要任務(wù):
? 替換 CreateChildControls。
? 實(shí)現(xiàn) Rows 集合屬性以跟蹤控件的所有構(gòu)成項(xiàng)。
Rows 屬性不同于 Items,因?yàn)樗槐4嬖谝晥D狀態(tài)中,且具有與請(qǐng)求相同的生存期,并引用幫助程序?qū)ο蠖皇墙壎〝?shù)據(jù)項(xiàng)。
public virtual HeadlineRowCollection Rows
{
get
{
if (_rows == null)
_rows = new HeadlineRowCollection();
return _rows;
}
}
Rows 集合在控件生成時(shí)填充。讓我們看一下 CreateChildControls 的替換方法。該方法采用了兩個(gè)參數(shù):綁定項(xiàng)和一個(gè)布爾標(biāo)記,其中布爾標(biāo)記用于指明該控件是通過數(shù)據(jù)綁定創(chuàng)建還是通過視圖狀態(tài)創(chuàng)建。(請(qǐng)注意示例程序文件中的程序員注釋使用的是英文,本文中將其譯為中文是為了便于參考。)
override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
if (dataBinding)
{
string textField = DataTextField;
string titleField = DataTitleField;
if (dataSource != null)
{
foreach (object o in dataSource)
{
HeadlineItem elem = new HeadlineItem();
elem.Text = DataBinder.GetPropertyValue(o, textField, null);
elem.Title = DataBinder.GetPropertyValue(o, titleField, null);
Items.Add(elem);
}
}
}
// 開始生成控件層次結(jié)構(gòu)
Table t = new Table();
Controls.Add(t);
Rows.Clear();
int itemCount = 0;
foreach(HeadlineItem item in Items)
{
HeadlineRowType type = HeadlineRowType.Simple;
HeadlineRow row = CreateHeadlineRow(t, type,
item, itemCount, dataBinding);
_rows.Add(row);
itemCount++;
}
return itemCount;
}
在數(shù)據(jù)綁定的情況下,首先要填充 Items 集合。遍歷綁定集合,提取數(shù)據(jù),然后填充 HeadlineItem 類的新建實(shí)例。接下來,遍歷 Items 集合(該集合中可能包含以編程方式添加的附加項(xiàng)),并在控件中創(chuàng)建行。
HeadlineRow CreateHeadlineRow(Table t, HeadlineRowType rowType,
HeadlineItem dataItem, int index, bool dataBinding)
{
// 為最外部表創(chuàng)建新行
HeadlineRow row = new HeadlineRow(rowType);
// 為子控件創(chuàng)建單元格
TableCell cell = new TableCell();
row.Cells.Add(cell);
Headline item = new Headline();
cell.Controls.Add(item);
// 此時(shí)引發(fā) HeadlineRowCreated 事件
// 將此行添加到所創(chuàng)建的 HTML 表
t.Rows.Add(row);
// 處理數(shù)據(jù)對(duì)象綁定
if (dataBinding)
{
row.DataItem = dataItem;
Headline ctl = (Headline) cell.Controls[0];
ctl.Text = dataItem.Text;
ctl.Title = dataItem.Title;
// 此時(shí)引發(fā) HeadlineRowDataBound 事件
}
return row;
}
CreateHeadlineRow 方法會(huì)創(chuàng)建并返回 HeadlineRow 類(從 TableRow 派生而來)的一個(gè)實(shí)例。在這種情況下,此行會(huì)包含一個(gè)由 Headline 控件填充的單元格。在其他情況下,您可以更改此部分代碼以根據(jù)需要添加多個(gè)單元格并相應(yīng)填充內(nèi)容。
重要的是,要將所需完成的任務(wù)分為兩個(gè)不同的步驟:創(chuàng)建和數(shù)據(jù)綁定。首先,創(chuàng)建行的布局,引發(fā)行創(chuàng)建事件(如果有),并最后將其添加到父表中。接下來,如果要將控件綁定到數(shù)據(jù),則設(shè)置對(duì)綁定數(shù)據(jù)敏感的子控件屬性。完成操作后,則引發(fā)一個(gè)行數(shù)據(jù)綁定事件(如果有)。
請(qǐng)注意,該模式更準(zhǔn)確描述了 ASP.NET 自帶復(fù)合控件的內(nèi)部體系結(jié)構(gòu)。
可以使用以下代碼來引發(fā)事件。
HeadlineRowEventArgs e = new HeadlineRowEventArgs();
e.DataItem = dataItem;
e.RowIndex = index;
e.RowType = rowType;
e.Item = row;
OnHeadlineRowDataBound(e);
請(qǐng)注意,只在要引發(fā)數(shù)據(jù)綁定事件時(shí)才設(shè)置 DataItem 屬性。事件數(shù)據(jù)結(jié)構(gòu)被任意設(shè)置為以下形式。如果您認(rèn)為有必要,盡可以對(duì)其進(jìn)行更改。
public class HeadlineRowEventArgs :EventArgs
{
public HeadlineItem DataItem;
public HeadlineRowType RowType;
public int RowIndex;
public HeadlineRow Item;
}
若要實(shí)際引發(fā)一個(gè)事件,通常的做法是使用一個(gè)如下定義的受保護(hù)方法。
protected virtual void OnHeadlineRowDataBound(HeadlineRowEventArgs e)
{
if (HeadlineRowDataBound != null)
HeadlineRowDataBound(this, e);
}
若要聲明此事件,可在 ASP.NET 2.0 中使用新的一般事件處理程序委托。
public event EventHandler<HeadlineRowEventArgs> HeadlineRowDataBound;
在示例頁中,一切均照常執(zhí)行。您可在控件標(biāo)記上定義處理程序并將某方法寫入代碼文件。示例如下。
<cc1:HeadlineListEx runat="server" ID="HeadlineListEx1"
DataTextField="notes" DataTitleField="lastname"
DataSourceID="MySource" OnHeadlineRowDataBound="HeadlineRowCreated" />
HeadlineRowCreated 事件處理程序的代碼顯示如下。
protected void HeadlineRowCreated(object sender, HeadlineRowEventArgs e)
{
if (e.DataItem.Title.Contains("Doe"))
e.Item.BackColor = Color.Red;
}
圖 7:運(yùn)行中的 HeadlineListEx 控件
通過掛接數(shù)據(jù)綁定事件,所有含有 Doe 的項(xiàng)都將以紅色背景呈現(xiàn)。
結(jié)論
復(fù)合控件是通過將其他控件聚合在某一公用 API 頂下創(chuàng)建而成的控件。復(fù)合控件將保留自己子控件的活動(dòng)實(shí)例,并且不僅限于呈現(xiàn)這些實(shí)例。通過檢查頁面跟蹤輸出中的控件樹部分,您就可以很容易看到這一點(diǎn)。使用復(fù)合控件可以帶來幾點(diǎn)好處,例如可以簡(jiǎn)化對(duì)事件和回發(fā)的處理。在 ASP.NET 1.x 中生成復(fù)雜的數(shù)據(jù)綁定控件有點(diǎn)棘手,需要您深入了解一些實(shí)現(xiàn)細(xì)節(jié)。在引進(jìn) CompositeDataBoundControl 基類的情況下,這種復(fù)雜性在 ASP.NET 中基本可以迎刃而解。最后,如果在 ASP.NET 2.0 中需要非數(shù)據(jù)綁定的復(fù)合控件,則可以使用 CompositeControl 基類。對(duì)于數(shù)據(jù)綁定復(fù)合控件,則可以改為考慮 CompositeDataBoundControl。無論是哪種情況,您都必須提供一個(gè) CreateChildControls 的有效替換方法,這是所有復(fù)合控件的核心,用于創(chuàng)建子控件層次結(jié)構(gòu)。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
