简介
在先前的Visual Basic介绍中,属性窗口是真正实现快速应用开发的一个关键工具(RAD:Rapid Application Development)。在Visual Studio .NET中,属性窗口继续提供更多的特性来支持开发。如果你在使用Visual Studio .NET开发环境编写组件或者是其他对象,当然可以用到属性窗口提供给的特性来丰富你的组件。
属性窗口可以做什么
以前的属性窗口版本自然处理基于COM的信息并且显示内在的属性。一个COM组件的公开API一般用IDL(Interface Definition Language)来声明并且带有自己的属性。比如:nonbrowsable可以让属性窗口不显示它,或者是bindable可以让属性实现数据绑定。其他的显示特性,比如standard value list(标准值列表)和categorized properties(属性类别)需要组件实现COM接口IPerPropertyBrowsing和ICategorizedProperties。.NET framework和属性窗口以一种更加简单、统一的方式提供这些支持,同时有更多的新特性。
自然,.NET属性窗口继续支持以前版本的功能,他从ITypeInfo中得到类型信息并且支持以上提到的性能。不过,如果要使用这些功能强大的特性,就必须用managed code来实现组件。下面是一些特性的列表:
lMetadata attribute(元数据特性)
属性的特性很大程度上决定了属性窗口怎么和你的组件交互。特性可以很方便的让组件编写者来控制属性在属性窗口中是否可见、如何分类、是否可以包括在多选中、是否影响其他属性的值。这些特性都是可以简单使用的。
lHierarchical support(继承支持)
在逻辑上,属性还可以拥有子属性。
lGraphical value representation(属性值图形化表示)
除了可以提供属性值的文字表示外,我们还可以提供属性值的图形化表示
lCustom type editing(订制类型编辑)
组件可以提供订制的用户界面,比如日期控件的日期选择方式,或者是色彩控件的颜色选取方式。不再是属性窗口来决定所支持的类型,现在组件来决定它的订制类型。Framework提供了工具来支持所有内嵌类型的编辑。
lExtensible views(扩展属性视图)
像“property tabs”一样,组件可以在属性和事件上增加自己的视图,这样在设计状态就可以支持特性了。
lReusable component(重用组件)
.NET属性窗口主要是使用System.Windows.Forms.PropertyGrid控件组成的,我们同样可以在应用中重用它。
很明显,属性窗口还有更多的特性。这篇文章就是告诉你如何使用这些特性
基础知识:使用Attribute来订制属性窗口的显示
控制显示的机制和用IDL定义的组件是一样的,不过是增加了元数据特性。控制显示使用最普遍的特性是BrowsableAttribute。默认状态下,属性窗口显示对象中定义的所有的公开的、可读的(即public、有get或者set方法的)属性,并且把他们放在“Misc”类别中。下面是一个简单的组件例子:
public class SimpleComponent : System.ComponentModel.Component
{
private string data = "(none)";
private bool dataValid = false;
public string Data {
get {
return data;
}
set {
if (value != data) {
dataValid = true;
data = value;
}
}
}
public bool IsDataValid {
get {
// perform some check on the data
//
return dataValid;
}
}
}
下图是这个例子在属性窗口中的显示:

