ASP.NET自定義控件複雜屬性聲明持久性淺析

在自定義控件的開發過程中,我們經常要給控件添加一些複雜類型的 屬性。利用聲明持久性(Declarative Persistence)可使得頁面開發人員能夠讓頁面開發人員在ASP.NET頁面中,聲明性地設置這些複雜屬性值,而無需編寫任何C#或者 VB.NET代碼。

參見下面的例子:

  • GridView的DataKeyNames屬性,其數據類型是string[]:
    <asp:GridView ID="GridView1" runat="server" DataKeyNames="ID, Title, Author">
    </asp:GridView>
  • GridView的RowStyle屬性,其數據類型是System.Web.UI.WebControls.TableItemStyle:

    <asp:GridView ID="GridView1" runat="server">
        
    <RowStyle BackColor="Red" ForeColor="Black"/>
    </asp:GridView>
  • GridView的Columns屬性,其數據類型是:System.Web.UI.WebControls.DataControlFieldCollection

    <asp:GridView ID="GridView1" runat="server" DataSourceID="ObjectDataSource1" >
        
    <Columns>
             
    <asp:BoundField DataField="Title" HeaderText="Title" SortExpression="Title" />
             
    <asp:BoundField DataField="ID" HeaderText="ID" SortExpression="ID" />
        
    </Columns>
    </asp:GridView>
  • GridView的PagerTemplate屬性, 其數據類型是System.Web.UI.ITemplate:

    <asp:GridView ID="GridView1" runat="server">
        
    <PagerTemplate>
             
    <div>
                 
    <span>Pager Template</span>
             
    </div>
        
    </PagerTemplate>
    </asp:GridView>

那如何才能實現在ASPX中聲明性地設置這些複雜屬性哪?

下面我將逐一講述這些屬性背後的故事,本文的重點不在於如何維護這些屬性的狀態,而是如何由ASPX Markup到複雜屬性的構建。

一、由ASPX Markup 到C# 或者VB.NET class

1.ASP.NET管道


對於ASPX頁面的請求,ASP.NET管道的目標是找到一個完全代表被請求頁面的託管類,如果該類不存在,則即時創建並且編譯。

ASP.NET頁 面由標記(Markup)和代碼(Codebehind)文件組成,整個頁面編譯過程包含兩個主要步驟:首先將ASPX Markup裝換成一個適合ASP.NET類層次結構的C#或者VB.NET臨時類,我們可以從ASP.NET的臨時文件夾中找到包含該類的文件;其次將 該臨時類編譯成一個程序集,最後將得到的程序集裝入托管該應用程序的AppDomain中。

對於特定的請求,HttpApplication對象從 config文件中獲取處理對象以服務該請求,通過下面的代碼片斷我們可以看出.aspx資源與PageHandlerFactory相關聯。 在ASP.NET管道中PageHandlerFactory對象將創建當前請求頁面的實例,隨後將請求交由該頁面處理。

<httpHandlers>
     
<add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="True"/>
     
<add path="*.ashx" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="True"/>
</httpHandlers>

2.PageHandlerFactory

PageHandlerFactory負責找到包含請求頁面類的程序集,如果該程序集還沒有被創建,則即時動態創建。請求頁面類是通過解析ASPX資源的Markup代碼創建的,並且存放在ASP.NET的臨時文件夾%AppData%"Local"Temp"Temporary ASP.NET Files中。

3.ControlBuilder

而ControlBuilder類就是負責將ASPX Markup聲明解析成爲ASP.NET Server控件,通常頁面上的每個控件都有一個默認的 ControlBuilder 類相關聯。在ASPX頁面解析過程中,ASP.NET 頁框架首先會生成與頁面控件樹對應的 ControlBuilder 對象樹,然後 ControlBuilder 樹用於生成頁代碼並創建控件樹。

ControlBuilder 定義瞭如何解析控件標記中的內容的,我們可以通過自定義ControlBuilder類來重寫此默認行爲。

在頁面解析過程中,ControlBuilder將會檢查ASP.NET Server控件是否標示了ParseChildren(true) Attribute。如果被標示則該控件內部嵌套的子節點將被解析爲控件的子屬性,否則該節點會被解析爲ASP.NET Server控件,並添加到原控件的Controls集合,關於該Attribute見下一節。

