Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Property Grid - Dynamic List ComboBox, Validation, and More

4.57/5 (20 votes)
25 Sep 2009CPOL17 min read 1   11.2K  
A PropertyGrid implementation showing how-to use, best practices, validation, and more.

Introduction

While working on a project, I was asked to include a PropertyGrid control to a screen to allow for configuration of equipment and to do validation. As I had not used a PropertyGrid control before, I started searching for examples, and was dismayed at what I was finding, or in the case of validation, not finding.

In an effort to remember for the next time I need to use a PropertyGrid, and for others starting out, hopefully, this will provide some insights into the multiple aspects of using a PropertyGrid.

Topics

This article will provide information and a code example for the following:

  1. Display – the basics, ordering, creating views, ComboBox, and TypeConverter
  2. Data – how to get and add data to a list at runtime, and various array accessor methods
  3. Validation – how to create validation rules, and use a generic PropertyGrid to perform validation

Project Code

The code listed at the top of this article contains an example of each of the topics that will be discussed as well as some utilities for reflection, parsing, and obtaining a class type. These utilities will not be discussed, but are important aspects in obtaining the data dynamically at runtime, and should be reviewed for complete understanding.

The code is broken into application, controls, model, and utilities. The views in the application are not necessarily laid out using best coding practices, but were done so to allow an example of each of the topics listed above.

The code was written using VS.NET 2005, and doesn't require any other external libraries.

PropertyGrid Control

Display

  1. Basics – The PropertyGrid control has a property, SelectedObject, that you set in order to display information in the PropertyGrid. The PropertyGrid will reflect over your object and display the properties of your class. The display of the names are not necessarily the best user experience as programmers usually use Camel casing for a naming convention, and a description is not included as to what the user should be filling in.

    Microsoft provides some attributes you can add to the top of your property that will provide a user friendly experience. Category will group items with the same arbitrary name (e.g., “Person”) together, while DisplayName and Description provide a friendly text to display and describe the property, respectively.

    C#
    [Category( "Person" )] 
    [DisplayName( "Last Name" )] 
    [Description( "Enter the last name for this person" )] 
    public string PersonLastName 
    { 
        get {…} 
        set {…} 
    }
  2. Read only – Items on the PropertyGrid can be made read only by providing a get accessor only, or by using a converter method for collections, which will be described below.
  3. Ordering properties – Properties, by default, will be ordered alphabetically by category, sub-ordered by property name. This could be either the actual property name, or the text used in the DisplayName attribute. This can be changed by using the PropertySort property within VS.NET, but for most developers, this property is ineffective as the properties are what are in need of a specific ordering, as in: name, address, city, state, and zip. Having these properties in any other order would provide a poor user experience.

    Finding an easy way to change the default ordering of properties was rather frustrating. It would have been nice to have an attribute that would allow you to order your properties in a particular order. I found a relatively easy way to accomplish ordering, albeit rather ugly. The tab character, “\t”, can be used to order the items, and you get the benefit that the character will not be displayed on the screen. The ViewPerson2 class has an example of this. The only thing to note is that it is in reverse order. The more tab characters you add, the greater likelihood the property will be displayed first. In order to create a more elegant solution, I created a new class that derives from the DisplayNameAttribute, OrderedDisplayNameAttribute. The attribute will add the tab character for you. This will help with your own categories, but trying to use this in pre-existing Microsoft categories won't work so well.

  4. Views – As described above, attributes are added to your class properties. Adding these attributes directly to your business objects is not necessarily the best approach. Views are highly recommended for use in a PropertyGrid. While creating and maintaining the views are a little more involved, they will provide the flexibility to display the data in different ways. Views also give you a way provide a different DisplayName and Description for the same business object.

    The ViewPerson and ViewPerson2 classes are such an example. The ViewPerson class will be displayed using a TypeConverter of type PersonConverter, and the ViewPerson2 class is of type ListNonExpandableConverter. The views allow the same exact business object to be displayed in two completely different ways. While the functionality of the Converters will be described later, the PersonConverter will display the entire contents of the Person in the PropertyGrid, and will allow you to drill down the person’s children and grandchildren, whereas the ListNonExpandableConverter will display the name of the person only.

ComboBox

A ComboBox, in general, is a great way to provide a list of items to the user to prevent data entry errors. Creating a ComboBox for use in a PropertyGrid is not as straightforward as dropping a control on the form and adding data to it. The PropertyGrid has no idea that a property should be selected from a pre-existing list of data or where to obtain the data from. I will describe my implementation strategy for obtaining data dynamically, later in the article.

