Click here to Skip to main content
15,902,938 members
Articles / Programming Languages / C#
Article

Bending the .NET PropertyGrid to Your Will

Rate me:
Please Sign up or sign in to vote.
4.89/5 (284 votes)
19 Dec 200211 min read 1.3M   29.5K   412   317
A set of classes to simplify using custom properties in the .NET PropertyGrid control.

Sample Image - bending_property.png

Introduction

With the initial release of the .NET Framework, I was impressed that Microsoft finally provided one of their coveted custom controls for public use—in this case, the Visual Basic-like property grid, System.Windows.Forms.PropertyGrid.  Upon using it, however, I realized that while the control is very flexible and powerful, it takes quite a bit of work to customize.  Below, I present a set of classes that increase the usability of the property grid.

Background

The property grid provided with the .NET Framework operates via reflection.  By using the SelectedObject or SelectedObjects property, you can select one or more objects into the control.  It then queries the objects for all their properties, organizes them into groups based on their CategoryAttribute, and uses various UITypeEditor and TypeConverter classes to allow the user to edit the properties as strings.

The problem with this is that it's not very flexible out of the box.  To use the grid, you must write a "surrogate class" that exposes properties that you want to show in the grid, instantiate an object of this class, and then select it into the grid.

Many times, however, it may not be desirable to create such a class.  Each surrogate has to be specific to your data, because the runtime merely uses reflection to ask the class what its properties are.  You wouldn't be able to easily write a surrogate class that you could reuse across multiple pieces of data unless they all happened to share the same properties.

Depending on the design of your system and the organization of your data, it may not be convenient to write a surrogate class.  Imagine you have a database of a hundred properties that are stored as key-value pairs.  To create a class to wrap this database, you would have to hand-code the get and set calls for each of the hundred properties.  Even if you wrote an automated tool to generate the code for you, there is still the issue of code bloat in your executable.

Lastly, creating a surrogate class doesn't lend itself well to "beautifying" the contents of the grid.  The standard behavior is to display the actual name of the property from the code.  This makes sense for a component like a form designer, where the developer needs to be able to quickly and easily make the connection between a property in the grid and the same property in code.  If the grid is being used as part of an interface for end-users, however, they should be presented with something a little less obtuse than "UniqueCustID."

Custom Type Descriptors

If you take a look at the project settings dialog in Visual Studio.NET, you'll notice that the grid contains properties with nicer-looking names like "Use of MFC," which opens a drop-down list that contains values such as "Use MFC in a Shared DLL."  Clearly, there is more going on here than a simple property with an enumerated type.

The answer lies in the System.ComponentModel.ICustomTypeDescriptor interface.  This interface allows a class to provide customized type information about itself.  In short, this allows you to modify the properties and events that the property grid sees when it queries the object for this information.

ICustomTypeDescriptor provides a number of functions that look like functions in the standard TypeDescriptor class.  In fact, when you implement an ICustomTypeDescriptor, you want to be able to leverage much of the standard behavior already provided by the framework.  For most functions, you can simply pass control to the standard TypeDescriptor, like so:

C#
AttributeCollection ICustomTypeDescriptor.GetAttributes()
{
     return TypeDescriptor.GetAttributes(this, true);
}

The first parameter for most of these functions is the object on which the information is being requested.  The second parameter tells the TypeDescriptor not to call the custom type descriptor to get the requested information.  Since we are the custom type descriptor, omitting this parameter would result in infinite recursion, as the custom type descriptor would be invoked again and again.

However, note that I originally said that ICustomTypeDescriptor allows a class to provide information about itself.  This would seem to imply that a class must exist about which we can provide type information, and this is true.  Some class must exist that can implement the ICustomTypeDescriptor interface to provide a list of properties to the grid.  But the object's interface does not need to look anything like the properties that it will expose, and that is where the flexibility lies.

The Solution