二、相關的Attributes

1.ParseChildrenAttribute

ParseChildrenAttribute應用於自定義控件類上,該Attribute將會告訴ASPX頁面解析器如何解析自定義控件內部的嵌套節點。

下表詳細的描述了ParseChildrenAttribute的用法:

Attribute Usage

描述

 ParseChildren(true)

嵌套的子節點必須對應着當前控件的屬性,如果找不到對應屬性將會產生一個解析錯誤。另外在當前控件的Tag內部也不允許任何文字節點。

例子:Repeater 以及其他數據綁定控件。

ParseChildrenAttribute(true, "PropertyName")

當前控件必須包含一個Public的屬性,屬性名等同於參數PropertyName。該屬性應該是一個集合類的數據類型。

而嵌套的字節點必須對應着該屬性的子Element.

例子:HtmlTable, HtmlTableRow控件。

ParseChildrenAttribute(false)

ParseChildrenAttribute(false, "PropertyName")

ParseChildrenAttribute is not applied to the control.

嵌套的子節點必須是ASP.NET 服務器控件。頁面解析器會根據該節點創建一個子控件,然後在當前控件上調用IParserAccessor.AddParsedSubObject方法,該方法的默認實現是將解析到的子控件添加到當前控件的Controls集合。

任何Literal文字節點將被創建爲LiteralControl的示例。

例子:Panel控件。


ParseChildrenAttribute (Type childControlType)

嵌套的子節點必須是指定的ASP.NET 服務器控件類型。

例子:MultiView控件。

WebControl類上已經被ParseChildrenAttribute(True)標示了,所以每個直接或者間接從WebControl派生的控件都會默認支持內部屬性聲明持久性。

在下面的例子中RowStyle節點將被解析爲GridView控件的子屬性:

<asp:GridView ID="GridView1" runat="server">
    
<RowStyle BackColor="Red" ForeColor="Black"/>
</asp:GridView>

ASP.NET Server控件可以通過ControlBuilderAttribute來指定特定的ControlBuilder,來修改上述的解析邏輯。

2.  PersistChildrenAttribute

PersistChildrenAttribute應用於自定義控件類上,是一個DesignTime的Attribute。用於指定是否將自定義控件內部的嵌套節點解析爲子控件,True將意味着解析該節點爲控件。

WebControl類上已經被PersistChildrenAttribute (False)標示了,而Panel類則被PersistChildrenAttribute (True)標示。

示例:

[ParseChildren(false), PersistChildren(true)]
public class MyControl : WebControl
{ }

在設計時添加一個Button控件到MyControl中:

<cc1:MyControl2 ID="MyControl21" runat="server" BorderStyle="Dotted" Height="56px" Width="349px">
      <asp:Button ID="Button2" runat="server" Text="Button" />
</cc1:MyControl2>

這個時候用戶可以在VS IDE的Design View中選中子Button控件,而如果MyControl被PersistChildrenAttribute (False)標示的話,子控件Button不能被選中。

3.  PersistenceModeAttribute

PersistenceModeAttribute應用在ASP.NET 服務器控件屬性上,是一個DesignTime的Attribute,用於指定用如何在設計時將ASP.NET Server控件屬性(Property)保存到ASP.NET 頁面

或者說在 ASPX Mrakup中以何種方式聲明該屬性。

PersistenceMode.InnerProperty則指定屬性在 ASP.NET 服務器控件中保持爲嵌套標記。

三、再看例子

1. GridView的DataKeyNames屬性,其數據類型是string[]:

<asp:GridView ID="GridView1" runat="server" DataKeyNames="ID, Title, Author">
</asp:GridView>

默認情況下ASP.NET頁面解析引擎會將ASPX Markup中賦予DataKeyNames屬性的值直接設置到該屬性上,由於該屬性的數據類型爲string[],直接設置將會失敗。

那GridView做了些什麼哪,見下面的代碼片斷:

[TypeConverter(typeof(StringArrayConverter))]
public virtual string[] DataKeyNames
{getset;}