图1.显示在属性窗口中的简单组件
在这个例子中,SimpleComponent有2个属性:Data和IsDataValid。实际上,由于IsDataValid是只读的,因此在这里并没有多大意义,设计人员在设计状态并不需要知道这个属性的值。因此,我们给他加上BrowsableAttribute特性让属性窗口不显示他。
Browsable(false)]
public bool IsDataValid {
get {
// perform some check on the data
//
return dataValid;
}
}
C#编译器会自动在特性类名后添加“Attribute”字符,我们可以在代码中省略掉他。当然,输入“[BrowsableAttribute(false)]”是一样的效果。对于那些没有指定特性的属性或者类,都默认为使用默认值。在这个例子中,BrowsableAttribute的默认值为true。这个原则对于Visual Basic .NET同样是一致的。两者唯一的区别就是Visual Basic .NET使用’<’和’>’来标记特性,而不是在C#中使用的中括号。
同时,我们注意一下在图1中Data属性的值“abc”是粗体。显示为粗体的值意味着它不是默认值,而且这个值在设计器为form或者control生成代码的时候将会保存下来(即会生成一个赋值语句)。而对于属性的默认值就没有必要来生成赋值语句,不然就会增加组件初始化的时间(InitializeComponent方法),和代码文件的大小。不过SimpleComponent是如何告诉属性窗口他的属性的默认值,而让他不会生成针对默认值的代码呢?要实现这个特性,我们就可以使用DefaultValueAttribute特性,就可以在对象的构建器中为属性赋值。当属性窗口显示属性值的时候,它就会比较当前值和DefaultValueAttribute指定的默认值,如果两者不相等的话,就会把值显示成粗体。在这个例子里,任何不是“(none)”的值都会被显示成粗体。
[DefaultValue("(none)")]
public string Data {
// . . .
}
我们同样可以决定是否给属性加上更强的逻辑而不是一些简单的固有值,给组件增加方法就可以实现这个。方法的名字必须是以“ShouldSerialize”开头,并且接着就是属性的名字,而且方法的返回值为“Boolean”。在这个例子里,这种方法就叫“ShouldSerializeData”。在SimpleComponent组件中增加下面的代码就可以实现和DefaultValueAttribute同样的效果,不过他却可以有更强的逻辑代码。
private bool ShouldSerializeData()
{
return Data != "(none)";
}
一般来说,将属性分类对设计者来说更加有效。我们就是用CategoryAttribute特性来给属性分类。这个特性就使用一个简单的类别字符串,属性窗口可以根据这个类别字符串来按类别显示属性。这个类别名称是我们自己设计的。
DefaultValue("(none)"), Category("Sample")]
public string Data {
// . . .
}
组件开发者遇到的一个通用问题就是实现这个类别字符串的本地化。我们看看System.ComponentModel.CategoryAttribute类,就可以看到他的GetLocalizedString方法就提供了这样的功能。要实现类别字符串的本地化,就要从CategoryAttribute类导出新的特性类别。我们在这个例子中从组件的资源清单中得到以键值表示的本地类别字符串。在指定属性的CategoryAttribute特性时,这个键值(Key)就作为代替实际类别名称的参数。一旦查询属性的CategoryAttribute就会调用GetLocalizedString方法并且把key值作为参数传入方法。返回值就会作为属性的类别显示在属性窗口中。
internal class LocCategoryAttribute : CategoryAttribute {
public LocCategoryAttribute(string categoryKey) : base(categoryKey)
{
}
protected override string GetLocalizedString(string key)
{
// get the resource set for the current locale.
//
ResourceManager resourceManager = new ResourceManager();
string categoryName = null;
// walk up the cultures until we find one with
// this key as a resource as our category name
// if we reach the invariant culture, quit.
//
for (CultureInfo culture = CultureInfo.CurrentCulture;
categoryName == null &&
culture != CultureInfo.InvariantCulture;
culture = culture.Parent)
{
categoryName = (string)resourceManager.GetObject(key, culture);
}
return categoryName;
}
}
使用这个本地化类别名称,我们要先定义一个包含有这个key的资源文件,并且把这个特性使用到属性上去。
[LocCategory("SampleKey")]
public string Data {
// . . .
}
当我们选择了设计窗口上的多个组件的时候,属性窗口就显示他们属性的交集或者是合成,这个选择过程是基于组件的属性名和类型的。接着,一旦改变一个属性的值,那么所有的被选择的组件的这个属性值都会跟着改变。而把一个属性包括在和其他属性的合成里一般来说意义不大。一般的,这样的属性都有唯一的值,比如另一个组件的名字。由于当选择了多个组件的时候,改变某个属性的值,所有的被选择的属性值都会改变,因此,把一个属性不合成在这个选择之中是有一定作用的。MergablePropertyAttribute就可以做到这点。只要简单的把这个特性的false值赋给属性,就可以在选择多个组件的时候隐藏掉这个属性,自然他的值就不会跟着变化了。
一些属性的值可以影响到另外的一些属性值。在实现数据绑定的组件中,清除掉DataSource的值,自然的就会清除掉DataMember的值。RefreshPropertiesAttribute就让我们实现这个功能。他的默认值是“none,”不过如果指定其他的特性,属性窗口就可以在改变这个属性值的同时,自动的更新被更新属性的值。另外的两个值是Repaint,它让属性窗口重新获取属性的值并且重画他们,还有就是All,它就意味着组件自己要重新获取属性的值。如果值的改变导致属性的数目的增减,那么我们就要使用All。不过要注意到这个一般用在高级场合,并且速度要比Repaint慢。RefreshProperties.Repaint适用于大部分的情况。在这里,这个特性是使用在要改变别的属性的属性上,而不是被改编的属性上。
最后,DefaultPropertyAttribute和DefaultEventAttribute使用在类级别的场合,他们让属性窗口高亮显示这些属性和事件。当选择其他的组件的时候,属性窗口一般是试图显示和前一个组件的被选择的属性具有相同的属性名和类别的属性。如果没有这样的属性的话,它就会显示DefaultPropertyAttribute指定的属性。在Event视图就会显示DefaultEventAttribute。同时,这个事件也是当你双击组件时,自动使用的事件。
.NET属性窗口最重要的一个特性就是可以显示嵌套的属性,这样就提供了比属性类别更加细化和更有逻辑的分类。嵌套属性对于类别显示和排序显示都是适用的。这样可以让属性列表更加紧凑。比如我们把带有子属性X和Y的Location属性来代替Top和Left属性就更加的合理。

