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:
- Display – the basics, ordering, creating views,
ComboBox
, and TypeConverter
- Data – how to get and add data to a list at runtime, and various array accessor methods
- 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
- 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.
[Category( "Person" )]
[DisplayName( "Last Name" )]
[Description( "Enter the last name for this person" )]
public string PersonLastName
{
get {…}
set {…}
}
- 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. - 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.
- 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
.
[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.
- 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.
[TypeConverter( typeof( ListConverter<ViewPersonCollection> ) )]
public List<ViewPersonCollection> Children
{
…
}
- ListExpandableConverter – Expands list into the object type.
[TypeConverter( typeof( ListExpandableConverter ) )]
public class ViewPersonCollection : IDisplay
{
…
}
- ListNonExpandableConverter – Displays name on the property side.
[TypeConverter( typeof( ListNonExpandableConverter ) )]
public class ViewPerson2 : IDisplay
{
…
}
- PersonConverter – Another method to display or filter properties.
[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.
- 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.
- 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.
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.
public class Wrapper
{
IDisplay _dataObject;
public Wrapper( IDisplay dataObject )
{
_dataObject = dataObject;
}
public override string ToString()
{
return ( _dataObject.Text );
}
}
- 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.
[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.
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.
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.
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.
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
.
[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).
[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