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.