图2.嵌套属性
不过,如何来决定一个属性可以展开呢?这些不是由属性窗口来决定,而是取决于属性自己的类型。在.NET framework中,每一个属性都是和一个TypeConverter联系在一起的。比如Boolean和string的TypeConverter就不会允许展开。因为让boolean类型含有子属性是没有多少意义的。
在.NET framework中,TypeConverter实际上是执行了不少的方法,在属性窗口中就更多了。正像他的名字所说明的那样,TypeConverter提供了一种动态的从一种类型改变到另一种类型的标准方式。而,属性窗口实际上是只和string打交道的。所以他就依赖于TypeConverter来进行类型之间的转换(主要是和string类型的转换)。TypeConverter同样是可以提供扩展性能以及复杂类型来和属性窗口交互。
比如,看下面这个Person类:
[TypeConverter(typeof(PersonConverter))]
public class Person {
private string firstName = "";
private string lastName = "";
private int age = 0;
public int Age {
get {
return age;
}
set {
age = value;
}
}
public string FirstName {
get {
return firstName;
}
set {
this.firstName = value;
}
}
public string LastName {
get {
return lastName;
}
set {
this.lastName = value;
}
}
}
注意到,我们在这里把TypeConverterAttribute特性赋给了这个类,指定用于这个类的TypeConverter。如果没有指定TypeConverterAttribute特性,那么就相当于默认为使用TypeConverter类,它不过就不会像我们想要做的那样来显示数据了。在PersonConverter中,我们重载GetPropertiesSupported和GetProperties方法来限制属性是否可以展开。
internal class PersonConverter : TypeConverter {
public override PropertyDescriptorCollection
GetProperties(ITypeDescriptorContext context,
object value,
Attribute[] filter){
return TypeDescriptor.GetProperties(value, filter);
}
public override bool GetPropertiesSupported(
ITypeDescriptorContext context) {
return true;
}
}
这个让.NET framework包括一个从TpyeConverter导出的类的过程已经足够通用了。简单的扩展就是从TypeConverter直接导出你所要的类。现在我们修改PersonConverter来转换一个Person类并且显示一个字符串。
internal class PersonConverter : ExpandableObjectConverter {
public override bool CanConvertFrom(
ITypeDescriptorContext context, Type t) {
if (t == typeof(string)) {
return true;
}
return base.CanConvertFrom(context, t);
}
public override object ConvertFrom(
ITypeDescriptorContext context,
CultureInfo info,
object value) {
if (value is string) {
try {
string s = (string) value;
// parse the format "Last, First (Age)"
//
int comma = s.IndexOf(',');
if (comma != -1) {
// now that we have the comma, get
// the last name.
string last = s.Substring(0, comma);
int paren = s.LastIndexOf('(');
if (paren != -1 &&
s.LastIndexOf(')') == s.Length - 1) {
// pick up the first name
string first =
s.Substring(comma + 1,
paren - comma - 1);
// get the age
int age = Int32.Parse(
s.Substring(paren + 1,
s.Length - paren - 2));
Person p = new Person();
p.Age = age;
p.LastName = last.Trim();
.FirstName = first.Trim();
return p;
}
}
}
catch {}
// if we got this far, complain that we
// couldn't parse the string
//
throw new ArgumentException(
"Can not convert '" + (string)value +
"' to type Person");
}
return base.ConvertFrom(context, info, value);
}
public override object ConvertTo(
ITypeDescriptorContext context,
CultureInfo culture,
object value,
Type destType) {
if (destType == typeof(string) && value is Person) {
Person p = (Person)value;
// simply build the string as "Last, First (Age)"
return p.LastName + ", " +
p.FirstName +
" (" + p.Age.ToString() + ")";
}
return base.ConvertTo(context, culture, value, destType);
}
}
图3. 实现展开的TypeConverter

