Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating a Custom TypeConverter: Part 1 - Getting Started

0.00/5 (No votes)
30 Jul 2013 2  
How to use TypeConverter allowing complex objects to be edited as though they were simple strings

It is common practice to create a class that describes something, a person, a product - some entity or other. Your application may provide a sublime UI for editing these objects, or rely on something more basic such as a PropertyGrid. However, if you use this approach, you may find that some of your properties can't be edited. Yet examples abound of non-simple editing via the grid, such as colors, enumerations and image selection to name a few.

By making use of the TypeConverter and UITypeEditor classes, you can quite easily provide the ability to create richer editing support for your objects. This first article in this series will detail how to use TypeConverter allowing complex objects to be edited as though they were simple strings.

The Scenario

As with most of my articles, I'm starting with a real world example and a solid required. I need to store units of measurement, so for this I have a simple class that has a pair of properties describing a given measurement.

public class Length
{
  public override string ToString()
  {
    string value;
    string unit;

    value = this.Value.ToString(CultureInfo.InvariantCulture);
    unit = this.Unit.ToString();

    return string.Concat(value, unit);
  }

  public Unit Unit { get; set; }

  public float Value { get; set; }
}

A fairly standard class that simply has two properties along with a default (implicit) constructor. I'm also overriding ToString, as it's useful both for debugging purposes and for having something other than CustomTypeConverter1.Length displayed in the PropertyGrid.

And for the purposes of this demonstration, I have created a sample class which has three length properties.

internal class SampleClass
{
  public Length Length1 { get; set; }

  public Length Length2 { get; set; }

  public Length Length3 { get; set; }
}

Just for completeness sake, here's the Unit enum.

public enum Unit
{
  None,
  cm,
  mm,
  pt,
  px
}

Isn't that an ugly enum? For this example, it will suffice, but there is another article which describes an alternative approach.

First Steps

I've set up a sample project which binds an instance of our SampleClass to a PropertyGrid, with the Length1 property pre-set to 32px. When you run this project, you are left with a very unsatisfactory editing experience as you can't edit anything.

So, what can we do about this?

The TypeConverterAttribute Class

The TypeConverterAttribute allows you to associate your class with a type that can handle conversion of instances of your type to and from other objects. You can only have one occurrence of this attribute per type. As with a lot of these types of attributes, you can provide the conversion type one of two ways:

[TypeConverter(typeof(LengthConverter))]

Here, we pass in a type object, meaning the type has to be directly referenced by your project and distributed as a dependency.

[TypeConverter("CustomTypeConverter1.LengthConverter, CustomTypeConverter1")]

Another alternative is to use a direct string, as shown above. This string is the fully qualified type name, meaning it could be located in a differently assembly, but one that isn't referenced directly or flagged as a dependency.

Which one you use depends on your needs, but bear in mind no compile time checking can be done of the string version, so if you get the name wrong, you won't find out until you are unable to edit the type!

The ExpandableObjectConverter

This class is built into the .NET Framework and will provide a minimum of functionality at minimum cost.

[TypeConverter(typeof(ExpandableObjectConverter))]
public class Length
{

If we change the declaration of our Length class to be the above and run our sample, we get this:

The first property can now be expanded, and each property of the Length class can be individually set. However, there are two immediate problems with this approach:

  • Properties can only be edited one at a time, you can't combine values via the root property.
  • Properties with a null value (the second and third properties in the example screenshot) cannot be instantiated.

Again, depending on your requirements, this might be perfectly acceptable. In my case, it isn't, so on with the custom converter!

Writing a Custom Converter

In order to create a custom converter, you need to have a class which inherits from TypeConverter. At a minimum, you would override the CanConvertFrom and ConvertFrom methods.

Here's a sample converter for our simple Length class:

public class LengthConverter : TypeConverter
{
  public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  {
    return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
  }

