18.5 简单属性和子属性

简单属性是一个类型为字符串或者易于映射到字符串的类型的属性,它在控件的开始标记上自行保留为特性。String类型的属性和.NET Framework类库中的基元值类型(如Boolean、Int16、Int32和Enum)均为简单属性。可以通过添加代码将简单属性存储在ViewState字典中,以便在回发间进行状态管理。

如果一个属性的类型是本身具有属性(称为子属性)的类,则该属性就称为复杂属性。例如,WebControl的Font属性的类型是本身具有属性(如Bold和Name)的FontInfo类。Bold和Name是WebControl的Font属性的子属性。ASP.NET页框架可通过使用带有连字符的语法(例如Font-Bold="true")在控件的开始标记上保存子属性,但如果在控件的标记(例如<font Bold="true">)中保存子属性,则子属性在页中的可读性更强。

其实,对于一些复杂一点的自定义控件,子属性在控件定义中应用非常广泛。为了演示如何在控件中创建子属性,下面将通过创建一个MyBook控件来进行详细阐述。

18.5.1 定义MyBook控件

对于MyBook控件,需要为它定义如下几个属性:

1)Author:具有子属性的属性。Author类型拥有自己的属性,如FirstName和LastName,这些属性是Author属性的子属性。Author属性和Author类的属性需要设计时特性以在控件的标记中进行保持,如下面的示例所示:


<cc1:MyBook>

<Author FirstName="Judy"LastName="Lew"/>

</cc1:MyBook>


2)BookType:自定义枚举BookType的简单属性。其中,BookType枚举包括诸如Fiction和NonFiction之类的值。

3)CurrencySymbol、Price与Title简单属性。默认情况下,页框架将这些简单属性在控件的标记上保持为特性。如下面的示例所示:


<cc1:MyBook Title="Wingtip Toys Stories"

CurrencySymbol="$"

Price="16"

BookType="Fiction">

</cc1:MyBook>


MyBook类的全部源代码如下所示:


[AspNetHostingPermission(SecurityAction.Demand,

Level=AspNetHostingPermissionLevel.Minimal),

AspNetHostingPermission(SecurityAction.InheritanceDemand,

Level=AspNetHostingPermissionLevel.Minimal),

DefaultProperty("Title"),

ToolboxData("<{0}:MyBook runat=\"server\"></{0}:MyBook>")]

public class MyBook:WebControl

