将 .NET PropertyGrid 随心所欲地调整






4.89/5 (272投票s)
2002年12月20日
11分钟阅读

1387402

29568
一组类,用于简化在 .NET PropertyGrid 控件中使用自定义属性。
引言
随着 .NET Framework 的初始发布,我对其最终提供了一个备受瞩目的自定义控件供公众使用感到印象深刻——在这种情况下,就是类似 Visual Basic 的属性网格,System.Windows.Forms.PropertyGrid
。然而,在使用它之后,我意识到虽然该控件非常灵活且功能强大,但要进行自定义却需要付出相当大的努力。下面,我将介绍一组类,它们可以提高属性网格的可用性。
背景
。NET Framework 提供的属性网格通过反射进行操作。通过使用 SelectedObject
或 SelectedObjects
属性,您可以将一个或多个对象选择到控件中。然后,它会查询对象的所有属性,根据它们的 CategoryAttribute
将它们组织到组中,并使用各种 UITypeEditor
和 TypeConverter
类来允许用户将属性编辑为字符串。
这样做的坏处是,开箱即用的灵活性不高。要使用网格,您必须编写一个“代理类”,该类公开您想在网格中显示的属性,实例化该类的对象,然后将其选择到网格中。
然而,许多时候可能不希望创建这样的类。每个代理类都必须针对您的数据进行特定化,因为运行时仅通过反射来询问类其属性是什么。您无法轻松编写一个代理类,该类可以在多个数据之间重用,除非它们恰好共享相同的属性。
根据您的系统设计和数据组织方式,编写代理类可能不方便。想象一下,您有一个数据库,其中存储了包含一百个属性的键值对。要创建一个类来包装此数据库,您必须为这所有一百个属性手动编写 get
和 set
调用。即使您编写了一个自动化工具来为您生成代码,仍然存在可执行文件代码膨胀的问题。
最后,创建代理类并不适合“美化”网格的内容。标准行为是显示代码中属性的实际名称。这对于像窗体设计器这样的组件来说是有意义的,因为开发人员需要能够快速轻松地在网格中的属性与代码中的同一属性之间建立联系。但是,如果网格用作最终用户界面的部分,那么它们应该看到比“UniqueCustID”更直观的东西。
自定义类型描述符
如果您查看 Visual Studio.NET 中的项目设置对话框,您会注意到网格包含名称更优美的属性,如“Use of MFC”,它会打开一个下拉列表,其中包含“Use MFC in a Shared DLL”等值。显然,这里发生的事情比简单的带枚举类型的属性要复杂得多。
答案在于 System.ComponentModel.ICustomTypeDescriptor
接口。该接口允许一个类提供关于自身的自定义类型信息。简而言之,这允许您修改属性网格在查询对象以获取此信息时看到的属性和事件。
ICustomTypeDescriptor
提供了许多看起来与标准 TypeDescriptor
类中的函数相似的函数。事实上,当您实现 ICustomTypeDescriptor
时,您希望能够利用框架已提供的许多标准行为。对于大多数函数,您可以简单地将控制权传递给标准 TypeDescriptor
,如下所示:
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
return TypeDescriptor.GetAttributes(this, true);
}
大多数这些函数的第一个参数是请求信息的对象。第二个参数告诉 TypeDescriptor
不要调用自定义类型描述符来获取请求的信息。因为我们 *就是* 自定义类型描述符,所以省略此参数将导致无限递归,因为自定义类型描述符会被反复调用。
然而,请注意,我最初说 ICustomTypeDescriptor
允许一个类提供关于 *自身* 的信息。这似乎暗示必须存在一个类,我们可以为其提供类型信息,这是正确的。必须存在 *某个* 类,该类可以实现 ICustomTypeDescriptor
接口,为网格提供属性列表。但是对象的接口不必与其将公开的属性看起来一样,而这正是灵活性所在。
解决方案
为此,我创建了 PropertyBag
类。它实现了 ICustomTypeDescriptor
,但以尽可能通用的方式实现。PropertyBag
不依赖于类本身的硬编码属性,而是管理一组描述应在网格中显示的属性的对象。它通过重写 ICustomTypeDescriptor.GetProperties()
来实现这一点,生成自己的自定义属性描述符,并返回该集合。由于控制权从未传递给标准类型描述符,因此网格甚至不知道属于 PropertyBag
类本身的“实际”属性。
这样做的优点是可以在需要时添加和删除属性,而典型的代理类将在编译时固定。
类型描述符仅提供有关属性的存在和类型的信息,因为它们实现了可以应用于该类型任何对象的*方法。这意味着需要某种方式来检索和存储属性的值。我已经实现了以下两个方法:
方法 1:引发事件
基类 PropertyBag
实现的方法在查询或更改属性值时引发事件。当属性网格需要知道某个属性的值时,会发生 GetValue
事件;当用户通过网格交互式更改属性的值时,会发生 SetValue
事件。
当处理存储在文件和数据库中的属性时,此方法很有用。当事件发生时,您可以使用属性名来索引到数据源,对每个属性使用相同的简单查找代码。
方法 2:将值存储在表中
为了获得更简单的方法,可能在某些情况下更合适,我从 PropertyBag
派生了一个名为 PropertyTable
的类。该类提供了 PropertyBag
的所有通用功能,但还包含一个按属性名称索引的哈希表,用于存储属性值。当请求值时,会在表中查找属性;当用户更新它时,哈希表中的值会相应更新。
自己动手
由于 PropertyBag
提供了与上述事件对应的虚拟函数 OnGetValue
和 OnSetValue
,因此您也可以派生一个类并覆盖这两个函数来提供您自己的方法。
包含内容
PropertyBag 类
PropertyBag
类非常基础,仅公开两个属性、两个事件和两个方法。
public string DefaultProperty { get; set; }
DefaultProperty
属性指定属性包中默认属性的名称。这是当包被选择到 PropertyGrid
中时默认选择的属性。
public PropertySpecCollection Properties { get; }
Properties
集合管理包中的各种属性。与许多其他 .NET Framework 集合一样,它实现了 IList
,因此可以使用 Add
、AddRange
和 Remove
等函数来操作包中的属性。
public event PropertySpecEventHandler GetValue;
public event PropertySpecEventHandler SetValue;
protected virtual void OnGetValue(PropertySpecEventArgs e);
protected virtual void OnSetValue(PropertySpecEventArgs e);
每当属性网格需要请求属性的值时,都会引发 GetValue
事件。这可能由于多种原因发生,例如显示属性、将其与默认值进行比较以确定是否可以重置属性等。
每当用户通过网格修改属性的值时,都会引发 SetValue
事件。
派生类可以覆盖 OnGetValue
和 OnSetValue
来代替添加事件处理程序。
PropertyTable 类
PropertyTable
类提供了 PropertyBag
的所有操作,以及一个索引器属性。
public object this[string key] { get; set; }
此索引器用于获取和设置存储在表内部哈希表中的属性值。属性按名称索引。
PropertySpec 类
PropertySpec
提供了 16 个构造函数重载,数量太多,无法在此详细描述。这些重载是以下参数的各种组合,并与其对应的属性一起列出:
name (Name)
:属性的名称。type (TypeName)
:属性的类型。在构造函数中,它可以是Type
对象(例如,由typeof()
运算符返回的对象),也可以是表示完全限定类型名称的字符串(即,"System.Boolean"
)。category (Category)
:一个字符串,指示属性所属的类别。如果为 null,则使用默认类别(通常是“Misc”)。此参数的效果与将CategoryAttribute
附加到属性相同。description (Description)
:一个帮助字符串,显示在属性网格底部的描述区域。如果为 null,则不显示描述。此参数的效果与将DescriptionAttribute
附加到属性相同。defaultValue (DefaultValue)
:属性的默认值。如果属性的当前值不等于默认值,则属性会以粗体显示,以指示它已被更改。此属性的效果与将DefaultValueAttribute
附加到属性相同。editor (EditorTypeName)
:指示用于编辑属性的类型(派生自UITypeEditor
)。这可用于为没有显式关联编辑器类型的类型提供自定义编辑器,或覆盖与类型关联的编辑器。在构造函数中,此参数可以指定为Type
对象或完全限定的类型名称。它与将EditorAttribute
附加到属性的效果相同。typeConverter (ConverterTypeName)
:指示用于将属性值转换为字符串或从字符串转换属性值的类型转换器(派生自TypeConverter
)。在构造函数中,它可以指定为Type
对象或完全限定的类型名称。此参数与将TypeConverterAttribute
附加到属性的效果相同。
此外,还提供了以下属性:
public Attribute[] Attributes { get; set; }
使用 Attributes
属性,您可以包含 PropertySpec
不直接支持的任何附加属性,例如 ReadOnlyAttribute
。
Using the Code
使用 PropertyBag
是一个简单的过程:
- 实例化
PropertyBag
类的对象。 - 将
PropertySpec
对象添加到PropertyBag.Properties
集合中,每个要显示在网格中的属性都添加一个。 - 为属性包的
GetValue
和SetValue
事件添加事件处理程序,并设置它们以访问您用于存储属性值的任何数据源。 - 将属性包选择到
PropertyGrid
控件中。
如果您使用的是 PropertyTable
,则在步骤 3 中不向包添加事件处理程序,而是使用表的索引器指定属性的适当初始值。
一个基本示例
PropertyBag bag = new PropertyBag(); bag.GetValue += new PropertySpecEventHandler(this.bag_GetValue); bag.SetValue += new PropertySpecEventHandler(this.bag_SetValue); bag.Properties.Add(new PropertySpec("Some Number", typeof(int))); // ... add other properties ... propertyGrid.SelectedObject = bag;
本文随附的项目展示了这些类的更强大的演示。
关注点
在演示应用程序中,如果您从列表中选择多个对象,可以看到一个无需额外工作即可实现的有趣的好处。多项选择完全由属性网格处理,因此即使对象是具有完全自定义属性的属性包,网格仍然按预期工作——只显示所有对象共享的属性和属性值。
未来计划
此类最终将被我正在编写的一个更大的用户界面实用程序库吸收(因此源代码中的命名空间名称有些笨拙)。
我计划在未来添加的一个增强功能是,使为属性指定字符串值列表更容易,而不是为这种列表要求枚举类型。例如,前面提到的项目属性对话框有一个“Use of MFC”属性,其值为“Use Standard Windows Libraries”、“Use MFC in a Static Library”和“Use MFC in a Shared DLL”。目前,实现此目的的方法是为属性编写一个 TypeConverter
,并重写三个 GetStandardValues*
方法来提供允许值的列表。理想情况下,我想将此内置到属性包中,以便用户可以仅提供字符串列表作为属性,而无需显式编写任何外部类型转换器。一种特别有趣的方法可能是使用 System.Reflection.Emit
功能在运行时在内存中生成合适的类型转换器。
其次,我计划也使可用属性在运行时动态化,因为属性包在构建网格的属性列表时会触发事件。收听该事件的任何对象都可以在那时提供其他属性。这对于动态数据源来说是很有意义的,因为不断添加和删除属性或在选定的对象更改时创建新包可能不方便。
当然,对于这些类的其他可能的增强和扩展,我乐于接受建议。
版本历史
- 2002/12/20 (v1.0):初始发布。