  public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  {
    string stringValue;
    object result;

    result = null;
    stringValue = value as string;

    if (!string.IsNullOrEmpty(stringValue))
    {
      int nonDigitIndex;

      nonDigitIndex = stringValue.IndexOf(stringValue.FirstOrDefault(char.IsLetter));

      if (nonDigitIndex > 0)
      {
        result = new Length
        {
          Value = Convert.ToSingle(stringValue.Substring(0, nonDigitIndex)),
          Unit = (Unit)Enum.Parse(typeof(Unit), stringValue.Substring(nonDigitIndex), true)
        };
      }
    }

    return result ?? base.ConvertFrom(context, culture, value);
  }
}

So, what is this short class doing?

The first override, CanConvertFrom, is called when .NET wants to know if it can convert from a given type. Here, I'm saying "if you are a string, then yes I can convert" (or at least try!), otherwise it falls back and requests if the base converter can do the conversion. In most cases, that'll probably be a "no", but it's probably a good idea to leave it in regardless;

Now for the interesting method. ConvertFrom does the type conversion. I'm going to ignore the context parameter for now as I haven't had a need for it. You can use the culture parameter as a guide if you need to do any conversions such as numbers or dates. The key parameter, is value as this contains the raw data to convert.

  • The first thing this method does is see if value is a non-null non-empty string. (If you're using .NET 4 or above, you'd probably use the IsNullOrWhitespace method instead).
  • Next I try and find the index of the first letter character - the method assumes the input is in the form of <number><unit>.
  • If I find a letter, then I create a new Length object and use object initialization to set the Value property to be the first part of the string converted to a float, and Enum.Parse to set the Unit property using the latter part of the string. And that explains the horribly named enum. I'll still show you a better way though!

And that is all you need. Well almost, we need to change our class header:

[TypeConverter(typeof(LengthConverter))]
public class Length
{

Now when we run the sample project, we can directly type in a value into the different Length based properties and have them converted to the correct values, including creating new values.

Note that this example doesn't cover clearing a value - for example if you enter an empty string. You could return a new Length object in this case and then change the ToString method to return an empty string. Simply returning null from ConvertFrom doesn't actually work, so at the moment, I don't know the best method for accomplishing a value reset.

Error Handling

I haven't demonstrated error handling, firstly as this is a bare bones example, and also due to .NET providing it for you, at least in the case of the property grid. It will automatically handle the failure to convert a value. The disadvantage is the rather unhelpful error message. If you throw an exception yourself, the exception text you provide is displayed in the Details section of the dialog, allowing you to specifying a more succinct message.

Converting to a Different Data Type

As well as converting a type into our class, we can also use a type converter to convert our class into another type by overriding the ConvertTo method.

In this example, the Length class overrides the ToString method. I would still recommend doing that in additional to this next tip, but as with everything, it depends on your purpose. In this case, we can use the ConvertTo method to convert our Length object into a string.

public override object ConvertTo(ITypeDescriptorContext context, 
CultureInfo culture, object value, Type destinationType)
{
  Length length;
  object result;

  result = null;
  length = value as Length;

  if (length != null && destinationType == typeof(string))
    result = length.ToString();

  return result ?? base.ConvertTo(context, culture, value, destinationType);
}

As with all the methods you override, if you can't explicitly handle the passed values, then ask the base class to attempt to handle it. The above method shows how I check to ensure the value is a Length object, and then if the destinationType is a string, I simply return value.ToString(). Whatever is returned via this method will appear in the PropertyGrid, so use caution if you decide to return formatted strings - you'll need to handle them in ConvertFrom.

There is another, more useful, purpose for this override, but I'll defer that for the next article.

Summing Up

Adding a basic type converter is a very simple thing to do, and is something that can help enrich editing functionality, or even debugging (in lieu of an immediate window or scripting support, Cyotek products have a sample add-in which displays documents in a PropertyGrid for simple editing and querying). Even if you only go as far as adding the ExpandableObjectConverter attribute to a base class, it's more useful than nothing!

You can download the complete example from the link at the top of this post.

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