{

private Author authorValue;

private String initialAuthorString;

[Bindable(true),

Category("Appearance"),

DefaultValue(""),

Description("The name of the author."),

DesignerSerializationVisibility(

DesignerSerializationVisibility.Content),

PersistenceMode(PersistenceMode.InnerProperty)]

public virtual Author Author

{

get

{

if(authorValue==null)

{

authorValue=new Author();

}

return authorValue;

}

}

[Bindable(true),

Category("Appearance"),

DefaultValue(BookType.NotDefined),

Description("Fiction or Not")]

public virtual BookType BookType

{

get

{

object t=ViewState["BookType"];

return(t==null)?BookType.NotDefined:((BokType)t;

}

set

{

ViewState["BookType"]=value;

}

}

[Bindable(true),

Category("Appearance"),

DefaultValue(""),

Description("The symbol for the currency."),

Localizable(true)]

public virtual string CurrencySymbol

{

get

{

string s=((sring)ViewState["CurrencySymbol"];

return(s==null)?String.Empty:s;

}

set

{

ViewState["CurrencySymbol"]=value;

}

}

[Bindable(true),

Category("Appearance"),

DefaultValue("0.00"),

Description("The price of the book."),

Localizable(true)]

public virtual Decimal Price

{

get

{

object price=ViewState["Price"];

return(price==null)?Decimal.Zero:((Dcimal)price;

}

set

{

ViewState["Price"]=value;

}

}

[Bindable(true),

Category("Appearance"),

DefaultValue(""),

Description("The title of the book."),

Localizable(true)]

public virtual string Title

{

get

{

string s=((sring)ViewState["Title"];

return(s==null)?String.Empty:s;

}

set

{

ViewState["Title"]=value;

}

}

protected override void RenderContents(HtmlTextWriter writer)

{

writer.RenderBeginTag(HtmlTextWriterTag.Table);

writer.RenderBeginTag(HtmlTextWriterTag.Tr);

writer.RenderBeginTag(HtmlTextWriterTag.Td);

writer.WriteEncodedText(Title);

writer.RenderEndTag();

writer.RenderEndTag();

writer.RenderBeginTag(HtmlTextWriterTag.Tr);

writer.RenderBeginTag(HtmlTextWriterTag.Td);

writer.WriteEncodedText(Author.ToString());

writer.RenderEndTag();

writer.RenderEndTag();

writer.RenderBeginTag(HtmlTextWriterTag.Tr);

writer.RenderBeginTag(HtmlTextWriterTag.Td);

writer.WriteEncodedText(BookType.ToString());

writer.RenderEndTag();

writer.RenderEndTag();

writer.RenderBeginTag(HtmlTextWriterTag.Tr);

writer.RenderBeginTag(HtmlTextWriterTag.Td);

writer.Write(CurrencySymbol);

writer.Write(" ");

writer.Write(String.Format("{0:F2}",Price));

writer.RenderEndTag();

writer.RenderEndTag();

writer.RenderEndTag();

}

protected override void LoadViewState(object savedState)

{

base.LoadViewState(savedState);

Author auth=((Athor)ViewState["Author"];

if(auth!=null)

{

authorValue=auth;

}

}

protected override object SaveViewState()

{

if(authorValue!=null)

{

String currentAuthorString=authorValue.ToString();

if(!((crrentAuthorString.Equals(initialAuthorString)))

{

ViewState["Author"]=authorValue;

}

}

return base.SaveViewState();

}

protected override void TrackViewState()

{

if(authorValue!=null)

{

initialAuthorString=authorValue.ToString();

}

base.TrackViewState();

}

}


在上面的代码中,应用MyBook控件的Author属性的DesignerSerializationVisibilityAttribute和PersistenceModeAttribute特性来对Author类的属性进行序列化和保持。通过ViewState属性中的读/写属性来定义控件中的简单属性(即BookType、CurrencySymbol、Price和Title属性),这样,就可以自行对存储在ViewState属性中的属性的状态进行管理了。

除此之外,MyBook控件还将Author属性定义为只读属性,并实现自定义状态管理。如下所示:

1)在TrackViewState方法中,MyBook控件先将初始Author属性保存到字符串,然后通过调用基类的TrackViewState方法开始跟踪状态。

2)在SaveViewState方法中,MyBook控件确定Author属性是否已从初始值发生更改。如果该属性已更改,则MyBook控件使用"Author"键将Author属性保存到ViewState字典中。然后MyBook控件调用基类的SaveViewState方法。由于启动了状态跟踪,因此保存在ViewState中的Author对象将自动标记为已修改,并被保存为基类视图状态的一部分。

3)在LoadViewState中,MyBook控件首先调用基类的LoadViewState方法,此调用自动还原ViewState字典。然后确定ViewState字典的"Author"下是否存储了某个项。如果有,该控件就会将相应的视图状态值加载到Author属性中。

设计完MyBook类之后,还需要设计一个BookType枚举变量。如下面的代码所示:


public enum BookType

{

NotDefined=0,

Fiction=1,

NonFiction=2

}


18.5.2 定义子属性Author

定义好MyBook控件的主类之后,下面来定义子属性类Author。Author类很简单,只在该类中定义三个属性即可(即FirstName、LastName和MiddleName属性)。其中,将NotifyParent-PropertyAttribute特性应用于FirstName、LastName和MiddleName属性并将特性的构造函数参数设置为true(即NotifyParentProperty(true)]),会使可视化设计器将这些属性的更改传播和序列化到其父属性(一个Author实例)。

除此之外,还需要在该类前定义一个TypeConverter特性,即自定义类型转换器。有了这个自定义类型转换器,就可以在视图状态中存储一个Author实例。该类型转换器既可以将Author实例转换为字符串,也可以进行反向转换。通过定义类型转换器,可以在可视化设计器中设置Author的子属性。Author类的详细代码如下所示:


[TypeConverter(typeof(AuthorConverter))]

public class Author

{

private string firstnameValue;

private string lastnameValue;

private string middlenameValue;

public Author()

:this(String.Empty, String.Empty, String.Empty)

{

}

public Author(string firstname, string lastname)

:this(firstname, String.Empty, lastname)

{

}

public Author(string firstname,

string middlename, string lastname)

{

firstnameValue=firstname;

middlenameValue=middlename;

lastnameValue=lastname;

}

[Category("Behavior"),

DefaultValue(""),

Description("First name of author."),

NotifyParentProperty(true)]

public virtual String FirstName

{

get

{

return firstnameValue;

}

set

{

firstnameValue=value;

}

}

[Category("Behavior"),

DefaultValue(""),

Description("Last name of author."),

NotifyParentProperty(true)]

public virtual String LastName

{

get

{

return lastnameValue;

}

set

{

lastnameValue=value;

}

}

[Category("Behavior"),

DefaultValue(""),

Description("Middle name of author."),

NotifyParentProperty(true)]

public virtual String MiddleName

{

get

{

return middlenameValue;

}

set

{

middlenameValue=value;

}

}

public override string ToString()

{

return ToString(CultureInfo.InvariantCulture);

}

public string ToString(CultureInfo culture)

{

return TypeDescriptor.GetConverter(

GetType()).ConvertToString(null, culture, this);

}

}


18.5.3 定义类型转换器AuthorConverter

创建好MyBook类与子属性Author类之后,还需要创建一个名为AuthorConverter的类型转换器,该类型转换器将与来自其他控件创作示例的Author对象一起使用。ASP.NET在运行时使用类型转换器来对控件状态和视图状态中存储的对象进行序列化和反序列化。也就是说,AuthorConverter可以将一个Author对象转换为String类型,也可以将一个String表示形式转换为Author对象。

而在子属性Author类中使用TypeConverterAttribute可使类型转换器与类型(或为其定义转换器的类型的属性)关联。AuthorConverter允许MyBook控件以视图状态存储Author属性。仅当自定义类型已经定义了类型转换器并与类型关联时,才可以将该自定义类型存储在视图状态中。

AuthorConverter类派生自ExpandableObjectConverter类,因此可视化设计器中的属性浏览器可以提供用于编辑Author类型的子属性的展开/折叠用户界面。AuthorConverter类的详细代码如下所示:


public class AuthorConverter:ExpandableObjectConverter

{

public override bool CanConvertFrom(

ITypeDescriptorContext context, Type sourceType)

{

if(sourceType==typeof(string))

{

return true;

}

return base.CanConvertFrom(context, sourceType);

}

public override bool CanConvertTo(

ITypeDescriptorContext context, Type destinationType)

{

if(destinationType==typeof(string))

{

return true;

}

return base.CanConvertTo(context, destinationType);

}

public override object ConvertFrom(ITypeDescriptorContext

context, CultureInfo culture, object value)

{

if(value==null)

{

return new Author();

}

if(value is string)

{

string s=((sring)value;

if(s.Length==0)

{

return new Author();

}

string[]parts=s.Split('');

if(((prts.Length<2)||((prts.Length>3))

{

throw new ArgumentException(

"Name must have 2 or 3 parts.","value");

}

if(parts.Length==2)

{

return new Author(parts[0],parts[1]);

}

if(parts.Length==3)

{

return new Author(parts[0],parts[1],parts[2]);

}

}

return base.ConvertFrom(context, culture, value);

}

public override object ConvertTo(

ITypeDescriptorContext context,

CultureInfo culture, object value, Type destinationType)

{

if(value!=null)

{

if(!((vlue is Author))

{

throw new ArgumentException(

"Invalid Author","value");

}

}

if(destinationType==typeof(string))

{

if(value==null)

{

return String.Empty;

}

Author auth=((Athor)value;

if(auth.MiddleName!=String.Empty)

{

return String.Format("{0}{1}{2}",

auth.FirstName,

auth.MiddleName,

auth.LastName);

}

else

{

return String.Format("{0}{1}",

auth.FirstName,

auth.LastName);

}

}

return base.ConvertTo(context, culture, value,

destinationType);

}

}


从上面的代码中可以看出,AuthorConverter类实际上只重写了4个方法,这4个方法阐述了要在Author的实例与字符串之间进行双向转换所必须执行的任务。如下所示:

1)重写CanConvertFrom方法。该方法确定是否可由特定类型创建Author实例。如果传入的类型属于String类型,则它返回true。

2)重写CanConvertTo方法。该方法确定Author实例是否可以转换为特定类型。如果传入的类型属于String类型,则它返回true。

3)重写ConvertFrom方法。该方法返回一个包含Author实例的FirstName、MiddleName和LastName属性的字符串。

4)重写ConvertTo方法。该方法对包含Author实例的串联的FirstName、MiddleName和LastName属性的字符串进行分析。它返回Author类的新实例,其中有来自串联字符串的数据。

18.5.4 使用MyBook控件

到目前为止,一个完整的MyBook控件就创建完成了。为了测试这个MyBook控件,首先在页面上添加一个MyBook控件和一个Button控件。如下面的代码所示:


<%@Page Language="C#"AutoEventWireup="true"

CodeBehind="WebForm2.aspx.cs"Inherits="_18_1.WebForm2"%>

<%@Register Assembly="18-1"Namespace="_18_1"TagPrefix="cc1"%>

<!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">

<cc1:MyBook ID="Book1"runat="server"

Title="易学C#"CurrencySymbol="¥"

BackColor="#FFE0C0"Font-Names="Tahoma"

Price="45"BookTyp="NotDefined">

<Author FirstName="马伟"LastName="马登伟"/>

</cc1:MyBook>

<br/>

<asp:Button ID="Button1"OnClick="Button1_Click"

runat="server"Text="切换"/>

<br/>

<asp:HyperLink ID="Hyperlink1"

NavigateUrl="WebForm2.aspx"runat="server">

Reload Page</asp:HyperLink>

</form>

</body>

</html>


然后在Button1控件的Button1_Click事件中对Book1控件做如下处理:


public void Button1_Click(object sender, EventArgs e)

{

Book1.Author.FirstName="马伟";

Book1.Author.LastName="马登伟";

Book1.Title="ASP.NET4程序设计";

Book1.Price=100;

Button1.Visible=false;

}


示例运行结果如图18-7所示。

figure_0661_0503

图 18-7 MyBook控件的示例运行结果