Now is a good to time to add, for those of you digging through the code, that I am actually using a ListBox and not a ComboBox. Because I am creating a control that acts and looks like a ComboBox, I call it such. The PropertyGrid actually provides the inverted triangle, denoting a ComboBox on my behalf. If I were to use a ComboBox as my control, internal to my GridComboBox, when you clicked on the drop down arrow, another ComboBox would show, in which case you would have to click on the drop down arrow again in order to select the data. This behavior is not ideal.

I've created a base ComboBox control called GridComboBox that you can derive from to create your own ComboBox. Two such controls are included in the project: EnumGridComboBox and ListGridComboBox.

The GridComboBox control derives from UITypeEditor. There are two methods that were overridden: EditValue and GetEditStyle. EditValue is responsible for populating the ComboBox, attaching the ComboBox to the PropertyGrid and retrieving the new value selected from the ComboBox. The GetEditStyle denotes how you want to display the control. In the case of a ComboBox, a UITypeEditorSyle.DropDown was used. You could set it to Modal as well if you were using a form to collect information. I do not use the Modal setting.

It is important to remember that if you are binding a collection of data to the ComboBox, you override the ToString() method so you get something pleasant displayed and not the fully qualified business object name.

It is important to note that the EnumGridComboBox class is not necessary under normal circumstances. Anytime a business object is bound to a PropertyGrid that contains a property of type enum, a default ComboBox is created for you and will display all the values automatically. I have added the EnumGridComboBox implementation to show another ComboBox, and is only really necessary if you want to do something after selecting an enumeration from the ComboBox. The ViewCar class has two enumerations: Engine and BodyStyle that show the default behavior, and the ViewWheel class contains SupplyParts that uses the EnumGridComboBox implementation.

In order to get the custom ComboBox to display in the PropertyGrid, you have to tell the PropertyGrid to display the data in the control. This is accomplished by adding another attribute, Editor, to the top of your property. You will have to do this for each property that you wish to show in a ComboBox.

C#
[Editor( typeof( EnumGridComboBox ), typeof( UITypeEditor ) )]
[EnumList( typeof(SupplyStore.SupplyParts))]
[Category( "…" ), Description( "…" ), DisplayName( "…" )]
public SupplyStore.SupplyParts SupplyParts
{
    get { return _supplyPartsEnum; }
    set { _supplyPartsEnum = value; }
}

Converters

These are classes that help you to transform an embedded property of type class so it may be displayed in a particular manner. For instance, an example of this would be the ViewCar class containing a property of type Wheel. This is a brief description of the Converters that are included in the project.

  1. ListConverter – Takes a collection and creates a list of objects. By default, it will display the fully qualified name of the business object in the list. This will be used in conjunction with one following converters which is placed on the business object that will be displayed.
    C#
    [TypeConverter( typeof( ListConverter<ViewPersonCollection> ) )]
    public List<ViewPersonCollection> Children
    {
        …
    }
  2. ListExpandableConverter – Expands list into the object type.
    C#
    [TypeConverter( typeof( ListExpandableConverter ) )]
    public class ViewPersonCollection : IDisplay
    {
        …
    }
  3. ListNonExpandableConverter – Displays name on the property side.
    C#
    [TypeConverter( typeof( ListNonExpandableConverter ) )]
    public class ViewPerson2 : IDisplay
    {
        …
    }
  4. PersonConverter – Another method to display or filter properties.
    C#
    [TypeConverter( typeof( PersonConverter ) )]
    public class ViewPerson : IDisplay
    {
        …
    }

The ListPropertyDescriptor is included in the Converters as it is used in conjunction with ListConverter to build the list of objects to display. This is a generic class that can be reused, and really needs no explanation for how it is being used in conjunction with the PropertyGrid. To see how to use a functional PropertyDescriptor, see my first article: Dynamic Properties - A Database Created At Runtime.

ComboBox Data

The data for the ComboBox is provided by you, the developer. This section of the article will describe how to get data to put in the ComboBox.

