ASP.NET自定义控件复杂属性声明持久性浅析

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中去。

全文完。

 已接近深夜,有不妥的地方下来再改。

posted @ 2008-05-10 00:26  ted  阅读(4991)  评论(7编辑  收藏  举报