To this end, I have created the PropertyBag class.  It implements ICustomTypeDescriptor, but in the most generic fashion possible.  Rather than relying on hard-coded properties in the class itself, PropertyBag manages a collection of objects that describe the properties that should be displayed in the grid.  It does this by overriding ICustomTypeDescriptor.GetProperties(), generating its own custom property descriptors, and returning this collection.  Since control is never passed to the standard type descriptor, the grid doesn't even know about the "actual" properties that belong to the PropertyBag class itself.

The benefits of this are that properties can be added and removed at will, whereas a typical surrogate class would be fixed at compile-time.

Type descriptors only provide information about the existence and type of properties, however, because they implement methods that can be applied to any object of that type.  This means there needs to be some way to retrieve and store the values of the properties.  I have implemented the two methods below:

Method 1: Raising Events

The method implemented by the base PropertyBag class raises events whenever property values are queried or changed.  The GetValue event occurs when the property grid needs to know the value of a certain property, and SetValue occurs when the user has interactively changed the value of the property in the grid.

This method is useful in situations dealing with properties that are stored in files and databases.  When the event occurs, you can use the property name to index into the data source, using the same simple lookup code for every property.

Method 2: Storing the Values in a Table

For a simpler approach that might be more appropriate in some cases, I've derived a class from PropertyBag called PropertyTable.  This class provides all the generic functionality of PropertyBag but also contains a hashtable, indexed by property name, to store property values.  When a value is requested, the property is looked up in the table, and when the user updates it, the value in the hashtable is updated accordingly.

Rolling Your Own

Since PropertyBag provides virtual functions OnGetValue and OnSetValue that correspond to the events discussed above, you can also derive a class and provide your own method by overriding these two functions.

What's Included

PropertyBag Class

The PropertyBag class is very basic, exposing only two properties, two events, and two methods.

C#
public string DefaultProperty { get; set; }

The DefaultProperty property specifies the name of the default property in the property bag.  This is the property that is selected by default when the bag is selected into a PropertyGrid.

C#
public PropertySpecCollection Properties { get; }

The Properties collection manages the various properties in the bag.  Like many other .NET Framework collections, it implements IList, so functions like Add, AddRange, and Remove can be used to manipulate the properties in the bag.

C#
public event PropertySpecEventHandler GetValue;
public event PropertySpecEventHandler SetValue;
protected virtual void OnGetValue(PropertySpecEventArgs e);
protected virtual void OnSetValue(PropertySpecEventArgs e);

The GetValue event is raised whenever the property grid needs to request the value of a property.  This can happen for several reasons, such as displaying the property, comparing it to the default value to determine if it can be reset, among others.

The SetValue event is raised whenever the user modifies the value of the property through the grid.

Derived classes can override OnGetValue and OnSetValue as an alternative to adding an event handler.

PropertyTable Class

The PropertyTable class provides all the operations of PropertyBag, as well as an indexer property:

C#
public object this[string key] { get; set; }

This indexer is used to get and set property values that are stored in the table's internal hashtable.  Properties are indexed by name.

PropertySpec Class

PropertySpec provides 16 constructor overloads—too many to describe in detail here.  The overloads are various combinations of the following parameters, listed with the property to which it corresponds:

  • name (Name): The name of the property.
  • type (TypeName): The type of the property.  In the constructor, this can be either a Type object (such as one returned by the typeof() operator), or it can be a string representing the fully qualified type name (i.e., "System.Boolean").
  • category (Category): A string indicating the category to which the property belongs.  If this is null, the default category is used (usually "Misc").  This parameter has the same effect as attaching a CategoryAttribute to a property.
  • description (Description): A help string that is displayed in the description area at the bottom of the property grid.  If this is null, no description is displayed.  This parameter has the same effect as attaching a DescriptionAttribute to a property.
  • defaultValue (DefaultValue): The default value of the property.  If the current value of the property is not equal to the default value, the property is displayed in bold to indicate that it has been changed.  This property has the same effect as attaching a DefaultValueAttribute to a property.
  • editor (EditorTypeName): Indicates the type (derived from UITypeEditor) used to edit the property.  This can be used to provide a custom editor for a type that does not have one explicitly associated with it, or to override the editor associated with the type.  In the constructor, this parameter can be specified either as a Type object or a fully qualified type name.  It has the same effect as attaching an EditorAttribute to a property.
  • typeConverter (ConverterTypeName): Indicates the type converter (derived from TypeConverter) used to convert the property value to and from a string.  In the constructor, it can be specified either as a Type object or a fully qualified type name.  This parameter has the same effect as attaching a TypeConverterAttribute to the property.