也就是說通過給屬性添加特定的TypeConvertor來實現屬性的設置,在上面的例子中StringArrayConvter將string裝化爲string[]後,設置到DataKeyNames屬性上。

2. GridViewRowStyle屬性,其數據類型是TableRowStyle
<asp:GridView ID="GridView1" runat="server">
    
<RowStyle BackColor="Red" ForeColor="Black"/>
</asp:GridView>

由於GridView控件已經標示了ParseChildrenAttribute(True),所以其內部嵌套的節點將被解析爲自身的屬性。

另外RowStyle屬性也添加了PersistenceModeAttribute來指定如何生成ASPX Markup代碼。

[PersistenceMode(PersistenceMode.InnerProperty)]
public TableItemStyle RowStyle
{get;}

3. GridView的Columns屬性,其數據類型是DataFieldCollection:

<asp:GridView ID="GridView1" runat="server" DataSourceID="ObjectDataSource1" >
    
<Columns>
         
<asp:BoundField DataField="Title" HeaderText="Title" SortExpression="Title" />
         
<asp:BoundField DataField="ID" HeaderText="ID" SortExpression="ID" />
    
</Columns>
</asp:GridView>

ASP.NET頁面是如何來解析上面的一段代碼哪?首先Columns節點將被解析爲GridView的屬性,可是Columns內部的子節點是如何被解析並添加到Columns集合中去的哪?

另外在VS IDE Source View中準備添加Columns屬性的子節點代碼的時候,爲什麼VS IDE會幫我們列出所有可實例化的子類型?


讓我們來看一下Columns屬性的數據類型:DataControlFieldCollection.

ASP.NET頁面解析器在解析控件屬性的時候,如果發現該屬性實現了IList接口,解析完成該屬性的子節點後,會去調用該屬性的Add方法來添加這些子Element。

另外在設計時也會通過該屬性數據類型的Item子屬性獲取可以實例化的子Element類型,並且通過智能提示表現出來。

也就是說這些集合類屬性必須實現IList接口才可以實現屬性的聲明持久化。

看一下上面示例的Markup代碼對應的C#代碼,注意代碼行16,17。


 1[System.Diagnostics.DebuggerNonUserCodeAttribute()]
 2private global::System.Web.UI.WebControls.BoundField @__BuildControl__control23()
 3{
 4      global::System.Web.UI.WebControls.BoundField @__ctrl;
 5      @__ctrl = new global::System.Web.UI.WebControls.BoundField();
 6      @__ctrl.DataField = "ID";
 7      @__ctrl.HeaderText = "ID";
 8      @__ctrl.SortExpression = "ID";
 9      return @__ctrl;
10}

11 
12[System.Diagnostics.DebuggerNonUserCodeAttribute()]
13private void @__BuildControl__control22(System.Web.UI.WebControls.DataControlFieldCollection @__ctrl)
14{
15      global::System.Web.UI.WebControls.BoundField @__ctrl1;
16      @__ctrl1 = this.@__BuildControl__control23();
17      @__ctrl.Add(@__ctrl1);
18}

19 
20[System.Diagnostics.DebuggerNonUserCodeAttribute()]
21private global::System.Web.UI.WebControls.GridView @__BuildControlGridView1()
22{
23      global::System.Web.UI.WebControls.GridView @__ctrl;
24      @__ctrl = new global::System.Web.UI.WebControls.GridView();
25      this.GridView1 = @__ctrl;
26      @__ctrl.ApplyStyleSheetSkin(this);
27      @__ctrl.ID = "GridView1";
28 
29      this.@__BuildControl__control22(@__ctrl.Columns);
30 
31      return @__ctrl;
32}

參照上面說的幾點我們就可以寫出自定義的集合類屬性的聲明持久化。

當然了這只是實現了ASPX到CS,我們還需要給集合類以及子Element添加關於狀態管理的代碼(實現IStateManager接口)纔可以在實際項目中使用,這一點我就不再贅述了。

4. GridView的PagerTemplate屬性,其數據類型是ITemplate:

其實ITemplate節點內部就是一個控件集合,TemplateBuilder負責將該Tag構建成爲一個控件組。

在運行時我們通過ITemplate接口的InstantiateIn方法將一個實例化的Template放到指定的Container中去。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章