Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Converting values to string and back with Type Converters


:P
On this page:

 

It was pointed out to me by Neil Colvin today that my wwAppConfiguration class could be a lot more flexible by supporting TypeConverters for performing the string conversions that are used to serialize properties into config file strings.

 

I hadn’t really given this much thought but Neil pointed out that if TypeConverter were supported, many more of the .NET types could be used as part of the configuration class. For example, you can attach a Font object to your configuration and it persists and restores properly through this config file serialization. This is pretty much the same thing that the Property Sheet in Visual Studio does when it displays font information as Verdana, 12 pt, Bold in the designer in its ‘summary’ format. More useful though, you could store things like Rectangle structures for window sizes in WinForms for example, to easily remember window positions which is kinda handy. What’s nice is good chunk of .NET types support Type Converters so you get a bunch more functionality from string conversions.

 

But more importantly it also gives you a chance to create your own type converters for complex types of your own so you can in fact store complex objects as part of your configuration class. Now you probably won’t want to do this with really complex or deeply nested objects - it works but would be pretty ugly and there are some limitations in what you can store inside of an XML attribute of a Config file. But it does allow for additional functionality that was not previously available.

 

I hadn’t done a whole lot with type converters in generic code – I’d worked with them a bit while working on some custom Web Controls a while back, so I had to look up and recall how this process works. In the process I also found that type converters can be pretty handy to convert standard .NET types much more easily and consistently since they all support type converters. If you’ve read my articles or check in this BLOG occasionally you’ve probably seen code that does string/object conversions and there’s always a big conditional block of code that deals with converting each known type to a string and back. Well, with type converters this sort of code can be simplified a bit:

 

public static string TypedValueToString(object RawValue,CultureInfo Culture)

{

      Type ValueType = RawValue.GetType();

      string Return = null;

 

      // Any type that supports a type converter

      System.ComponentModel.TypeConverter converter =

            System.ComponentModel.TypeDescriptor.GetConverter( ValueType );

     

      if (converter != null && converter.CanConvertTo(typeof(string)) )

            Return = converter.ConvertToString(null,Culture, RawValue );

      else

            // Last resort - just call ToString() on unknown type

            Return = RawValue.ToString();

 

      return Return;

}

 

 

public static object StringToTypedValue(string SourceString, Type TargetType,

                                        CultureInfo Culture )

{

 

      object Result = null;

 

      System.ComponentModel.TypeConverter converter =

                    System.ComponentModel.TypeDescriptor.GetConverter(TargetType);

      if (converter != null && converter.CanConvertFrom(typeof(string)) )

            Result = converter.ConvertFromString(null,Culture, SourceString );

 

      return Result;

}

 

These two simple functions can a replace a fairly significant chunk of code. It may seem really simple to convert strings/objects, but if you need to do this right there a bunch of things you need to watch out for and it looks like a type converter can handle most of this for you with the above code.

 

Specifically you need to be careful with string persistence in regards to culture information. My original Configuration class actually suffered from this problem – if you switched current culture in your Web application on the fly (like I do based on browser settings) you need to make sure you always store numeric and date time values in the proper culture format. For configuration settings these values should be stored as InvariantCulture so a language switch won’t affect the data.  If you need to deal with Enums – those require special handling too. Bool  can often be represented in a bunch of different formats. And then there’s the issue of complex types if type converters are available. The above code handles all these scenarios.

 

Now, I’m not sure though what performance of this looks like. I suspect that using a type converter in this fashion is probably more expensive than directly looking at the type, so my actual routines I use in my utility library sticks with the big case structure for the most common known types and uses the type converter as the last resort. The full routines look more like this:

 

public static string TypedValueToString(object RawValue,CultureInfo Culture)

{

      Type ValueType = RawValue.GetType();

      string Return = null;

 

      if (ValueType == typeof(string) )

            Return = RawValue.ToString();

      else if ( ValueType == typeof(int) || ValueType == typeof(decimal) ||

            ValueType == typeof(double) || ValueType == typeof(float))

            Return = string.Format(Culture.NumberFormat,"{0}",RawValue);

      else if(ValueType == typeof(DateTime))

            Return =  string.Format(Culture.DateTimeFormat,"{0}",RawValue);

      else if(ValueType == typeof(bool))

            Return = RawValue.ToString();

      else if(ValueType == typeof(byte))

            Return = RawValue.ToString();

      else if(ValueType.IsEnum)

            Return = RawValue.ToString();

      else

      {

            // Any type that supports a type converter

            System.ComponentModel.TypeConverter converter =

                  System.ComponentModel.TypeDescriptor.GetConverter( ValueType );

            if (converter != null && converter.CanConvertTo(typeof(string)) )

                  Return = converter.ConvertToString( RawValue );

            else

                  // Last resort - just call ToString() on unknown type

                  Return = RawValue.ToString();

      }

 

      return Return;

}

 

public static object StringToTypedValue(string SourceString, Type TargetType, CultureInfo Culture )

{

 

      object Result = null;

 

 

      if ( TargetType == typeof(string) )

            Result = SourceString;

      else if (TargetType == typeof(int)) 

            Result = int.Parse( SourceString,

                       System.Globalization.NumberStyles.Integer, Culture.NumberFormat );

      else if (TargetType  == typeof(byte) ) 

            Result = Convert.ToByte(SourceString);                     