Additionally, the following property is provided:

C#
public Attribute[] Attributes { get; set; }

With the Attributes property, you can include any additional attributes not supported directly by PropertySpecReadOnlyAttribute, for example.

Using the Code

Using a PropertyBag is a simple process:

  1. Instantiate an object of the PropertyBag class.
  2. Add a PropertySpec object to the PropertyBag.Properties collection for each property that you want to display in the grid.
  3. Add event handlers for the GetValue and SetValue events of the property bag, and set them up to access whichever data source you are using to store the property values.
  4. Select the property bag into a PropertyGrid control.

If you are using a PropertyTable, you would not add event handlers to the bag in step 3, but instead specify appropriate initial values to the properties, using the table's indexer.

A basic example:

PropertyBag bag = new PropertyBag();
bag.GetValue += new PropertySpecEventHandler(this.bag_GetValue);
bag.SetValue += new PropertySpecEventHandler(this.bag_SetValue);
bag.Properties.Add(new PropertySpec("Some Number", typeof(int)));
// ... add other properties ...
propertyGrid.SelectedObject = bag;

The project included with the article shows a more robust demonstration of the classes.

Points of Interest

One interesting benefit that requires no extra work at all is shown in the demo application, if you select multiple objects from the list.  Multiple selection is handled entirely by the property grid, so even if the objects are property bags with completely custom properties, the grid still works as expected—only the properties and property values that are shared among all the objects are displayed.

Future Plans

This class will eventually be absorbed into a larger user-interface utility library that I'm writing (hence the awkward namespace name in the source code).

One enhancement I plan to add in the future is to make it easier to specify a list of string values for a property, instead of requiring an enumerated type for such a list.  For example, the project properties dialog mentioned before has a "Use of MFC" property with the values "Use Standard Windows Libraries," "Use MFC in a Static Library," and "Use MFC in a Shared DLL."  Currently, the way you could do this is to write a TypeConverter for the property and override the three GetStandardValues* methods to provide a list of allowable values.  Optimally, I would like to have this built-in to the property bag so the user can simply provide a list of strings for a property, without requiring any external type converters to be explicitly written.  One particularly interesting way to accomplish this may be to use the System.Reflection.Emit functionality to generate an appropriate type converter in memory at runtime.

Secondly, I plan to make the available properties dynamic at runtime as well, in the sense that the property bag would fire an event when it builds the property list for the grid.  Any objects listening to the event could provide additional properties at that time.  This would make sense for dynamic data sources where it might be inconvenient to constantly add and remove properties from the bag or create a new bag when the selected object changes.

Of course, I'm open to suggestions for other possible enhancements and extensions to these classes.

Version History

  • 12/20/2002(v1.0): Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
Tony is a graduate student in Computer Science at Virginia Tech, and also a teaching assistant for the department. Currently pursuing a Ph.D., he plans to teach at the university level after leaving school.

His research interest areas are digital image processing, programming languages, and compilers.

Comments and Discussions

 
GeneralCollection Pin
mikelb28-Aug-03 8:03
mikelb28-Aug-03 8:03 
Questionhow can I create remote object from the same class and assign it to ? Pin
caoyg4-Aug-03 21:21
caoyg4-Aug-03 21:21 
GeneralExpandableObjectConverter Pin
Shutty25-Jul-03 3:17
Shutty25-Jul-03 3:17 
GeneralPropertyBag Collection question Pin
mel_sadek24-Jul-03 15:45
mel_sadek24-Jul-03 15:45 
QuestionCategory Description? Pin
fredb24-Jul-03 3:07
fredb24-Jul-03 3:07 
GeneralHere is the VB.net version Pin
lbarbou17-Jul-03 9:45
lbarbou17-Jul-03 9:45 
GeneralRe: Here is the VB.net version Pin
jcontract22-Aug-03 9:58
jcontract22-Aug-03 9:58 
GeneralRe: Here is the VB.net version Pin
segaa30-Nov-03 3:05
segaa30-Nov-03 3:05 
The sample code will be something like this:
_______________________________________
Imports System
Imports System.Drawing
Imports System.Collections
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Data

