Creating a custom type converter part 3: Types to string

I have discussed creating type converters a few times on this blog, and recently came across another use for them. I have a .NET Standard library that uses a variety of struct instances that are sometimes properties of concrete classes. I also have a .NET Framework 4.7.2 demonstration application to work with the library that uses the PropertyGrid control to provide easy editing facilities.

Note: While in this article I'm talking about using type converters purely in relation for use with the PropertyGrid control, this isn't their only purpose (just as well otherwise they wouldn't be a part of .NET Standard I assume). You can learn more about the TypeConverter class on MSDN.

Why not ExpandableObjectConverter

In a previous article, I noted you could use the ExpandableObjectConverter type to provide a reasonably decent editing experience for basic objects out the box. However, this doesn't work for structs, usually for one of two reasons

  1. A well designed struct is generally immutable
  2. A struct is a value type, meaning that the PropertyGrid is actually editing a copy of the value, not the original

There is also a related issue - the PropertyGrid will default to calling ToString on the source type. If you haven't overrode ToString, then this will default to the name of the type but even if you have it is possible that you followed the conventions of similar types and return a list of names and values, great for debugging purposes but less so from the end user in a consumer application.

In short, there isn't an "easy fix" to allow easy editing of value types. But the solution isn't onerous or convoluted.

Creating a custom Type Converter

For the purposes of this article, I'm going to be working with a read-only struct named UserTuple, using the following definition

[TypeConverter(typeof(UserTupleConverter))]
struct UserTuple
{
  UserTuple(double x, double y, double z, double w)

  double X { get; }
  double Y { get; }
  double Z { get; }
  double W { get; }

  static UserTuple Parse(string value);
  bool TryParse(string value, out UserTuple result);
  string ToString() => "{X=" + _x + ",Y=" + _y + ",Z=" + _z + ",W=" + _w + "}";
}

This in turn is used by the fictitious UserClass type.

class UserClass : INotifyPropertyChanged
{
  event PropertyChangedEventHandler PropertyChanged;
  
  UserTuple TupleA { get; set; }
  UserTuple TupleB { get; set; }

  void OnPropertyChanged(string propertyName)
}

I have also created a UserTupleConverter class that inherits from TypeConverter and is bound to the UserTuple type via the TypeConverter attribute.

class UserTupleConverter : TypeConverter
{

}

The full types are in the available demonstration download.

I originally covered the basics of creating a type converter in parts 1 and 2 of this series. However, to make this article standalone I'll re-cover the basics.

At the most basic level, we have a class that inherits from TypeConverter and we override either the CanConvertFrom and ConvertFrom method pair, or the CanConvertTo and ConvertTo pair, or both.

The ConvertFrom method is used to create an instance of our type from another value - often (as in this example), a string.

The ConvertTo method is used to take an instance of our type and convert it to another value. Again, this example is going to use a string but you can do many things - including design time code generation.

Although I won't cover it here, there are some more advanced features you can do, for example making your type "expandable" so that you can edit individual properties, or providing pre-defined standard values.

Converting a String into a Value

To provide our string to UserTuple functionality, we first override CanConvertFrom and make sure we return true if the sourceType is string.

For performing the actual conversion, we override ConvertFrom, see if the passed value is a string and if so call our Parse function to perform the conversion.

Note that I am calling Parse and not TryParse. This is so if the user enters an invalid string, they'll get a (hopefully!) tailored exception message - otherwise the exception will be worded similar to "UserTupleConverter cannot convert from System.String.", which is clearly incorrect.

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)
{
  object result;

  if (value is string s && !string.IsNullOrEmpty(s))
  {
    result = UserTuple.Parse(s);
  }
  else
  {
    result = null;
  }

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

That provides the first part of the functionality we need - I can now type in a separated list of values and have a brand new value created and assigned, thus neatly bypassing both of the problems outlined in the list at the start of the article.

However, as you can see in the animation it is hardly a neat process due to the inclusion of all the additional data from ToString.

Converting a Value into a String

To clean up the raw text displayed in the PropertyGrid, we can make use of the CanConvertTo and CanConvertTo methods. I touched on these in a previous post regarding generating design time code but glossed over the string aspects.

It seems you don't need to override CanConvertTo for the string type as the base TypeConvertor class has a default ToString implementation, but I choose to include it below for completeness.

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
  return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}

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

  if (value is UserTuple tuple && destinationType == typeof(string))
  {
    result = string.Format("{0}, {1}, {2}, {3}", tuple.X, tuple.Y, tuple.Z, tuple.W);
  }
  else
  {
    result = base.ConvertTo(context, culture, value, destinationType);
  }

  return result;
}

First I test to see if the value is a UserTuple and if it is I return a manually constructed comma separated list of the four properties.

I'm not actually sure what best practice is in this situation. Certainly, I wouldn't want the parsing functionality part of this class as it has definite use outside of it, hence having Parse and TryParse methods on the core type. But for this "simpler" string representation I have no idea and have chosen to keep it a private part of the converter class.

And that's it - with this simple converter in place, my UI behaves exactly as I wanted.

Downloading the demonstration

A demonstration application is available from the link below.

Note that as always, this is demonstration code and therefore may elide extended details or contain less than stellar functionality (for example the parsing of the UserTuple value is neither efficient or locale aware).

Downloads

Filename Description Version Release Date
CustomTypeConverter3Strings.zip
  • sha256: 8dbbf6fd753aea44f5184a1d839af54fe47d9e1acf64ff7c018e22024eca9afe

Sample project for the Creating a custom type converter part 3: Types to string blog post.

21/07/2019 Download

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

Leave a Comment

While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?

Styling with Markdown is supported