Introduction
I have used the PropertyGrid
control in some projects and have concluded that is extremely useful, flexible, and professional looking. But unfortunately, it is not available for the .NET Compact Framework. I guess the reason is that it is too heavy to provide all its functionality in a constrained environment like Windows CE. PropertyGrid
will require the support of lots of classes, interfaces, and attributes.
Since I needed to develop a Pocket PC version of my desktop software, I had to reproduce all the functionality of the PropertyGrid
and related elements, matching the name of the real .NET equivalents. I did it with success, but the resulting source code didn't satisfy me at all because it was too complex.
Based on this experience, I developed this new implementation of my control; this time, I ignored the name-compatibility requirements and produced a simpler solution, but keeping most of the great PropertyGrid
features, as I will explain further.
Basic Usage - Person Class
The supplied demo project will allow you to test all the PropertyGridCE
capabilities. There are three classes defined in DemoClasses.cs, with different features, like custom attributes. Here is the declaration of the first and simplest one:
|
public class Person
{
public enum Gender { Male, Female }
#region private fields
private string[] _Names = new string[3];
private Gender _Gender;
private DateTime _BirthDate;
private int _Income;
private System.Guid _Guid;
#endregion
#region Public Properties
[Category("Name")]
[DisplayName("First Name")]
public string FirstName
{
set { _Names[0] = value; }
get { return _Names[0]; }
}
[Category("Name")]
[DisplayName("Mid Name")]
public string MidName
{
set { _Names[1] = value; }
get { return _Names[1]; }
}
[Category("Name")]
[DisplayName("Last Name")]
public string LastName
{
set { _Names[2] = value; }
get { return _Names[2]; }
}
[Category("Characteristics")]
[DisplayName("Gender")]
public Gender PersonGender
{
set { _Gender = value; }
get { return _Gender; }
}
[Category("Characteristics")]
[DisplayName("Birth Date")]
public DateTime BirthDate
{
set { _BirthDate = value; }
get { return _BirthDate; }
}
[Category("Characteristics")]
public int Income
{
set { _Income = value; }
get { return _Income; }
}
[DisplayName("GUID"),
ReadOnly(true),
ParenthesizePropertyName(true)]
public string GuidStr
{
get { return _Guid.ToString(); }
}
[Browsable(false)]
public System.Guid Guid
{
get { return _Guid; }
}
#endregion
public Person()
{
for (int i = 0; i > 3; i++)
_Names[i] = "";
_Gender = Gender.Male;
_Guid = System.Guid.NewGuid();
}
public override string ToString()
{
return string.Format("{0} {1} {2}",
FirstName, MidName,
LastName).Trim().Replace(" ", " ");
}
}
|
Notice that there are two fields (private
) and properties (public
) declared with similar names. The control will show just the properties, not the fields. If you are using C# 3.0, you can avoid declaring the underlying fields by using auto-implemented properties
To show the properties of a Person
object is quiet simple; just assign it to the control's SelectedObject
property, as shown:
PropertyGrid1.SelectedObject = thePerson;
Basic Attributes
In the Person
class implementation, you will notice there are some properties that have attributes (those with square brackets); they won't have any effect on your class behaviour, but will do with the property grid. These attributes are similar to those implemented in the desktop .NET Framework. Let's see them in detail.
Category
: Lets you specify a category group for the affected property. A category appears by default at the property grid with a gray background, as you can see in the first screenshot. If the property doesn't have a Category
attribute, it will belong to a blank category group, as with the GUID property in the previous screenshot. It is recommended to always specify a category for each property.
DisplayName
: Will be useful when you want to display a property name different from the real one. Usually, it is used when you have to increment readability with white spaces, or abbreviate the name.
ReadOnly
: When set to true
, will prevent the property from being edited; it will be just shown in the property grid.
ParenthesizePropertyName
: When set to true
, will display the names between parenthesis. This is used to enhance some important properties, and force them to show at the beginning of a category group.
Browsable
: When set to false
, the property will not be shown. It is useful when you have a property that you don't want to show at all, like the GUID property in the first example.
All these attributes are declared in the System.ComponentModel
namespace, to keep some degree of compatibility with the original PropertyGrid
control.
Custom Properties - Vehicle Class
While the simplest implementation of PropertyGridCE
exposes all the properties of a class (with the exception of those with the Browsable
attribute set to false
), the ICustomProperties
interface will allow to conditionally expose some properties. There are a few steps to accomplish this, as in the following example:
|
public class Vehicle :
PropertyGridCE.ICustomProperties
{
public enum CarType
{ Sedan, StationWagon,
Coupe, Roadster, Van, Pickup, Truck }
public enum CarBrand
{ Acura, Audi, BMW,
Citroen, Ford, GMC, Honda, Lexus,
Mercedes, Mitsubishi, Nissan,
Porshe, Suzuki,
Toyota, VW, Volvo }
#region Private fields
private CarBrand _Brand;
private CarType _CarType;
private string _Model;
private int _Year;
private string _Plate;
private int _Seats;
private int _Volume;
private int _Payload;
#endregion
#region Public Properties
[Category("Classification")]
public CarBrand Brand
{
get { return _Brand; }
set { _Brand = value; }
}
[Category("Classification")]
[ParenthesizePropertyName(true)]
[DisplayName("Type")]
public CarType TypeOfCar
{
get { return _CarType; }
set { _CarType = value; }
}
[Category("Classification")]
public string Model
{
get { return _Model; }
set { _Model = value; }
}
[Category("Identification")]
[DisplayName("Manuf.Year")]
public int Year
{
get { return _Year; }
set { _Year = value; }
}
[Category("Identification")]
[DisplayName("License Plate")]
public string Plate
{
get { return _Plate; }
set { _Plate = value; }
}
[Category("Capacity")]
public int Seats
{
get { return _Seats; }
set { _Seats = value; }
}
[Category("Capacity")]
[DisplayName("Volume (ft3)")]
public int Volume
{
get { return _Volume; }
set { _Volume = value; }
}
[Category("Capacity")]
[DisplayName("Payload (pnd)")]
public int Payload
{
get { return _Payload; }
set { _Payload = value; }
}
#endregion
#region ICustomProperties Members
PropertyInfo[] PropertyGridCE.
ICustomProperties.GetProperties()
{
List<PropertyInfo> props =
new List<PropertyInfo>();
foreach
(System.Reflection.PropertyInfo
info in GetType().GetProperties())
{
if ((info.Name == "Volume" ||
info.Name == "Payload") &&
(this._CarType !=
CarType.Pickup &&
this._CarType !=
CarType.Truck))
continue;
props.Add(info);
}
return props.ToArray();
}
#endregion
}
|
Notice that the unique method needed to implement the PropertyGrideCE.ICustomProperties
is GetProperties()
. This method should return all the property names you want to expose as an array, depending on some conditions. In this example, if the car type is a Pick Up or Truck, the Volume
and Payload
properties will be exposed.
Custom Editors - Place Class
Custom editors is the most powerful feature of this control. There are several tricks you can do with it. By default, the control will provide an editor for all the fundamental classes: int
, float
, double
, etc., and also for strings and enumerations, the latter as a ComboBox
. If you have a custom class' object as a property, it will show the contents but just as readonly, because the grid control doesn't know how to edit it.
A custom editor must de declared by using the CustomEditor
attribute, as shown in the following example. There are two kinds of custom editors: derived from Control
, and derived from Form
. The Place
class implementation shows both. Despite the kind of editor, it has to inherit the ICustomEditor
interface, as we will see in detail later.
The attribute declaration can be done in two places: before the class declaration, as with the CountryInfo
class, or before the property itself, like with the Picture
property. The first will have effect in every property of the class, the second will have effect only in the specific property; other properties of the same class will remain unaffected.
|
public class Place
{
[CustomEditor(typeof(CountryEditor))]
public struct CountryInfo
{
public enum Continent
{ Africa=1, America=2,
Asia=3, Europe=4, Oceania=5 }
public static readonly
CountryInfo[] Countries = {
new CountryInfo
( 1, "AO", "ANGOLA" ),
new CountryInfo
( 1, "CM", "CAMEROON" ),
new CountryInfo
( 2, "BO", "BOLIVIA" ),
new CountryInfo
( 2, "PE", "PERU" ),
new CountryInfo
( 3, "JP", "JAPAN" ),
new CountryInfo
( 3, "MN", "MONGOLIA" ),
new CountryInfo
( 4, "DE", "GERMANY" ),
new CountryInfo
( 4, "NL", "NETHERLANDS" ),
new CountryInfo
( 5, "AU", "AUSTRALIA" ),
new CountryInfo
( 5, "NZ", "NEW ZEALAND" )
};
public Continent Contin;
public string Abrev;
public string Name;
public override string ToString()
{
return Name;
}
public CountryInfo(int _continent,
string _abrev, string _name)
{
this.Contin =
(Continent)_continent;
this.Abrev = _abrev;
this.Name = _name;
}
}
#region Private fields
private string[] _Address =
new string[4];
public CountryInfo _Country;
private Image _Picture;
public int _CurrentValue;
public int _Floors;
#endregion
#region Public properties
[Category("Address")]
public string Street
{
get { return _Address[0]; }
set { _Address[0] = value; }
}
[Category("Address")]
public string City
{
get { return _Address[1]; }
set { _Address[1] = value; }
}
[Category("Address")]
public string Province
{
get { return _Address[2]; }
set { _Address[2] = value; }
}
[Category("Address")]
public string Postal
{
get { return _Address[3]; }
set { _Address[3] = value; }
}
[Category("Address")]
public CountryInfo Country
{
get { return _Country; }
set { _Country = value; }
}
[Category("Characteristics")]
[CustomEditor(typeof(PictureEditor))]
public Image Picture
{
get { return _Picture; }
set { _Picture = value; }
}
[Category("Characteristics")]
public int Floors
{
get { return _Floors; }
set { _Floors = value; }
}
[Category("Characteristics")]
public int CurrentValue
{
get { return _CurrentValue; }
set { _CurrentValue = value; }
}
#endregion
public Place()
{
for (int i = 0; i <
_Address.Length; i++)
_Address[i] = "";
}
}
|
The ICustomEditor
interface provides some elements to allow the PropertyGridCE
control to interact with the editor control. There are events, properties, and methods. It is declared as:
public interface ICustomEditor
{
event EventHandler ValueChanged;
EditorStyle Style { get; }
object Value { get; set; }
void Init(Rectangle rect);
}
As I mentioned, there are two examples of custom editor implementations in the Place
class; the first one, CountryEditor
, is a control editor. It asks you for a country with two ComboBox
es: one for Continent
, and one for Country
, as you can see in the screenshot. To avoid source code clutter, I will show just the partial implementation:
public partial class CountryEditor :
UserControl, PropertyGridCE.ICustomEditor
{
#region Private fields
private Place.CountryInfo Info;
private EventHandlerList ValueChangedColl = new EventHandlerList();
private EventArgs EventArguments = new EventArgs();
private object ID = new object();
#endregion
#region ICustomEditor Members
event EventHandler PropertyGridCE.ICustomEditor.ValueChanged
{
add { ValueChangedColl.AddHandler(ID, value); }
remove { ValueChangedColl.RemoveHandler(ID, value); }
}
PropertyGridCE.EditorStyle PropertyGridCE.ICustomEditor.Style
{
get { return PropertyGridCE.EditorStyle.DropDown; }
}
object PropertyGridCE.ICustomEditor.Value
{
get { return Info; }
set
{
if (value is Place.CountryInfo)
{
Info = (Place.CountryInfo)value;
this.ComboContinent.SelectedItem = Info.Contin;
this.ComboCountry.SelectedItem = Info;
}
}
}
void PropertyGridCE.ICustomEditor.Init(Rectangle rect)
{
this.Location = rect.Location;
ComboContinent.Items.Clear();
foreach (FieldInfo info in typeof(
Place.CountryInfo.Continent).GetFields(
BindingFlags.Public | BindingFlags.Static))
{
ComboContinent.Items.Add(Enum.Parse(
typeof(Place.CountryInfo.Continent), info.Name, false));
}
ComboContinent.SelectedIndex = 0;
ComboContinent.SelectedIndexChanged +=
new EventHandler(ComboContinent_SelectedIndexChanged);
ComboCountry.SelectedIndexChanged +=
new EventHandler(ComboCountry_SelectedIndexChanged);
}
#endregion
}
First, you have to declare the Style
property; in this case, it has the DropDown
value for this kind of control editor. Then, you have to implement the Value
property; this will allow the PropertyGridCE
control to set and get the value inside the editor control.
The Init
method will pass the suggested place to show the control, it is the same as the rectangle containing the value at the grid. You should resize or relocate your control as pertinent. Also, you can do some private initialization here.
The ValueChanged
event also is important to tell the PropertyGridCE
control that the value of the property has been changed so it can update the selected object property properly. It has the same implementation as the example.
Also, it will be important to control the LostFocus
event to allow the control to update the property value and close when the user taps the stylus outside the control. You will find an example in the full source code provided.
The second example of a custom editor is PictureEditor
; it is a form editor, so it will occupy all the screen workable area. As in the previous example, I will show the class partially for abbreviation purposes:
public partial class PictureEditor : Form, PropertyGridCE.ICustomEditor
{
#region Private fields
private EventHandlerList ValueChangedColl = new EventHandlerList();
private EventArgs EventArguments = new EventArgs();
private object ID = new object();
private Size PictureBoxSize;
#endregion
#region ICustomEditor Members
event EventHandler PropertyGridCE.ICustomEditor.ValueChanged
{
add { ValueChangedColl.AddHandler(ID, value); }
remove { ValueChangedColl.RemoveHandler(ID, value); }
}
PropertyGridCE.EditorStyle PropertyGridCE.ICustomEditor.Style
{
get { return PropertyGridCE.EditorStyle.Modal; }
}
object PropertyGridCE.ICustomEditor.Value
{
get { return PictureBox1.Image; }
set
{
PictureBox1.Image = (Image)value;
ScaleView();
}
}
void PropertyGridCE.ICustomEditor.Init(Rectangle rect)
{
PictureBoxSize = PictureBox1.Size;
}
#endregion
}
Notice this time, the Style
property returns the Modal
value, because it is a form control. Also, the rect
parameter in the Init
method is ignored.
Final Comments
Future releases will consider showing members of properties, including collections, collapse/expand option, and keypad support.
The sample application has been built with Visual Studio 2008. You cannot load the solution with Visual Studio 2005 directly, but as it is designed for C# 2.0 and .NET 2.0 and up, you can create a new solution and attach the project file without problems. I have tested the application with an emulator and a real Pocket PC 2003, but you can use it in other platforms.
To use this control, you just need to add the PropertyGridCE.cs file into your project, no extra DLL is needed.
History
- June 23, 2008: First edition.
- October 15, 2008: Moved to the Mobile section.