Public Class PropBagTestForm
Inherits System.Windows.Forms.Form

#Region " Windows Form Designer generated code "

Public Sub New()
MyBase.New()

'This call is required by the Windows Form Designer.
InitializeComponent()

'Add any initialization after the InitializeComponent() call
' Create the first property bag and add some properties.
bag1 = New PropertyBag
AddHandler bag1.GetValue, AddressOf Me.bag1_GetValue
AddHandler bag1.SetValue, AddressOf Me.bag1_SetValue
bag1.Properties.Add(New PropertySpec("Fruit", GetType(Fruit), Nothing, Nothing, Fruit.Orange))
bag1.Properties.Add(New PropertySpec("Picture", GetType(Image), "Some Category", "This is a sample description."))
Dim item1 As New ListViewItem("Bag 1", 0)
item1.Tag = bag1
listView.Items.Add(item1)

' Create the second property bag and add some properties.
bag2 = New PropertyBag
AddHandler bag2.GetValue, AddressOf Me.bag2_GetValue
AddHandler bag2.SetValue, AddressOf Me.bag2_SetValue
bag2.Properties.Add(New PropertySpec("Fruit", GetType(Fruit), Nothing, Nothing, Fruit.Banana))
bag2.Properties.Add(New PropertySpec("Typeface", GetType(Font), "Another Category", Nothing, New Font("Tahoma", 8.25F)))
bag2.Properties.Add(New PropertySpec("Some Boolean", "System.Boolean", "Some Category", Nothing, False))

Dim item2 As New ListViewItem("Bag 2", 0)
item2.Tag = bag2
listView.Items.Add(item2)

' This time, create a property table. It uses a Hashtable to store
' values, so we don't need to wire GetValue and SetValue events.
bag3 = New PropertyTable
bag3.Properties.Add(New PropertySpec("Fruit", GetType(Fruit), Nothing, Nothing, Fruit.Orange))
bag3.Properties.Add(New PropertySpec("Picture", GetType(Image), "Some Category", "This is a sample description."))
bag3.Properties.Add(New PropertySpec("Typeface", GetType(Font), "Another Category", Nothing, New Font("Tahoma", 8.25F)))
bag3.Properties.Add(New PropertySpec("Some Boolean", "System.Boolean", "Some Category", Nothing, False))
bag3.Properties.Add(New PropertySpec("Number", "System.Int64", Nothing, "A big number.", 1234567890L))

' Create a property that uses additional attributes.
Dim ps As New PropertySpec("Can't Touch This", GetType(String), Nothing, "This property is read-only.", "Some Default String")
ps.Attributes = New Attribute() {ReadOnlyAttribute.Yes}
bag3.Properties.Add(ps)

' Assign values to the properties above.
bag3("Fruit") = Fruit.Apple
bag3("Picture") = Nothing
bag3("Typeface") = New Font("Times New Roman", 12.0F)
bag3("Some Boolean") = True
bag3("Number") = 1234567890L
bag3("Can't Touch This") = "Some Default String"

Dim item3 As New ListViewItem("Table", 0)
item3.Tag = bag3
listView.Items.Add(item3)

End Sub

'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub

'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer

