服务器控件属性
本章内容
4.1 控件属性的作用
4.2 简单属性
4.3 属性的设计时特性
4.4 复杂属性
4.5 深入研究——定制自己的属性编辑器
4.6 类型转换器
4.7 实现自定义属性
4.1 控件属性的作用
属性、方法和事件是控件使用者与控件交互的接口。本节主要介绍控件属性。属性分为系统属性和自定义的属性。
4.1.1 系统属性
当开发控件时如果选择基类,比如选择继承WebControl 基类,一旦继承于此类,一些默认的系统属性就会成为当前控件的属性集的一部分,图4-1 所示的是WebControl 类的系统属性。
可以看到一个通用Web 控件所应具备的基本属性都已经有了,在实际开发控件时选择某个基类。
4.1.2 自定义属性
4.1.1 节所讲的是系统已有的属性,在开发控件时一般都要为自己的控件增加一些自定义属性。自定义属性与系统属性完全一样。只是由于不具有系统属性的通用性而需要开发者自己去实现。下面看一下属性的语法格式:
string strText = "默认值";
public string Text
{
get
{
return strText;
}
set
{
strText = value;
}
}
以上是一个最简单的属性,由一个set 和get 语段组成。注意,set 和get 段不是必需的,比如可以去掉set 段表示此属性只允许读取而不允许接收值。
事实上属性的特性范畴还比较多,如简单属性、复杂属性,以及属性在设计时的特性和标记形式的格式等,下面将对这些特性一一进行介绍。
4.2 简单属性
简单属性是类型为字符串的或容易转换为字符串的属性。简单属性在控件的开始标记上自行保留为属性。.NET Framework 类库中的基元值类型,如String 、Boolean 、Int16 、Int32 、DateTime 、Byte 、Char 、Double 和Enum 均为简单属性。可以通过添加代码将简单属性存储在ViewState 字典中,以便在回发间进行状态管理。请看例子:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public string Value
{
get
{
String s = (String)ViewState["Value"];
return ((s == null) ? String.Empty : s);
}
set
{
ViewState["Value"] = value;
}
}
上面声明的简单属性中,属性可接收及其返回值的类型是String ,表示本属性为简单属性。另外,简单属性可以直接使用ViewState 存储其值,因为简单属性可以直接映射为字符串,而ViewState 中可以直接接收的格式也是字符串。
ViewState 是页面视图状态机制中的存储机制,是为解决在Web 浏览器两次访问之间无状态保持而提供的一种机制,视图信息存储在网页中专用HiddenField 区域,而且每次页面提交都会往返于客户端和服务器,因此一般视图主要用于存储少量的文本数据信息,而不适合存储数据量比较大的业务数据。另外,复杂属性的存储也要自己实现视图机制功能,这一点在后面讨论视图机制的章节会详细介绍,这里仅作了解即可。
只要控件中定义了上面的代码段,对应在页面设计器属性窗口中就会包含此项,如图4-2 所示。
图4-2 属性窗口中的属性
在属性窗口中输入一些文本,打开设计器中的源代码会看到如下标记的ASP.NET 代码:
<cc1:controlproperty id="ControlProperty1" runat="server" Value="简单属性">
</cc1:controlproperty>
同样,Boolean 、Int16 、Int32 、DateTime 、Byte 、Char 、Double 和Enum 等类型的属性与上面的String 类型属性代码标记完全一样。简单属性比较简单,就讲解到这里。
4.3 属性的设计时特性
.NET Framework 为控件设计时属性提供了很多丰富的类,这些属性的功能非常灵活,控制范围广泛,比如可以控制该属性在属性窗口中的显示模式,如:是否在属性窗口中显示该属性,也可以指定此属性必须接收值类型描述,按组分类等,也可以控制文本的标记呈现格式等,甚至可以自己定义一个属性类,实现自己想实现的功能。下面讲一下常用的.NET Framework 的属性类对控件的支持功能。
Bindable
指定属性是否可以绑定一个有效数据源,通常使用布尔值进行设置。例如:Bindable(true) 。如果使用值true 标记属性,表示该属性可以绑定一个有效数据源 。
Browsable
指定属性是否应该在属性窗口中显示,使用布尔值设置。一般情况下 ,对于 常用的和比较重要的属性设置Browsable 为true ,否则 , 设置Browsable 为false 。
EditorBrowsable
设置属性在编辑器中的可见性,比如设置在智能提示列表不显示或高级用户才可以看到该属性。
Category
指定属性在属性浏览器中进行分组显示的类别。该设计时特性帮助可视化编辑器将属性进行逻辑分组。通常分为:外观(Appearance )、行为(Behavior )、布局(Layout )、数据(Data )、操作(Action )、键盘(Key )和鼠标(Mouse )等。如果您安装的是中文版的IDE , 则默认情况下中文分类和英文分类是通用的 , 即设置成“数据”或“Data ”类别是等价的 。
Description
设置显示在属性窗口最下面的描述属性功能的文字说明。
DesignOnly
如果此属性设置为true ,表示该属性只能在设计期间使用,不能在页面代码中设置其值。
ReadOnly
设置该属性是否为只读状态。如果此特性设置为true ,则在属性窗口能看到属性,但不能设置其值。另外,通过在属性语句体中把set 语句段去掉也可以起到相同的效果。
Themeable
设置该属性是否支持主题特性,默认情况下属性都支持主题。当该属性与界面无关时可以设置其值为false ,禁用该属性的主题功能。
DesignerSerializationVisibility
指定属性是否以及如何在代码中序列化,其值为DesignerSerializationVisibility 的枚举值,存在3 种设置方式:
- DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) 指定序列化程序不应该序列化属性值;
- DesignerSerializationVisibility(DesignerSerializationVisibility.Visible) 指定应该允许序列化程序序列化属性的值;
- DesignerSerializationVisibility(DesignerSerializationVisibility.Content) 指定序列化程序应该序列化属性的内容,而不是属性本身。此字段为只读。Visible 为其默认值。
这里说的序列化是指在IDE 中的设计器界面切换到代码视图时,看到的代码标记,或反向切换时把代码标记转化到设计器界面。后面讲复杂属性时会通过示例介绍此属性功能。
NotifyParentProperty
指示当此设计特性应用到的属性的值被修改时将通知其父属性。换言之,如果属性的父属性应该在该属性值被修改时接到通知,则向该属性应用NotifyParentProperty 特性。通常使用布尔值进行设置。一般常用于复杂属性 , 通知转换器更新到父级标记 。
ParseChildren
使用该设计特性指示当在页面上以声明方式使用控件时,嵌套在服务器控件标记内的XML 元素是应该视为属性还是应视为子控件。通常情况下,包含两种声明方式:
- ParseChildren(true) 表示将子XML 元素作为服务器控件的属性分析;
- ParseChildren(bool childrenasProperty, string defaultProperty) ,其中childrenasProperty 和上面的方式中的布尔值参数意义相同,defaultProperty 定义默认情况下将子控件分析为服务器控件的集合属性。
PersistChildren
该设计特性指示设计时是否应将服务器控件的子控件作为内部嵌套控件保持。如果该特性为PersistChildren(true) ,则将服务器控件的子控件作为嵌套服务器控件标记保持。如果为PersistChildren(false) ,则将该控件的属性作为嵌套元素保持。
PersistenceMode
指定如何将服务器控件属性或事件保持到ASP.NET 页面的元数据属性,共存在4 种枚举设置方式:
- PersistenceMode (PersistenceMode.Attribute )指定属性或事件保持为属性;
- PersistenceMode (PersistenceMode.EncodedInnerDefaultProperty )指定属性作为服务器控件的唯一内部文本,如果属性值是HTML 编码的,只能对字符串作这种指定;
- PersistenceMode (PersistenceMode.InnerDefaultProperty )指定属性在服务器控件中保持为内部文本,还指示将该属性定义为元素的默认属性,只能指定一个属性为默认属性;
- PersistenceMode (PersistenceMode.InnerProperty )指定属性在服务器控件中保持为嵌套标记,通常用于复杂对象,它们具有自己的持久性属性。
关于以上4 种标记的具体用法,下一节会详细介绍 。
DefaultValue
指定属性的默认值 。 此特性的设置需要特别谨慎 , 假如设置的值不为空 , 则开发人员在使用时如果自己输入的值与默认值相同 , 则控件不会装载开发人员输入的值 。 也就是说此默认值不能指定为具有有效意义或业务意义的实际值 。 一般设置为空即可。
DisplayName
指定在属性窗口中显示的别名。此别名仅在属性窗口中看到,当转换器转换到代码视图,以及在页面后面的代码中编码还是以实际的属性名称为准,而不是以该别名为准。
ParenthesizedPropertyName
指定属性在属性窗口中显示时 , 是否带有括号 , 相当于在Category 分组特性基础上的对属性窗口属性集的排序功能 , 如果不带括号该属性会自动排在该组的前面 。
PasswordPropertyText
指定是否设置成密码文本。如果设置为true ,则在属性窗口中输入的文本会用特定的密码符号显示,而不是显示原文本;另外,在代码视图中看到的仍为原文本。
TypeConverter
指定用作此特性所绑定到的对象的转换器的类型。用于转换的类必须从TypeConverter 继承。使用ConverterTypeName 属性来获取为该特性所绑定到的对象提供数据转换的类名。后面会通过代码示例讲解如何自定义一个自己的类型转换器。
Editor
指定该属性的编辑器,如系统的文件编辑器、文本编辑器、颜色编辑器,还有集合编辑器等,也可以自己实现编辑器,具体用法后面会讲到。
ToolBoxItem
此属性为类特性。属于工具箱属性,可以设置当前控件是否在工具箱中显示,以及所在工具箱项的类型名称等信息。默认生成的控件都显示在工具箱中。
ToolBoxData
此特性为类特性,即不是属性的特性,而是类的特性,设置位置也是在类的上面。ToolBoxData 表示从工具箱中拖一个控件到设计界面上时默认显示标记格式,如:
[ToolboxData("<{0}:ControlProperty runat=server></{0}:ControlProperty>")]
可以修改参数字符串,定制为自己想要的格式,但要保证所添加的属性为有意义的属性。
DefaultProperty
此特性为类特性 。它 指定服务器控件的默认属性,例如:[DefaultProperty("Text")] 。
指定用黑色粗体显示默认属性特性的属性名称。一般设置比较重要或常用的属性为默认的属性。如TextBox 控件的Text 属性。
DefaultEvent
此特性为类特性。指定服务器控件的默认事件,例如:[DefaultEvent("OnClicked")] 。
指定用黑色粗体显示默认事件特性的事件名称。一般设置比较重要或常用的属性为默认的事件,如Button 控件的OnClick 事件。
ValidationProperty
此特性为类特性,指定该控件的哪个属性作为验证属性。当该控件与验证控件组合使用时,验证控件会自动验证该特性指定的属性。
AspNetHostingPermission
此属性为JIT 编译时代码访问安全属性。需要使用此属性确保链接到控件的代码具有适当的安全权限。Control 类带有两个JIT 编译时代码访问安全属性标记:
AspNetHostingPermission(SecurityAction.Demand,Level=AspNetHostingPermissionLevel.Minimal) 和AspNetHostingPermission(SecurityAction.InheritanceDemand,Level=AspNetHosting PermissionLevel.Minimal). 在使用时应把第一个属性应用于当前开发的控件,第二个属性是可选的,因为继承请求是可传递的,在派生类中仍有效。
ControlBuilder
分析时特性,将自定义控件生成器与控件关联。只有在您希望使用自定义控件生成器,对页分析器用分析控件的声明性语法的默认逻辑进行修改时,才需要应用此特性。如果仅希望指定控件标记中的内容是否与属性或子控件对应,请使用ParseChildrenAttribute ,而不要使用自定义控件生成器。
Designer
设计时特性,指定与控件关联的设计器类。控件设计器类用于控制关联的控件在可视化设计器的设计图面上的外观和行为。
还有一些更复杂的,包括在设计模式下的元数据属性类在这里没有列出,因为在后面有专门的章节详细介绍,通过代码示例更容易理解。在这里只要理解上面这些属性类功能,开发一般的控件是没有问题了。
4.4 复杂属性
4.4.1 概述
复杂属性是属性的类型不是简单值类型,或者是一个包含其他属性的类。例如.NET Framework 中的Style ,Font ,Point 等都是复杂属性。另外还有集合属性,这里也将它作为复杂属性归类,对于集合属性在本章后面会单独拿出来一节进行详细讲解。
4.4.2 复杂属性的几种标记形式
先看看一个典型的代码段:
<asp:GridView ID="GridView1" runat="server">
<FooterStyle BackColor="#507CD1" Font-Bold="true" ForeColor="White" />
<RowStyle BackColor="#EFF3FB" />
<PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign= "Center" />
<SelectedRowStyle BackColor="#D1DDF1" Font-Bold="true" ForeColor= "#333333" />
<HeaderStyle BackColor="#507CD1" Font-Bold="true" ForeColor="White" />
<EditRowStyle BackColor="#2461BF" />
<AlternatingRowStyle BackColor="White" />
</asp:GridView>
<asp:ListBox ID="ListBox1" runat="server">
<asp:ListItem Value="1">男</asp:ListItem>
<asp:ListItem Value="0">女</asp:ListItem>
</asp:ListBox>
代码非常简单,一段是GridView 控件的一些属性,另一段是ListBox 控件的一些属性。仔细观察一下这些控件的属性标记,我们能很容易给它们归类,比如:GridView 的ID/Runat 属性标记类型相似,FootStyle/RowStyle 这样的标记类似,还有Font-Bold 这样的属性标记,ListBox 的集合项ListItem 标记也比较特殊等这么多标记类型。我们在开发控件时当然也希望能够生成这么多灵活的标记类型,那么本节就详细介绍一下服务端控件的这些标记类型是怎样生成的。
开始之前,有必要说明一下,下面所有代码示例在调试时都是在设计模式下进行的。关于在设计模式下如何调试代码在第2 章已经详细讲解过了,如果读者还有疑问请再回顾一下第2 章的内容。
通常情况下,复杂属性表现为几种形式:连字符形式属性、内部嵌套形式属性和内部嵌套形式默认属性。下面将介绍以上几种形式复杂属性的具体实现方法。
4.4.2.1 连字符形式的复杂属性标记
连字符复杂属性标记是指属性通过“复杂属性名称- 复杂属性的子属性名称”的格式追加到主控件的标记形式。下面用一个例子来讲解这种标记。
首先,定义一个复合类Person ,结构如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class Person
{
private string strName;
/// <summary>
/// 姓名
/// </summary>
public string Name
{
get { return strName; }
set { strName = value; }
}
private int intAge;
/// <summary>
/// 年龄
/// </summary>
public int Age
{
get { return intAge; }
set { intAge = value; }
}
}
再在控件中增加一个类型为Person 的属性,将以下代码增加到控件中:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
private Person pPerson;
[Description("复杂属性")]
[Category("复杂属性")]
public Person Person
{
get
{
if (pPerson == null)
{
pPerson = new Person();
}
return pPerson;
}
}
此属性与简单属性的区别有两点:第一,属性接收和返回的类型不是简单类型(int ,string 等),而是用我们自己定义的Person 类;第二,复杂属性一般没有set 语句,因为一般是对复杂属性的子属性(或子对象)赋值,只要保证它的子属性(子对象)中具有get/set 语句即可。编译此控件,在IDE 中打开页面,并打开控件的属性窗口,会看到如图4-3 所示的界面。
另外,在属性窗口中比较这两个属性,您会发现上节讲的简单属性的Value 属性可以设置其值,复杂属性Person 是只读的,上面我们没有设置ReadOnly 特性。这是因为复杂属性中类型比较复杂,甚至还有嵌套。如果把所有复杂属性包含其子属性的值都放到这一个框中,显然不太方便。这就要求我们自己根据复杂属性类型增加一些序列化的特性。
解决办法是,为主控件属性Person 增加PersistenceMode 和DesignerSerializationVisibility 两个设计特性,片段代码如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
… …
[PersistenceMode(PersistenceMode.Attribute)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Person Person
{
… …
}
1 .PersistenceMode 特性
PersistenceMode 特性指定在页面*.aspx 或*.ascx 文件中如何保持复杂属性,理解此特性用法非常重要,这里详细介绍一下它的用法。PersistenceMode 有四种枚举状态:
- PersistenceMode.Attribute
表示复杂属性的标记作为主控件的属性,如果复杂属性包含子属性,则子属性持久化成破折号连接的样式,比如:
<asp:GridView ID="GridView1" runat="server">
<HeaderStyle BackColor="#507CD1" Font-Bold="true" ForeColor="White" />
</asp:GridView>
上面代码中的Font-Bold 对于<HeaderStyle> 来说就是使用了PersistenceMode 下的Attribute 枚举标记类型。本节例子中就是实现此标记形式。
- PersistenceMode.InnerProperty
表示用属性名称作为嵌套标签表示复杂属性,比如GridView 的HeaderStyle 属性,就是使用了PersistenceMode 下的InnerProperty 标记形式 。代码如下:
<asp:GridView ID="GridView1" runat="server">
<HeaderStyle BackColor="#507CD1" Font-Bold="true" ForeColor="White" />
</asp:GridView>
- PersistenceMode.InnerDefaultProperty
该特性值与InnerProperty 类似 , 都是在主控件外标记复杂属性 。 不同的是 ,InnerDefaultProperty 不需要像InnerProperty 那样把属性名作为最外标签,一般用于常用的或重要复杂属性或集合 , 如:
<asp:ListBox ID="ListBox1" runat="server">
<asp:ListItem Value="1">男</asp:ListItem>
<asp:ListItem Value="0">女</asp:ListItem>
</asp:ListBox>
以上代码中的ListItem , 它的特点是直接把ListItem 单项放到ListBox 的标记内部 , 而没有增加一个类似<Items> 的标记在ListItem 的外面 。 另外 ,InnerDefaultProperty 在一个控件类中只能设置一个复杂属性 , 而InnerProperty 可以设置任意多个复杂属性 。
一般情况下会把最重要的一个集合属性设置为InnerDefaultProperty 枚举标记类型 。
- PersistenceMode.EncodedInnerDefaultProperty
在上面的代码中ListItem.Text 属性(值为“男”或“女”) , 除了标记方式与InnerDefaultProperty 有点区别外 , 其内容会进行HTML 编码 , 比如把HTML 标记<div> 编码为<div> ;,即要保证其内容不能再存储HTML 标记和子标签。
2 .DesignerSerializationVisibility 特性
此特性表示指定在设计时序列化复杂对象的方式 。 它有三个枚举类型 :
- DesignerSerializationVisibility.Visible
表示代码生成器要对属性本身生成代码。
- DesignerSerializationVisibility.Hidden
表示代码生成器不对属性生成代码 , 即在属性窗口设置的值不会被代码生成器生成到*.aspx 或*.ascx 文件中 。
- DesignerSerializationVisibility.Content
表示代码生成器生成复杂属性内容的代码 , 而不是其本身 。 比如在上面的People 类中 , 我们实际要操作的数据是People 类下面的 Name/Sex/Age 属性 , 即我们在属性窗口中修改了Name/Sex/Age 的值后 , 会仅把这些值通过代码生成器映射到*.aspx 或*.axcx 页面中 。
如果没有设置DesignerSerializationVisibility 特性 , 则其值默认为DesignerSerialization Visibility.Visible ;一般复杂属性都要设置为DesignerSerializationVisibility.Content 。
理解了PersistenceMode 和DesignerSerializationVisibility 两个特性的用法 , 我们再继续完成上面进行中的代码部分 。 为属性Person 增加了这两个特性后,再打开Person 类定义代码,为该类增加一个类特性TypeConverter ,如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public class Person
{
… …
}
TypeConverter 特性指定转换器的类型,ExpandableObjectConverter 表示可扩展对象与其他类型的转换器类,该类为系统提供。另外,也可以自己定义转换器规则类,本章后面会有专门介绍。
增加以上属性之后,编译控件再查看属性窗口,就可以在属性窗口中进行设置Person 属性的值了,如图4-5 所示。
图4-5 设置Person 属性值
在属性浏览器中为Person 设置值,后切换到代码视图,会看到如下标记:
<cc1:controlproperty id="ControlProperty1" runat="server" Person-Age="26" Person-Name="King Zheng"></cc1:controlproperty>
到此我们就实现以上功能:连字符复杂属性的标记形式。
4.4.2.2 内部嵌套复杂属性标记
连字符复杂属性标记虽然能够实现复杂属性,且代码生成器能够进行正/ 反向转换,但它把所有复杂属性都挤到主控件的属性上,显示比较臃肿,设想一下,如果GridView 把它的<HeadStyle><Rowstyle> 等属性都挤到GridView 主标记内部,会是什么样子,为了解决这个问题,下面我们就实现一个类似以下代码中的RowStyle 标记形式的复杂属性。
<asp:GridView ID="GridView1" runat="server">
<RowStyle BackColor="#EFF3FB" />
</asp:GridView>
在控件所在项目中增加一个类文件 RowStyle.cs ,定义其内容如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public class RowStyle //TableItemStyle: Table的Row和Cell样式基类,也可以直
//接继承此类
{
private Color bcBackColor;
[NotifyParentProperty(true)]
public Color BackColor
{
get { return bcBackColor; }
set { bcBackColor = value; }
}
}
注意不要漏掉TypeConverter 和NotifyParentProperty ,其用途在前面中已经讲过了。
再在主控件中增加一个RowStyle 类型的属性,属性名为RowStyle ,增加后的属性代码片段如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[Category("复杂属性")]
[Description("复杂属性——内部嵌套形式")]
public RowStyle RowStyle
{
get
{
if (rsRowStyle == null)
{
rsRowStyle = new RowStyle();
}
return rsRowStyle;
}
}
选择PersistenceMode 特性的InnerProperty 枚举项,表示生成嵌套标记;至于DesignerSerializationVisibility 特性,依然选择Content 枚举值,这里也是对复杂属性RowStyle 的类对象子属性进行序列化。如还不清楚这两个属性的使用,请到前面的4.1.1 节回顾一下。
然后,在主控件加两个类特性,如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ParseChildren(true), PersistChildren(false)] //继承WebControl时可以省略此行
public class ControlProperty : WebControl
{
… …
}
PerseChildren 特性指定页面分析器把控件标记中的内容解析为属性还是子控件,该属性值设置为true ,则表示解析为属性。PersistChildren 指定设计器把控件标记中的内容保存为属性还是子控件,该属性值设置为false ,表示保存为属性。
设置了如上几个重要特性后,编译控件,在设计器属性窗口中设置RowStyle 属性值,并切换到代码视图,会看到RowStyle 的标记形式如下所示:
<cc1:controlproperty id="ControlProperty1" runat="server">
<RowStyle BackColor="CornflowerBlue" />
</cc1:controlproperty>
只要实现RowStyle 复杂类型,那么类似GridView 的其他嵌套属性如:<HeaderStyle> ,<FooterStyle> ,<SelectedRowStyle> ,<EditRowStyle> 等实现方法用同样方式也可以实现。
在嵌套标记属性比较多的情况下,这些属性看起来效果比上节讲过的连字符复杂属性标记要清晰许多。
另外,还可以按上面所说的步骤对集合类型生成类似的内部嵌套默认属性,如:
<asp:DropDownList id="DropDownList1" runat="server" >
<Items>
<asp:ListItem Value="red">红色</asp:ListItem>
<asp:ListItem Value="green">绿色</asp:ListItem>
<Items>
</asp:DropDownList>
基于实现原理与RowStyle 类似 , 且本章后面有专门章节详细探讨集合属性,这里不作代码示范 。 集合属性也是非常重要和常用的复杂属性类型。
4.4.2.3 内部嵌套默认复杂属性标记
内部嵌套默认属性与内部嵌套属性非常类似,一般用于设置某个控件的集合属性。比如标准服务器控件中的DropDownList 控件中的属性均为内部嵌套默认属性,代码如下:
<asp:DropDownList id="DropDownList1" runat="server" >
<asp:ListItem Value="red">红色</asp:ListItem>
<asp:ListItem Value="green">绿色</asp:ListItem>
</asp:DropDownList>
内部嵌套默认属性的ListItem 标记外部没有像内部集合属性一样嵌套在<Items></Items> 中,然后把<Items> 嵌套在主控件标记中,而是直接把<asp:listItem></asp:listItem> 嵌套在主控件标记内部,一般当该控件只有一个集合复杂属性的情况时使用;而当一个集合中有多个集合或复杂属性时一般设置为内部嵌套复杂属性标记形式。
为主控件增加集合属性之前,先要建立两个类:
- ListItem 类:集合中的单项定义类。
- Items 类:集合类,提供ListItem 的容器以及一些常用的添加/ 删除等子项方法。
1 .ListItem 类完整代码
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxItem(false)]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ListItem : Control
{
private string _Text;
private string _Value;
public ListItem()
{ }
public ListItem(string strText,string strValue)
{
this._Text = strText;
this._Value = strValue;
}
/// <summary>
/// 文本属性
/// </summary>
[NotifyParentProperty(true)]
public string Text
{
get { return _Text; }
set { _Text = value; }
}
/// <summary>
/// 值属性
/// </summary>
[NotifyParentProperty(true)]
public string Value
{
get { return _Value; }
set { _Value = value; }
}
}
此子项类的代码比较简单,唯一要说明的是上面的[ToolBoxItem(false)] 表示不在IDE 工具箱的控件集合中显示。很显然这不是一个控件,不能在工具箱集合列表中显示,一般除了主控件之外的其余类都要把ToolBoxItem 类元数据特性置为false ,否则当使用者拖一个不完整的控件标记到页面上时可能出现控件不能使用的情况。
2 .Items 类的完整代码
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 菜单实现类[实用泛型集合]
/// </summary>
[
ToolboxItem(false),
ParseChildren(true)
]
public class Items : List<ListItem>
{
#region 定义构造函数
public Items()
: base()
{
}
#endregion
/// <summary>
/// 得到集合元素的个数
/// </summary>
public new int Count
{
get
{
return base.Count;
}
}
/// <summary>
/// 表示集合是否为只读
/// </summary>
public bool IsReadOnly
{
get
{
return false;
}
}
/// <summary>
/// 添加对象到集合
/// </summary>
/// <param name="item"></param>
public new void Add(ListItem item)
{
base.Add(item);
}
/// <summary>
/// 清空集合
/// </summary>
public new void Clear()
{
base.Clear();
}
/// <summary>
/// 判断集合中是否包含元素
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public new bool Contains(ListItem item)
{
return base.Contains(item);
}
/// <summary>
/// 移除一个对象
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public new bool Remove(ListItem item)
{
return base.Remove(item);
}
/// <summary>
/// 设置或取得集合索引项
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public new ListItem this[int index]
{
get
{
return base[index];
}
set
{
base[index] = value;
}
}
}
这里的Items 采用泛型集合,继承list<T> 强类型集合作为基类,此外在System.Collections. Generic 命名空间中还有其他一些强类型集合。
增加完上面两个类后,实现内部默认集合属性,还需要设置两个类设计特性:一是在控件类前设置ParseChildren(true ,“默认属性名称”) ,指定主控件中的属性名称表示是属性,而不是子控件,ParseChildren 在4.3 节已经做了讲解;二是设置[PersistChildren(false)] 类特性 , 表示要把集合标记作为属性方式保持和进行序列化 。
在主控件的集合属性前要设置如下三个特性:
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
第一个特性,指定集合属性为内部默认属性;第二个特性,指定要序列化的是集合属性的内容,而不是集合属性本身;第三个特性,指定集合属性的子属性修改时会通知父属性。
新建一个Web 自定义控件文件,并按以上所述进行设置,控件主类核心代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 控件主类[复杂属性-内部默认属性]
/// </summary>
[DefaultProperty("Text")]
[ToolboxData("<{0}:CollectionControlProperty runat=server></{0}:Collection ControlProperty>")]
[PersistChildren(false)]
[ParseChildren(true, "Items")]
public class CollectionControlProperty : WebControl
{
private Items items;
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[TypeConverter(typeof(CollectionConverter))]
[Category("复杂属性")]
[Description("复杂属性——内部默认嵌套形式")]
public Items Items
{
get
{
if (this.items == null)
{
this.items = new Items();
}
return this.items;
}
}
… …
}
除了设置上面所提到的属性外,本集合类还多了一个特性[TypeConverter(typeof(collection Converter)] 。此特性指定本集合属性转换到代码视图时采用系统默认的集合转换器。针对常用的类型,系统提供了一组默认的转换器,后面章节会介绍怎样创建自定义复杂类型的类型转换器。
经过以上设置后,在页面上拖动一个控件,并在属性窗口中增加填加几个子项,如图4-6 所示。
图4-6 集合编辑器
设置完后,回到源代码视图,会看到刚才设置好的几个子项:
<cc1:CollectionControlProperty ID="CollectionControlProperty1" runat="server">
<cc1:ListItem ID="ListItem1" runat="server" Text="红色" Value="red">
</cc1:ListItem>
<cc1:ListItem ID="ListItem2" runat="server" Text="蓝色" Value="blue">
</cc1:ListItem>
<cc1:ListItem ID="ListItem3" runat="server" Text="绿色" Value="green">
</cc1:ListItem>
</cc1:CollectionControlProperty>
本节主要是完成一个复杂集合属性,并把集合属性设置为默认属性。本节示例控件的所有源代码请参阅随书光盘中的内容。
4.4.2.4 内部嵌套编码默认属性
请看下面这段我们经常使用的代码:
<asp:DropDownList id="DropDownList1" runat="server" >
<asp:ListItem Value="red">红色</asp:ListItem>
<asp:ListItem Value="green">绿色</asp:ListItem>
</asp:DropDownList>
细心的读者可能看到,表示Text( “红色”位置的属性) 的属性不像Value 属性是附属于ListItem 标记,而是在两个<asp:ListItem></asp:ListItem> 标记之间呈现。这样的标记主要用于显示非HTML 标记或非子控件的纯文本,本节主要完成这种格式属性的实现。
为了保留前面控件已有的功能,重新定义两个类Item2 和ListItem2 。
1 .Items 集合类
代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 菜单实现类[实用泛型集合]
/// </summary>
[
ToolboxItem(false),
ParseChildren(true)
]
public class Items2 : List<ListItem2>
{
//省略,此集合类内部代码与Items完全相同
}
2 .ListItem2 子项类
代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 子项类
/// </summary>
[ToolboxItem(false)]
[TypeConverter(typeof(ExpandableObjectConverter))]
[ParseChildren(true, "Text")]
[PersistChildren(false)]
public class ListItem2 : Control
{
private string _Text;
private string _Value;
public ListItem2()
{ }
public ListItem2(string strText, string strValue)
{
this._Text = strText;
this._Value = strValue;
}
/// <summary>
/// 文本属性
/// </summary>
[NotifyParentProperty(true)]
[PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
[Description("复杂属性——内部默认嵌套形式")]
public string Text
{
get { return _Text; }
set { _Text = value; }
}
/// <summary>
/// 值属性
/// </summary>
[NotifyParentProperty(true)]
public string Value
{
get { return _Value; }
set { _Value = value; }
}
}
此子项类要做一些特性设置,类元特性需要增加两个特殊的特性:
- [ParseChildren(true, "Text")]
- [PersistChildren(false)]
第一个特性表示将子Text 元素作为服务器控件的属性分析;第二个特性表示将该控件的属性作为嵌套元素保持。
另外,还要注意要对作为编码内部属性的属性进行设置,比如这里为Text 属性加上:
[PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
进行如上设置后,增加一个主控件文件,并进行如下所示设置:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Text")]
[ToolboxData("<{0}:EncodedInnerDefaultPropertyControl runat=server></{0}: EncodedInnerDefaultPropertyControl>")]
[PersistChildren(false)]
[ParseChildren(true, "Items")]
public class EncodedInnerDefaultPropertyControl : WebControl
{
public EncodedInnerDefaultPropertyControl()
{
}
private Items2 items;
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility. Content)]
[NotifyParentProperty(true)]
[TypeConverter(typeof(CollectionConverter))]
[Category("复杂属性")]
[Description("复杂属性——内部默认嵌套形式")]
public Items2 Items
{
get
{
if (this.items == null)
{
this.items = new Items2();
}
return this.items;
}
}
… …
}
上面主控件类与 4.4.2.3 中主控件类设置完全相同,这里就不再作说明。
设置完成后编译控件库,拖动此控件到页面中,可以看到在属性窗口增加了几个集合项,如图4-7 所示。
图4-7 属性窗口
集合设置界面与4.4.2.3 节中的完全相同,但切换到代码视图界面,会发现序列化后的代码变化了,如下所示:
<cc1:EncodedInnerDefaultPropertyControl ID="EncodedInnerDefaultProperty Control1" runat="server" Items-Capacity="4">
<cc2:ListItem2 ID="ListItem22" runat="server" Value="red">红色</cc2:ListItem2>
<cc2:ListItem2 ID="ListItem23" runat="server" Value="blue">蓝色</cc2:ListItem2>
</cc1:EncodedInnerDefaultPropertyControl>
可以看到Text 属性已经不再作为ListItem 的直接属性,而是嵌套在<ListItem2> </ListItem2> 之间。
本节主要说明控件内部嵌套编码默认属性格式的实现。在实现时需要注意的是,对ListItem2 子项类进行一些元数据特性设置,因为Text 是属于ListItem2 类的属性。
4.4.3 深入研究——复杂属性分析器
4.4.3.1 使用AddParsedSubObject 控制复杂内容(子控件)
在4.4.2 节已经把各种各样的复杂属性类型都实现了,这些都是在实际开发中常用的属性格式,能够满足绝大多数开发需要。
这一节讲解稍微复杂一点的属性格式。一般在一个控件中只能设置单个的属性为内部默认属性,比如4.4.2.3 节中实现的属性:
<cc1:CollectionControlProperty ID="CollectionControlProperty1" runat="server">
<cc1:ListItem ID="ListItem1" runat="server" Text="红色" Value="red">
</cc1:ListItem>
<cc1:ListItem ID="ListItem2" runat="server" Text="蓝色" Value="blue">
</cc1:ListItem>
<cc1:ListItem ID="ListItem3" runat="server" Text="绿色" Value="green">
</cc1:ListItem>
</cc1:CollectionControlProperty>
其中Items 属性设置成了内部默认属性,如果控件中需要多个内部默认属性的格式,默认分析器对此是不支持的。如果强行设置了两个默认属性格式的属性,控件可以编译通过,但在页面的属性窗口设置多个复杂属性后,进行代码与设计器视图切换时系统会报以下错误:
这说明不能利用默认的解析器分析多个设置了默认属性格式的子标记。为了解决这个问题,其中一种方法可以重写AddParsedSubObject 来定制自己的页面解析子控件方法。主控件核心源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 本控件包含三个集合复杂属性: 两个内部默认嵌套形式; 一个内部嵌套形式.
/// </summary>
[ToolboxData("<{0}:MultiCollectionControlProperty runat=server></{0}:Multi CollectionControlProperty>")]
[ParseChildren(false)]
public class MultiCollectionControlProperty : WebControl
{
private Items items;
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[TypeConverter(typeof(CollectionConverter))]
[Category("复杂属性")]
[Description("复杂属性——内部默认嵌套形式")]
public Items Items
{
get
{
if (this.items == null)
{
this.items = new Items();
}
return this.items;
}
}
private Items2 items2;
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[TypeConverter(typeof(CollectionConverter))]
[Category("复杂属性")]
[Description("复杂属性——内部默认嵌套形式")]
public Items2 Items2
{
get
{
if (this.items2 == null)
{
this.items2 = new Items2();
}
return this.items2;
}
}
private Items3 items3;
[PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[TypeConverter(typeof(CollectionConverter))]
[Category("复杂属性")]
[Description("复杂属性——内部编码嵌套形式")]
public Items3 Items3
{
get
{
if (this.items3 == null)
{
this.items3 = new Items3();
}
return this.items3;
}
}
protected override void AddParsedSubObject(object obj)
{
if (obj is ListItem)
{
if (this.items == null)
{
this.items = new Items();
}
this.items.Add((ListItem)obj);
}
if (obj is ListItem2)
{
if (this.items2 == null)
{
this.items2 = new Items2();
}
this.items2.Add((ListItem2)obj);
}
if (obj is ListItem3)
{
if (this.items3 == null)
{
this.items3 = new Items3();
}
this.items3.Add((ListItem3)obj);
}
}
}
本主控件类包含三个集合复杂属性 : 两个内部默认嵌套形式属性(Items 和Items2 );一个内部编码嵌套形式属性(Items3 )。这三个集合类的子项也是使用前面示例中的子项类, 其中ListItem3 代码省略 , 它与前面的ListItem1 类的内部代码完全一样 , 只是类名不同 。
主类MultiCollectionControlProperty 前面要加个很重要的元特性 [ParseChildren(false)] ,指定页解析器把嵌套的内容作为子控件解析。
另外,还可以重写AddParseSubObject ,定制自定义的页面解析实现,页面在设计模式下解析(从代码视图切换到设计视图)时,每检测到一个内部子控件都会触发此方法,此方法的参数object 就是当前内部子控件标记生成的对象。方法体中的三个if 语句分别判断当前对象是什么类型,如果是ListItem 类型就把它添加到相关的类集合中,如上面代码把ListItem 类型的对象增加到了Items 集合中。只有这样,我们在设计视图中查看属性窗口中值时,当前集合才有值显示在属性集合编辑器中(弹出窗口编辑器)。在增加一个子项到集合中时,还要注意第一次往集合中增加子项时,集合值为null ,要先为当前集合生成对象实例。
事实上控件中的嵌套标记,不仅可以内置集合类型子标记,还可以增加任意类型的标记,只要子标记具有前缀标志和runat 属性即可;如果没有前缀和runat 属性,系统也不会报错,只是页面解析器会把不具有前缀和runat 属性的整个块标记都用LiteralControl 包装后 , 返回LiteralControl 的对象(返回给AddParseSubObject 的参数obj ),而不管此块有多大。
编译此控件后 , 拖一个控件到页面中 , 会在属性窗口中看到三个并列的集合属性 , 如图4-8 所示。
图4-8 属性窗口
为三个集合属性分别设置几个子项 , 切换到源代码视图会看到如下源代码:
<cc1:MultiCollectionControlProperty ID="MultiCollectionControlProperty1" runat="server">
<cc2:ListItem runat="server" Text="红色" Value="red" ID="ListItem1"> </cc2:ListItem>
<cc2:ListItem runat="server" Text="绿色" Value="green" ID="ListItem2"> </cc2:ListItem>
<cc2:ListItem2 runat="server" Value="blue" ID="ListItem21">蓝色</cc2:ListItem2>
<cc2:ListItem2 runat="server" Value="gray" ID="ListItem22">灰色</cc2:ListItem2>
<cc2:ListItem3 runat="server" Text="黄色" Value="yellow" ID="ListItem31"> </cc2:ListItem3>
<cc2:ListItem3 runat="server" Text="淡蓝" Value="lightblue" ID="ListItem32"> </cc2:ListItem3>
</cc1:MultiCollectionControlProperty>
可以看到 ,ListItem ,ListItem2 ,ListItem3 非常有序地嵌套在主控件内部 , 从而实现了主控件内部多个复杂默认属性嵌套功能 。
AddParseSubObject 方法固然能够帮助我们实现控件内部多个复杂默认属性的嵌套功能 , 但它也有局限性 : 就是前面提到过的子标记必须是子控件形式标记 , 且 子标记要具有前缀标志和runat 属性, 否则 整个非子控件类型块标记都用LiteralControl 包装后 , 返回LiteralControl 的对象(返回给AddParseSubObject 的参数obj ),而不管此块有多大 。
以上是通过重写AddParseSubObject 方法实现页面解析功能;另外,.NET Framework 为控件设计模式支持专门提供了一个控件构造类:System.Web.UI.ControlBuilder ,通过继承此类也可以实现定制页面解析,而且更灵活,后面会专门对比进行介绍。
4.4.3.2 使用ControlBuilder解析 复杂内容
通过System.Web.UI.ControlBuilder 类定制页面解析逻辑,可以定制任意类型的标记,而不像重写AddParseSubObject 方法那样限定子标记必须是子控件,且必须有前缀和runat 属性,下面直接通过一个例子来说明一下此类的用法。
首先建立两个文件ScriptItem.cs 和ScriptItemCollection.cs ,分别定义ScriptItem 类和ScriptItemCollection 类。其中,ScriptItem 类主要存储用户自定义的客户端脚本命令(JavaScript 块),ScriptItemCollection 可以定义一个集合容器,每个项都是一个ScriptItem 项。与前面讲的集合实现非常类似。这两个类的完整代码如下:
1 .ScriptItem 类
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
private string _Text;
[DefaultValue("")]
[Editor("System.ComponentModel.Design.MultilineStringEditor,System.Design", typeof(UITypeEditor))]
[PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)]
[NotifyParentProperty(true)]
/// <summary>
/// JavaScript脚本块
/// </summary>
public string Text
{
get
{
return _Text;
}
set
{
_Text = value;
}
}
该类中的Text 就是用于存储用户定义的脚本块;Editor 元数据特性指定在属性窗口中Text 属性的编辑器是一个下拉块输入编辑器,关于属性编辑器下一节会详细讲解,这里仅知道它的功能即可。
需要注意的是,在上一节使用AddParsedSubObject 实现页面解析子控件时,要嵌套的三个集合的子标记:ListItem ,ListItem2 ,ListItem3 都继承了Control 基类,目的是把这些子标记作为子控件(也就具有了前缀和runat 属性),而这里的ScriptItem 没有继承任何基类,这样就避免了继承一些基类中的冗余属性和方法。
2 .ScriptItemCollecton 类
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxItem(false)]
public class ScriptItemCollection : List<ScriptItem>
{
public ScriptItemCollection() : base() { }
public ScriptItemCollection(int capacity) : base(capacity) { }
public ScriptItemCollection(IEnumerable<ScriptItem> collection):base (collection) { }
}
定义这两个类之后,实现我们自己的ControlBuilder 类,可以直接继承该类并实现自己的方法,已经预先实现好了的构造器类代码如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class ScriptItemBuilder : ControlBuilder
{
public override Type GetChildControlType(string tagName, IDictionary attributes)
{
if (string.Compare(tagName.ToLower(), "scriptitem", false, CultureInfo. InvariantCulture) == 0)
{
return typeof(ScriptItem);
}
return null;
}
public override bool AllowWhitespaceLiterals()
{
return false;
}
}
在该类中要做的最重要的事情是重写方法GetChildControlType ,在页面解析器分析主控件的每个子标记时,都会调用一次此方法。
该方法的第一个参数表示当前正在解析的控件标记字符串,第二个参数表示标记上所有特性的字典集合。方法体中的if 语句的功能是,假如当前解析的标记是“scriptitem ”(就是后面定义到主控件的集合属性名称),则返回ScriptItem 类的类型,且通过ToLower() 方法实现不区分大小写。需要注意的是,这里我们做的工作非常简单,只是匹配相应的字符串标记并返回一个类型。而AddParsedSubObject 则要自己处理当前对象的值。还有个重写方法AllowWhitespaceLiterals 用于指定控件的开始标记和结束标记之间是否允许存在空白。
定义完自己的构造器后,通过为主控件增加如下元数据特性,指定主控件的解析器:
[ControlBuilder(typeof(ScriptItemBuilder))]
设置完后,完整的主控件类源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:ControlBuilderControl runat=server></{0}:ControlBuilder Control>")]
[ParseChildren(true, "ScriptItems")]
[ControlBuilder(typeof(ScriptItemBuilder))]
public class ControlBuilderControl : WebControl
{
private ScriptItemCollection _ScriptItems = new ScriptItemCollection();
/// <summary>
/// 脚本命令集合属性
/// </summary>
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Description("工具按钮集设置")]
[Category("工具按钮——属性设置")]
public ScriptItemCollection ScriptItems
{
get
{
if (_ScriptItems == null)
{
_ScriptItems = new ScriptItemCollection();
}
return _ScriptItems;
}
}
//… …
}
整个主控件只包括一个集合属性。需要注意的是我们把这个属性定义成内部嵌套标记形式,即我们在<ScriptItem> 标记外面又嵌套了一个<ScriptItems> ,把ScriptItems 作为主控件的直接嵌套标记。
编译此控件后往页面中添加一个控件,并在属性窗口中增加命令项,然后切换到代码视图会看到如下格式的标记代码:
<cc1:controlbuildercontrol id="ControlBuilderControl1" runat="server">
<ScriptItems>
<cc1:ScriptItem>alert('Hello King');</cc1:ScriptItem>
<cc1:ScriptItem>alert('Hello Rose');</cc1:ScriptItem>
<cc1:ScriptItem>alert('Hello James');</cc1:ScriptItem>
</ScriptItems>
</cc1:controlbuildercontrol>
上面生成了一个具有ScriptItem 集合属性的标记集合。与上节我们使用的AddParsedSubObject 相比,嵌套标记中多了一个<ScriptItems> 内部嵌套标记,且ScriptItem 没有前缀和runat 属性,如果使用AddParsedSubObject ,会把整个<ScriptItems> 块(包括其中的ScriptItem ) 一块作为文本块LiteralControl 返回,这显然不符合我们的要求;另外,这里的<ScriptItem> 虽然具有前缀,但它也不具有runat 的属性,但也能够正确被页面解析器进行正反向解析 。
到目前为止,已经分别使用重载基类AddParsedSubObject 方法和继承ControlBuilder 的构造实现类实现了自定义的页面解析功能。那么使用它们两个的场景是怎样呢?其实在讲解它们的过程中笔者已经作了不少比较,再总结一下:
- (1 )在绝大多数情况下,如果页面中只要设置一个内部嵌套标记属性或不需要设置内部嵌套标记属性,则不需要重写AddParsedSubObject 和实现ControlBuilder 的继承类。这两种方式主要是在实现页面中有多个默认嵌套属性时使用。
- (2 )AddParsedSubObject 实现比较简单,仅实现一个方法,一般用于复杂属性单一且比较少的情况。
- (3 )实现ControlBuilder 的定制构造器类比重载AddParsedSubObject 要麻烦些,但功能更强,能处理更灵活的嵌套标记。AddParsedSubObject 最大的限制是它的内部必须是子控件类型。
- (4 )两种方式都是ASP.NET 提供的两种解决方案,都有它们使用的场景,可以根据自己喜好选择,当习惯使用构造器后,会发现构造器功能更强大、更灵活,用起来更顺手,它可以完全替代重载AddParsedSubObject 方式。
到现在为止基本上已经把我们见过的所有的属性标记格式都实现了一遍,4.4.3.2 节也把平时很少用到的定制页面解析器功能详细地讲解了一下,其中有些标记在平常开发中比较少用到。本章可以作为查找手册使用,什么时候用到这些内容,什么时候过来查即可。下一节会有更精彩的内容。
4.5 深入研究——定制自己的属性编辑器
对于控件的所有属性,如果都提供非常友好的属性编辑器,使用者使用起来会更加方便。本节主旨就是讲解一下控件属性编辑器,4.5.1 节提供一些系统通用编辑器;在复杂属性中,集合属性是最重要的最常用的属性,4.5.2 节将主要讲解怎样定制复杂集合类型编辑器以及一些特殊比较酷的编辑器类型。
这些设计器的执行主要是在设计模式下,直接与IDE 交互,在编程时可以直接使用System.Windows 命名空间开头的一些命名空间下的类。这里首先加入几个本节需要使用到的引用。右击控件库工程,选择“添加引用”命令,如图4-9 所示。
选择“添加引用”命令后会打开“添加引用”对话框,如图4-10 所示。
图4-9 添加引用
图4-10 “添加引用”对话框
在对话框中找到以下三个引用程序集:
(1 )System.Designer
(2 )System.Drawing.Design
(3 )System.Windows.Forms
单击“确定”按钮,这样在需要使用的地方打开程序集中的命名空间,就可以使用程序集中的系统类了。
4.5.1 系统属性编辑器
很有必要用一小节讲解一下系统提供的一些编辑器。读者对这些编辑器可能都比较熟悉,但它们是怎么使用的呢?其实使用都很简单,仅在每个需要配置的属性前面指定一个标志某种属性编辑器的元数据特性即可。下面就分别介绍一下它们。
4.5.1.1 多行下拉文本属性编辑器
1 .配置方式
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[Editor("System.ComponentModel.Design.MultilineStringEditor,System.Design", typeof(UITypeEditor))]
public string TxtEditor
{
//... ...
}
Editor 特性就是指定属性编辑器类型,后面的几种系统属性编辑器类型也是如此。
2 .属性浏览器中的效果 (如图4-11 所示)
图4-11 多行下拉文本属性编辑器
4.5.1.2 色值选择属性编辑器
1 .配置方式
[Editor("System.ComponentModel.Design.ColorEditor,System.Design", typeof(UITypeEditor))]
public Color ColorEditor
{
//... ...
}
2 .属性浏览器中的效果 (如图4-12 所示)
图4-12 色值选择属性编辑器
4.5.1.3 文件选择属性编辑器
1 .配置方式
[Editor(typeof(FileNameEditor), typeof(UITypeEditor))]
public string FileName
{
//... ...
}
2 .属性浏览器中的效果 (如图4-13 所示)
图4-13 文件选择属性编辑器
上图即为单击属性窗口中“文件名属性”按钮弹出的“文件选择属性编辑器”对话框,其实也是调用的Windows 系统的“打开文件”对话框。
4.5.1.4 目录选择属性编辑器
1 .配置方式
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
public string FolderNameEditor
{
//... ...
}
2 .属性浏览器中的效果 (如图4-14 所示)
图4-14 目录选择属性编辑器
4.5.1.5 连接字符串属性编辑器
1 .配置方式
[Editor(typeof(System.Web.UI.Design.ConnectionStringEditor), typeof(UITypeEditor))]
public string ConnectionStringEditor
{
//... ...
}
2 .属性浏览器中的效果 (如图4-15 所示)
图4-15 连接字符串属性编辑器
4.5.1.6 表达式绑定集合属性编辑器
1 .配置方式
[Editor(typeof(System.Web.UI.Design.ExpressionsCollectionEditor), typeof(UITypeEditor))]
public string ExpressionsCollectionEditor
{
//... ...
}
2 .属性浏览器中的效果 (如图4-16 所示)
图4-16 表达式绑定集合属性编辑器
在此窗口可以为属性指定要绑定到应用程序的配置文件、连接字符串或者资源文件。
4.5.1.7 用户控件对话框编辑器
1 .配置方式
[Editor(typeof(System.Web.UI.Design.UserControlFileEditor), typeof(UITypeEditor))]
public string UserControlFileEditor
{
//... ...
}
2 .属性浏览器中的效果 (如图4-17 所示)
图4-17 用户控件对话框编辑器
此窗口用于选择当前站点下的用户控件文件(*.ascx ),且默认的可选择路径不像文件和目录选择是本计算机硬盘,而是当前站点。
主控件的完整源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:EditorControl runat=server></{0}:EditorControl>")]
public class EditorControl : WebControl
{
string strTxtEditor;
[Category("编辑器")]
[Description("下拉多行文本编辑器")]
[Editor("System.ComponentModel.Design.MultilineStringEditor, System.Design", typeof(UITypeEditor))]
public string TxtEditor
{
get
{
return strTxtEditor;
}
set
{
strTxtEditor = value;
}
}
Color cColorEditor;
[Category("编辑器")]
[Description("颜色编辑器")]
[Editor("System.ComponentModel.Design.ColorEditor,System.Design", typeof(UITypeEditor))]
public Color ColorEditor
{
get
{
return cColorEditor;
}
set
{
cColorEditor = value;
}
}
string strFileName;
[Category("编辑器")]
[Description("文件选择编辑器")]
[Editor(typeof(FileNameEditor), typeof(UITypeEditor))]
public string FileName
{
get
{
return strFileName;
}
set
{
strFileName = value;
}
}
string strFolderNameEditor;
[Category("编辑器")]
[Description("目录选择编辑器")]
[Editor(typeof(FolderNameEditor), typeof(UITypeEditor))]
public string FolderNameEditor
{
get
{
return strFolderNameEditor;
}
set
{
strFolderNameEditor = value;
}
}
string strConnectionStringEditor;
[Category("编辑器")]
[Description("连接字符串编辑器")]
[Editor(typeof(System.Web.UI.Design.ConnectionStringEditor), typeof(UITypeEditor))]
public string ConnectionStringEditor
{
get
{
return strConnectionStringEditor;
}
set
{
strConnectionStringEditor = value;
}
}
string strExpressionsCollectionEditor;
[Category("编辑器")]
[Description("编辑表达式绑定集合的编辑器")]
[Editor(typeof(System.Web.UI.Design.ExpressionsCollectionEditor), typeof(UITypeEditor))]
public string ExpressionsCollectionEditor
{
get
{
return strExpressionsCollectionEditor;
}
set
{
strExpressionsCollectionEditor = value;
}
}
string strUserControlFileEditor;
[Category("编辑器")]
[Description("用户控件(ascx)对话框编辑器")]
[Editor(typeof(System.Web.UI.Design.UserControlFileEditor), typeof(UITypeEditor))]
public string UserControlFileEditor
{
get
{
return strUserControlFileEditor;
}
set
{
strUserControlFileEditor = value;
}
}
//... ...
}
这些代码比较简单,就不作解释了。以上仅列出一些可能会经常用到的属性编辑器,系统提供的属性编辑器不止这些,像前面讲的集合也是使用系统默认的集合属性编辑器。其他的属性编辑器可以在使用的过程中慢慢研究。下一节通过几个例子详细讲一下怎样定制自己的属性编辑器。
4.5.2 定制属性编辑器
系统提供了很多属性编辑器,能够满足绝大多数复杂度不是很高的控件的需要。这一节主要通过四个小例子讲解怎样定制个性化的属性编辑器,其实只要能够想到,我们就能够做到。
4.5.2.1 定制多类型集合属性编辑器
前面我们在为控件增加集合属性时,默认情况下会使用系统默认的集合编辑器,而我们这里自定义的集合属性编辑器功能更强,先看一下它实现后的效果图,如图4-18 所示。
可以看到,该编辑器除了能够实现基本的集合属性编辑功能外,通过单击“添加”按钮右边的下拉按钮还可以选择添加项的类型,即我们可以定义任意个不同类型的集合项作为属性集合内容,这里我们定义两个集合子项类别:CommonItem (子项)和CommandSeperator (分隔符),以它们作为示例讲解。下面就来看一下它的实现过程。
图4-18 定制多类型集合属性编辑器
由于涉及集合,首先还是定义几个与集合实现相关的类。定义一个抽象类ItemBase ,表示每个集合子项类的子类,任何集合子类都要继承该类。其类代码如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 命令项基类
/// </summary>
public abstract class ItemBase
{
private bool _EnableViewState = true;
public bool EnableViewState
{
get
{
return _EnableViewState;
}
set
{
_EnableViewState = value;
}
}
}
这里为了简化代码,只定义了一个EnableViewState 的属性,表示是否启用视图状态,在使用时还可以继承Control 等基类,使用Control 等类的类成员。这里要注意的是此类定义成了抽象类,此类不会单独生成实例添加到集合中,也不会被作为一种集合子类型显示到属性编辑窗口中供用户选择,因为ItemBase 在这里没有表示具体的集合子项,也不具有集合子项意义。所有集合子类型最终是以它们的基类型ItemBase 添加到集合中统一管理的。
下面定义一个具体的集合子项类CommonItem ,表示一个按钮类型,类结构代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 命令按钮类
/// </summary>
[ToolboxItem(false)]
public class CommandItem : ItemBase
{
private CommandActionType _CommandActionType;
//命令按钮文本
private string _Text = null;
//快捷键
private string _AccessKey = null;
//提示
private string _ToolTip = null;
//是否可用
private bool _Enable = true;
/// <summary>
/// 默认构造方法
/// </summary>
public CommandItem()
{
}
/// <summary>
/// 构造方法[ButtonCommand]
/// </summary>
/// <param name="bitButtonItemType"></param>
/// <param name="strCommandText"></param>
/// <param name="strAccessKey"></param>
/// <param name="strToolTip"></param>
public CommandItem(CommandActionType commandActionType, string strText, string strAccessKey, string strToolTip)
{
this._CommandActionType = commandActionType;
this._Text = strText;
this._AccessKey = strAccessKey;
this._ToolTip = strToolTip;
}
/// <summary>
/// 命令按钮类型
/// </summary>
[NotifyParentProperty(true)]
public CommandActionType CommandActionType
{
get
{
return _CommandActionType;
}
set
{
_CommandActionType = value;
}
}
/// <summary>
/// 命令按钮文本
/// </summary>
[NotifyParentProperty(true)]
[Browsable(false)]
public string Text
{
get
{
return _Text;
}
set
{
_Text = value;
}
}
/// <summary>
/// 快捷键
/// </summary>
[NotifyParentProperty(true)]
[Browsable(false)]
public string AccessKey
{
get
{
return _AccessKey;
}
set
{
_AccessKey = value;
}
}
/// <summary>
/// 帮助提示文本
/// </summary>
[NotifyParentProperty(true)]
[Browsable(false)]
public string ToolTip
{
get
{
return _ToolTip;
}
set
{
_ToolTip = value;
}
}
/// <summary>
/// 是否可用
/// </summary>
[NotifyParentProperty(true)]
[Browsable(false)]
public bool Enable
{
get
{
return _Enable;
}
set
{
_Enable = value;
}
}
}
类代码很简单,主要包括描述按钮的一些基本信息:文本、快捷键、提示、可用性以及按钮类型(新增/ 保存/ 删除等)。这里仅需要注意CommandItem 类继承了上面我们定义的抽象集合子项基类ItemBase ,所有类型的子项都要继承于该类。
CommandItem 的第一个属性是枚举类型,表示此按钮的功能类型(新增/ 删除/ 上一页/ 下一页等),此属性CommandActionType 对应的枚举代码结构如下所示
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 命令项枚举
/// </summary>
public enum CommandActionType
{
//保存
Save,
//新增
Add,
//编辑
Edit,
//删除
Delete,
//关闭
Close
//...
}
接下来再定义一个子项类型:CommandSeperator 类型,表示分隔符类型,即一组按钮与另一组按钮之间的分隔符。比如:“首页”、“上一页”、“下一页”、“末页”,这是一组具有类似功能的一级按钮。另一组:“新增”、“修改”、“删除”、“查看”属于一组功能类似按钮,这两组按钮之间需要用某个分隔符分开,这样可以使使用者更容易区分各个按钮的功能,外观布局也不会显示零乱。类代码如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 分隔符类
/// </summary>
[ToolboxItem(false)]
public class CommandSeperator : ItemBase
{
private Unit width;
private Unit Width
{
get
{
return width;
}
set
{
width = value;
}
}
private Unit height;
private Unit Height
{
get
{
return height;
}
set
{
height = value;
}
}
}
此分隔符类仅包括两个属性:宽度和高度。另外,它也继承了ItemBase 抽象类。
到现在为止,已经定义完四个类:抽象基类(ItemBase ),按钮类(CommandItem ),分隔按钮类(CommandSeperator ),以及一个功能枚举类(CommandActionType )。然后就可以定义一个存储以上各个按钮类型的集合类了,类名为CommandCollection ,此集合为强类型集合类,类代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 工具按钮集合类
/// </summary>
[ToolboxItem(false)]
[ParseChildren(true)]
[Editor(typeof(CommandCollectionEditor), typeof(UITypeEditor))]
public class CommandCollection : Collection<ItemBase>
{
#region 定义构造函数
public CommandCollection()
: base()
{
}
#endregion
/// <summary>
/// 得到集合元素的个数
/// </summary>
public new int Count
{
get
{
return base.Count;
}
}
/// <summary>
/// 表示集合是否为只读
/// </summary>
public bool IsReadOnly
{
get
{
return false;
}
}
/// <summary>
/// 添加对象到集合
/// </summary>
/// <param name="item"></param>
public new void Add(ItemBase item)
{
base.Add(item);
}
/// <summary>
/// 清空集合
/// </summary>
public new void Clear()
{
base.Clear();
}
/// <summary>
/// 判断集合中是否包含元素
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public new bool Contains(ItemBase item)
{
return base.Contains(item);
}
/// <summary>
/// 移除一个对象
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public new bool Remove(ItemBase item)
{
return base.Remove(item);
}
/// <summary>
/// 设置或取得索引项
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public new ItemBase this[int index]
{
get
{
return base[index];
}
set
{
base[index] = value;
}
}
}
该集合类继承Collection<ItemBase> 类,表示强类型集合,且每个子项的类型为ItemBase ,从这里可以想到我们上面定义的两个子项类CommandItem 和CommandSeperator 都要继承于ItemBase 的原因了。[ParseChilderen(true)] 表示把当前属性作为主控件的属性(而非子控件)进行解析。
本节的重点,也是最重要的一个属性[Editor(typeof(CommandCollectionEditor) ,typeof (UITypeEditor))] , 表示指定此集合类的集合编辑器为CommandCollectionEditor ,即在主控件中凡是定义为CommandCollection 类的属性都会把CommandCollectionEditor 作为它的编辑器。下面详细介绍一下编辑器类是怎么使用的。还是先看一下CommandCollectionEditor 编辑器类的源代码:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 集合属性编辑器
/// </summary>
public class CommandCollectionEditor : CollectionEditor
{
public CommandCollectionEditor(Type type)
: base(type)
{ }
protected override bool CanSelectMultipleInstances()
{
return true;
}
protected override Type[] CreateNewItemTypes()
{
return new Type[] { typeof(CommandItem), typeof(CommandSeperator) };
}
protected override object CreateInstance(Type itemType)
{
if (itemType == typeof(CommandItem))
{
return new CommandItem();
}
if (itemType == typeof(CommandSeperator))
{
return new CommandSeperator();
}
return null;
}
}
实现一个集合编辑器一般要继承System.ComponentModel.Design.CollectionEditor ,并重写该类的一些方法。下面分别说一下各个方法的作用。
- 集合编辑器中的构造方法 CommandCollectionEditor ,主要完成自定义的初始化功能。该方法中的参数返回该编辑器作用的对象实例(在这里是CommandCollection 的一个对象实例),可以取到当前CommandCollection 对象的所有数据。
- 方法CanSelectMultipleInstances 的返回值表示是否能够在编辑窗口选择多个实例,这里设置返回true 。
- 重写方法CreateNewItemTypes ,返回我们定义的两个集合类型:
return new Type[] { typeof(CommandItem), typeof(CommandSeperator) };
CommandItem 和CommandSeperator 是我们定义的两个集合类型。在单击主控件属性窗口中集合属性编辑器“… ”形状按钮时,此方法执行,把所有定义的类型加载到系统集合中缓存起来,然后根据此集合值在编辑器界面中呈现可能选择类型的列表。
- CreateInstance 方法主要是负责建立一个集合子项类型实例。
至此所有功能类都建立完成了,最后建立主控件类,并应用CommandCollection 集合类作为控件的一个属性,代码如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("ToolBarItems")]
[ToolboxData("<{0}:MultiTypeCollectionEditorControl runat=server></{0}: MultiTypeCollectionEditorControl>")]
[ParseChildren(true, "ToolBarItems")]
public class MultiTypeCollectionEditorControl : WebControl
{
private CommandCollection _ToolBarItems = new CommandCollection();
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility. Content)]
[Description("工具按钮集设置")]
[Category("集合设置")]
public CommandCollection ToolBarItems
{
get
{
if (_ToolBarItems == null)
{
_ToolBarItems = new CommandCollection();
}
return _ToolBarItems;
}
}
//....
}
主控件定义了一个重要的类元数据特性[ParseChildren(true ,"ToolBarItems")] ,表示把ToolBarItems 作为控件的属性进行解析。其他属性在前面的章节已经讲解多次了,这里就不再赘述。
编译控件源代码库,在页面设计器中置一个控件,然后单击属性窗口中对应的集合属性,会打开我们刚刚定义的集合属性编辑器,如图4-19 所示。
图4-19 集合编辑器
在这里就可以选择命令按钮或分隔符按钮填充集合了。另外,在成员列表中如果不想看到带命名空间的类名项,比如只让它显示CommandItem 而不是KingControls.CommandItem ,只要为其增加一个类型转换器即可。后面4.6 节会详细讲解类型转换器的实现。这个功能比较简单,如果需要,读者可以自己实现它。
好了,本节内容已经讲解完,在讲解过程中使用到了很多类,这些类都是在实际开发中常用的一些类,只是限于篇幅笔者把它们都精简化了,读者也可以体会一下它们的用途。
4.5.2.2 定制模态属性编辑器
这一节我们学习定制另一种属性编辑器:模态编辑器,在此编辑器中单击一个按钮将弹出一个窗体,从窗体选择数据后会把值返回到属性窗口中。最重要的一点是我们可以自定义此选择数据的模态窗口内容,比上面的集合编辑器更灵活。还是先看一下效果图,如图4-20 所示。
图4-20 模态属性编辑器
上图是以一个表示选择食品(水果/ 肉类/ 蔬菜等)的属性为例而定制的一个模态选择窗口,单击属性旁边的“… ”按钮就会弹出图中左侧的模态数据选择窗口。
下面就来说一下它是怎么实现的。首先要说明的是由于在设计模式下且模态是直接供IDE 接口调用的,因此这里弹出的窗口就是一个非常普通的WinForm 窗口。在我们控件中新增一个WinForm 文件CategoryWindow.cs ,如图4-21 所示。
图4-21 “添加新项”对话框
增加完后,放置一个ComboBox (提供绑定食品类别数据的选择列表)和两个Button 控件(“确定”和“取消”)到窗体中,再在“确定”按钮的事件中增加数据返回功能。
系统会把窗体类分成两个部分类文件:CategoryWindow.cs 和ategoryWindow.Designer.cs 。CategoryWindow.Designer.cs 主要存储窗体和内部控件内容信息;CategoryWindow.cs 主要供开发人员完成交互逻辑使用。下面分别来看一下它们的源代码。
1 .CategoryWindow.Designer.cs 文件中窗体部分的类代码
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
partial class CategoryWindow
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows 窗体设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.comboBox1 = new System.Windows.Forms.ComboBox();
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// comboBox1
//
this.comboBox1.FormattingEnabled = true;
this.comboBox1.Location = new System.Drawing.Point(23, 12);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size(217, 20);
this.comboBox1.TabIndex = 0;
//
// button1
//
this.button1.DialogResult = System.Windows.Forms.DialogResult.OK;
this.button1.Location = new System.Drawing.Point(84, 53);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.TabIndex = 1;
this.button1.Text = "确定";
this.button1.UseVisualStyleBackColor = true;
//
// button2
//
this.button2.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.button2.Location = new System.Drawing.Point(165, 53);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(75, 23);
this.button2.TabIndex = 2;
this.button2.Text = "取消";
this.button2.UseVisualStyleBackColor = true;
//
// CategoryWindow
//
this.AcceptButton = this.button1;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(252, 96);
this.Controls.Add(this.button2);
this.Controls.Add(this.button1);
this.Controls.Add(this.comboBox1);
this.Cursor = System.Windows.Forms.Cursors.Default;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Fixed3D;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "CategoryWindow";
this.StartPosition=System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "CategoryWindow";
this.TopMost = true;
this.ResumeLayout(false);
}
#endregion
public System.Windows.Forms.ComboBox comboBox1;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
}
需要说明的一点是上面代码中把两个Button 设置为窗体返回结果的枚举值,如下:
this.button1.DialogResult = System.Windows.Forms.DialogResult.OK;
this.button2.DialogResult = System.Windows.Forms.DialogResult.Cancel;
以上语句表示:单击“确定”按钮则返回选中的数据给父窗体;单击“取消”按钮则不返回数据。
2 .CategoryWindow.cs 文件中窗体部分的类代码
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public partial class CategoryWindow : Form
{
public CategoryWindow()
{
InitializeComponent();
SetSelectData();
}
public void SetSelectData()
{
try
{
this.comboBox1.Items.Add("水果");
this.comboBox1.Items.Add("蔬菜");
this.comboBox1.Items.Add("肉类");
this.comboBox1.Items.Add("蛋类");
this.comboBox1.Items.Add("面食");
}
catch (Exception eee)
{
throw eee;
}
finally
{
}
}
}
该页面没有复杂的交互逻辑,仅在类构造方法中调用SetSelectData 方法为窗体中的ComboBox 控件绑定食品数据列表。这里限于篇幅仅做了一个尽量简单的窗体,在实际开发中还可以定制任意复杂的窗体,在本节最后还提供了一个比较复杂的可以实现计算器功能的模态窗体。
数据选择窗体已经建立好之后,再创建控件的属性编辑器文件,该编辑器文件中的类主要用于调用上面创建的数据选择窗体,包括打开窗体,选择完数据后,接收值并赋给属性窗口的对应属性。在讲解源代码之前,要先打开几个命名空间:
using System.Drawing;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;
这些命名空间主要是提供控件对WinForm 的设计模式支持。下面还是先看一下此编辑器类的代码:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class CategoryModalEditor : System.Drawing.Design.UITypeEditor
{
public CategoryModalEditor()
{
}
public override System.Drawing.Design.UITypeEditorEditStyle GetEditStyle (System.ComponentModel.ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(System.ComponentModel.ItypeDescriptor Context context, System.IServiceProvider provider, object value)
{
IWindowsFormsEditorService service = (IWindowsFormsEditorService) provider.GetService(typeof(IWindowsFormsEditorService));
if (service == null)
{
return null;
}
CategoryWindow form = new CategoryWindow();
if (service.ShowDialog(form) == DialogResult.OK)
{
return form.comboBox1.SelectedItem;
}
return value;
}
}
这里使用的编辑器基类与前面我们定义的集合编辑器不一样,前面集合编辑器是使用System.ComponentModel.Design 下的集合基类,这里使用的是System.Drawing.Design 下的UItypeEdit 基类。在实际开发中可以任意选择系统提供的编辑器基类,在4.5.1 节已经列出了很多基类,也可以直接继承这些类定制自己的编辑器。
方法GetEditStyle 的System.ComponentModel.ITypeDescriptorContext 类型参数,表示要转换的对象的上下文;方法GetEditStyle 中UITypeEditorEditStyle.Modal 的UITypeEditorEditStyle 枚举表示以什么样的形式打开编辑窗体,它有三个枚举值:Modal ,DropDown ,None 。其中Modal 表示以模态形式弹出编辑属性界面;DropDown 表示以下拉形式显示属性编辑界面;None 表示不提供任何形式的UI 界面。这里我们选择的是Modal 枚举值,表示以模态形式弹出上面我们建立好的食品类别选择窗体。
EditValue 方法是主要的属性编辑方法,当单击属性窗口中的属性按钮时会执行此方法。它有三个参数:第一个参数表示当前上下文对象,可以从此对象获得当前父窗口和属性的设计时元数据信息等;第二个参数是服务提供者对象,可以根据此对象获取当前我们需要的服务;第三个参数为当前属性的默认值,即编辑当前属性之前的值。比如:
IWindowsFormsEditorService service = (IWindowsFormsEditorService)provider. GetService(typeof(IWindowsFormsEditorService));
以上语句表示获取IwindowsFormsEditorService 类型的服务对象(专门为WinForm 窗体编辑器提供一些功能),获取对象后就可以使用服务的方法:
if (service.ShowDialog(form) == DialogResult.OK)
{
return form.comboBox1.SelectedItem;
}
ShowDialog 就是IWindowsformsEditorService 类型对象的一个方法,表示打开form 对象窗体。另外,ShowDialog 还有一个DialogResult 枚举的返回值,这里表示如果返回值为OK 枚举项,才真正把窗体中当前ComboBox 的SelectedItem 项返回。看到这里,我们可能会想起前面把数据选择窗体中的“确定”和“取消”两个按钮的DialogResult 属性值分别设置为DialogResult.OK 和DialogResult.Cancel 的用途了。
本方法中第三个参数表示当前属性窗口中对应属性的当前值。有时您可以根据此值写一些相关的交互逻辑,比如根据此值设置弹出窗口的默认选中项(该功能比较简单,您可以扩展该控件功能,自己去实现它)。
整个EditValue 方法返回一个object 类型的值,系统会根据此返回值对属性窗口进行填充。
整个数据选择就是这样的一个过程,最后我们在主控件代码中对上面定义的数据选择窗体和编辑器进行应用。主控件源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("SelectFood")]
[ToolboxData("<{0}:CustomeModalEditorControl runat=server></{0}: CustomeModalEditorControl>")]
public class CustomeModalEditorControl : WebControl
{
[Bindable(true)]
[Category("类别")]
[DefaultValue("")]
[Localizable(true)]
[Editor(typeof(CategoryModalEditor), typeof(System.Drawing.Design.UIType Editor))]
[Description("选择食品类别")]
public string SelectFood
{
get
{
String s = (String)ViewState["SelectFood"];
return ((s == null) ? String.Empty : s);
}
set
{
ViewState["SelectFood"] = value;
}
}
//… …
}
主控件中有个选择食品类别的属性SelectFood ,该属性上面有一句:
[Editor(typeof(CategoryModalEditor),typeof(System.Drawing.Design.UITypeEditor))]
该句代码指定属性的编辑器为CategoryModalEditor 。最后,编译控件,在属性窗口中即可看到我们定义的属性,如图4-22 所示。
图4-22 模态属性编辑器
4.5.2.3 定制下拉控件属性编辑器
这一节我们再学习定制另一种形式的属性编辑器:下拉编辑器,单击按钮会下拉一个控件,当使用者从控件选择数据后该数据值被返回到属性窗口中。并且此选择数据的模态窗口内容也是可以自定义的。还是先看一下效果图,如图4-23 所示。
上图也是以一个表示选择食品(水果/ 肉类/ 蔬菜等)属性为例而定制的编辑器示例,单击属性旁边的下拉按钮会下拉一个数据选择的界面,并且此界面也是可以任意定制的。
下面就来说一下此编辑器是怎么实现的,在控件中新增一个用户控件(这次不是Windows 窗体),如图4-24 所示。
图4-23 下拉控件属性编辑器
图4-24 添加新项对话框
然后放置一个ComboBox (提供绑定食品类别数据的选择列表)和两个Button 控件(“确定”和“取消”)到窗体中,再在“确定”按钮的事件中增加数据返回功能。
增加用户控件文件后,系统会把窗体类分成两个部分类文件:CategoryDropDown.cs 和CategoryDropDown.Designer.cs ,CategoryDropDown.cs 主要供开发人员完成交互逻辑;CategoryDropDown.Designer.cs 主要存储窗体和内部控件内容信息,下面分别来看一下它们的源代码。
1 .CategoryDropDown.Designer.cs 文件中用户控件部分类的代码
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
partial class CategoryDropDown
{
/// <summary>
/// 必需的设计器变量。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清理所有正在使用的资源。
/// </summary>
/// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region 组件设计器生成的代码
/// <summary>
/// 设计器支持所需的方法 - 不要
/// 使用代码编辑器修改此方法的内容。
/// </summary>
private void InitializeComponent()
{
this.btnCancel = new System.Windows.Forms.Button();
this.btnOK = new System.Windows.Forms.Button();
this.comboBox1 = new System.Windows.Forms.ComboBox();
this.SuspendLayout();
//
// btnCancel
//
this.btnCancel.DialogResult=System.Windows.Forms.DialogResult.Cancel;
this.btnCancel.Location = new System.Drawing.Point(161, 56);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(75, 23);
this.btnCancel.TabIndex = 5;
this.btnCancel.Text = "取消";
this.btnCancel.UseVisualStyleBackColor = true;
this.btnCancel.Click+=new System.EventHandler(this.btnCancel_Click);
//
// btnOK
//
this.btnOK.DialogResult = System.Windows.Forms.DialogResult.OK;
this.btnOK.Location = new System.Drawing.Point(80, 56);
this.btnOK.Name = "btnOK";
this.btnOK.Size = new System.Drawing.Size(75, 23);
this.btnOK.TabIndex = 4;
this.btnOK.Text = "确定";
this.btnOK.UseVisualStyleBackColor = true;
this.btnOK.Click += new System.EventHandler(this.btnOK_Click);
//
// comboBox1
//
this.comboBox1.FormattingEnabled = true;
this.comboBox1.Location = new System.Drawing.Point(19, 15);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size(217, 20);
this.comboBox1.TabIndex = 3;
//
// CategoryDropDown
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnOK);
this.Controls.Add(this.comboBox1);
this.Name = "CategoryDropDown";
this.Size = new System.Drawing.Size(254, 95);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Button btnOK;
public System.Windows.Forms.ComboBox comboBox1;
}
这里没有像模态编辑器示例一样设置控件的DialogResult 属性,而是换了一种方式,分别为“确定”和“取消”两按钮定义事件,在事件中进行数据返回逻辑处理,关于事件将在接下来要讲解的另一个部分类中介绍。
2 .CategoryDropDown.cs 文件中用户控件部分类的代码
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public partial class CategoryDropDown : UserControl
{
public string strReturnValue = "";
private IWindowsFormsEditorService service=null;
public CategoryDropDown(IWindowsFormsEditorService service)
{
InitializeComponent();
SetSelectData();
this.service = service;
}
public void SetSelectData()
{
try
{
this.comboBox1.Items.Add("水果");
this.comboBox1.Items.Add("蔬菜");
this.comboBox1.Items.Add("肉类");
this.comboBox1.Items.Add("蛋类");
this.comboBox1.Items.Add("面食");
}
catch (Exception eee)
{
throw eee;
}
finally
{
}
}
private void btnOK_Click(object sender, EventArgs e)
{
strReturnValue = this.comboBox1.SelectedItem.ToString();
service.CloseDropDown();
}
private void btnCancel_Click(object sender, EventArgs e)
{
strReturnValue = "";
service.CloseDropDown();
}
}
跟4.5.2.2 节讲的模态编辑器一样 , 构造函数中的SetSelectData 方法是提供控件ComboBox 下拉界面中的食品类别数据列表 。 另外 , 构造函数中多了一个IWindowsFormsEditorService 类型的参数(在4.5.2.2 节对该类型进行了说明) , 把窗体编辑器服务对象传递过来 , 因为这里我们要在“确定”和“取消”事件中调用关闭当前界面的方法:
编辑器的EditValue 方法负责打开下拉界面 , 在下拉界面中的两个按钮中要分别调用关闭自己的代码 。
单击“确定”按钮时会把当前选择的值赋给类内部变量strReturnValue , 在下面讲的编辑器中会获取该变更的值 , 并赋值到属性 。
至此下拉界面已经定义完成 , 接下来定义编辑器类 , 代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class CategoryDropDownEditor : System.Drawing.Design.UITypeEditor
{
public CategoryDropDownEditor()
{
}
public override System.Drawing.Design.UITypeEditorEditStyle GetEditStyle (System.ComponentModel.ITypeDescriptorContext context)
{
//指定编辑样式为下拉形状, 且基于Control类型
return UITypeEditorEditStyle.DropDown;
}
public override object EditValue(System.ComponentModel.ItypeDescriptor Context context, System.IServiceProvider provider, object value)
{
//取得编辑器服务对象
IWindowsFormsEditorService service = (IWindowsFormsEditorService) provider.GetService(typeof(IWindowsFormsEditorService));
if (service == null)
{
return null;
}
//定义一个用户控件对象
CategoryDropDown form = new CategoryDropDown(service);
service.DropDownControl(form);
string strReturn = form.strReturnValue;
if (strReturn + String.Empty != String.Empty)
{
return strReturn;
}
return (string)value;
}
}
下拉编辑器与前面小节讲的弹出式模态编辑器都通过继承System.Drawing.Design. UITypeEditor 类实现定制编辑器。
在GetEditStyle 方法中的代码:
return UITypeEditorEditStyle.DropDown;
指定编辑器类型为下拉模式。
在EditValue 中主要创建数据选择界面并以下拉模式打开。下面说一下它内部代码实现逻辑。
IWindowsFormsEditorService service = (IWindowsFormsEditorService)provider. GetService(typeof(IWindowsFormsEditorService));
以上代码主要获取窗体编辑对象的服务对象,在本例中用到了它的下拉和关闭数据选择界面的方法。
CategoryDropDown form = new CategoryDropDown(service);
service.DropDownControl(form);
以上代码主要是创建一个数据选择界面(用户控件类型),并使用service 对象的DropDownControl 方法把参数指定的用户控件以下拉形式显示出来,展现形式为模态形式(暂停执行,直到单击界面中的按钮关闭下拉窗体程序才继续执行)。而EditValue 方法是在单击属性窗口中属性旁边的下拉按钮时触发。
string strReturn = form.strReturnValue;
return (string)value;
以上代码是获取下拉窗体中当前选择的数据(单击“确定”按钮时把值暂存到form.str ReturnValue 变量中)并返回,系统会自动把返回值赋给当前属性。
最后定义主控件代码类,代码如下;
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("SelectFood")]
[ToolboxData("<{0}:CustomeDropDownEditorControl runat=server></{0}: CustomeDropDownEditorControl>")]
public class CustomeDropDownEditorControl : WebControl
{
[Bindable(true)]
[Category("类别")]
[DefaultValue("")]
[Localizable(true)]
[Editor(typeof(CategoryDropDownEditor), typeof(System.Drawing.Design. UITypeEditor))]
[Description("选择食品类别")]
public string SelectFood
{
get
{
String s = (String)ViewState["SelectFood"];
return ((s == null) ? String.Empty : s);
}
set
{
ViewState["SelectFood"] = value;
}
}
//… …
}
主控件中仅包括一个SelectFood 属性。这里仅需要说明的是它的设计时元数据代码段,如下:
[Editor(typeof(CategoryDropDownEditor), typeof(System.Drawing.Design.UITypeEditor))]
以上代码指定编辑器为我们上面定义的CategoryDropDownEditor 编辑器。
最后,编译控件,在属性窗口中即可看到我们定义的属性,如图4-25 所示。
图4-25 属性窗口
4.5.2.4 定制计算器属性编辑器
本节没有讲解新的控件开发知识,而是利用前面所讲解的知识编写了一个有用的自定义属性编辑器 ——计算器属性编辑器,如图4-26 所示。
图4-26 计算器属性编辑器
这个属性编辑器比较简单,下面就简要地说一下它的实现过程。首先看一下计算器面板Form 的实现代码:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class FormKeyBoard : System.Windows.Forms.Form
{
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox expressBox;
private System.ComponentModel.Container components = null;
public string strReturnValue = "";
//定义存放运算符(包括:'+','-',...,'sin',...,'arcsin',...,'(',...等)及其特
//性的数据结构
public struct opTable //定义存放运算符及其优先级和单双目的结构
{
public string op; //用于存放运算符,op为operator的简写
public int code; //用于存放运算符的优先级
public char grade; //用于判断存放的运算符是单目还是双目
}
//用于存放制定好的运算符及其特性(优先级和单双目)的运算符表,其初始化在方
//法Initialize()中
public opTable[] opchTbl=new opTable[19];
public opTable[] operateStack=new opTable[30];//用于存放从键盘扫描的运算符的栈
//定义优先级列表1,2,3,4,5,6,7,8,9,
//数组中元素依次为: //"sin","cos","tan","cot","arcsin","arccos","arctan", "sec","csc","ln","^","*","/","+","-","(",")","" 的栈外(因为有的运算符是从右向左计算,有的是从左往右计算,用内外优先级可以限制其执行顺序)优先级
public int[]osp=new int[19]{6,6,6,6,6,6,6,6,6,6,6,5,3,3,2,2,7,0,1};
//数组中元素依次为: //"sin","cos","tan","cot","arcsin","arccos","arctan", "sec","csc","ln","^","*","/","+","-","(" ,"end" 的栈内(因为有的运算符是从右向左计算,有的是从左往右计算,用内外优先级可以限制其执行顺序)优先级
public int[]isp=new int[18]{5,5,5,5,5,5,5,5,5,5,5,4,3,3,2,2,1,1};
//定义存放从键盘扫描的数据的栈
public double[]dataStack=new double[30]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0};
//定义表态指针
public int opTop=-1; //指向存放(从键盘扫描的)运算符栈的指针
public int dataTop=-1;//指向存放(从键盘扫描的)数据栈指针
//定义存放从键盘输入的起始字符串
public string startString;
public int startTop=0;
public double variableX=0;
public double variableY=0;
const double PI=3.1415926;
int number=1;
public int startTopMoveCount=0;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.Button button3;
private System.Windows.Forms.Button button4;
private System.Windows.Forms.Button button5;
private System.Windows.Forms.Button button6;
private System.Windows.Forms.Button button7;
private System.Windows.Forms.Button button8;
private System.Windows.Forms.Button button9;
private System.Windows.Forms.Button button10;
private System.Windows.Forms.Button button11;
private System.Windows.Forms.Button button12;
private System.Windows.Forms.Button button13;
private System.Windows.Forms.Button button14;
private System.Windows.Forms.Button button15;
private System.Windows.Forms.Button button16;
private System.Windows.Forms.Button button17;
private System.Windows.Forms.Button button18;
private System.Windows.Forms.Button button19;
private System.Windows.Forms.Button button20;
private System.Windows.Forms.Button button21;
private System.Windows.Forms.Button button22;
private System.Windows.Forms.Button button23;
private System.Windows.Forms.Button button24;
private System.Windows.Forms.Button button25;
private System.Windows.Forms.Button button26;
private System.Windows.Forms.Button button27;
private System.Windows.Forms.Button button28;
private System.Windows.Forms.Button button29;
private System.Windows.Forms.Button button30;
private System.Windows.Forms.Button button31;
private System.Windows.Forms.Button button32;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox endbox;
private System.Windows.Forms.Button button33;
private Button btnClear;
private Button button34;
private System.Windows.Forms.Button btnReturn;
#region Windows Form Designer generated code
public FormKeyBoard()
{
InitializeComponent();
}
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#endregion
#region Windows Form Designer generated code
/// <summary>
/// 设计器支持所需的方法- 不要使用代码编辑器修改
/// 此方法的内容。
/// </summary>
private void InitializeComponent()
{
//本方法主要完成控件的创建,初始化,注册事件等逻辑。完整代码在本书随书光盘中
}
#endregion
private void Form1_Load(object sender, System.EventArgs e)
{
}
//制定运算符及其特性(优先级和单双目)的运算符表
public void InitializeOpchTblStack()
{
opchTbl[0].op="sin"; opchTbl[0].code=1; opchTbl[0].grade='s';
opchTbl[1].op="cos"; opchTbl[1].code=2; opchTbl[1].grade='s';
opchTbl[2].op="tan"; opchTbl[2].code=3; opchTbl[2].grade='s';
opchTbl[3].op="cot"; opchTbl[3].code=4; opchTbl[3].grade='s';
opchTbl[4].op="arcsin"; opchTbl[4].code=5; opchTbl[4].grade='s';
opchTbl[5].op="arccos"; opchTbl[5].code=6; opchTbl[5].grade='s';
opchTbl[6].op="arctan"; opchTbl[6].code=7; opchTbl[6].grade='s';
opchTbl[7].op="arccot"; opchTbl[7].code=8; opchTbl[7].grade='s';
opchTbl[8].op="sec"; opchTbl[8].code=9; opchTbl[8].grade='s';
opchTbl[9].op="csc"; opchTbl[9].code=10; opchTbl[9].grade='s';
opchTbl[10].op="ln"; opchTbl[10].code=11; opchTbl[10].grade='s';
opchTbl[11].op="^"; opchTbl[11].code=12; opchTbl[11].grade='d';
opchTbl[12].op="*"; opchTbl[12].code=13; opchTbl[12].grade='d';
opchTbl[13].op="/"; opchTbl[13].code=14; opchTbl[13].grade='d';
opchTbl[14].op="+"; opchTbl[14].code=15; opchTbl[14].grade='d';
opchTbl[15].op="-"; opchTbl[15].code=16; opchTbl[15].grade='d';
opchTbl[16].op="("; opchTbl[16].code=17; opchTbl[16].grade='d';
opchTbl[17].op=")"; opchTbl[17].code=18; opchTbl[17].grade='d';
opchTbl[18].op=" "; opchTbl[18].code=19; opchTbl[18].grade='d';
startString=expressBox.Text;
}
public void CreterionFaction()
{
//以下代码消去待扫描字符串中的所有空格字符
for(int i=0;i<startString.Length;i++)
if(startString[i].Equals(' '))
{
startString=startString.Remove(i,1);
i--;
}
//以下代码使待扫描字符串的单目('+'和'-')变为双目
if(startString.Length!=0)
if(startString[0]=='+'||startString[0]=='-')
{
startString=startString.Insert(0,"0");
}
for(int i=0;i<startString.Length-1;i++)
{
if((startString[i]=='(')&&(startString[i+1]=='-'))
startString=startString.Insert(i+1,"0");
}
startString=startString.Insert(startString.Length,")");
//将待扫描字符串转化为小写字母
startString=startString.ToLower();
}
public bool CheckParentthese() //检查括号是否匹配
{
int number=0;
for(int i=0;i<startString.Length-1;i++)
{
if(i=='(') number++;
if(i==')') number--;
if(number<0) return false;
}
if(number!=0)
{
return false;
}
return true;
}
//给运算表达式分块(三角函数、算术运算符等),再根据其返回值来检验其属于哪类错误
public int CheckFollowCorrect()
{
string str,oldString="",newString="";
int dataCount=0,characterCount=0;
if(startString.Equals(")"))
return 0; //输入字符串为空返回值
if((startString[0]=='*')||(startString[0]=='/')||(startString[0]=='^')||
(startString[0]==')'))
return 11; //首字符输入错误返回值
for(int i=0;i<startString.Length;i++)
{
if((oldString.Equals("三角函数"))&&(newString.Equals("右括号")))
return 2; //三角函数直接接右括号错误返回值
if((oldString.Equals("左括号"))&&(newString.Equals("算术运算符")))
return 3; //左括号直接接算术运算符错误返回值
if((oldString.Equals("数字序列"))&&(newString.Equals("三角函数")))
return 4; //数字序列后直接接三角函数错误返回值
if((oldString.Equals("数字序列"))&&(newString.Equals("左括号")))
return 5; //数字序列后直接接左括号错误返回值
if((oldString.Equals("算术运算符"))&&(newString.Equals("右括号")))
return 6; //算术运算符后直接接右括号错误返回值
if((oldString.Equals("右括号"))&&(newString.Equals("左括号")))
return 7; //右括号直接接左括号错误返回值
if((oldString.Equals("右括号"))&&(newString.Equals("三角函数")))
return 8; //右括号直接接三角函数错误返回值
if((oldString.Equals("数字序列"))&&(newString.Equals("数字序列")))
return 9; //数字序列后直接接'pi'/'e'或'pi'/'e'直接接数字序列错误返回值
if((oldString.Equals("算术运算符"))&&(newString.Equals("算术运算符")))
return 10; //算术运算符后直接接算术运算符错误返回值
oldString=newString;
if(i<startString.Length-5&&startString.Length>=6)
{
str=startString.Substring(i,6);
if((str.CompareTo("arcsin")==0)||(str.CompareTo("arccos")==0)||(str.Compar
eTo("arctan")==0)||(str.CompareTo("arccot")==0))
{
newString="三角函数";
i+=5; characterCount++;
continue;
}
}
if(i<startString.Length-2&&startString.Length>=3)
{
str=startString.Substring(i,3);
if((str.CompareTo("sin")==0)||(str.CompareTo("cos")==0)||(str.CompareTo("tan")==0)||(str.CompareTo("cot")==0)||(str.CompareTo("sec")==0)||(str.CompareTo("csc")==0))
{
newString="三角函数";
i+=2; characterCount++;
continue;
}
}
if(i<(startString.Length-1)&&(startString.Length)>=2)
{
str=startString.Substring(i,2);
if(str.CompareTo("ln")==0)
{
newString="三角函数";
i+=1; characterCount++;
continue;
}
if(str.CompareTo("pi")==0)
{
newString="数字序列";
i+=1;dataCount++;
continue;
}
}
str=startString.Substring(i,1);
if(str.Equals("^")||str.Equals("*")||str.Equals("/")||str.Equals("+")||str.Equals("-"))
{
newString="算术运算符";
characterCount++;
continue;
}
if(str.Equals("e"))
{
newString="数字序列";
dataCount++;
continue;
}
if(str.Equals("("))
{
newString="左括号";
characterCount++;
continue;
}
if(str.Equals(")"))
{
newString="右括号";
characterCount++;
continue;
}
if(Char.IsDigit(startString[i]))
{
while(Char.IsDigit(startString[i]))
{
i++;
}
if(startString[i]=='.'&&(!Char.IsDigit(startString[i+1]))&&(i+1)!=startString.Length)
return 13;
if(startString[i]=='.')
{
i++;
}
while(Char.IsDigit(startString[i]))
{
i++;
}
newString="数字序列";
i--; dataCount++;
continue;
}
return 1; //非法字符
}
if((dataCount==0&&characterCount!=0)||(startString[0]=='0'&&dataCount==1&&
characterCount>1&&startString.Length!=2))
return 12;
return 100;
}
public int IsCharacterOrData(ref double num)
{
string str="";
startTop+=startTopMoveCount; startTopMoveCount=0;
int i=startTop;
if(i<startString.Length-5&&startString.Length>=6)
{
str=startString.Substring(i,6);
for(int j=4;j<=7;j++)
if(str.Equals(opchTbl[j].op))
{
startTopMoveCount=6;
return opchTbl[j].code;
}
}
if(i<startString.Length-2&&startString.Length>=3)
{
str=startString.Substring(i,3);
for(int j=0;j<10;j++)
if(str.CompareTo(opchTbl[j].op)==0)
{
startTopMoveCount=3;
return opchTbl[j].code;
}
}
if(i<(startString.Length-1)&&(startString.Length)>=2)
{
str=startString.Substring(i,2);
if(str.CompareTo("ln")==0)
{
startTopMoveCount=2;
return 11;
}
if(str.CompareTo("pi")==0)
{
startTopMoveCount=2;
num=Math.PI;
return 100;
}
}
//以下开始确认一个字符是属于什么值类型
if(i<startString.Length)
{
str=startString.Substring(i,1);
for(int j=11;j<19;j++)
{
if(str.Equals(opchTbl[j].op))
{startTopMoveCount=1;return opchTbl[j].code;}
}
if(str.CompareTo("e")==0)
{
startTopMoveCount=1; num=Math.E;
return 100;
}
if(Char.IsDigit(startString[i]))
{
double temp=0,M=10; int j=i;
while(Char.IsDigit(startString[j]))
{
temp=M*temp+Char.GetNumericValue(startString[j]);
startTop++;
j++;
}
if(startString[j]=='.')
{
j++;startTop++;
}
while(Char.IsDigit(startString[j]))
{
temp+=1.0/M*Char.GetNumericValue(startString[j]);
M/=10;j++;
startTop++;
}
startTopMoveCount=0;
num=temp;
return 100;
}
}
return -1;
}
public double DoubleCount(string opString,double data1,double data2)
{ //双目运算
if(opString.CompareTo("+")==0) return (data1+data2);
if(opString.CompareTo("-")==0) return (data1-data2);
if(opString.CompareTo("*")==0) return (data1*data2);
if(opString.CompareTo("/")==0) return (data1/data2);
if(opString.CompareTo("^")==0)
{
double end=data1;
for(int i=0;i<data2-1;i++)
end*=data1;
return (end);
}
return Double.MaxValue; //定义域不对,返回
}
public double DoubleCount(string opString,double data1)
{ //单目运算
if(opString.CompareTo("sin")==0) return Math.Sin(data1);
if(opString.CompareTo("cos")==0) return Math.Cos(data1);
if(opString.CompareTo("tan")==0) return Math.Tan(data1);
if(opString.CompareTo("cot")==0) return (1/(Math.Tan(data1)));
if(opString.CompareTo("arcsin")==0)
if(-1<=data1&&data1<=1) return Math.Asin(data1);
if(opString.CompareTo("arccos")==0)
if(-1<=data1&&data1<=1) return Math.Acos(data1);
if(opString.CompareTo("arctan")==0)
if(-Math.PI/2<=data1&&data1<=Math.PI/2)return Math.Atan(data1);
if(opString.CompareTo("arccot")==0)
if(-Math.PI/2<=data1&&data1<=Math.PI/2)return (-Math.Atan(data1));
if(opString.CompareTo("sec")==0) return (1/(Math.Cos(data1)));
if(opString.CompareTo("csc")==0) return (1/(Math.Sin(data1)));
if(data1>0) if(opString.CompareTo("ln")==0) return Math.Log(data1);
return Double.MaxValue; //定义域不对
}
public bool CountValueY(ref double tempY) //此方法功能为求解
{
int type=-1; //存放正在扫描的字符串是为数字类型还是单双目运算符
double num=0; //如果是数据,则返回数据的值
//进栈底结束符"end"
opTop++;
operateStack[opTop].op="end"; operateStack[opTop].code=18;
operateStack[opTop].grade=' ';
while(startTop<=startString.Length-1)
{
start:
type=IsCharacterOrData(ref num); //调用判断返回值类型函数
if(type==-1){return false;}
if(type==100)
{
dataTop=dataTop+1;
dataStack[dataTop]=num;
}
else
{
if(osp[type-1]>isp[operateStack[opTop].code-1]) //操作符进栈
{
opTop++;
operateStack[opTop].op=opchTbl[type-1].op;
operateStack[opTop].code=opchTbl[type-1].code;
operateStack[opTop].grade=opchTbl[type-1].grade;
}
else
{
//弹出操作符跟数据计算,并存入数据
while(osp[type-1]<=isp[operateStack[opTop].code-1])
{
//当遇到"end"结束符表示已经获得结果
if(operateStack[opTop].op.CompareTo("end")==0)
{
if(dataTop==0)
{
tempY=dataStack[dataTop]; startTop=0; startTopMoveCount=0;
opTop=-1; dataTop=-1;
return true;
}
else return false;//运算符和数据的个数不匹配造成的错误
}
if(operateStack[opTop].op.CompareTo("(")==0) //如果要弹出操作数为
//'( ',则消去左括号
{
opTop--; goto start;
}
//弹出操作码和一个或两个数据计算,并将计算结果存入数据栈
double data1,data2; opTable operate;
if(dataTop>=0) data2=dataStack[dataTop];
else return false;
operate.op=operateStack[opTop].op; operate.code=operateStack
[opTop].code; operate.grade=operateStack[opTop].grade;
opTop--; //处理一次,指针必须仅且只能下移一个单位
if(operate.grade=='d')
{
if(dataTop-1>=0) data1=dataStack[dataTop-1];
else return false;
double tempValue=DoubleCount(operate.op,data1,data2);
if(tempValue!=Double.MaxValue)dataStack[--dataTop]=tempValue;
else return false;
}
if(operate.grade=='s')
{
double tempValue=DoubleCount(operate.op,data2);
if(tempValue!=Double.MaxValue)
dataStack[dataTop]=tempValue;
else return false;
}
}
//如果当前栈外操作符比栈顶的操作符优先级别高,则栈外操作符进栈
opTop++;
operateStack[opTop].op=opchTbl[type-1].op;
operateStack[opTop].code=opchTbl[type-1].code;
operateStack[opTop].grade=opchTbl[type-1].grade;
}
}
}
return false;
}
public void StartExcute()
{
InitializeOpchTblStack();
CreterionFaction();
if(CheckParentthese()==false)
{
MessageBox.Show("括号不匹配,请重新输入!!!","错误",MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
switch(CheckFollowCorrect())
{
case 0: MessageBox.Show("表达式为空,请先输入表达式!!!","错误",
MessageBoxButtons.OK,MessageBoxIcon.Warning);
return;
case 1: MessageBox.Show("表达式中有非法字符!!!","错误",
MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 2: MessageBox.Show("三角函数运算符与) 之间应输入数据或其他表达式!!!","
错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 3: MessageBox.Show("' ( ' 与算术运算符之间应输入数据或其他表达
式!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 4: MessageBox.Show("数字数列与三角函数之间应输入算术运算符或其他表达
式!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 5: MessageBox.Show("数字数列与 ' ( ' 之间应输入算术运算符或其他表达
式!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 6: MessageBox.Show("算术运算符与右括号之间应输入数据或其他表达式!!!","
错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 7: MessageBox.Show("' ) ' 与' ( ' 之间应输入算术运算符或其他表达
式!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 8: MessageBox.Show("' ) ' 与三角函数之间应输入算术运算符或其他表达
式!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 9: MessageBox.Show("常量' PI ' 或 ' E ' 或 ' X ' 与数字数据
之间应输入算术运算符或其他表达式!!!","错误",
MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 10: MessageBox.Show("算术运算符与算术运算符之间应输入数据或其他表达
式!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 11: MessageBox.Show("表达式头部不能为' + ',' - ',' * ',' / ', ' ^ ',' )'
!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 12: MessageBox.Show("仅有运算符号没有数字数据或数据缺少而无法计算,请输入数
字数据!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
case 13: MessageBox.Show("小数点后面缺少小数部分,请输入小数部分!!!","错误",
MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
}
double tempY=0;
switch(CountValueY(ref tempY))
{
case false:MessageBox.Show("输入的表达式不正确或反三角函数定义域在其定义域范围
之外!!!","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);
return;
}
endbox.Text=tempY.ToString();//依次存档计算结果
number++;
}
private void button30_Click(object sender, System.EventArgs e)
{
StartExcute();
}
private void button10_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button10.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button11_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart,".");
expressBox.SelectionStart=expressBox.TextLength;
}
private void button27_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart,"^");
expressBox.SelectionStart=expressBox.TextLength;
}
private void button1_Click_1(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button1.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button4_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button4.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button3_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button3.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button2_Click_1(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button2.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button14_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button14.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button15_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button15.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button5_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button5.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button6_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button6.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button9_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button9.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button8_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button8.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button7_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button7.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button12_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button12.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button13_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button13.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button29_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button29.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button28_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button28.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button16_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button16.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button20_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button20.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button17_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button17.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button21_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button21.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button24_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button24.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button18_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button18.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button22_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button22.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button25_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button25.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button19_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button19.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button23_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button23.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button26_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart, button26.Text);
expressBox.SelectionStart=expressBox.TextLength;
}
private void button31_Click(object sender, System.EventArgs e)
{
if(expressBox.Text.Length>0)
expressBox.Text=expressBox.Text.Remove(expressBox.Text.Length-1,1);
}
private void button32_Click(object sender, System.EventArgs e)
{
expressBox.Text="";
endbox.Text="0.000000";
}
private void button33_Click(object sender, System.EventArgs e)
{
expressBox.SelectedText=null;
expressBox.Text=expressBox.Text.Insert(expressBox.SelectionStart,"PI");
expressBox.SelectionStart=expressBox.TextLength;
}
private void button35_Click(object sender, System.EventArgs e)
{
strReturnValue = endbox.Text.Trim();
}
private void btnClear_Click(object sender, EventArgs e)
{
expressBox.Text = "";
}
private void button34_Click(object sender, EventArgs e)
{
StartExcute();
}
}
上面代码为一个WinForm 窗体 , 主要实现展示一个计算器功能 。 此计算器可以一次性计算多个操作项的值 。 例如:y = 3 + 64 * (2 + 3^5) + sinPI 的值,这是它与其他计算器的不同之处 , 比如Windows 自带的计算器一次只能计算两个操作数 , 这里可以支持您任意输入多个操作数的表达式 , 最后一块计算出结果 。 支持:sin ,cos ,tan ,cot ,arcsin ,arccos ,arctan ,sec ,csc ,ln ,^ ,* ,/ ,+ ,- ,( ,) 运算符 , 并用括号区分优先级 。 另外 , 此计算器的实现算法也不错 , 采用经典的Stack 编译算法 , 感兴趣的读者可以研究一下。
然后定义一个编辑器,其实所有这些的编辑器功能相当于一个“桥接器”,使属性与自定义Form 窗体关联起来。代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class CalculatorSelectEditor : System.Drawing.Design.UITypeEditor
{
public CalculatorSelectEditor()
{
}
public override System.Drawing.Design.UITypeEditorEditStyle GetEditStyle
(System.ComponentModel.ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(System.ComponentModel.ItypeDescriptor
Context context, System.IServiceProvider provider, object value)
{
IWindowsFormsEditorService service = (IWindowsFormsEditorService)
provider.GetService(typeof(IWindowsFormsEditorService));
if (service == null)
{
return null;
}
FormKeyBoard form = new FormKeyBoard();
if (service.ShowDialog(form) == DialogResult.OK)
{
object strReturn = form.strReturnValue;
return strReturn;
}
return value;
}
}
此类的功能是弹出一个模式的计算器Form 窗体,与4.5.2.2 节定义的编辑器功能几乎一样,这里就不作多讲,如果还有不明白的地方请回顾一下前面章节的内容。
最后定义主控件代码类,如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Money")]
[ToolboxData("<{0}:CalculatorSelectControl
runat=server></{0}:CalculatorSelectControl>")]
public class CalculatorSelectControl : WebControl
{
[Bindable(true)]
[Category("自定义计算机属性")]
[DefaultValue("")]
[Localizable(true)]
[Editor(typeof(CalculatorSelectEditor), typeof(System.Drawing.Design. UITypeEditor))]
[Description("请输入金额")]
public string Money
{
get
{
string s = (string)ViewState["Money"];
return ((s == null) ? "" : s);
}
set
{
ViewState["Money"] = value;
}
}
//… …
}
主控件中定义了一个表示金额的Money 属性,并指定设计时的编辑器为我们上面定义的计算器编辑器类:
[Editor(typeof(CalculatorSelectEditor), typeof(System.Drawing.Design.UITypeEditor))]
编译此控件,在属性窗口中单击属性旁边的“… ”按钮即可以看到我们定义的计算器编辑器,如图4-27 所示。
图4-27 属性窗口
本节内容已经讲解完。本节主要实现了几个自定义功能的编辑器:多类型子项集合编辑器、弹出式模态数据选择编辑器、下拉式数据选择编辑器,最后综合运用前面的知识实现了一个计算器编辑器。限于篇幅,示例都比较简单,但已经足以说明其用法了。另外,在定义自己的编辑器时,除了使用编辑器基类外,还可以把4.5.1 节列出的那些系统编辑器作为基类使用。
4.6 类型转换器
类型转换器是什么?它主要完成什么样的功能呢?类型转换器可用于在数据类型之间转换值,并通过提供文本到值的转换或待选值的下拉列表来帮助在设计时配置属性。如果配置正确,通过使用InstanceDescriptor 和System.Reflection 对象来给设计器序列化系统提供生成在运行时初始化属性的代码所需的信息,类型转换器可以生成属性配置代码。
类型转换器可用于字符串到值的转换,或用于在设计时和运行时数据类型之间的双向翻译。在宿主(如窗体设计器中的属性浏览器)中,类型转换器允许以文本形式向用户表示属性值,并且可以将用户输入的文本转换为相应数据类型的值。
大多数本机数据类型(Int32 、String 、枚举类型和其他类型)具有默认的类型转换器,提供从字符串到值的转换并执行验证检查。默认的类型转换器位于System.ComponentModel 命名空间中,名为TypeConverterNameConverter 。当默认功能无法满足需要时,可以扩展类型转换器;当定义的自定义类型没有关联的类型转换器时,可以实现自定义类型转换器。
4.6.1 系统类型转换器
系统默认提供了许多常用的类型转换器,其中有不少我们在使用控件时已经用到了。本节主要列举一些常用的转换器,并以其中几个经典的转换器为例说明其使用方式。
4.6.1.1 整型类型转换器
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[TypeConverter(typeof(Int32Converter))]
public int IntConverter
{
//… …
}
转换器类名为Int32Converter ,用于32 位有符号整数对象与其他类型之相互转换的类型转换。它的展示样式与一般字符串属性完全一样,只是假如我们输入非整型的值,它会提示格式不正确,要求重新输入,例如输入一个字符“a ”,会弹出如图4-28 所示的提示输入格式错误的窗口。
图4-28 类型不匹配提示窗口
在实际应用中,像string ,int 等类型的属性不需要我们指定转换器,它会自动关联系统默认的转换器,即上面的[TypeConverter(…)] 语句可以去掉。
这里要说明的重点是,为属性增加转换器的方法:
[TypeConverter(typeof(Int32Converter))]
该方法在属性上方增加TypeConverter 设计时属性,参数为转换器的类型(系统提供的或自定义的),后面小节会介绍怎样为特性的属性类型定制自定义转换器。
4.6.1.2 WebColor类型转换器
[TypeConverter(typeof(WebColorConverter))]
public Color WebColorConverter
{
//… …
}
属性窗口中显示效果如图4-29 所示。
这也是系统提供的一个常用转换器,转换器类为WebColor Converter ,注意单击下拉的颜色选择面板不是WebColorConverter 提供的,是由默认的颜色编辑器提供(在4.5.1.2 节有讲解),WebColorConverter 主要用于设计或运行时从WebColor 类型到字符串类型的转换。WebColor 与Color 相比,提供了更多表示颜色格式的类型。
4.6.1.3 控件ID列表类型转换器
如果在设计器中的某几个控件具有关联关系,比如一个控件在运行时要获取另一个控件 的一些属性值,则可以用控件列表转换器ControlIDConverter 来建立两个控件之间的关联关系。
[TypeConverter(typeof(ControlIDConverter))]
public string TargetControl
{
//… …
}
转换器类型为System.Web.UI.WebControls.ControlIDConverter ,指定此类型转换器类型的属性展示效果如图4-30 所示。
图4-30 属性展示效果
在属性列表中,已经列出了设计器中其他几个控件的ID 。在实际应用中,知道了控件的ID ,就可以通过FindControl 方法获取到整个控件了,FindControl 使用方法在第3 章有讲解。
除了上面介绍的三个外,系统还提供了好多转换器,限于篇幅就不一一作讲解,请看表4-1 。
在实际开发时,可以参考上面列表选择适合的类型转换器。提醒一点,上面的转换器除了可以用作开发控件设计时的属性类型转换器;也可以在其他地方直接使用类转换器中的功能方法,即把类型转换器作为普通类使用,这种方式也用得比较广泛,在后面讲解视图状态机制时就利用了自定义类型转换器(接下来要讲的SolidCoordinateConverter 类型转换器)对视图对象进行正反序列化。
表4-1 系统转换器
系统转换器类型 | 功 能 |
Int32Converter | 将32 位有符号整数对象与其他表示形式相互转换 |
Int64Converter | 将64 位有符号整数对象与各种其他表示形式相互转换 |
Int16Converter | 将16 位有符号整数对象与各种其他表示形式相互转换 |
续表
系统转换器类型 | 功 能 |
ByteConverter | 字节类型与其他类型相互转换 |
BooleanConverter | 将Boolean 对象与其他各种表示形式相互转换 |
CharConverter | 将Unicode 字符对象与各种其他表示形式相互转换 |
UnitConverter | 从Unit 对象转换为其他数据类型的对象,或从其他类型转换为UNIT 对象 |
EnumConverter | 将Enum 对象与其他各种表示形式相互转换 |
DateTimeConverter | 将日期类型与其他类型相互转换 |
DecimalConverter | 将Decimal 对象与其他各种表示形式相互转换 |
StringConverter | 在字符串对象与其他表示形式之间实现相互转换 |
DoubleConverter | 将双精度对象与其他各种表示形式相互转换 |
SingleConverter | 将单精度浮点数字对象与各种其他表示形式相互转换 |
TimeSpanConverter | 将TimeSpan 对象与其他表示形式相互转换 |
WebColorConverter | 在预定义的颜色名称或RGB 颜色值与System.Drawing.Color 对象之间相互转换 |
ArrayConverter | 将Array 对象与其他各种表示形式相互转换 |
CollectionConverter | 将集合对象与各种其他表示形式相互转换 |
ExpandableObjectConverter | 在可扩展对象与其他各种表示形式之间实现转换 |
GuidConverter | 将Guid 对象与其他各种表示形式相互转换 |
BaseNumberConverter | 为非浮点数字类型提供基类型转换器,上面几个整型转换器就是从此类派生的 |
ReferenceConverter | 将对象引用与其他表示形式相互转换 |
TypeListConverter | 以可用类型填充列表框的类型转换器 |
ObjectConverter | 将Object 类型与其他类型相互转换 |
PropertyConverter | 用于在属性值和字符串之间进行转换的转换器 |
DataBindingCollectionConverter | DataBindingCollection 对象的类型转换器 |
DataFieldConverter | 可从当前组件的选定数据源中检索数据字段的列表 |
DataMemberConverter | 可从当前组件选定的数据源中检索数据成员的列表 |
DataSourceConverter | 数据源类型转换器 |
CursorConverter | 将Cursor 对象与其他各种表示形式相互转换 |
FontNamesConverter | 将包含字体名称列表的字符串转换为包含个别名称的字符串数组,它还执行反转功能 |
FontUnitConverter | 转换字体单位类型 |
StringArrayConverter | 在以由逗号分隔的值组成的字符串与字符串数组之间进行转换 |
ControlIDConverter | 控件ID 列表转换器,4.6.1.3 小节已经作过示例 |
TargetConverter | 将从Web 导航产生的内容的位置(目标)的值转换为字符串。该类还将字符串转换为目标值 |
ValidatedControlConverter | 初始化ValidatedControlConverter 类的新实例 |
4.6.2 定制自己的类型转换器
系统已经提供了很多的类型转换器,能够满足一般情况下开发的需要。但开发控件时,并不是所有的属性类型都是那些简单的且系统已知的int, string 等类型,即控件的属性类型可以是我们定义的任意类型,因此系统不能够自动检测到该使用哪个类型转换器,这种情况就需要我们为自己的属性定制专门的类型转换器。
实现自己的类型转换器,一般需要以下5 个步骤:
- 定义一个从TypeConverter 派生的类,TypeConverter 类提供了将值的类型转换为其他类型,以及访问标准值和子属性的统一方法。其主要是重载类的一些正反向转换方法。
- 重写CanConvertFrom 方法,在方法中指定是否可以从字符串转换为指定的类型。
- 重写ConvertFrom 方法,实现从字符串到指定类型的转换。
- 重写CanConvertTo 方法,指定 是否能从SolidCoordinate类转换为string或InstanceDescriptor类型 。InstanceDescriptor 是提供创建对象实例所需信息的类。转换为字符串类型不需要重写此方法。
- 重写ConvertTo 方法,实现转换。
其中上面2 ,3 ,4 ,5 都是重载方法。下面就以两个例子说明类型转换器的创建过程。
4.6.2.1 三维坐标类型转换器
大家都知道在.NET Framework 中有Point 类,如果把该类作为属性的类型,则系统会自动调用它的类型转换器进行类型转换。比如在属性窗口中设置属性值,切换到源代码视图时即调 用类型转换器进行转换;或在运行时控件状态或视图状态对存储的对象进行序列化和反序列化。
这里我们定义一种新的坐标类型SolidCoordinate 类,并为其定义匹配的类型转换器,以此说明如何自定义和使用类型转换器。
先来看一下实现后的效果,在属性窗口中设置SolidCoordinate 类型的属性,如图4-31 所示。
然后,切换到源代码视图,则会看到如下代码:
<cc1:CustomeTypeConverterControl ID="CustomeTypeConverterControl1" runat= "server" SolidCoordinate="3, 5, 8" />
在切换到源代码视图时,转换器类就起作用了,它会把SolidCoordinate 转换成字符串类型,因为在源代码模式下所有代码类型只能以字符串格式存在,所以要求转换为字符串格式;反之,会把字符串逆向转换为SolidCoordinate 类。这就是类型转换器的功能。
格式SolidCoordinate="3,5,8" 是可以自定义的,比如可以定义成SolidCoordinate="3-5-8" 格式,规则可以在转换器类中任意指定,只要是字符串格式且保证正反向转换规则一致即可。
接下来开始讲解代码部分。SolidCoordinate 类共有三个属性(X ,Y ,Z ),前两个值(X ,Y )与Point 类型的(X ,Y )属性一致,表示平面上的横坐标和纵坐标;(Z )属性表示平面之外的第三维坐标,类代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[TypeConverter(typeof(SolidCoordinateConverter))]
public class SolidCoordinate
{
private int x;
private int y;
private int z;
public SolidCoordinate()
{
}
public SolidCoordinate(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
[NotifyParentProperty(true)]
public int X
{
get
{
return this.x;
}
set
{
this.x = value;
}
}
[NotifyParentProperty(true)]
public int Y
{
get
{
return this.y;
}
set
{
this.y = value;
}
}
[NotifyParentProperty(true)]
public int Z
{
get
{
return this.z;
}
set
{
this.z = value;
}
}
}
类代码就包括三个坐标属性,没有任何方法。需要注意的是上面有句:
[TypeConverter(typeof(SolidCoordinateConverter))]
其作用是指定该类的转换器为SolidCoordinateConverter ,即凡是SolidCoordinate 类型的控件属性都会使用此类型转换器。
SolidCoordinateConverter 类的源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class SolidCoordinateConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return ((sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType));
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return ((destinationType == typeof(InstanceDescriptor)) || base.CanConvertTo(context, destinationType));
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string str = value as string;
if (str == null)
{
return base.ConvertFrom(context, culture, value);
}
string str2 = str.Trim();
if (str2.Length == 0)
{
return null;
}
if (culture == null)
{
culture = CultureInfo.CurrentCulture;
}
char ch = culture.TextInfo.ListSeparator[0];
string[] strArray = str2.Split(new char[] { ch });
int[] numArray = new int[strArray.Length];
TypeConverter converter = TypeDescriptor.GetConverter(typeof(int));
for (int i = 0; i < numArray.Length; i++)
{
numArray[i] = (int)converter.ConvertFromString(context, culture, strArray[i]);
}
if (numArray.Length != 3)
{
throw new Exception("格式不正确!");
}
return new SolidCoordinate(numArray[0], numArray[1], numArray[2]);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
{
throw new Exception("目标类型不能为空!");
}
if (value is SolidCoordinate)
{
if (destinationType == typeof(string))
{
SolidCoordinate solidCoordinate = (SolidCoordinate)value;
if (culture == null)
{
culture = CultureInfo.CurrentCulture;
}
string separator = culture.TextInfo.ListSeparator + " ";
TypeConverter converter=TypeDescriptor.GetConverter(typeof(int));
string[] strArray = new string[3];
int num = 0;
strArray[num++] = converter.ConvertToString(context, culture, solidCoordinate.X);
strArray[num++] = converter.ConvertToString(context, culture, solidCoordinate.Y);
strArray[num++] = converter.ConvertToString(context, culture, solidCoordinate.Z);
return string.Join(separator, strArray);
}
if (destinationType == typeof(InstanceDescriptor))
{
SolidCoordinate solidCoordinate2 = (SolidCoordinate)value;
ConstructorInfo constructor = typeof(SolidCoordinate). GetConstructor(new Type[] { typeof(int), typeof(int), typeof(int) });
if (constructor != null)
{
return new InstanceDescriptor(constructor, new object[] { solidCoordinate2.X, solidCoordinate2.Y, solidCoordinate2.Z });
}
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
{
return true;
}
public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
{
if (propertyValues == null)
{
throw new Exception("属性值不能为空!");
}
object obj2 = propertyValues["X"];
object obj3 = propertyValues["Y"];
object obj4 = propertyValues["Z"];
if (((obj2 == null) || (obj3 == null) || (obj4 == null)) || (!(obj2 is
int) || !(obj3 is int) || !(obj4 is int)))
{
throw new Exception("格式不正确!");
}
return new SolidCoordinate((int)obj2, (int)obj3, (int)obj4);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties (ITypeDescriptor Context context, object value, Attribute[] attributes)
{
return TypeDescriptor.GetProperties(typeof(SolidCoordinate), attributes). Sort(new string[] { "X", "Y", "Z" });
}
}
SolidCoordinateConverter 类继承于类型转换器基类TypeConverter ,主要重写TypeConverter 类的一些可重写方法,从而实现自定义的类型转换器。
方法CanConvertFrom 具有两个参数,第一个参数表示当前上下文变量,通过此参数可以获取当前容器、当前类实例和属性描述等信息;第二个参数sourceType 表示当前要转换的类型。在这里,此方法主要判断是否能从第二个参数的类型(源类型)转换为SolidCoordinate 类型,如果源类型是字符串,则返回true ,否则调用基方法,由基方法来决定返回值。
方法CanConvertTo 同样也具有两个参数,第一个参数与上面CanConvertTo 相同意义;第二个参数destinationType 表示要转化到的目标类型。该方法在这里表示是否能够把SolidCoordinate 转换为destinationType 类型。方法体中的InstanceDescriptor 类表示实例描述类,提供创建对象所需的信息,在后面ConvertTo 方法中会详细介绍。切换到源代码视图时所有代码都是以string 类型标记的。
方法ConvertFrom 具有三个参数,第一个参数为ITypeDescriptorContext 类型的context ,与前面CanConverFrom 中参数具有相同意义;第二个参数为CultureInfo 类型的culture ,提供有关特定区域性的信息(如区域性的名称、书写系统和使用的日历),以及设置日期和字符串 排序的格式。CultureInfo 类保存区域性特定的信息,如关联的语言、子语言、国家/ 地区、日历和区域性约定。此类还提供对DateTimeFormatInfo 、NumberFormatInfo 、CompareInfo 和TextInfo 的区域性特定实例的访问,在方法ConverFrom 中使用到了访问它的TextInfo 信息;第三个参数为object 的value ,表示要进行转换的类型,这里是要把value 转换为SolidCoordinate 类型。
ConverFrom 方法体中代码部分主要实现从源类型(第三个参数value )到类型SolidCoordinate 的转换,最后返回一个SolidCoordinate 类型的实例。value 在这里其实是一个“3,5,8 ”格式的字符串。首先根据区域文化对象获取到分隔符:
char ch = culture.TextInfo.ListSeparator[0];
分隔符也可以直接固定为逗号,如char ch ="," ;既然在curture 的TextInfo 对象中提供了分隔符,我们就直接使用它定义的。在TextInfo 下的ListSeparator 是一个分隔符列表数组,第一项为"," ,所以我们就取它的ListSeparator[0] 作为分隔符。
然后把value 值通过分隔符拆分到一个字符串数组中:
string[] strArray = str2.Split(new char[] { ch });
由于SolidCoordinate 类的三个属性(X,Y,Z )都是整型的,因此我们再定义一个整型数组numArray ,并使用系统的int 类型转换器的方法把字符串数组中的每项都转换为整型存储到整型数组中:
int[] numArray = new int[strArray.Length];
TypeConverter converter = TypeDescriptor.GetConverter(typeof(int));
for (int i = 0; i < numArray.Length; i++)
{
numArray[i]=(int)converter.ConvertFromString(context,culture,strArray[i]);
}
注意上面TypeDescriptor 的静态方法GetConverter 是怎样使用的,系统转换器和我们定义的任何转换器都是可以这么使用。
最后,返回一个SolidCoordinate 类型的实例:
return new SolidCoordinate(numArray[0], numArray[1], numArray[2]);
这就是ConvertFrom 方法的实现过程。在这里面的转换规则是任意定义的,只要与ConvertTo 规则一致即可。
下面我们来讲解一下ConvertTo 方法的实现逻辑。ConverTo 方法的前三个参数与ConvertFrom 方法中的前三个参数表示的意义相同;第四个参数表示要转换到的目标类型:string 或InstanceDescriptor 。ConverTo 代码中的逻辑与ConvertFrom 中的是一个相反转换的过程,即把SolidCoordinate 转换成string 或InstanceDescriptor ,代码逻辑部分就不再详细阐述。
这里仅说一下目标类型为类InstanceDescriptor 的情况,什么时候需要转换为InstanceDescriptor ?.NET 框架中提供了在设计时生成可动态配置的属性初始化代码的功能。开发人员可以构建一个产生基于构造函数的初始化代码的类型转换器。为了在运行时配置类型属性,这些类型转换器可以使用在设计时设置的值来动态生成构造函数代码。当某个属性是可读写时,就需要将属性的类型转换成InstanceDescriptor 类型,并且解析器也需要产生创建该类型实例的代码,InstanceDescriptor 对象提供了用来创建以参数类型传递给ConvertTo 方法的SolidCoordinate 实例的构造器的有关信息。这些信息是解析器产生创建SolidCoordinate 类型的一个实例的代码时所使用的。在构造函数中的使用示例:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class InstanceDescriptorControl : Control
{
SolidCoordinate solidCoordinate;
public InstanceDescriptorControl() {
solidCoordinate = new SolidCoordinate(1,2,3);
}
public SolidCoordinate SolidCoordinate {
get
{
return solidCoordinate;
}
set
{
solidCoordinate = value;
}
}
}
上面代码中SolidCoordinate 类型的属性SolidCoordinate 是可读写的(同时具有get 和set 语句),这就需要生成InstanceDescriptor 实例的代码。也就是说,如果使用了set 语句,但这句:
if (destinationType == typeof(InstanceDescriptor))
{
//… …
}
没有实现,这样编译后的控件所在的页面编译时不会通过。读者可以打开本书随书光盘中对应的源代码,把上段代码进行注释后,测试一下,理解起来会更加深刻。
方法GetCreateInstanceSupported 返回bool 类型。如果更改此对象的属性需要调用CreateInstance 来创建新值,则返回true ;否则返回false 。
方法CreateInstance 根据上下文和指定的属性字典创建实例。IDictionary 类型的参数propertyValues 是一个属性字典集合,存储了创建对象所需的值。例如:
object obj2 = propertyValues["X"];
object obj3 = propertyValues["Y"];
object obj4 = propertyValues["Z"];
就是从字典集合中取得三个坐标值。最后根据三个坐标值返回SolidCoordinate 类型的对象,如下所示:
return new SolidCoordinate((int)obj2, (int)obj3, (int)obj4);
CreateInstance 一般用于不可变但希望为其提供可变属性的对象。
接下来方法GetPropertiesSupported 表示使用指定的上下文返回该对象是否支持单独设置属性功能。如果返回true ,则属性窗口中属性布局如图4-32 所示。如果返回false ,则属性布局如图4-33 所示。
图4-32 返回true 时的属性布局
图4-33 返回false 时的属性布局
方法GetProperties 是对GetPropertiesSupported=true 时的属性列表的实现逻辑。代码就一句:
return TypeDescriptor.GetProperties(typeof(SolidCoordinate), attributes). Sort(new string[] { "X", "Y", "Z" });
取得SolidCoordinate 的属性,并以attributes 数组为筛选条件,取得属性后依次以X ,Y ,Z 的顺序进行排序,最后返回排序后的PropertyDescriptorCollection 类型的对象。
类型转换器类SolidCoordinateConverter 已经讲解完毕,可能一些重载方法理解起来比较费劲,在开始学习只要理解CanConvertFrom ,CanConvertTo ,ConvertFrom,ConvertTo 就可以了,最常用的也是这几个方法;其他的几个重载方法GetCreateInstanceSupported ,CreateInstance ,GetPropertiesSupported ,GetProprties 可以在开发过程中逐步理解,这里只是说明它们的功能,事实上对SolidCoordinate 转换器示例只需要重写CanConverterFrom, ConvertFrom 等前面几个方法就够了。
最后实现主控件,代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("SolidCoordinate")]
[ToolboxData("<{0}:SolidCoordinateTypeConverterControl runat=server></{0}:SolidCoordinateTypeConverterControl>")]
public class SolidCoordinateTypeConverterControl : WebControl
{
SolidCoordinate solidCoordinate;
public SolidCoordinate SolidCoordinate
{
get
{
if (solidCoordinate == null)
{
solidCoordinate = new SolidCoordinate();
}
return solidCoordinate;
}
set
{
solidCoordinate = value;
}
}
//… …
}
主控件仅包含一个SolidCoordinate 类型的属性。在属性上面并没有如下代码:
[TypeConverter (typeof(SolidCoordinateConverter))]
关联类型转换器的语句,因为在SolidCoordinate 类中指定了,这里就不必再指定。在属性前面和属性类型前面指定转换器的作用是相同的,但它们有两点区别:
- 在类中指定类型转换器一般用于一对一的时候,即SolidCoordinateConverter 就是只用于SolidCoordinate 类型的转换器。
- 在属性前面指定转换器一般用于此属性可能(不是必须)使用某个转换器的时候,换句话说就是此属性或许还使用其他的转换器,或不需要转换器;或者此属性的类型为系统类型,不是我们定义的类型,这时候我们也没有机会为类指定转换器,只能在属性上指定转换器,下节讲的集合列表类型转换器就是如此。另外,这样指定的好处是更加灵活。
编译主控件,并将控件放置到页面中,在页面设计器属性窗口就可以看到我们已经实现的转换器属性,如图4-33 所示。
然后,切换到源代码视图,则会看到如下代码:
<cc1:CustomeTypeConverterControl ID="CustomeTypeConverterControl1" runat= "server" SolidCoordinate="3, 5, 8" />
在源代码码下修改属性值,再切换到设计器属性窗口中,也会看到值已经被修改了。
4.6.2.2 集合列表类型转换器
一般常用的自定义类型转换器有以下两类:
- 值翻译的类型转换器。
- 提供集合列表展示形式的类型转换器。
上一节我们实现的SolidCoordinate 类的类型转换器就属于值翻译类型转换器,这一节我们来实现第二种类型的转换器。还是以显示食品列表为例,利用自定义转换器制作一个显示集合列表的属性,图4-34 是实现后的效果。
下面讲解一下集合转换器的实现,还是以代码为主展开讲解。请见如下代码:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class CustomCollectionPropertyConverter : StringConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return true;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
{
return false;
}
public override StandardValuesCollection GetStandardValues(ITypeDescriptor Context context)
{
string[] strArray = new string[]{"水果","蔬菜","肉食","面食","蛋类"};
StandardValuesCollection returnStandardValuesCollection = new StandardValuesCollection(strArray);
return returnStandardValuesCollection;
}
}
上面代码这次没有使用TypeConverter 基类,而是使用的StringConverter 类,把此类作为转换器基类进行扩展,实现我们需要的功能。StringConverter 提供三个可重写方法,可以在这三个方法中定义自己的代码逻辑,实现集合列表。三个方法都只有一个ITypeDescriptorContext 类型的参数,通过此参数可以获取当前容器、当前类实例和属性描述等信息。
方法GetStandardValuesSupported 根据上下文参数对象返回是否支持从列表中选取标准值集,显然这里我们要设置返回true 。
方法GetStandardValuesExclusive 指定返回标准值的集合是否为独占列表。即如果设置为独占列表(返回true ),属性值只能从集合下拉列表中选择;反之,如果设置为非独占列表(返回false ),则属性值既可以从下拉列表中选择,也可以手动输入(即与默认string 类型属性输入方式相同)。读者可以打开随书光盘中对应的源代码把此属性设置为独占列表模式,看一下效果,可以加深对该方法的理解。
方法GetStandardValues 是设置集合列表的主要方法,先定义了一个数组:
string[] strArray = new string[] { "水果", "蔬菜", "肉食", "面食", "蛋类" };
然后,以当前数组对象作为参数定义返回值类型集合,并把此集合作为方法返回值。代码如下:
StandardValuesCollection returnStandardValuesCollection = new
StandardValuesCollection(strArray);
return returnStandardValuesCollection;
这样系统就会把此返回值集合作为属性下拉集合列表的填充数据。注意这里的返回值集合类型为System.ComponentModel.TypeConverter.StandardValuesCollection ,是类型转换器专门提供的一种集合类型。
接下来讲解主控件的实现,其核心源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("SelectFood")]
[ToolboxData("<{0}:CustomCollectionPropertyConverterControl runat=server></{0}:CustomCollectionPropertyConverterControl>")]
public class CustomCollectionPropertyConverterControl : WebControl
{
private string strSelectFood;
[TypeConverter(typeof(CustomCollectionPropertyConverter))]
public string SelectFood
{
get
{
return strSelectFood;
}
set
{
strSelectFood = value;
}
}
//… …
}
主控件包含一个string 类型的属性,并指定了此属性的类型转换器为我们刚刚已经实现的转换器CustomCollectionPropertyConverter 。
这里我们指定类型转换器的位置与上节有所不同:在这里我们只能在属性上指定属性的类型转换器,因为string 类是系统基本类型,我们没有机会在System.String (String.String 的别名即是string ) 类上指定转换器,除非改写string 类。另外,这种方式的灵活性在于我们可以为此属性指定其他任意类型的转换器,仅需修改一下[TypeConverter(typeof(C))] 中C 位置的类型即可。
编译主控件,并将该控件放置到页面中,在属性窗口中就会看到我们实现的集合列表如图4-35 所示。
本节内容到此结束。本节以两个例子(值翻译的类型转换器和提供集合列表形式的类型转换器)说明了自定义类型转换器的实现方案。
4.7 实现自定义属性
在本章前面的章节中我们实现的许多功能都是借助于系统提供的设计时元数据属性支持实现的,如:
- 指定属性在属性窗口中类别的Category 设计属性
[Category(" 属性窗口中的类别名称")]
[Editor(typeof(FileNameEditor), typeof(UITypeEditor))]
- 指定属性类型转换器的TypeConverter 设计属性
[TypeConverter(typeof(SolidCoordinateConverter))]
这些都是系统提供的,每个属性都有它自己的功能。那么如果我们想自定义一个设计属性,实现自定义的功能,该怎么实现呢?要设计自定义属性,仅需要直接或间接地从System.Attribute 派生即可,与传统类功能完全一样。我们既可以使用System.Attribute 来定义控件设计期控制,也可以用System.Attribute 指定运行时的控制。
本节就以一个示例演示控件中的设计属性是怎么实现的。首先还是先说明一下本节控件属性要实现的功能,如图4-35 所示。
此控件具有一个Num 属性,允许开发人员指定一个值。在控件的属性上指定了一个NumValidate 类型的验证属性,该属性需要传入两个参数,供开发人员指定区间,如果在属性窗口中指定的属性不在区间内,则会有警告提示(如图4-36 中红色警告提示)。
图4-36 控件设计属性示例
或许读者会想这不就是一个验证输入的简单功能吗?这里与我们之前做的验证的情况是不一样的。之前我们经常用的验证方式是页面在浏览器中运行时的验证,即对用户输入的验证;而这里是在IDE 设计器界面验证,是在控件设计模式下的验证,是对使用控件的开发人员进行输入验证,而不是对最终用户输入验证。
本示例就是用来讲解怎样在设计模式下设置和获取我们自己定义的设计属性。还是从代码开始,NumValidate 属性的源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
/// <summary>
/// 自定义属性类(实现验证功能)
/// </summary>
[AttributeUsage(AttributeTargets.Property,AllowMultiple=true,Inherited=true)]
public class NumValidateAttribute : Attribute
{
/// <summary>
/// 构造方法
/// </summary>
/// <param name="intMinValue">最小值</param>
/// <param name="intMaxValue">最大值</param>
public NumValidateAttribute(int intMinValue, int intMaxValue)
{
this.intMinValue = intMinValue;
this.intMaxValue = intMaxValue;
}
private int intMinValue;
/// <summary>
/// 最大值
/// </summary>
public int MinValue
{
get
{
return intMinValue;
}
}
private int intMaxValue;
/// <summary>
/// 最小值
/// </summary>
public int MaxValue
{
get
{
return intMaxValue;
}
}
/// <summary>
/// 执行验证
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public bool ValidateResult(int value)
{
if (this.intMinValue <= value && value <= this.intMaxValue)
{
return true;
}
else
{
return false;
}
}
}
代码中验证类名为NumValidateAttribute ,从System.Attribute 派生而来,有些时候根据需要间接继承System.Attribute 也是可以的。
类中必须有一个带两个参数的构造方法,分别表示验证范围的最大值和最小值,然后定义两个属性:MinValue 和MaxValue ,分别存储验证范围的最小值和最大值。最后面的方法ValidateResult 是主要的验证方法,它有一个参数,表示要验证的值,如果此值在最大值和最小值区间,则返回true ,表示是合法输入;否则,返回false ,表示验证失败。
验证类NumValidateAttribute 上方有一个很重要的设计时属性:
[AttributeUsage(AttributeTargets.Property,AllowMultiple=true,Inherited=true)]
AttributeUsage 完整命名为System.AttributeUsageAttribute ,它包含三个对自定义属性的创建具有重要意义的成员:AttributeTargets 、AllowMultiple 和Inherited 。该类主要说明NumValidateAttribute 的用法(作用于类还是属性,是否允许被继承,等等)。我们只需要指定各个参数即可,如果使用位置不正确,系统会自动提示警告信息。
AttributeTargets 指定可以对应用程序的哪些元素应用此属性。在前面的示例中指定了AttributeTargets.Property ,指示该属性只可以应用到类中的属性,还可以指定AttributeTargets. Class ,表示属性只可以应用于类元素;或指定AttributeTargets.Method ,表示属性只可以应用于某个方法。还可通过"|" 设置AttributeTargets 的多个实例。下列代码段指定自定义属性可应用到任何类或方法:
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]
如果某个属性可以应用到AttributeTargets 下的所有类型,则需要作如下设置即可:
[AttributeUsage (AttributeTargets.All)]
AttributeUsage 枚举类应用的范围不仅仅包括类和属性,共有15 个可应用元素,下面是它的源代码:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
// 摘要:
// 指定哪些应用程序元素可以对它们应用属性
[Serializable]
[ComVisible(true)]
[Flags]
public enum AttributeTargets
{
// 摘要:
// 可以对程序集应用属性
Assembly = 1,
//
// 摘要:
// 可以对模块应用属性
Module = 2,
//
// 摘要:
// 可以对类应用属性
Class = 4,
//
// 摘要:
// 可以对结构应用属性,即值类型
Struct = 8,
//
// 摘要:
// 可以对枚举应用属性
Enum = 16,
//
// 摘要:
// 可以对构造函数应用属性
Constructor = 32,
//
// 摘要:
// 可以对方法应用属性
Method = 64,
//
// 摘要:
// 可以对属性 (Property) 应用属性 (Attribute)
Property = 128,
//
// 摘要:
// 可以对字段应用属性
Field = 256,
//
// 摘要:
// 可以对事件应用属性
Event = 512,
//
// 摘要:
// 可以对接口应用属性
Interface = 1024,
//
// 摘要:
// 可以对参数应用属性
Parameter = 2048,
//
// 摘要:
// 可以对委托应用属性
Delegate = 4096,
//
// 摘要:
// 可以对返回值应用属性
ReturnValue = 8192,
//
// 摘要:
// 可以对泛型参数应用属性
GenericParameter = 16384,
//
// 摘要:
// 可以对任何应用程序元素应用属性
All = 32767,
}
AllowMultiple 属性指示元素中是否可存在属性的多个实例。该属性为bool 类型,默认值为false ,标识我们自定义的Attribte 是否能在同一元素上使用多次;如果设置为false ,则同一语言元素上只能使用一次。如果设置为AllowMultiple=true ,则属性可以这么使用:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[CustomAttribute]
[CustomAttribute]
public void Method()
{
//……
}
最后一个参数Inherited ,也是bool 类型的。我们可以使用该属性来控制我们的自定义attribute 类的继承规则,该属性标识我们的自定义Attribute 在应用到元素(别名A )时,是否可以由派生类(继承于A 所属的类)继承。如果设置为true ,则可以被继承;反之,不可以被继承。
更深一点理解,AllowMultiple 和 Inherited 还可以组合使用,完成我们需要的设置功能。如果AllowMultiple=true ,且Inherited=true ,且基类A 和B 类(继承A 类)中都有相同名称的一个属性,则实际上B 类中有两个相同名称的属性,即两个不同的实例,自己定义的一个和从基类继承来的一个;如果设置AllowMultiple=false, Inherited=true ,且基类A 和B (继承A 类)中都有相同名称的一个属性,则这时B 类中只有一个属性,即自己定义的实例,因为AllowMultiple=false 指定不允许多实例并存,系统只能用B 类定义的属性重写A 类的属性定义。如果不按正确方式使用,编译程序会提示警告信息。
到此自定义属性类就已经讲解完了,下面讲解一下此属性类是怎么使用的。笔者专门做了个控件来讲解属性类在控件中是怎么使用的,主控件源代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("Num")]
[ToolboxData("<{0}:CustomPropertyControl runat=server></{0}:CustomPropertyControl>")]
public class CustomPropertyControl : WebControl
{
TextBox tb;
int intNum = 0;
[Category("Appearance")]
[NumValidate(0, 10)]
[Description("输入值范围(0~10)")]
public int Num
{
get
{
return intNum;
}
set
{
intNum = value;
}
}
protected override void Render(HtmlTextWriter writer)
{
Table t = new Table();
t.CellPadding = 0;
t.CellSpacing = 0;
TableRow tr = new TableRow();
TableCell td_left = new TableCell();
tb = new TextBox();
tb.Text = this.intNum.ToString();
td_left.Controls.Add(tb);
tr.Controls.Add(td_left);
NumValidateAttribute numValidateAttribute = this.GetNumValidate Arribute();
if (numValidateAttribute.ValidateResult(this.Num) == false)
{
TableCell td_right = new TableCell();
Label lb = new Label();
lb.ForeColor = System.Drawing.Color.Red;
lb.Text = "值输入范围必须在:" + numValidateAttribute.MinValue. ToString
() + "~" + numValidateAttribute.MaxValue.ToString() + "之间!";
td_right.Controls.Add(lb);
tr.Controls.Add(td_right);
}
t.Controls.Add(tr);
t.RenderControl(writer);
}
private NumValidateAttribute GetNumValidateArribute()
{
System.Type type = this.GetType();
PropertyInfo property = type.GetProperty("Num");
object[] attrs = (object[])property.GetCustomAttributes(true);
foreach (Attribute attr in attrs)
{
if (attr is NumValidateAttribute)
{
return attr as NumValidateAttribute;
}
}
return null;
}
}
主控件输出一个文本框,开发人员在设计器属性窗口中可以修改文本框的值。控件中的属性Num ,就是用来存储文本框的值的。Num 属性使用了我们刚刚定义的自定义属性验证证类NumValidateAttribute :
注意在使用时,关键字使用的不是我们定义的完整类名。实际上这里使用[NumValidate(0, 10)] 和 [NumValidateAttribute(0, 10)] 都是可以的,系统都会把后缀Attribute 省略,而且智能提示也会省略Attribute ,因此我们在使用时也把Attribute 省略了。
我们设置NumValidate 属性并传入两个参数:0 和10 ,以及Num 的类型为int 类型,表示要求这个Num 属性必须是0 ~ 10 之间的整型数值。在设计模式和运行模式执行时,系统会把0 和10 作为NumValidateAttribute 类的构造参数创建实例。
接下来是Render 方法,在Render 方法中主要执行两个功能:一是输出TextBox 内容,并嵌套到Table 对象中;二是实现对设计期间的输入验证。验证过程代码如下:
NumValidateAttribute numValidateAttribute = this.GetNumValidateArribute();
if (numValidateAttribute.ValidateResult(this.Num) == false)
{
TableCell td_right = new TableCell();
Label lb = new Label();
lb.ForeColor = System.Drawing.Color.Red;
lb.Text = "值输入范围必须在:" + numValidateAttribute.MinValue.ToString() + "~" + numValidateAttribute.MaxValue.ToString() + "之间!";
td_right.Controls.Add(lb);
tr.Controls.Add(td_right);
}
以上代码首先获取我们在属性上指定的自定义验证属性实例对象,然后以主控件的Num 值作为参数,调用验证属性对象的ValidateResult 方法进行输入合法验证,如果当前输入值验证失败(Num 在我们设定的0~10 之间,这里仅模拟验证逻辑),则会在当前行中增加一个单元格,并在单元格中增加一个显示出错警告的Label 控件,告知开发人员合法输入值正确范围;反之,验证成功,则不会呈现出单元格和提示警告信息Label 控件。
接下来说明自定义属性对象实例是怎样获取到的,方法GetNumValidateArribute 获取属性对象的代码,其使用反射机制取得。代码如下:
System.Type type = this.GetType();
PropertyInfo property = type.GetProperty("Num");
首先取得当前控件类的类型,再根据类型调用以Num 属性为参数,调用GetProperty 方法取得属性的PropertyInfo 对象property ,PropertyInfo 可以提供对属性元数据的访问。代码如下:
object[] attrs = (object[])property.GetCustomAttributes(true);
foreach (Attribute attr in attrs)
{
if (attr is NumValidateAttribute)
{
return attr as NumValidateAttribute;
}
}
return null;
通过PropertyInfo 对象实例的GetCustomAttributes 方法获取Num 属性的所有自定义属性类实例,放到attrs 数组中,再循环遍历数组中每个实例的类型,如果找到有NumValidateAttribute 类型的对象实例,则返回此实例;否则,返回null 。
GetCustomAttributes 方法主要是展示怎样获取属性元素的设计属性实例,如果自定义属性不是应用到属性,而是类或接口或方法等,则获取属性实例方式原理类似,都是通过反射机制实现。这里不作多讲,具体使用时可以查看官方文档。
编译主控件,并放置一个CustomPropertyControl 控件到页面中,则可以在属性窗口中通过修改控件Num 属性的值来看一下效果,如图4-37 所示。
自定义属性就讲到这里,在主控件类中可以通过this.GetType() 方法获取当前控件的类型,再根据类型取得属性,进而取得属性的设计属性Attribute ,那么在控件构造器类、控件编辑器类或控件类型转换器类等这样的一些类中是怎样获取到当前主控件类型呢?其实读者可能会想到了,一般在这些特殊类中都会有相应的上下文对象,比如在4.6 节讲解的自定义类型转换器中,像ConvertTo ,CanConvertFrom 等方法都会有个ITypeDescriptorContext 类型的上下文参数,基本上我们可能需要的所有信息都能够通过此对象得到,如图4-38 所示。
图4-37 属性窗口效果
图4-38 智能感知列表
如上图所示的智能感知列表,可以看到它下面有容器对象、实例、属性描述集合,还有属性描述类等。
4.8 本章总结
本章内容比较多,对控件的属性作了详细的介绍。本章按照不同的分类方法将控件属性分为系统属性和自定义属性;简单属性和复杂属性;并且分别阐述了这些属性类别的定义和属性的设计时特征。其中重点讲解了复杂属性,包括各种复杂属性标记的实现方式,及通过高级功能AddParsedSubObject 和ControlBuilder 实现自定义的复杂属性。一个使用方便的控件不仅要具备丰富的属性以实现灵活的控制,还应具备友好的属性编辑器。因此本章深入讲解属性编辑器,包括系统常用编辑器和自定义编辑器。在配置属性时常常需要用到类型转换,本章特意用一节讲解系统常用类型转换器和自定义的两个类型转换器:三维坐标类型转换器和集合列表类型转换器。本章堪称本书的精华章节之一,请读者务必仔细阅读,细细体会。
from:http://msdn.microsoft.com/zh-cn/vstudio/dd567281.aspx
下一篇:(四)庖丁解牛Asp.net3.5控件和组件开发技术系列 -- 事件和数据回发机制-郑健