属性网格 - 动态列表组合框、验证等






4.57/5 (18投票s)
一个属性网格实现,演示了如何使用、最佳实践、验证等。
引言
在一个项目中工作时,我被要求在一个屏幕上包含一个PropertyGrid
控件,以允许设备配置和进行验证。由于我之前没有使用过PropertyGrid
控件,我开始寻找示例,但发现的很少,尤其是在验证方面,更是找不到。
为了下次需要使用PropertyGrid
时能够记住,也希望这能为初学者提供一些关于使用PropertyGrid
多个方面的见解。
主题
本文将提供以下内容的信息和代码示例
- 显示 - 基本操作、排序、创建视图、
ComboBox
和TypeConverter
- 数据 - 如何在运行时获取和添加数据到列表,以及各种数组访问器方法
- 验证 - 如何创建验证规则,以及使用通用的
PropertyGrid
进行验证
项目代码
本文顶部列出的代码包含将要讨论的每个主题的示例,以及一些用于反射、解析和获取类类型的实用程序。这些实用程序不会在这里讨论,但它们是动态获取运行时数据的重要方面,应进行审查以求全面理解。
代码分为应用程序、控件、模型和实用程序。应用程序中的视图不一定遵循最佳编码实践进行布局,但这样做是为了举例说明上述每个主题。
代码使用VS.NET 2005编写,不需要任何其他外部库。
PropertyGrid控件
显示
- 基本操作 -
PropertyGrid
控件有一个名为SelectedObject
的属性,您可以通过设置它来在PropertyGrid
中显示信息。PropertyGrid
会反射您的对象并显示您类的属性。名称的显示不一定是最符合用户体验的,因为程序员通常使用驼峰命名约定,并且没有包含描述用户应填写的内容。Microsoft提供了一些属性,您可以添加到属性的顶部,以提供用户友好的体验。
Category
会将具有相同任意名称(例如“Person
”)的项目分组在一起,而DisplayName
和Description
分别提供要显示和描述属性的友好文本。[Category( "Person" )] [DisplayName( "Last Name" )] [Description( "Enter the last name for this person" )] public string PersonLastName { get {…} set {…} }
- 只读 -
PropertyGrid
上的项目可以通过仅提供get
访问器,或使用集合的转换器方法(稍后将介绍)来设为只读。 - 排序属性 - 默认情况下,属性将按类别字母顺序排序,然后按属性名称进行子排序。这可以是实际的属性名称,也可以是
DisplayName
属性中使用的文本。这可以通过在VS.NET中使用PropertySort
属性来更改,但对于大多数开发人员来说,此属性无效,因为需要对属性进行特定排序,例如:姓名、地址、城市、州和邮政编码。以任何其他顺序排列这些属性都会提供糟糕的用户体验。寻找一种简单的方法来更改属性的默认排序相当令人沮丧。如果有一个属性允许您按特定顺序排列属性就好了。我发现了一种相对容易实现排序的方法,尽管有点丑陋。制表符“\t”可用于对项目进行排序,并且您可以获得该字符不会在屏幕上显示的优点。
ViewPerson2
类中有一个示例。唯一需要注意的是,它是反向排序的。您添加的制表符越多,属性显示的可能性就越大。为了创建一个更优雅的解决方案,我创建了一个派生自DisplayNameAttribute
的新类,名为OrderedDisplayNameAttribute
。该属性会自动为您添加制表符。这将有助于您自己的类别,但尝试在预先存在的Microsoft类别中使用它效果不太好。 - 视图 - 如上所述,属性被添加到您的类属性中。直接将这些属性添加到您的业务对象不一定是最佳方法。强烈建议在
PropertyGrid
中使用视图。虽然创建和维护视图的工作量稍大,但它们提供了以不同方式显示数据的灵活性。视图还为您提供了一种为同一业务对象提供不同DisplayName
和Description
的方式。ViewPerson
和ViewPerson2
类就是这样的例子。ViewPerson
类将使用PersonConverter
类型的TypeConverter
显示,而ViewPerson2
类是ListNonExpandableConverter
类型的。视图允许以两种完全不同的方式显示完全相同的业务对象。虽然稍后将介绍转换器的功能,但PersonConverter
将在PropertyGrid
中显示Person
的全部内容,并允许您向下钻取该人的子代和孙代,而ListNonExpandableConverter
仅显示该人的姓名。
ComboBox
一般来说,ComboBox
是提供项目列表给用户以防止数据录入错误的好方法。为PropertyGrid
创建ComboBox
并不像将控件拖放到窗体上并添加数据那样直接。PropertyGrid
不知道某个属性应该从预先存在的数据列表中选择,也不知道从哪里获取数据。我将在本文后面描述我的动态获取数据实现策略。
现在是时候补充一点,对于那些正在查看代码的人来说,我实际上使用的是ListBox
而不是ComboBox
。因为我正在创建一个作用和外观都像ComboBox
的控件,所以我称之为ComboBox
。PropertyGrid
实际上代表我显示了一个倒三角形,表示一个ComboBox
。如果我将ComboBox
作为我的控件,在我的GridComboBox
内部,当您单击下拉箭头时,会显示另一个ComboBox
,在这种情况下,您需要再次单击下拉箭头才能选择数据。这种行为并不理想。
我创建了一个名为GridComboBox
的基类ComboBox
控件,您可以从中派生以创建自己的ComboBox
。项目中包含两个此类控件:EnumGridComboBox
和ListGridComboBox
。
GridComboBox
控件派生自UITypeEditor
。有两个方法被重写:EditValue
和GetEditStyle
。EditValue
负责填充ComboBox
、将ComboBox
附加到PropertyGrid
并检索从ComboBox
中选择的新值。GetEditStyle
表示您希望如何显示控件。对于ComboBox
,使用了UITypeEditorSyle.DropDown
。您也可以将其设置为Modal
,如果您使用窗体来收集信息。我没有使用Modal
设置。
请记住,如果您将数据集合绑定到ComboBox
,请重写ToString()
方法,以便显示有用的文本而不是完全限定的业务对象名称。
需要注意的是,在正常情况下,EnumGridComboBox
类不是必需的。当一个业务对象被绑定到一个包含enum
类型属性的PropertyGrid
时,会自动为您创建一个默认的ComboBox
,并自动显示所有值。我添加了EnumGridComboBox
实现是为了展示另一个ComboBox
,并且只有在您想在选择枚举值后执行某些操作时才真正需要它。ViewCar
类有两个枚举:Engine
和BodyStyle
,它们展示了默认行为;而ViewWheel
类包含使用EnumGridComboBox
实现的SupplyParts
。
为了让自定义ComboBox
在PropertyGrid
中显示,您必须告诉PropertyGrid
在控件中显示数据。这可以通过在属性顶部添加另一个属性Editor
来完成。您需要为每个希望在ComboBox
中显示的属性执行此操作。
[Editor( typeof( EnumGridComboBox ), typeof( UITypeEditor ) )]
[EnumList( typeof(SupplyStore.SupplyParts))]
[Category( "…" ), Description( "…" ), DisplayName( "…" )]
public SupplyStore.SupplyParts SupplyParts
{
get { return _supplyPartsEnum; }
set { _supplyPartsEnum = value; }
}
Converters
这些类可帮助您转换嵌入的class
类型属性,以便以特定方式显示。例如,ViewCar
类包含一个Wheel
类型的属性,这就是一个例子。这是对项目中包含的转换器的简要描述。
- ListConverter - 接收一个集合并创建一个对象列表。默认情况下,它会在列表中显示业务对象的完全限定名称。这将与以下之一的转换器结合使用,该转换器放置在要显示的业务对象上。
[TypeConverter( typeof( ListConverter<ViewPersonCollection> ) )] public List<ViewPersonCollection> Children { … }
- ListExpandableConverter - 将列表展开为对象类型。
[TypeConverter( typeof( ListExpandableConverter ) )] public class ViewPersonCollection : IDisplay { … }
- ListNonExpandableConverter - 在属性侧显示名称。
[TypeConverter( typeof( ListNonExpandableConverter ) )] public class ViewPerson2 : IDisplay { … }
- PersonConverter - 用于显示或过滤属性的另一种方法。
[TypeConverter( typeof( PersonConverter ) )] public class ViewPerson : IDisplay { … }
ListPropertyDescriptor
包含在转换器中,因为它与ListConverter
结合使用来构建要显示的对象列表。这是一个可以重用的通用类,对于它如何与PropertyGrid
结合使用,实际上不需要解释。要了解如何使用功能性的PropertyDescriptor
,请参阅我的第一篇文章:动态属性 - 运行时创建数据库。
组合框数据
ComboBox
的数据由您,开发者提供。本文的这一部分将描述如何获取数据放入ComboBox
。
根据您的需求和要求,您有不同的选择。
- 枚举 - 作为开发人员,您无需执行任何操作。
ComboBox
将自动填充枚举列表中的值。您可以在ViewCar.cs文件中找到BodyColor
和BodyStyle
的示例。如果枚举文本是可以接受的,那么无需额外工作。大多数情况下,枚举是使用驼峰命名、下划线或全大写字母组合的单词。这种显示对于最终用户来说通常是不可接受的。使用以下项目之一会是更好的方法。
- 静态数据 - 静态数据可以通过两种方式处理:自动发现或硬编码值。值的发现将在下一个项目符号中讨论,但应通过返回列表的方法来完成,以防止列表被修改。
至于硬编码值,最简单的方法是派生自己的
ComboBox
控件自GridComboBox
并重写RetrieveDataList
将base.DataList
设置为预定义的项目列表。protected override void RetrieveDataList( ITypeDescriptorContext context ) { List<string> list = new List<string>(); list.Add( "Tom" ); list.Add( "Jerry" ); base.DataList = list; }
对用于列表的数据类型没有限制。如果您使用的是值类型以外的数据类型,请确保提供一种在
ComboBox
中显示美观文本的方式。默认将是业务对象的完全限定名称。要实现这一点,您可以在类或视图中添加一个ToString()
。另一种方法是创建一个接口
IDisplay
,就像我为ViewPerson
所做的那样,并在GridComboBox
中编写一些额外的代码来包装业务对象。我没有提供此功能,但以下是一个示例,说明它会是什么样子。将业务对象添加到列表并返回业务对象在选择时需要进行更新。public class Wrapper { IDisplay _dataObject; public Wrapper( IDisplay dataObject ) { _dataObject = dataObject; } public override string ToString() { return ( _dataObject.Text ); } }
- 动态数据 - 在运行时查找数据以填充给定属性的
ComboBox
并允许添加新数据是一个有趣的挑战。本文的这一部分使用了.NET开发的先进技术:创建属性和反射。反射和属性的创建不在此文中讨论,因为它们是独立的主题。我在Utilities项目中提供了一个名为
Reflect
的类。该类包含许多static
方法,用于使用反射来get
/set
字段和属性、调用方法等。这些方法在实现动态数据时被广泛使用。开始实现动态数据的地方是在创建一个派生自
Attribute
的基类。这将允许基类ComboBox
GridComboBox
成为泛型,只需处理一种类型。[AttributeUsage( AttributeTargets.Field | AttributeTargets.Property )] public abstract class ListAttribute : Attribute { }
完成此操作后,就可以开始创建您的特定属性了。我在项目中创建了两个属性:
DataListAttribute
和EnumListAttribute
。我将逐步介绍DataListAttribute
的创建、GridComboBox
和ListGridComboBox
的基础知识,以及如何将它们连接起来。EnumListAttribute
是DataListAttribute
的简化形式,我将留给您自行查阅。DataListAttribute - 添加了几个构造函数,允许用户根据自己的需求定制属性。前三个构造函数允许获取类实例的一部分数据列表。
public DataListAttribute( string path ) public DataListAttribute( string path, bool allowNew ) public DataListAttribute( string path, bool allowNew, string eventHandler ) [DataList( "GetPeopleList", true, "OnAddedEventHandler" )] public ViewPersonCollection ChoosePerson { … }
后三个用于检索属于
static
类一部分的数据列表。public DataListAttribute( string dllName, string className, string path ) public DataListAttribute( string dllName, string className, string path, bool allowNew ) public DataListAttribute( string dllName, string className, string path, bool allowNew, string eventHandler ) [DataList( "CarApplication", "CarApplication.SupplyStore", "Instance.Wheels", false, "OnAddedEventHandler" )] public Wheel Wheel { … }
对于要访问
static
类的构造函数,需要提供DLL的名称和完全限定的类名,而path
、allowNew
和eventHandler
对两组构造函数都是通用的。allowNew
标志表示是否可以创建新业务对象并将其存储在属性中。这将在列表中显示一个额外的条目<Add New...>
。当用户选择此选项时,它将创建一个对象类型的新实例。如果您希望业务对象下次作为选项显示在ComboBox
中,您必须提供事件处理程序的名称。当调用事件处理程序时,您必须手动将其添加到列表中。private void OnAddedEventHandler( object sender, ObjectCreatedEventArgs arg ) { if ( arg != null ) { ViewPersonCollection collection = arg.DataValue as ViewPersonCollection; if ( collection != null ) { collection.Name = "New Person #" + new Random().Next( 1, 100 ); this.ChooseParent.Children.Add( collection ); this._list.Add( collection ); } } }
path定义了如何从一个业务对象导航到下一个业务对象以获取要显示的数据列表。如果您在代码中编写路径并将其复制粘贴到字符串中,它将非常接近完整。路径可以包括函数、字段、属性和带有下标的数组,下标可以是数字、字符串或枚举。以下是本项目中使用的。
"Instance.Wheels" "Instance.Supplies[Wheels]" "Instance.SuppliesArray[1]" "Instance.SuppliesArray [CarApplication.SupplyStore+SupplyParts.Wheels,CarApplication]" "GetPeopleList" "SupplyStore"
GridComboBox -
GridComboBox
的编写是为了派生以提供获取数据的功能。该类派生自UITypeEditor
,这是在PropertyGrid
中使用所必需的。
该类有两个方法需要由派生类实现。这些方法定义了如何检索数据列表以及在从ComboBox
中选择项目后做什么。我们将在下一节中介绍。protected abstract object GetDataObjectSelected( ITypeDescriptorContext context ); protected abstract void RetrieveDataList( ITypeDescriptorContext context );
文件中唯一其他有趣的сode是
PopulateListBox
和EditValue
,其余的都处理ComboBox
的行为。EditValue
是从基类UITypeEditor
重写的。此函数是ComboBox
功能的核心。当用户单击ComboBox
的箭头以获取项目列表时,会触发一个事件,该事件会调用此方法。然后,此方法调用PopulateListBox
方法,将用于PropertyGrid
的内部ListBox
附加,并等待用户执行操作(例如,单击项目或按ESC键)。通过调用派生的GetDataObjectSelected
方法继续处理,该方法返回数据值或对象实例。PopulateListBox
方法将调用派生的RetrieveDataList
方法来查找数据并填充ComboBox
。如果之前选择了某个值,该值将在列表中自动选中。ListGridComboBox -
RetrieveDataList
在此处定义。首先,从我们当前正在处理的属性中获取属性列表。我们正在寻找的属性是DataListAttribute
,它包含数据的路径。找到属性后,路径会被分解成各个部分。然后,通过确定数据是导航当前业务对象找到的,还是存储在静态类中来继续处理。两种路径的处理方式基本相同;将路径的每个段分解成不同的部分。这会考虑到可能使用的数组、列表和字典。一旦我们检索到当前段的组件,信息就会被传递给反射类
Reflect
,它将检索实际的值/对象。此过程将为路径的每个段继续进行,直到没有更多段,此时我们应该已经获得了所需的数据列表。如果还有更多段,则从前一段获取的属性值将用作此段的起点。
返回值,并保存对列表的引用以备将来使用。由于使用了反射,因此保存了对列表的引用。另一个原因是数据的存储位置通常是相同的。如果您发现情况并非如此,请不要保存对数据列表的引用。
另一个重写的方法
GetDataObjectSelected
也在这里实现。它负责从列表中检索值/对象并返回它。此实现会检查是否选择了“<Add New...>
”,然后创建一个新的数据对象实例。如果对象已创建,并且在DataListAttribute
中设置了该选项,则会发送通知。对象的创建和通知的发送再次通过调用Reflect
类中的方法来处理。事件处理程序的设计目的是执行设置对象默认值等操作,例如在
ToString()
方法中使用的名字或姓氏。事件处理程序还需要将对象添加到列表中,以便下次可以选择它。
验证
显示
使用PropertyGrid
时,数据的验证提出了另一个挑战。实际上没有真正的机制来执行此操作。我创建了一个适合我通常需求的实现,即在属性值更改时进行验证。如果您需要为每次按键进行验证,例如在掩码的情况下,您需要提供自己的实现。
当前实现有一个缺点,即如果输入的数据不正确,并且您在看到警告消息框后移开了字段,您仍然可以这样做。我是故意这样做的。试图阻止用户移开字段会带来太多含义,以及如何在某些情况下允许它。本文旨在帮助您开始提供验证。数据无效后该做什么的细节,留给您,开发者。
在CustomControls
项目中有一个名为Rule的文件夹,其中包含一个基类和两个实现。基类再次派生自Attribute
,并提供了一个错误消息字段和一个抽象的IsValid()
方法。其他类是
PatternRuleAttibute
- 此规则基于一个string
,该字符串是正则表达式。数据使用Regex.IsMatch
进行验证。您可以在各种视图类中看到几个示例。由于反斜杠在正则表达式中具有特殊含义,请确保在string
前面添加“@”符号。[Category( "…" ), DescriptionAttribute( "…" ), DisplayName( "…" )] [PatternRule( @"^(\d{5}-\d{4})|(\d{5})$" )] public string Zip { … }
LengthRuleAttribute
- 此规则将验证输入的string
的长度是否在两个值(最小值和最大值)之间。[Category( "…" ), DescriptionAttribute( "…" ), DisplayName( "…" )] [LengthRule( 4, 20 )] public string City { … }
添加其他验证规则可以很容易做到,只需从基类RuleBaseAttribute
派生,添加构造函数和字段,并实现IsValid()
方法。一旦将新属性添加到属性的顶部,规则就可以运行了。为了让这些规则神奇地工作,您必须使用解决方案中提供的PropertyGridControl
,或将其中的代码复制到您的实现中。PropertyGridControl
只是派生自PropertyGrid
,并连接了PropertyValueChanged
事件。PropertyValueChanged
事件处理程序的基本功能是
- 获取类类型
- 获取属性名称
- 获取属性信息
- 获取属性的自定义属性
- 对于每个属性
如果它是RuleBaseAttribute
,则使用正在更改的数据调用IsValid()
方法。如果数据无效,则显示带有错误的警告框。
实用程序
解决方案中包含一个名为Utilities的项目。该项目包含一些类,用于帮助获取ComboBox
数据和数据验证的各个方面。本文将对它们进行探讨。以下是它们的通用描述。
ClassType
- 用于使用各种输入返回Type
对象PathParser
- 用于分隔字段/属性/方法的完全限定路径Reflect
- 一个static
类,包含用于反射到类的静态实例或实例的字段/属性/方法的方法