'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents imageList As System.Windows.Forms.ImageList
Friend WithEvents menuReset As System.Windows.Forms.MenuItem
Friend WithEvents label1 As System.Windows.Forms.Label
Friend WithEvents propertyGrid As System.Windows.Forms.PropertyGrid
Friend WithEvents listView As System.Windows.Forms.ListView
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
Me.components = New System.ComponentModel.Container
Dim resources As System.Resources.ResourceManager = New System.Resources.ResourceManager(GetType(PropBagTestForm))
Me.imageList = New System.Windows.Forms.ImageList(Me.components)
Me.menuReset = New System.Windows.Forms.MenuItem
Me.label1 = New System.Windows.Forms.Label
Me.contextMenu = New System.Windows.Forms.ContextMenu
Me.propertyGrid = New System.Windows.Forms.PropertyGrid
Me.listView = New System.Windows.Forms.ListView
Me.SuspendLayout()
'
'imageList
'
Me.imageList.ImageSize = New System.Drawing.Size(32, 32)
Me.imageList.ImageStream = CType(resources.GetObject("imageList.ImageStream"), System.Windows.Forms.ImageListStreamer)
Me.imageList.TransparentColor = System.Drawing.Color.Transparent
'
'menuReset
'
Me.menuReset.Index = 0
Me.menuReset.Text = "Reset"
'
'label1
'
Me.label1.Location = New System.Drawing.Point(8, 8)
Me.label1.Name = "label1"
Me.label1.Size = New System.Drawing.Size(448, 23)
Me.label1.TabIndex = 5
Me.label1.Text = "Select one or multiple items in the list to see those objects in the property gri" & _
"d."
'
'contextMenu
'
Me.contextMenu.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.menuReset})
'
'propertyGrid
'
Me.propertyGrid.CommandsVisibleIfAvailable = True
Me.propertyGrid.ContextMenu = Me.contextMenu
Me.propertyGrid.LargeButtons = False
Me.propertyGrid.LineColor = System.Drawing.SystemColors.ScrollBar
Me.propertyGrid.Location = New System.Drawing.Point(176, 32)
Me.propertyGrid.Name = "propertyGrid"
Me.propertyGrid.Size = New System.Drawing.Size(272, 264)
Me.propertyGrid.TabIndex = 4
Me.propertyGrid.Text = "propertyGrid1"
Me.propertyGrid.ViewBackColor = System.Drawing.SystemColors.Window
Me.propertyGrid.ViewForeColor = System.Drawing.SystemColors.WindowText
'
'listView
'
Me.listView.HideSelection = False
Me.listView.LargeImageList = Me.imageList
Me.listView.Location = New System.Drawing.Point(0, 32)
Me.listView.Name = "listView"
Me.listView.Size = New System.Drawing.Size(168, 264)
Me.listView.TabIndex = 3
'
'PropBagTestForm
'
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(448, 301)
Me.Controls.Add(Me.listView)
Me.Controls.Add(Me.label1)
Me.Controls.Add(Me.propertyGrid)
Me.Name = "PropBagTestForm"
Me.Text = "Form1"
Me.ResumeLayout(False)

End Sub

#End Region

Private bag1 As PropertyBag
Private bag2 As PropertyBag
Private bag3 As PropertyTable

' A simple enumerated type used in the property bags.
Private Enum Fruit
Apple
Banana
Orange
Peach
Pear
End Enum 'Fruit

Private Sub listView_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles listView.SelectedIndexChanged
Dim objs As New ArrayList
Dim item As ListViewItem
For Each item In listView.SelectedItems
objs.Add(item.Tag)
Next item
propertyGrid.SelectedObjects = objs.ToArray()
End Sub 'listView_SelectedIndexChanged


Private Sub menuReset_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles menuReset.Click
If Not (propertyGrid.SelectedObject Is Nothing) And Not (propertyGrid.SelectedGridItem Is Nothing) Then
propertyGrid.ResetSelectedProperty()
End If
End Sub 'menuReset_Click

' Member variables associated with the properties of bag 1 and
' bag 2. Since events are fired to query these, you could use
' any source--variables, contents of a file, a database, etc.
Private bag1_Fruit As Fruit = Fruit.Orange
Private bag1_Picture As Image = Nothing

Private bag2_Fruit As Fruit = Fruit.Banana
Private bag2_Typeface As New Font("Tahoma", 8.25F)
Private bag2_SomeBoolean As Boolean = False