You have different options depending on your need and your requirements.

  1. Enumerations – As a developer, you don't have to do anything. The ComboBox will auto-fill with the values from the enumeration list. You can see an example of this in the ViewCar.cs file for the BodyColor and BodyStyle.

    If the text of the enumeration is acceptable, then no additional work is required. Most of the time, enumerations are a concatenation of words using Camel casing, underscores, or all caps. Such a display to an end-user is generally unacceptable. Using one of the next bullet items would be a better approach.

  2. Static Data – Static data can be handled in one of two ways, auto discover or hard code the values. Discovering the values will be discussed in the next bullet item but should be accomplished with a method returning a list so the list cannot be modified.

    As to hard coding the values, the easiest way to accomplish this would be to derive your own ComboBox control from GridComboBox and to override the RetrieveDataList to set the base.DataList to a predefined list of items.

    C#
    protected override void RetrieveDataList( ITypeDescriptorContext context ) 
    {
         List<string> list = new List<string>();
         list.Add( "Tom" );
         list.Add( "Jerry" );
         base.DataList = list;
    }

    There are no restrictions on the data type that is used for the list. If you are using a data type other than a value type, make sure you provide a way to display pretty text in the ComboBox. The default will be the fully qualified name of the business object. To accomplish this, you can add a ToString() to the class or view.

    Another approach would be to create an interface, IDisplay, as I have done in the case of ViewPerson, and write some additional code in the GridComboBox to wrap the business object. I have not provided this functionality, but an example of what this would look like is the following. The adding of the business object to the list and returning the business object during the selection would need to be updated.

    C#
    public class Wrapper
    {
        IDisplay _dataObject;
    
        public Wrapper( IDisplay dataObject )
        {
            _dataObject = dataObject;
        }
    
        public override string ToString()
        {
            return ( _dataObject.Text );
        }
    }
  3. Dynamic Data – The ability to find data at runtime to populate the ComboBox for a given property as well as allow new data to be added was an interesting challenge. This portion of the article uses advanced techniques of .NET development; creation of attributes and reflection.

    Reflection and the creation of attributes will not be discussed in this article as they are topics unto themselves. I have provided a class in the Utilities project named Reflect. This class contains a bunch of static methods for using reflection to get/set fields and properties, call methods, etc. These methods are used throughout for implementing dynamic data.

    The place to start implementing dynamic data is at the creation of a base class deriving from Attribute. This will allow for the base ComboBox, GridComboBox, to be generic by only working with one type.

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

    Once this is done, the creation of your specific attribute can begin. I have created two attributes in the project: DataListAttribute and EnumListAttribute. I will walk through the creation of the DataListAttribute, the basics of the GridComboBox and ListGridComboBox, and how to wire it all up. The EnumListAttribute is a simpler form of the DataListAttribute, and I will leave it for you to review.

    DataListAttribute – Several constructors have been added to allow the user to tailor the attribute to their needs. The first three constructors allow for obtaining the data list which is part of an instance of a class.

    C#
    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
    {
        …
    }

    The next three are for retrieving a data list that is part of a static class.

    C#
    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
    {
        …
    }

    For the constructors that are to be accessing a static class, the name of the DLL and the fully qualified class name need to be provided, while path, allowNew, and eventHandler are common to both sets of constructors. The allowNew flag denotes if a new business object can be created and stored in the property. This will display an additional entry in the list, <Add New...>. When the user selects this option, it will create a new instance of the object type. If you want to have the business object show up in the ComboBox as an option to choose from next time, you will have to provide the name of an event handler. When the event handler is called, you will have to add it into the list manually.

    C#
    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 );
            }
        }
    }

    The path defines how to navigate from one business object to the next in order to obtain the data list to display. If you were to write the path in code and cut and paste it into the string, it would be very close to being complete. The path can include functions, fields, properties, and arrays with subscripts that can be numeric, strings, or enumerations. Here are the ones used within this project.

    "Instance.Wheels"
    "Instance.Supplies[Wheels]"
    "Instance.SuppliesArray[1]"
    "Instance.SuppliesArray
    	[CarApplication.SupplyStore+SupplyParts.Wheels,CarApplication]"
    "GetPeopleList"
    "SupplyStore"

    GridComboBox - The GridComboBox has been written to be derived from to provide the functionality of retrieving data. The class has been derived from UITypeEditor, which is required for use in the PropertyGrid.

    The class has two methods that will need to be implemented by the derived class. These methods define how to retrieve the list of data and what to do after an item has been selected from the ComboBox. We will look at these in the next section.

    C#
    protected abstract object GetDataObjectSelected( ITypeDescriptorContext context );
    protected abstract void RetrieveDataList( ITypeDescriptorContext context );

    The only other code of interest in the file is PopulateListBox and EditValue, the rest deals with the behavior of the ComboBox.

    EditValue is overridden from the base class UITypeEditor. This function is the central point of the functionality of the ComboBox. When a user clicks on the arrow of the ComboBox to get the list of items, an event is fired which calls into this method. This method then calls the PopulateListBox method, attaches the internal ListBox that is used in the PropertyGrid, and waits for the user to perform an action (i.e., click on an item or press the ESC key). Processing continues by calling the derived GetDataObjectSelected method, which returns the data value or the instance of an object.

    The PopulateListBox method will call the derived method RetrieveDataList to find the data and populate the ComboBox. If a prior value was selected, the value is auto-selected in the list.

    ListGridComboBox – The RetrieveDataList is defined here. First, the list of attributes is obtained from the property that we are currently working with. The attribute that we are looking for is the DataListAttribute, which contains the path to the data. Once the attribute is found, the path is broken into its parts. Processing continues by determining if the data is found by navigating the current business object or if it is stored in a static class.

    The processing is essentially the same for both paths; break each segment of the path into various parts. This takes into account arrays, lists, and dictionaries that may be used. Once we retrieve the components of the current segment, the information is passed off to the reflection class, Reflect, which will retrieve the actual value/object.

    This process continues for each segment of the path until there are no more segments, at which time we should have obtained the list of data that we are looking for. If there are more segments, the value of the property obtained from the previous segment is used as the starting point for this segment.

    The value is returned, and a reference to the list is saved for future use. The reference to the list is saved because of the use of reflection. The other reason is because the location of the data will usually be stored in the same location. If you find that this is not true for your circumstances, don't store the reference to the data list.

    The other overridden method, GetDataObjectSelected, is implemented here as well. It is responsible for retrieving the value/object from the list and returning it. This implementation checks to see if the “<Add New...>” was selected, and then creates a new instance of the data object. If the object was created, a notification is sent if the option was set in the DataListAttribute. The creation of the object and the sending of the notification are once again handled by calling methods in the Reflect class.

    The event handler was designed with the intent of performing actions like setting default values on the object, such as a first or last name, which might be used in the ToString() method. The event handler would also need to add the object to the list so it can be reselected the next time.

