|
A struct TypeConverter
Exposing data in .NET is really simple - just throw your class at the
PropertyGrid control, and it does the rest. Well, it's almost that simple,
unless you want to provide behaviour like that of the Point class (and others),
as they are value types and not reference types. The difference comes when you
display the object on screen and attempt to edit its properties - after an edit
the object is recreated using the default constructor, and so all modifications
are lost.
All is not lost however if you know a little about how .NET exposes types in
the property grid, as what you need is a type converter which will tell the
framwork how to expose the object in the property grid. This article will show
how this can be accomplished. The end result is shown below...

This object displays the properties of the struct (You, Me),
orders them according to how I defined the sort criteria, permits you to type in
text on the 'Thingy' line, and creates the code behind if this object is used on
a user control.
Step 1 - Create the struct...
This example shows a simple struct which exposes two members to the property
grid. You can of course attribute these with [Description] and [Category]
attributes as appropriate.
/// <summary> /// A test struct to be exposed as a
property ///
</summary> [TypeConverter(typeof(DooferConverter))] public struct Doofer { ///
<summary> ///
Construct the struct ///
</summary> /// <param
name="me"></param> /// <param
name="you"></param> public Doofer ( int me , string you ) { _me = me ; _you = you ; }
///
<summary> ///
A value ///
</summary> public int Me { get { return _me ;
} set { _me = value ;
} }
///
<summary> ///
Another value ///
</summary> public string You { get { return _you ;
} set { _you = value ;
} }
private
int _me ; private
string _you ; }
|
Step 2 - Create the TypeConverter
This step's a little more involved, so I'll explain the code after showing
it.
/// <summary> /// Class used as a converter for the Doofer
class /// </summary> public class DooferConverter : TypeConverter { ///
<summary> ///
Can the framework call
CreateInstance? ///
</summary> /// <param
name="context"></param> ///
<returns></returns> public override bool GetCreateInstanceSupported (
ITypeDescriptorContext context ) { //
Yes! return true ; }
///
<summary> ///
Satisfy the CreateInstance call by reading data from the propertyValues
dictionary ///
</summary> /// <param
name="context"></param> /// <param
name="propertyValues"></param> ///
<returns></returns> public override object CreateInstance ( ITypeDescriptorContext context ,
IDictionary propertyValues ) { // Get values of "Me" and "You" properties from the dictionary,
and // create a new instance which is returned to the
caller return new Doofer ( (int)propertyValues["Me"] , propertyValues["You"] as string ) ; }
///
<summary> ///
Does this struct expose properties? ///
</summary> /// <param
name="context"></param> ///
<returns></returns> public override bool GetPropertiesSupported ( ITypeDescriptorContext context ) { //
Yes! return true ; }
///
<summary> ///
Return the properties of this struct ///
</summary> /// <param
name="context"></param> /// <param
name="value"></param> /// <param
name="attributes"></param> ///
<returns></returns> public override PropertyDescriptorCollection GetProperties ( ITypeDescriptorContext context
, object value , Attribute[] attributes ) { PropertyDescriptorCollection properties = TypeDescriptor.GetProperties ( value , attributes )
;
// Putz around with the property collection if you like - here
I'm changing the display
order. string[] sortOrder = new string[2]
;
sortOrder[0] = "You" ; sortOrder[1] = "Me" ;
// Return a sorted list of
properties return properties.Sort ( sortOrder ) ; }
///
<summary> ///
Check what this type can be created
from ///
</summary> /// <param
name="context"></param> /// <param
name="sourceType"></param> ///
<returns></returns> public override bool CanConvertFrom ( ITypeDescriptorContext context ,
System.Type sourceType ) { // Just strings for
now bool canConvert = ( sourceType == typeof ( string ) )
;
if ( !canConvert ) canConvert = base.CanConvertFrom ( context , sourceType )
;
return canConvert ; }
///
<summary> ///
Convert from a specified type to a Doofer, if
possible ///
</summary> /// <param
name="context"></param> /// <param
name="culture"></param> /// <param
name="value"></param> ///
<returns></returns> public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value) { string sValue = value as string ; object retVal = null ;
if ( sValue != null ) { // Check that the string actually has something in
it... sValue = sValue.Trim ( )
;
if ( sValue.Length != 0 ) { // Parse the
string if ( null == culture ) culture = CultureInfo.CurrentCulture ;
// Split the string based on the cultures list
separator string[] parms
= sValue.Split ( new char[]
{ culture.TextInfo.ListSeparator[0] } )
;
if ( parms.Length == 2 ) { // Should have an integer and a
string. int me
= Convert.ToInt32 ( parms[0] )
; string you = parms[1]
;
// And finally create the
object retVal = new Doofer ( me , you )
; } } } else retVal = base.ConvertFrom ( context , culture , value )
;
return retVal ; }
///
<summary> ///
Check what the type can be converted
to ///
</summary> /// <param
name="context"></param> /// <param
name="destinationType"></param> ///
<returns></returns> public override bool CanConvertTo ( ITypeDescriptorContext context ,
System.Type destinationType ) { // InstanceDescriptor is used in the code
behind bool canConvert = ( destinationType == typeof ( InstanceDescriptor ) )
;
if ( !canConvert ) canConvert = base.CanConvertFrom ( context , destinationType )
;
return canConvert ; }
///
<summary> ///
Convert to a specified type ///
</summary> /// <param
name="context"></param> /// <param
name="culture"></param> /// <param
name="value"></param> /// <param
name="destinationType"></param> ///
<returns></returns> public override object ConvertTo(System.ComponentModel.ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value, System.Type destinationType) { object retVal = null ; Doofer doofer = (Doofer) value ;
// If this is an instance
descriptor... if ( destinationType == typeof ( InstanceDescriptor )
) { System.Type[] argTypes = new System.Type[2]
;
argTypes[0] = typeof ( int )
; argTypes[1] = typeof ( string )
;
// Lookup the appropriate Doofer
constructor ConstructorInfo constructor = typeof(Doofer).GetConstructor ( argTypes )
;
object[] arguments = new object[2]
;
arguments[0] = doofer.Me ; arguments[1] = doofer.You ;
// And return an instance descriptor to the caller. Will fill
in the CodeBehind stuff in
VS.Net retVal = new InstanceDescriptor ( constructor , arguments )
; } else if ( destinationType == typeof ( string )
) { // If it's a string, return one to the
caller if ( null == culture ) culture = CultureInfo.CurrentCulture ;
string[] values
= new string[2]
;
// I'm a bit of a culture vulture - do it
properly! TypeConverter numberConverter = TypeDescriptor.GetConverter ( typeof ( int ) )
;
values[0] = numberConverter.ConvertToString ( context , culture , doofer.Me )
; values[1] = doofer.You ;
// A useful method - join an array of strings using a
separator, in this instance the culture specific
one retVal = String.Join ( culture.TextInfo.ListSeparator + " " , values )
; } else retVal = base.ConvertTo ( context , culture , value , destinationType )
;
return retVal ; } }
|
As you can see there's a fair amount of code here, so I'll explain each part
in turn.
A TypeConverter consists of a number of methods, generally paired together.
So for instance the first two methods,
GetCreateInstanceSupported and CreateInstance
form a pair, as do the other methods. I have grouped them together for easy
reading. Here's an overview of the methods.
| Method |
Description |
| GetCreateInstanceSupported |
Return true if you implement the
CreateInstance method. This is necessary for a struct, as each time the struct
is changed a new copy is created by the framework. If you do not support
CreateInstance, your struct's default constructor will be called, so any
modifications the user has made on screen will be wiped. |
| CreateInstance |
Called when changes have been made to the struct in
the property grid.
The propertyValues object is a dictionary containing all
public properties of the struct. You can therefore extract these and create a
new instance of your struct as appropriate. In my example, I read the 'Me' and
'You' values from the dictionary and return a newly created
struct. |
| GetPropertiesSupported |
Return true if there are properties
of this struct that should be displayed in the property grid. If you don't
return true, the object will appear without the expanding '+' to the left hand
side, and so you won't be able to edit the properties. |
| GetProperties |
This method is called when the property grid is about
to display your object, to find all sub-properties of the object.
You need to return a PropertyDescriptor collection from this method, as this
is used to elicit what to display. The easiest way to create an appropriate
PropertyDescriptorCollection is to utilise the static
TypeDescriptor.GetProperties method. This uses reflection to find all public
properties and constructs the collection for you.
In this example I've also shown that you can alter this collection as
appropriate before returning it to the caller. In this example I've changed the
sort order which is used when displaying the properties in the grid. You can of
course make any modifications to the returned collection. |
| CanConvertFrom |
Called when the PropertyGrid is attempting to convert
from one representation to the struct. In this case, I check if the
representation is 'string', and if so return true. It's always good practice to
call the base classes CanConvertFrom method if you don't directly support the
conversion.
Again, this method goes hand in hand with the following,
ConvertFrom. |
| ConvertFrom |
Called to actually convert from one object to the
struct.
Here I test of the source object is a string, and if so
parse the data (using the culture specific list separator character). The
runtime calls this when you type a value into the property grid, so it's always
worthwhile adding support to create your struct from a string, as the user gets
a better (and more standard) UI experience. |
| CanConvertTo |
Called to check if the object can be converted to a
destination type.
There's no need to check for string explicitly, as the base
class CanConvertTo method will return true for this conversion. Here I'm
explicitly supporting conversion to an InstanceDescriptor,
described in the following section. |
| ConvertTo |
Convert the struct to a different representation.
Here I support two representations, string and
InstanceDescriptor.
The string representation is simple - just return the values of the struct
separated by a comma.
The InstanceDescriptor is a little more tricky to understand. When you are
tinkering with an object in the property grid, from within Visual Studio .NET,
the IDE calls the CanConvertTo and ConvertTo methods with InstanceDescriptor to
produce the code in your source file. So, here I construct an array to pass to
the structs' constructor, find the constructor itself by using the
Type.GetConstructor method, and finally return an instance descriptor created
from the constructor information and the parameters used for the
constructor.
The net effect is that when you alter the properties of the object within the
IDE, the following code is automagically emitted for you...
#region Windows
Form Designer generated code ///
<summary> /// Required method for
Designer support - do not modify /// the
contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.userControl11 = new UserControlTest.UserControl1(); this.SuspendLayout(); //
//
userControl11 //
this.userControl11.Location = new System.Drawing.Point(16, 16); this.userControl11.Name = "userControl11"; this.userControl11.Size = new System.Drawing.Size(240,
152); this.userControl11.TabIndex = 0;
this.userControl11.Thingy = new UserControlTest.Doofer(45, "Me");
| // // Form1 // this.AutoScaleBaseSize = new System.Drawing.Size(5,
13); this.ClientSize = new System.Drawing.Size(292,
271); this.Controls.AddRange(new System.Windows.Forms.Control[]
{ this.userControl11}); this.Name = "Form1"; this.Text = "Form1"; this.ResumeLayout(false);
} #endregion
| |
Summary
Using a TypeConverter is the best way to integrate your structs' (and all
objects for that matter) into the Visual Studio UI. The design time experience
without a converter is far inferior to one that has a converter defined.
|