      else if (TargetType  == typeof(decimal)) 

            Result = Decimal.Parse(SourceString,System.Globalization.NumberStyles.Any, Culture.NumberFormat);

      else if (TargetType  == typeof(double))   

            Result = Double.Parse( SourceString,System.Globalization.NumberStyles.Any, Culture.NumberFormat);                   

      else if (TargetType == typeof(bool))

      {

            if (SourceString.ToLower() == "true" || SourceString.ToLower() == "on" || SourceString == "1")

                  Result = true;

            else

                  Result = false;

      }

      else if (TargetType == typeof(DateTime)) 

            Result = Convert.ToDateTime(SourceString,Culture.DateTimeFormat);

      else if (TargetType.IsEnum)

            Result = Enum.Parse(TargetType,SourceString);

      else  

      {

            System.ComponentModel.TypeConverter converter =

                      System.ComponentModel.TypeDescriptor.GetConverter(TargetType);

            if (converter != null && converter.CanConvertFrom(typeof(string)) )

                  Result = converter.ConvertFromString( SourceString );

            else 

            {

                  throw(new Exception("Type Conversion not handled in StringToTypedValue"));

            }

      }

 

      return Result;

}

 

Undoubtably TypeConverter performs these tasks internally in the stack on its own but doing it here is bound to save a few cycles. Since this is a fairly generic and low level routine this should pay off.

 

To create a custom type converter for your own classes involves creating your custom class and another class that implements a specific type converter. To associate the type with the type converter you use the TypeConverter attribute with a reference to the Converter type:

 

[TypeConverter(typeof(CustomCustomerTypeConverter)), Serializable()]

public class CustomCustomer

{

      public string Name = "Rick Strahl";

      public string Company = "West Wind";

      public decimal OrderTotal = 100.90M;

}

 

To implement the actual TypeConverter you have to implement custom logic that translates the content of the object to and from the types you want to support. In the above example I’m going to create a real simple type converter that persists and restores in a simple comma delimited list. In my configuration class the stored value in the .Config file will then look like this:

 

<add key="Customer" value="Rick Strahl,West Wind Technologies,109.22" />

 

There are a number of type converter objects available that you can inherit from – I’ll use the lowest level TypeConverter class here. Here’s the implementation:

 

public class CustomCustomerTypeConverter : System.ComponentModel.TypeConverter

{

     

      /// <summary>

      /// Allow only string conversions

      /// </summary>

      /// <param name="context"></param>

      /// <param name="destinationType"></param>

      /// <returns></returns>

      public override bool CanConvertTo(ITypeDescriptorContext context,

                                        Type destinationType)

      {

            if (destinationType == typeof(string) )

                  return true;

 

            return base.CanConvertTo (context, destinationType);

      }

 

      /// <summary>

      /// Allow only string conversions

      /// </summary>

      /// <param name="context"></param>

      /// <param name="sourceType"></param>

      /// <returns></returns>

      public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)

      {

            if (sourceType == typeof(string) )

                  return true;

 

            return base.CanConvertFrom (context, sourceType);

      }

 

 

      /// <summary>

      /// Convert into a string resource with a comma delimited list

      /// </summary>

      /// <param name="context"></param>

      /// <param name="culture"></param>

      /// <param name="value"></param>

      /// <param name="destinationType"></param>

      /// <returns></returns>

      public override object ConvertTo(ITypeDescriptorContext context,

                                    System.Globalization.CultureInfo culture,

                                    object value,

                                    Type destinationType)

      {

           

            CustomCustomer Cust = (CustomCustomer) value;

            if ( destinationType == typeof(string) )

                  return Cust.Name + "," + Cust.Company + "," +

                          string.Format( culture.NumberFormat,"{0}",Cust.OrderTotal);

 

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

      }

 

      /// <summary>

      /// Convert back into properties from a comma delimited list.

      /// Note the list is position specific.

      /// This code doesn't demonstrate proper error handling for

      /// manually invalidated values.

      /// </summary>

      /// <param name="context"></param>

      /// <param name="culture"></param>

      /// <param name="value"></param>

      /// <returns></returns>

      public override object ConvertFrom(ITypeDescriptorContext context,

                               System.Globalization.CultureInfo culture, object value)

      {                

     

            if (! (value is string) )

                  return base.ConvertFrom (context, culture, value );

 

            string Persisted = (string) value;

 

            CustomCustomer Cust = new CustomCustomer();

 

            if (Persisted != null || Persisted != "")

            {

                  string[] Fields = Persisted.Split(new char[1] {','});

 

                  Cust.Company = Fields[1];

                  Cust.Name = Fields[0];

                  Cust.OrderTotal = (decimal) wwUtils.StringToTypedValue(Fields[2],

                                                              Cust.OrderTotal.GetType());

            }

 

            return Cust;

      }

 

}

 

All that’s left to do is stick the class onto the configuration object as a property and now you have automatic persistence of this object as part of your configuration object. This is a bit of work, but it’s fairly straight forward.

 

Happy persistence to you!


The Voices of Reason


 

Rick Strahl's Web Log
October 15, 2006

# Switching CurrentCulture in ASP.NET Pages Gotchas - Rick Strahl's Web Log

Switching the CurrentCulture in ASP.NET is almost trivial to do, but beware of some of the side effects it might have on your applications.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024