要使用上面的代码,我们就生成一个UserControl并且写下如下的代码:
private Person p = new Person();
public Person Person {
get {
return p;
}
set {
this.p = value;
}
}
扩展属性和字符串转换:TypeConverter和属性窗口
编写和显示订制的类型
属性窗口中的编辑可以按下面3种方式工作:一,有些场合可以作为字符串来编辑,然后由TypeConverter来实现类型的转换。二,可以显示一个下拉列表来选择值。三,一个省略按钮提供其他的UI界面来编辑值,比如FileDialog和FontPicker。我们已经讲过了字符串形式,接着我们就来看下拉列表。

图4. 下拉列表编辑器
同样是TypeConverter来实现下拉的工作。如果看TypeConverter的说明,可以看到有3个虚函数来实现这个功能:GetStandardValuesSupported(),GetStandardValues()和GetStandardValuesExclusive()。使用这些方法,我们可以为属性提供预先定义好的值列表。实际上,是TypeConverter实现了下拉列表中的枚举值。属性窗口自己本身并没有代码来处理这种下拉列表的编辑,而仅仅是使用TypeConverter的方法。
举个例来说,我们有一个包含Relation属性的FamilyMember组件,允许用户选择人与其他人之间的关系。如果要更简单的话,属性窗口使用下拉列表来提供一些通用的选择:如mother,father,daughter和sister等。除了提供大部分的关系之外,同样也可以让用户自己输入一个字符串。
public class FamilyMember : Component {
private string relation = "Unknown";
[TypeConverter(typeof(RelationConverter)),
Category("Details")]
public string Relation {
get { return relation;}
set { this.relation = value;}
}
}
internal class RelationConverter : StringConverter {
private static StandardValuesCollection defaultRelations =
new StandardValuesCollection(
new string[]{"Mother", "Father", "Sister",
"Brother", "Daughter", "Son",
"Aunt", "Uncle", "Cousin"});
public override bool GetStandardValuesSupported(
ITypeDescriptorContext context) {
return true;
}
public override bool GetStandardValuesExclusive(
ITypeDescriptorContext context) {
// returning false here means the property will
// have a drop down and a value that can be manually
// entered.
return false;
}
public override StandardValuesCollection GetStandardValues(
ITypeDescriptorContext context) {
return defaultRelations;
}
}
不过如何做一个更加定制化的UI呢?我们可以使用UITypeEditor类。UITypeEditor类包括了在生成属性或者是点击按钮的时候(比如下拉列表和省略按钮)可以由属性窗口调用的方法。
一些类似于Image,Color,Font.Name的属性类型会在属性值的左边画出一个小的图形表示。这是通过实现UITypeEditor的PaintValue方法来完成的。当属性窗口生成定义了编辑类的属性值的时候,它就提供给编辑器一个矩形框对象(Rectangle)和画图的对象(Graphic)。举个例子来说,我们有一个Grade类需要有图形化的表示。下面就是我们的Grade类。
[Editor(typeof(GradeEditor), typeof(System.Drawing.Design.UITypeEditor))]
[TypeConverter(typeof(GradeConverter))]
public struct Grade
{
private int grade;
public Grade(int grade)
{
this.grade = grade;
}
public int Value
{
get
{
return grade;
}
}
}
当我们输入一个年龄的时候,我们可以看到左边的一个图形表示。

