65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (18投票s)

2008 年 1 月 27 日

CPOL

17分钟阅读

viewsIcon

171705

downloadIcon

11277

一个属性网格实现,演示了如何使用、最佳实践、验证等。

引言

在一个项目中工作时,我被要求在一个屏幕上包含一个PropertyGrid控件,以允许设备配置和进行验证。由于我之前没有使用过PropertyGrid控件,我开始寻找示例,但发现的很少,尤其是在验证方面,更是找不到。

为了下次需要使用PropertyGrid时能够记住,也希望这能为初学者提供一些关于使用PropertyGrid多个方面的见解。

主题

本文将提供以下内容的信息和代码示例

  1. 显示 - 基本操作、排序、创建视图、ComboBoxTypeConverter
  2. 数据 - 如何在运行时获取和添加数据到列表,以及各种数组访问器方法
  3. 验证 - 如何创建验证规则,以及使用通用的PropertyGrid进行验证

项目代码

本文顶部列出的代码包含将要讨论的每个主题的示例,以及一些用于反射、解析和获取类类型的实用程序。这些实用程序不会在这里讨论,但它们是动态获取运行时数据的重要方面,应进行审查以求全面理解。

代码分为应用程序、控件、模型和实用程序。应用程序中的视图不一定遵循最佳编码实践进行布局,但这样做是为了举例说明上述每个主题。

代码使用VS.NET 2005编写,不需要任何其他外部库。

PropertyGrid控件

显示

  1. 基本操作 - PropertyGrid控件有一个名为SelectedObject的属性,您可以通过设置它来在PropertyGrid中显示信息。PropertyGrid会反射您的对象并显示您类的属性。名称的显示不一定是最符合用户体验的,因为程序员通常使用驼峰命名约定,并且没有包含描述用户应填写的内容。

    Microsoft提供了一些属性,您可以添加到属性的顶部,以提供用户友好的体验。Category会将具有相同任意名称(例如“Person”)的项目分组在一起,而DisplayNameDescription分别提供要显示和描述属性的友好文本。

    [Category( "Person" )] 
    [DisplayName( "Last Name" )] 
    [Description( "Enter the last name for this person" )] 
    public string PersonLastName 
    { 
        get {…} 
        set {…} 
    }
  2. 只读 - PropertyGrid上的项目可以通过仅提供get访问器,或使用集合的转换器方法(稍后将介绍)来设为只读。
  3. 排序属性 - 默认情况下,属性将按类别字母顺序排序,然后按属性名称进行子排序。这可以是实际的属性名称,也可以是DisplayName属性中使用的文本。这可以通过在VS.NET中使用PropertySort属性来更改,但对于大多数开发人员来说,此属性无效,因为需要对属性进行特定排序,例如:姓名、地址、城市、州和邮政编码。以任何其他顺序排列这些属性都会提供糟糕的用户体验。

    寻找一种简单的方法来更改属性的默认排序相当令人沮丧。如果有一个属性允许您按特定顺序排列属性就好了。我发现了一种相对容易实现排序的方法,尽管有点丑陋。制表符“\t”可用于对项目进行排序,并且您可以获得该字符不会在屏幕上显示的优点。ViewPerson2类中有一个示例。唯一需要注意的是,它是反向排序的。您添加的制表符越多,属性显示的可能性就越大。为了创建一个更优雅的解决方案,我创建了一个派生自DisplayNameAttribute的新类,名为OrderedDisplayNameAttribute。该属性会自动为您添加制表符。这将有助于您自己的类别,但尝试在预先存在的Microsoft类别中使用它效果不太好。

  4. 视图 - 如上所述,属性被添加到您的类属性中。直接将这些属性添加到您的业务对象不一定是最佳方法。强烈建议在PropertyGrid中使用视图。虽然创建和维护视图的工作量稍大,但它们提供了以不同方式显示数据的灵活性。视图还为您提供了一种为同一业务对象提供不同DisplayNameDescription的方式。

    ViewPersonViewPerson2类就是这样的例子。ViewPerson类将使用PersonConverter类型的TypeConverter显示,而ViewPerson2类是ListNonExpandableConverter类型的。视图允许以两种完全不同的方式显示完全相同的业务对象。虽然稍后将介绍转换器的功能,但PersonConverter将在PropertyGrid中显示Person的全部内容,并允许您向下钻取该人的子代和孙代,而ListNonExpandableConverter仅显示该人的姓名。

ComboBox

一般来说,ComboBox是提供项目列表给用户以防止数据录入错误的好方法。为PropertyGrid创建ComboBox并不像将控件拖放到窗体上并添加数据那样直接。PropertyGrid不知道某个属性应该从预先存在的数据列表中选择,也不知道从哪里获取数据。我将在本文后面描述我的动态获取数据实现策略。

现在是时候补充一点,对于那些正在查看代码的人来说,我实际上使用的是ListBox而不是ComboBox。因为我正在创建一个作用和外观都像ComboBox的控件,所以我称之为ComboBoxPropertyGrid实际上代表我显示了一个倒三角形,表示一个ComboBox。如果我将ComboBox作为我的控件,在我的GridComboBox内部,当您单击下拉箭头时,会显示另一个ComboBox,在这种情况下,您需要再次单击下拉箭头才能选择数据。这种行为并不理想。

