9.4.1 处理复杂的属性
默认情况下,在设计期间,控件属性返回的值存储在控件标记的特性中。例如,如果控件BookInfo的属性是MainTitle和SubTitle,则该控件的标记如下所示。
<cc1:BookInfo ID="BookInfo1" runat="server"
MainTitle="Custom Controls and Web Parts"
SubTitle="ASP.NET 2.0"/>
属性设置嵌入到控件的标记中,所以开发人员很容易在设计期间设置MainTitle和SubTitle属性:只需重写特性即可。在Visual Studio 2005的属性列表中修改属性的值,也会更新这些特性。但如果属性生成了大量文本,把这些文本存储在一个特性中就不合适。把大量的文本值存储在元素的文本中更方便。
1. 把属性值保存为标记的内容
例如,BookInfo的MainTitle和SubTitle属性可以以如下方式保存。
<cc1:BookInfo ID="BookInfo1" runat="server">
<MainTitle>Custom Controls and Web Parts</MainTitle>
<SubTitle>ASP.NET 2.0</SubTitle>
</cc1:BookInfo>
要实现这个设计,需要标记控件,将其属性保存为自定义控件的标记内容。这需要设置四个特性:一个特性在类声明上设置,其他三个在属性上设置。这些特性如下所示。
● 类声明上的ParseChildren特性:它告诉类,应处理控件标记中的内容,并与对应的属性匹配。
● PersistenceMode:它必须设置为PersistenceMode.InnerProperty,才能把属性保存为嵌套的元素。
● 属性上的DesignerSerializationVisibility:它必须设置为DesignerSerializationVisibility. Content,Visual Studio 2005才能在设计期间处理控件标记的内容。
● 属性上的NotifyParentProperty:它在修改嵌套的元素时更新自定义控件的属性。
在Visual Basic 2005中,配置BookInfo类,将MainTitle和SubTitle属性保存为嵌套的标记,如下所示。
<ParseChildren(True), _
ToolboxData("<{0}:BookInfo runat=server></{0}:BookInfo>")> _
Public Class BookInfo
Inherits System.Web.UI.WebControls.WebControl
Private _MainTitle As String
Private _SubTitle As String
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
NotifyParentProperty(True), _
PersistenceMode(PersistenceMode.InnerProperty)> _
Public Property MainTitle() As String
...property code...
End Property
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
NotifyParentProperty(True), _
PersistenceMode(PersistenceMode.InnerProperty)> _
Public Property SubTitle() As String
...property code...
End Property
C#的对应代码如下所示。
[ParseChildren(true)]
[ToolboxData("<{0}:BookInfo runat=server></{0}:BookInfo>")]
public class BookInfo : System.Web.UI.WebControls.WebControl
{
private string _MainTitle;
private string _SubTitle;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public string MainTitle
{
...property code...
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public string SubTitle
{
...property code...
}
}
提示:
在插入标记,保存属性值时,可能会遇到Visual Studio .NET伪造的错误:模式不支持这些嵌套的标记。可以忽略这些错误。
2. 保存复杂的属性值
只要属性返回简单的数据类型,可以轻松地转换为文本,在特性或嵌套元素中保存属性值就是有效的。但对于较复杂的属性(例如对象的多个数据项与一个属性关联),就需要一种更复杂的机制。可以使用数据类型转换器,把对象转换为字符串(如第6章所述)。另外,指定属性的数据在元素中存储,可以让使用控件的开发人员在设计期间在source视图中设置这些属性。
例如,自定义控件BookInfo需要为一本书处理多个作者。为此,BookInfo控件有一个Authors属性,它接受一个Author对象数组。下面的Visual Basic 2005代码把Authors属性声明为一个ArrayList。
Public Property Authors() As ArrayList
Get
Return _Authors
End Get
Set(ByVal value As ArrayList)
_Authors = value
End Set
End Property
C#的对应代码如下所示。
public ArrayList Authors
{
get
{
return _Authors;
}
set
{
_Authors = value;
}
}
提示:
较好的设计方案是,使用定制的集合替代ArrayList,来控制传送给ArrayList的对象类型。但是,这需要一本书来讨论面向对象设计中的最佳实践方式。
每个Author对象都有多个属性(例如FirstName、LastName)。在Visual Basic 2005中,Author类如下所示。
Public Class Author
Private _FirstName As String
Private _LastName As String
Public Property FirstName() As String
Get
Return _FirstName
End Get
Set(ByVal value As String)
_FirstName = value
End Set
End Property
Public Property LastName() As String
Get
Return _LastName
End Get
Set(ByVal value As String)
_LastName = value
End Set
End Property
End Class
C#的对应代码如下所示。
public class Author
{
private string _FirstName;
private string _LastName;
public string FirstName
{
get
{
return _FirstName;
}
set
{
_FirstName = value;
}
}
public string LastName
{
get
{
return _LastName;
}
set
{
_LastName = value;
}
}
}
如果Author对象可以使用BookInfo标记中的嵌套标记,在设计期间添加到Authors属性中,就非常方便,如下所示。
<cc1:BookInfo ID="BookInfo1" runat="server">
<cc1:Author FirstName="Peter" LastName="Vogel"/>
<cc1:Author FirstName="Sara" LastName="Shlaer"/>
<cc1:Author FirstName="Richard" LastName="Purchas"/>
<cc1:BookInfo>
为此,必须把ParseChildren特性添加到自定义控件的类声明中,并传送True参数和要从嵌套元素中加载的属性名。把ParseChildren特性的第一个参数设置为True,将指定属性值在自定义控件的开闭标记之间嵌套,而不是放在控件的特性中。第二个参数指定从嵌套标记中设置哪个属性。
提示:
ParseChildren特性的第一个参数设置特性的ChildrenAsControls属性,第二个参数设置特性的DefaultProperty属性。
注意:
这个标记的名称必须匹配对象的名称,而不是要加载的属性名,这是很重要的。在这个例子中,类叫做Author,所以在BookInfo元素中使用的标记也叫做Author。另外,即使自定义控件标记只包含一个嵌套标记,ParseChildren特性引用的属性也必须接受一个相同类型的集合对象(例如ArrayList)。
下面的Visual Basic 2005示例指定,BookInfo对象的Authors属性值是嵌套在自定义控件的开闭标记中的一个元素。
<ParseChildren(True, "Authors"), _
ToolboxData("<{0}:BookInfo runat=server></{0}:BookInfo>")> _
Public Class BookInfo
C#的对应代码如下所示。
[ParseChildren(true, "Authors")]
[ToolboxData("<{0}:BookInfo runat=server></{0}:BookInfo>")]
public class BookInfo
在ParseChildren特性中指定的属性自动用自定义控件的开闭标记之间的数据加载。在这个例子中,Authors属性自动用Author对象加载,在BookInfo标记中,每个Author对象的属性在标记Author的特性上设置。
加载ArrayList后,就可以象处理其他列表那样处理其内容了。例如,在下面的Visual Basic 2005代码中,Render方法使用Author对象迭代Authors属性,在HTML表中显示作者信息。
Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
writer.write("<Table>")
For Each aut As Author In Me.Authors
writer.Write("<tr>")
writer.Write("<tc>" & aut.FirstName & "</tc>")
writer.Write("<tc>" & aut.LastName & "</tc>")
writer.Write("</tr>")
Next
writer.write("</Table>")
End Sub
C#的对应代码如下所示。
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
writer.Write("<Table>");
foreach(Author aut in this.Authors)
{
writer.Write("<tr>");
writer.Write("<tc>" + aut.FirstName + "</tc>");
writer.Write("<tc>" + aut.LastName + "</tc>");
writer.Write("</tr>");
}
writer.Write("</Table>");
}
这个机制有一个重要的限制:只能把一个属性指定为ParseChildren特性的默认属性。如果多个属性都有复杂的数据类型,就需要一种更复杂的机制。而且,一旦在ParseChildren特性中指定每默认属性,其他属性就不能在嵌套标记中存储其数据了,如上一节所述。
一种解决方法是,把默认属性看做一个可以存储任意类型的对象的库。假定指定了ParseChildren特性的默认属性,ASP.NET就会自动把自定义控件的开闭标记之间的所有对象都放在默认属性返回的集合中。我们需要正确地处理集合中的每个对象类型。这种方法允许开发人员把组成控件的标记嵌套在自定义控件中,从而给自定义控件添加组成控件。
例如,要在设计期间给BookInfo控件添加一个ASP.NET标签控件,开发人员可以把Label标记插入到BookInfo标记中。
<cc1:BookInfo ID="BookInfo1" runat="server" >
<asp:Label ID="Label1" runat="server" Text="Your book"></asp:Label>
</cc1:BookInfo>
为此,需要创建一个属性,保存添加到自定义控件中的所有控件。如下面的Visual Basic 2005属性所示。
Private _ChildControls As ArrayList
Public Property ChildControls() As ArrayList
Get
Return _ChildControls
End Get
Set(ByVal value As ArrayList)
_ChildControls = value
End Set
End Property
C#的对应代码如下所示。
private ArrayList _ChildControls;
public ArrayList ChildControls
{
get
{
return _ChildControls;
}
set
{
_ChildControls = value;
}
}
在类声明中,必须把这个“库”属性指定为控件的默认属性,如下面的Visual Basic 2005代码所示。
<ParseChildren(True, "ChildControls"), _
ToolboxData("<{0}:BookInfo runat=server></{0}:BookInfo>")> _
Public Class BookInfo
C#的对应代码如下所示。
[ParseChildren(true, "ChildControls")]
[ToolboxData("<{0}:BookInfo runat=server></{0}:BookInfo>")]
public class BookInfo
在Render方法中,可以迭代ChildControls集合,调用每个控件的RenderControl方法,显示每个控件。在Visual Basic 2005中,代码如下所示。
Protected Overrides Sub Render(ByVal writer As System.Web.UI.HtmlTextWriter)
For Each ctl As Control In Me.ChildControls
ctl.RenderControl(writer)
Next
End Sub
C#的对应代码如下所示。
protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
foreach(Control ctl in this.ChildControls)
{
ctl.RenderControl(writer);
}
}
这为生成组成控件提供了一种简单的机制,但并没有真正解决多个数据类型比较复杂的属性问题。它只是创建了一个包含所有复杂对象的属性。最好每个对象都有一个专用属性,利用某种机制来管理嵌套在自定义控件中的标记,而不只是盲目地显示它们。