Validation

Display

Validation of the data poses another challenge when using the PropertyGrid. There is no real mechanism for doing this. I have created an implementation that suited my general need of validating when the value of the property changed. If you need to do validation for each key stroke as in the case of a mask, you will have to provide your own implementation.

The down side to the current implementation is that if the data entered is incorrect and you move off the field after seeing the warning message box, you will be allowed to do so. I did this intentionally. There are too many implications that can arise from trying to prevent the user from moving off the field and how to allow it under some conditions. This article is meant to help you get going on providing validation. I leave the details of what to do after the data is not valid, to you, the developer.

In the CustomControls project is a folder named Rule that contains a base class and two implementations. The base class derives from Attribute once again, and provides for an error message field and an abstract IsValid() method. The other classes are:

  • PatternRuleAttibute – This rule is based on a string which is a regular expression. The data is validated using Regex.IsMatch. You can see several examples in the various view classes. Due to the fact that the backslash has special meaning with regular expressions, please make sure to add the ‘@’ symbol in front of the string.
    C#
    [Category( "…" ), DescriptionAttribute( "…" ), DisplayName( "…" )]
    [PatternRule( @"^(\d{5}-\d{4})|(\d{5})$" )]
    public string Zip
    {
        …
    }
  • LengthRuleAttribute – This rule will validate that the length of the string entered falls between two values (min and max).
    C#
    [Category( "…" ), DescriptionAttribute( "…" ), DisplayName( "…" )]
    [LengthRule( 4, 20 )]
    public string City
    {
        …
    }

The addition of other validation rules can be easily done, just derive from the base class RuleBaseAttribute, add a constructor and fields, and implement the IsValid() method. Once the new attribute is added to the top of the property, the rule will be ready to run. In order for these rules to magically work, you will have to use the PropertyGridControl provided in the solution, or copy the code in it to your implementation. The PropertyGridControl just derives from the PropertyGrid, and has the PropertyValueChanged event wired up. The basic functionality of the PropertyValueChanged event handler is:

  • Get the class type
  • Get the property name
  • Get the property information
  • Get the custom attributes of the property
  • For each attribute:
    If it is a RuleBaseAttribute, then call the IsValid() method with the data that is changing. If the data is not valid, then show the message box with the error.

Utilities

Included in the solution is a project named Utilities. This project contains some classes to help with various aspects of obtaining the data for the ComboBox and for data validation. This article will look at them. Here is a general description of them.

  • ClassType – used to return a Type object using various inputs
  • PathParser – used to separate fully qualified paths to fields/properties/methods
  • Reflect – a static class of methods to use to reflect into a field/property/method of a static, or instance of a, class

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)