TypeConverters and XAML

When we’re setting margins, backgrounds etc. in XAML we use string representations which get converted to the actual objects. For example

<Button Margin="0,3,0,3" />

in this example the string 0,3,0,3 is converted into a Margin object by the MarginConverter.

Strings are converted to types using TypeConverters. A simple example of one is listed below

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

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

   public override object ConvertFrom(ITypeDescriptorContext context, 
                            CultureInfo culture, object value)
   {
      string text = value as string;
      if (text == null)
      {
         return base.ConvertFrom(context, culture, value);
      }

      if (String.IsNullOrWhiteSpace(text))
      {
         return 0.0;
      }

      if (culture == null)
      {
         culture = CultureInfo.CurrentCulture;
      }

      double number;
      if (AbbreviatedNumeric.ValidateDouble(text, out number, culture))
         return number;

      return 0.0;
   }

   public override object ConvertTo(ITypeDescriptorContext context, 
                     CultureInfo culture, object value, Type destinationType)
   {
      if (destinationType != null && value is Double)
      {
         if (destinationType == typeof(string))
         {
            return value.ToString();
 	 }
      }
      return base.ConvertTo(context, culture, value, destinationType);
   }
}

So the above demonstrated a very simple TypeConverter that converts strings like “134m” into 134000000 or the likes of “10k” to 10000. The actual code for the conversion occurs in the AbbreviatedNumeric.ValidateDouble method which I’ll list at the end of this post but will exclude the tests just to save space. This is not an all encompassing converter, it will only convert k for thousands, m for millions and b for billions and also doesn’t handle multiple languages, but it’s here as an example.

Now let’s assume we’ve created some edit control which has an Amount dependency property which we want to allow the user to enter abbreviated numeric strings into. So the dependency property might look like

public Double Amount
{
   get { return (Double)GetValue(AmountProperty); }
   set { SetValue(AmountProperty, value); }
}

public static readonly DependencyProperty AmountProperty =
                    DependencyProperty.Register("Amount", typeof(Double), 
                    typeof(OnlineStatusControl), new PropertyMetadata(0.0));

To apply our type converter we simple add the TypeConverterAttribute to the Amount property as below

[TypeConverter(typeof(AbbreviatedNumberConverter))]
public Double Amount
{
   get { return (Double)GetValue(AmountProperty); }
   set { SetValue(AmountProperty, value); }
}

and finally when using this new control we can do the following

<Controls:AbbreviatedNumericEditor Amount="123m" />

The type converter is called on this and 123m is converted to 123000000 which is now stored as a Double in the dependency property Amount.

For completeness, here’s the simple AbbreviatedNumeric class

public static class AbbreviatedNumeric
{
   public static bool ValidateDouble(string value, out double? numeric, 
              CultureInfo cultureInfo = null)
   {
      double result;
      if(ValidateDouble(value, out result, cultureInfo))
      {
         numeric = result;
         return true;
      }
      numeric = null;
      return false;
   }

   public static bool ValidateDouble(string value, out double numeric, 
              CultureInfo cultureInfo = null)
   {	
      if (String.IsNullOrEmpty(value))
      {
         numeric = 0;
         return false;
      }

      if (Double.TryParse(value, out numeric))
      {
         return true;
      }
      if (value.Length > 0)
      {
         if (cultureInfo == null)
         {
	    cultureInfo = CultureInfo.CurrentCulture;
	 }

	 NumberFormatInfo numberFormat = cultureInfo.NumberFormat;
 	 if (value.Substring(0, 1) == numberFormat.NumberDecimalSeparator)
	 {
	    value = "0" + value;
	 }
	 if (Double.TryParse(value.Substring(0, value.Length - 1), 
                     NumberStyles.AllowLeadingWhite | 
                     NumberStyles.AllowTrailingWhite |                      
                     NumberStyles.AllowLeadingSign |
 		     NumberStyles.AllowDecimalPoint | 
                     NumberStyles.AllowThousands | 
		     NumberStyles.AllowExponent, cultureInfo, out numeric))
	 {
	    switch (Char.ToUpper(value[value.Length - 1]))
	    {
	        case 'B':
		   numeric = numeric * 1000000000;
		   break;
		case 'M':
		   numeric = numeric * 1000000;
		   break;
		case 'K':
		   numeric = numeric * 1000;
		   break;
		default:
		   return false;
	    }
            return true;
	 }
      }
      return false;
   }
}