' This is a pretty basic way to handle the properties. Optimally,
' you might have some kind of table that indexes into a database
' or file where the values are stored. But for the purposes of this
' example, a simple case statement will do.
Private Sub bag1_GetValue(ByVal sender As Object, ByVal e As PropertySpecEventArgs)
Select Case e.Property.Name
Case "Fruit"
e.Value = bag1_Fruit
Case "Picture"
e.Value = bag1_Picture
End Select
End Sub 'bag1_GetValue


Private Sub bag1_SetValue(ByVal sender As Object, ByVal e As PropertySpecEventArgs)
Select Case e.Property.Name
Case "Fruit"
bag1_Fruit = CType(e.Value, Fruit)
Case "Picture"
bag1_Picture = CType(e.Value, Image)
End Select
End Sub 'bag1_SetValue


Private Sub bag2_GetValue(ByVal sender As Object, ByVal e As PropertySpecEventArgs)
Select Case e.Property.Name
Case "Fruit"
e.Value = bag2_Fruit
Case "Typeface"
e.Value = bag2_Typeface
Case "Some Boolean"
e.Value = bag2_SomeBoolean
End Select
End Sub 'bag2_GetValue


Private Sub bag2_SetValue(ByVal sender As Object, ByVal e As PropertySpecEventArgs)
Select Case e.Property.Name
Case "Fruit"
bag2_Fruit = CType(e.Value, Fruit)
Case "Typeface"
bag2_Typeface = CType(e.Value, Font)
Case "Some Boolean"
bag2_SomeBoolean = CBool(e.Value)
End Select
End Sub 'bag2_SetValue

End Class

_______________________________________
Generalmultiline textbox in the propertygrid Pin
Anonymous24-Jun-03 2:05
Anonymous24-Jun-03 2:05 
GeneralRe: multiline textbox in the propertygrid Pin
artzi25-Jun-03 4:13
artzi25-Jun-03 4:13 
GeneralGet Properties By Category (New Method) Pin
spook19-Jun-03 10:16
spook19-Jun-03 10:16 
GeneralUsing TypeConverters Pin
spook18-Jun-03 3:38
spook18-Jun-03 3:38 
GeneralRe: Using TypeConverters Pin
Anonymous30-Jun-03 13:27
Anonymous30-Jun-03 13:27 
GeneralRe: Using TypeConverters Pin
Naqsh5-Oct-03 15:23
sussNaqsh5-Oct-03 15:23 
GeneralList of Values Pin
karuppasamy natarajan13-May-03 4:22
karuppasamy natarajan13-May-03 4:22 
GeneralRe: List of Values Pin
Member 22727328-May-03 4:30
Member 22727328-May-03 4:30 
GeneralRe: List of Values Pin
Jeff Bramwell28-May-03 7:32
Jeff Bramwell28-May-03 7:32 
GeneralRe: List of Values Pin
Member 22727328-May-03 8:21
Member 22727328-May-03 8:21 
GeneralRe: List of Values Pin
Rajesh Sadashivan28-May-03 13:26
Rajesh Sadashivan28-May-03 13:26 
GeneralTony, you ROCK!!!!!!!!!!!! :-D Thanks! -NT Pin
spazzzmo6-May-03 13:10
spazzzmo6-May-03 13:10 
QuestionHow to display multilayer category??? Pin
chenjian2-May-03 21:32
chenjian2-May-03 21:32 
GeneralLinked labels in the PropertyGrid Pin
Raphy30-Apr-03 4:58
Raphy30-Apr-03 4:58 
QuestionRequired / optional properties rendered differently? Pin
gapcio6-Apr-03 18:07
gapcio6-Apr-03 18:07 
AnswerRe: Required / optional properties rendered differently? Pin
Tony Allowatt8-Apr-03 3:01
Tony Allowatt8-Apr-03 3:01 
AnswerRe: Required / optional properties rendered differently? Pin
totaljerk20-Oct-03 22:31
susstotaljerk20-Oct-03 22:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.