介绍 在.NET框架的最初版本中,微软最终提供了他们梦寐以求的一个公共定制控件,这给我留下了深刻的印象——在这个例子中,是可视化的基础类属性网格,System.Windows.Forms.PropertyGrid. 。然而,在使用它时,我意识到尽管这个控件非常灵活和强大,但需要进行大量工作来定制。下面,我将提供一组提高属性网格可用性的类。 背景 .NET框架提供的属性网格通过反射操作。通过使用SelectedObject或SelectedObjects属性,您可以选择一个或多个对象到控件。然后,它查询对象的所有属性,根据它们的CategoryAttribute将它们组织到组中,并使用各种UITypeEditor和TypeConverter类允许用户将属性编辑为字符串。 这样做的问题是它在开箱即用时不是很灵活。要使用网格,您必须编写一个“代理类”来公开您想要在网格中显示的属性,实例化这个类的一个对象,然后将其选择到网格中。 然而,在很多时候,创建这样的类可能是不可取的。每个代理都必须特定于您的数据,因为运行时仅仅使用反射来询问类的属性是什么。除非所有数据片段都共享相同的属性,否则您将无法轻松地编写可跨多个数据片段重用的代理类。 根据系统的设计和数据的组织,编写代理类可能不太方便。假设您有一个包含100个属性的数据库,这些属性存储为键-值对。要创建一个类来包装这个数据库,您必须为这100个属性中的每一个手工编写get和set调用。即使您编写了自动工具来为您生成代码,您的可执行文件中仍然存在代码膨胀的问题。 最后,创建代理类并不能很好地“美化”网格的内容。标准的行为是从代码中显示属性的实际名称。这对于像表单设计器这样的组件很有意义,因为开发人员需要能够快速、轻松地在网格中的属性和代码。然而,如果网格被用作终端用户界面的一部分,他们应该使用比“UniqueCustID”稍微少一些迟钝的东西。 自定义类型描述符 如果你看一下Visual Studio中的项目设置对话框。你会注意到这个网格包含了一些属性,它们的名字看起来更漂亮,比如“Use of MFC”,这会打开一个下拉列表,其中包含一些值,比如“在一个共享DLL中使用MFC”。显然,这里不仅仅是枚举类型的简单属性。 答案在System.ComponentModel中。ICustomTypeDescriptor接口只;这个接口允许类提供关于自身的自定义类型信息。简而言之,这允许您修改属性网格在查询对象此信息时看到的属性和事件。 ICustomTypeDescriptor提供了许多类似于标准TypeDescriptor类中的函数的函数。实际上,当您实现ICustomTypeDescriptor时,您希望能够利用框架已经提供的许多标准行为。对于大多数函数,您可以简单地将控制权传递给标准类型描述符,如下所示:复制Code
AttributeCollection ICustomTypeDescriptor.GetAttributes() { return TypeDescriptor.GetAttributes(this, true); }
大多数这些函数的第一个参数是被请求信息的对象。第二个参数告诉类型描述符不要调用自定义类型描述符来获取请求的信息。由于我们是自定义类型描述符,省略此参数将导致无限递归,因为自定义类型描述符将被一次又一次调用。 但是,请注意,我最初说过ICustomTypeDescriptor允许类提供关于自身的信息。这似乎意味着必须存在一个类,我们可以为其提供类型信息,这是真的。必须存在一些可以实现ICustomTypeDescriptor接口的类,以向网格提供属性列表。但是对象的接口不需要看起来像它将要公开的属性,这就是灵活性所在。 解决方案 为此,我创建了PropertyBag类。它实现了ICustomTypeDescriptor,但以最通用的方式。PropertyBag并不依赖于类本身中硬编码的属性,而是管理一个对象集合,这些对象描述了应该在网格中显示的属性。它通过覆盖ICustomTypeDescriptor.GetPro来实现这一点perties(),生成它自己的自定义属性描述符,并返回这个集合。由于控件从来没有传递给标准类型描述符,网格甚至不知道属于PropertyBag类本身的“实际”属性。 这样做的好处是可以随意添加和删除属性,而典型的代理类将在编译时固定。 然而,类型描述符只提供关于属性存在和类型的信息,因为它们实现的方法可以应用于该类型的任何对象。这意味着需要某种方法来检索和存储属性的值。我已经实现了下面两种方法: 方法1:引发事件 基类PropertyBag实现的方法在查询或更改属性值时引发事件。GetValue事件发生在属性网格需要知道某个属性的值时,而SetValue事件发生在用户交互地更改了网格中属性的值时。 这种方法在处理存储在文件和数据库中的属性时非常有用。当事件发生时,您可以使用属性名在数据源中建立索引,对每个属性使用相同的简单查找代码。 方法2:将值存储在表中 为了一种更简单的方法(在某些情况下可能更合适),我从PropertyBag派生了一个名为PropertyTable. 这个类提供了PropertyBag的所有泛型功能,但也包含了一个hashtable,以属性名为索引,用于存储属性值。当请求一个值时,会在表中查找该属性,当用户更新它时,散列表中的值也会相应地更新。 滚你自己的 由于PropertyBag提供了与上面讨论的事件对应的虚拟函数OnGetValue和OnSetValue,所以您还可以通过覆盖这两个函数派生一个类并提供您自己的方法。 包括什么 PropertyBag类 PropertyBag类非常基本,只公开两个属性、两个事件和两个方法。隐藏,复制Code
public string DefaultProperty { get; set; }
DefaultProperty属性指定了属性包中默认属性的名称。当包被选中到PropertyGrid中时,这是默认选中的属性。隐藏,复制Code
public PropertySpecCollection Properties { get; }
属性集合管理包中的各种属性。像许多其他的。net框架集合一样,它实现了IList,所以像Add、AddRange和Remove这样的函数可以用来操作包中的属性。隐藏,复制Code
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的所有操作,以及一个索引器属性:Hide复制Code
public object this[string key] { get; set; }
这个索引器用于获取和设置存储在表内部哈希表中的属性值。属性按名称建立索引。 PropertySpec类 PropertySpec提供了16个构造函数重载——太多了,无法在这里详细描述。重载是下列参数的各种组合,列出了与之对应的属性: name (name):属性的名称。 类型:属性的类型。在构造函数中,它可以是类型对象(比如typeof()操作符返回的类型),也可以是表示完全限定类型名的字符串(即类型对象)。“System.Boolean”)。 category (category):表示属性所属类别的字符串。如果该值为null,则使用默认类别(通常为“Misc”)。此参数的效果与将CategoryAttribute附加到属性的效果相同。 描述(description):在属性网格底部的描述区域中显示的帮助字符串。如果该值为null,则不显示任何描述。此参数与将DescriptionAttribute附加到属性具有相同的效果。 defaultValue:属性的默认值。 如果属性的当前值不等于默认值,该属性将以粗体显示,以表明它已被更改。此属性与将DefaultValueAttribute附加到属性具有相同的效果。 指示用于编辑属性的类型(派生自UITypeEditor)。这可用于为没有显式关联的类型提供自定义编辑器,或覆盖编辑器ass与类型有关。在构造函数中,可以将此参数指定为类型对象或完全限定类型名。它与将EditorAttribute附加到属性具有相同的效果。 表示类型转换器(派生自typeConverter),用于将属性值与字符串进行转换。在构造函数中,可以将其指定为类型对象或完全限定类型名。此参数的效果与将TypeConverterAttribute附加到属性的效果相同。 此外,还提供了以下属性:复制Code
public Attribute[] Attributes { get; set; }
使用Attributes属性,您可以包括PropertySpec-ReadOnlyAttribute不直接支持的任何其他属性。 使用的代码 使用PropertyBag是一个简单的过程: 实例化PropertyBag类的一个对象。 将PropertySpec对象添加到PropertyBag中。要在网格中显示的每个属性的属性集合。 为属性包的GetValue和SetValue事件添加事件处理程序,并设置它们以访问用于存储属性值的任何数据源。 在PropertyGrid控件中选择属性包。 如果您使用的是PropertyTable,那么您不应该在步骤3中向包添加事件处理程序,而应该使用表的索引器为属性指定适当的初始值。 A 基本示例:隐藏,复制Code
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;
本文附带的项目展示了这些类的更健壮的演示。 的兴趣点 如果您从列表中选择多个对象,那么演示应用程序中显示了一个非常有趣的优点,它完全不需要额外的工作。多重选择完全由属性网格处理,因此即使对象是具有完全自定义属性的属性包,网格仍然按照预期工作—只有在所有对象之间共享的属性和属性值被显示。 未来的计划 这个类最终将被吸收到我正在编写的更大的用户界面实用程序库中(因此在源代码中使用了笨拙的名称空间名称)。 我计划在未来添加的一个增强是使为属性指定字符串值列表变得更容易,而不是要求这样的list. 例如,前面提到的项目属性对话框有一个“使用MFC”属性,其值为“使用标准Windows库”、“在静态库中使用MFC”和“在共享DLL中使用MFC”。目前,你可以这样做的方法是为属性写一个TypeConverter并覆盖三个GetStandardValues*方法来提供一个允许的值列表。最理想的情况是,我希望将这个内置到属性包中,这样用户就可以简单地为属性提供字符串列表,而不需要显式地编写任何外部类型转换器。实现这一点的一种特别有趣的方法可能是使用System.Reflection。发出在运行时在内存中生成适当类型转换器的功能。 其次,我计划让可用的属性在运行时也是动态的,也就是说属性包在为网格构建属性列表时将触发一个事件。任何侦听事件的对象都可以在那个时候提供额外的属性。这对于动态数据源来说很有意义,因为在动态数据源中,不断地从包中添加和删除属性或者在选中对象发生更改时创建新包可能不方便。 当然,对于这些类的其他增强和扩展,我愿意听取建议。 版本历史 12/20/2002 (v1.0):最初的版本。 本文转载于:http://www.diyabc.com/frontweb/news177.html