我创建了一个名为GridComboBox的基类ComboBox控件,您可以从中派生以创建自己的ComboBox。项目中包含两个此类控件:EnumGridComboBoxListGridComboBox

GridComboBox控件派生自UITypeEditor。有两个方法被重写:EditValueGetEditStyleEditValue负责填充ComboBox、将ComboBox附加到PropertyGrid并检索从ComboBox中选择的新值。GetEditStyle表示您希望如何显示控件。对于ComboBox,使用了UITypeEditorSyle.DropDown。您也可以将其设置为Modal,如果您使用窗体来收集信息。我没有使用Modal设置。

请记住,如果您将数据集合绑定到ComboBox,请重写ToString()方法,以便显示有用的文本而不是完全限定的业务对象名称。

需要注意的是,在正常情况下,EnumGridComboBox类不是必需的。当一个业务对象被绑定到一个包含enum类型属性的PropertyGrid时,会自动为您创建一个默认的ComboBox,并自动显示所有值。我添加了EnumGridComboBox实现是为了展示另一个ComboBox,并且只有在您想在选择枚举值后执行某些操作时才真正需要它。ViewCar类有两个枚举:EngineBodyStyle,它们展示了默认行为;而ViewWheel类包含使用EnumGridComboBox实现的SupplyParts

为了让自定义ComboBoxPropertyGrid中显示,您必须告诉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类型的属性,这就是一个例子。这是对项目中包含的转换器的简要描述。

  1. ListConverter - 接收一个集合并创建一个对象列表。默认情况下,它会在列表中显示业务对象的完全限定名称。这将与以下之一的转换器结合使用,该转换器放置在要显示的业务对象上。
    [TypeConverter( typeof( ListConverter<ViewPersonCollection> ) )]
    public List<ViewPersonCollection> Children
    {
        …
    }
  2. ListExpandableConverter - 将列表展开为对象类型。
    [TypeConverter( typeof( ListExpandableConverter ) )]
    public class ViewPersonCollection : IDisplay
    {
        …
    }
  3. ListNonExpandableConverter - 在属性侧显示名称。
    [TypeConverter( typeof( ListNonExpandableConverter ) )]
    public class ViewPerson2 : IDisplay
    {
        …
    }
  4. PersonConverter - 用于显示或过滤属性的另一种方法。
    [TypeConverter( typeof( PersonConverter ) )]
    public class ViewPerson : IDisplay
    {
        …
    }

ListPropertyDescriptor包含在转换器中,因为它与ListConverter结合使用来构建要显示的对象列表。这是一个可以重用的通用类,对于它如何与PropertyGrid结合使用,实际上不需要解释。要了解如何使用功能性的PropertyDescriptor,请参阅我的第一篇文章:动态属性 - 运行时创建数据库

组合框数据

ComboBox的数据由您,开发者提供。本文的这一部分将描述如何获取数据放入ComboBox

根据您的需求和要求,您有不同的选择。

  1. 枚举 - 作为开发人员,您无需执行任何操作。ComboBox将自动填充枚举列表中的值。您可以在ViewCar.cs文件中找到BodyColorBodyStyle的示例。

    如果枚举文本是可以接受的,那么无需额外工作。大多数情况下,枚举是使用驼峰命名、下划线或全大写字母组合的单词。这种显示对于最终用户来说通常是不可接受的。使用以下项目之一会是更好的方法。

  2. 静态数据 - 静态数据可以通过两种方式处理:自动发现或硬编码值。值的发现将在下一个项目符号中讨论,但应通过返回列表的方法来完成,以防止列表被修改。

    至于硬编码值,最简单的方法是派生自己的ComboBox控件自GridComboBox并重写RetrieveDataListbase.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 );
        }
    }
  3. 动态数据 - 在运行时查找数据以填充给定属性的ComboBox并允许添加新数据是一个有趣的挑战。本文的这一部分使用了.NET开发的先进技术:创建属性和反射。

    反射和属性的创建不在此文中讨论,因为它们是独立的主题。我在Utilities项目中提供了一个名为Reflect的类。该类包含许多static方法,用于使用反射来get/set字段和属性、调用方法等。这些方法在实现动态数据时被广泛使用。

    开始实现动态数据的地方是在创建一个派生自Attribute的基类。这将允许基类ComboBoxGridComboBox成为泛型,只需处理一种类型。

    [AttributeUsage( AttributeTargets.Field | AttributeTargets.Property )]
    public abstract class ListAttribute : Attribute
    {
    }

    完成此操作后,就可以开始创建您的特定属性了。我在项目中创建了两个属性:DataListAttributeEnumListAttribute。我将逐步介绍DataListAttribute的创建、GridComboBoxListGridComboBox的基础知识,以及如何将它们连接起来。EnumListAttributeDataListAttribute的简化形式,我将留给您自行查阅。

    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的名称和完全限定的类名,而pathallowNeweventHandler对两组构造函数都是通用的。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是PopulateListBoxEditValue,其余的都处理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类,包含用于反射到类的静态实例或实例的字段/属性/方法的方法
© . All rights reserved.