图5. 输入年龄
实现它并不困难。注意到赋给Grade类的EditorAttribute特性,它就是下面的这个类:
public class GradeEditor : UITypeEditor
{
public override bool GetPaintValueSupported(
ITypeDescriptorContext context)
{
// let the property browser know we'd like
// to do custom painting.
return true;
}
public override void PaintValue(PaintValueEventArgs pe)
{
// choose the right bitmap based on the value
string bmpName = null;
Grade g = (Grade)pe.Value;
if (g.Value > 80)
{
bmpName = "best.bmp";
}
else if (g.Value > 60)
{
bmpName = "ok.bmp";
}
else
{
bmpName = "bad.bmp";
}
// draw that bitmap onto the surface provided.
Bitmap b = new Bitmap(typeof(GradeEditor), bmpName);
pe.Graphics.DrawImage(b, pe.Bounds);
b.Dispose();
}
}
像我们上面提到的,UITypeEditor可以实现属性的下拉选择和弹出对话框选择。后面的例子会包括这样的代码。如果想知道进一步的信息的话,就要参考UITypeEditor.GetEditStyle和UITypeEditor.EditValue方法以及IWindowsFormsEditorService接口。
提供可交互的属性视图
当你在Visual C# .NET中创建一个项目的时候,你可能会注意到属性窗口的工具栏上有一个像闪电的按钮,按下这个按钮属性窗口就会切换到事件视图,这样就可以来编辑事件处理了。这个实际上是给开发者的一个扩展机制。
属性窗口的视图来自“Property Tabs”,因此添加视图使用的主要的类是System.Windows.Forms.Design.PropertyTab。一个属性页可以和一个特别的组件、设计文档关联起来,或者是总是可以使用的静态关联。和组件或者是文档关联起来的属性页在类上用PropertyTabAttribute特性来指定。这个特性指定要创建的Tab的类型,它在属性窗口上是否显示由PropertyTabAttribute的PropertyTabScope参数来指定。指定为Component范围的属性页的可见性由有PropertyTabAttribute特性的组件的可见性来决定。Document范围的属性页则可以在当前项目的设计中都可见。他的默认值是PropertyTabScope.Component。
举一个例子来说,看看“FunkyButton”项目。FunkyButton是一个扩展了PropertyTab的UserControl,而且可以让我们把控件定为不规则的多边形。

图6. FunkyButton
当前选择的属性页就是属性窗口从被选择的控件的属性中得到的。属性页因此就允许来操纵显示属性的不同集合。Events页就是像属性一样以某种方式来处理事件。在这个例子中,属性页就创建了表示控件顶点的属性。
.NET framework中的属性用PropertyDescriptor类来封装。PropertyDescriptor本身是一个抽象类,framework中由他导出的类提供了访问组件开放的属性的方法。不过,属性窗口是直接作用在PropertyDescriptor上,而不是直接作用在属性上。因此,我们就可以写自己的PropertyDescriptor来做一些特殊的工作。在这个例子里,我们就有一个属性表示控件的顶点数,另一个就表示每一个顶点。再次注意一下,我们在属性窗口上增加页并不相应的作用在其他对象上。
当属性窗口向PropertyTab询问Properties的时候,它就调用GetProperties方法。对于我们的示例程序,这个方法就像下面的一样:
public override PropertyDescriptorCollection
GetProperties(ITypeDescriptorContext context, object component,
Attribute[] attrs)
{
// our list of props.
//
ArrayList propList = new ArrayList();
// add the property for our count of vertices
//
propList.Add(new NumPointsPropertyDescriptor(this));
// add a property descriptor for each vertex
//
for (int i = 0; i < ((FunkyButton)component).Points.Count; i++)
{
propList.Add(new VertexPropertyDescriptor(this,i));
}
// return the collection of PropertyDescriptors.
PropertyDescriptor[] props =
(PropertyDescriptor[])
propList.ToArray(typeof(PropertyDescriptor));
return new PropertyDescriptorCollection(props);
}
GetProperties仅仅是返回一些属性描述的集合。PropertyDescriptors是相当的简单,看看这些代码来了解他们是怎么工作的。
FunkyButton同时示例了下拉列表编辑器的实现。对于每一个点向量,我们不是简单的输入坐标的X和Y值,我们会图示FunkyButton的形状,而且可以图示改变向量。这样设置按钮样子更加的简单。

图7. 图形化的点向量
由于订制的PropertyTab提供了属性,重载这个属性的编辑器也是很容易的。要这样做的话,只要简单的重载PropertyDescriptor的GetEditor方法,然后返回订制组件的实例就可以了。
public override object GetEditor(Type editorBaseType)
{
// make sure we're looking for a UITypeEditor.
//
if (editorBaseType == typeof(System.Drawing.Design.UITypeEditor))
{
// create and return one of our editors.
//
if (editor == null)
{
editor = new PointUIEditor(owner.target);
}
return editor;
}
return base.GetEditor(editorBaseType);
}
设计编辑器同样简单。编辑器就是一个简单的UserControl,所以我们就可以像设计其他的windowsForms对象一样来做。

图8. Designing the editor
最后,当用户在属性窗口中点击下拉箭头时,我们的编辑器就可以来创建UI并且下拉了。PointUIEditor中的UITypeEditor.EditValue重载后就可以实现了。
public override object EditValue(
ITypeDescriptorContext context,
IServiceProvider sp, object value)
{
// get the editor service.
IWindowsFormsEditorService edSvc =
(IWindowsFormsEditorService)sp.GetService(typeof(IWindowsFormsEditorService));
// create our UI
if (ui == null)
{
ui = new PointEditorControl();
}
// initialize the ui with the settings for this vertex
ui.SelectedPoint = (Point)value;
ui.EditorService = edSvc;
ui.Target = (FunkyButton)context.Instance;
// instruct the editor service to display the control as a
// dropdown.
edSvc.DropDownControl(ui);
// return the updated value;
return ui.SelectedPoint;
}
我们同样可以使用它
我们已经告诉了你.NET中属性窗口的核心思想,你就可以用在自己的应用中了。叫做System.Windows.Forms.PropertyGrid的控件,可以添加到.NET中的ToolBox中,通过获取在ToolBox的Component标签里的PropertyGrid。
PropertyGrid和其他的控件工作是一样的。你可以anchor或者是Dock他,改变它的色彩等。下面的列表列出了PropertyGrid的一些有趣的属性。
•SelectedObject
PropertyGrid要显示的对象
•ToolbarVisible
显示或者隐藏PropertyGrid顶端的Toolbar
•HelpVisible
显示或者隐藏PropertyGrid底端的帮助文本
•PropertySort
设置PropertyGrid的排序类型 (categorized, alphabetical, etc.).
这些属性都可以在设计时期设置。然而,在运行时期PropertyGrid就可以操作他显示的对象了。下面是一个显示一个button的PropertyGrid例子。在这个例子中,PorpertyGrid的帮助和toolbox都被隐藏了。就像上面提到的,你可以设置他自己的属性。

结论
.NET framework和Visual Studio .NET给属性窗口增加了相当多的功能。由于属性窗口是RAD的核心,这些特性可以在保持易用性的同时有很多的扩展,也因此在Visual Basic中用的很普遍。就像可以在我们的程序中使用PropertyGrid,我们可以把更多的时间放在如何写好程序上,而可以简化我们的UI工作。
来自:http://www.microsoft.com/china/community/Column/79.mspx