Handling Intervals – the XAML Way
Continuing On
In my last post (read it here) I looked at using XAML for the configuration instead of NetReflector and its custom attributes. Rob Relyea from the XAML team at Microsoft saw my post and made a couple of suggestions (read his post here). One of these suggestions was using a TypeConverter to handle the conversion from a string to a value (I was using a MarkupExtension.)
Now the TypeConverter class has been around in .NET for a long time, but I have never actually used one. They are what gives the nice conversion from a value like “1cm” or “1in” to the correct number of pixels. The XAML readers use them extensively to handle converting from a string to another value type. So I thought I’d have a quick attempt at writing one.
The basis for writing a converter is to inherit from TypeConverter – nothing too hard about that – and then overriding the desired methods. To deserialise values we need to override CanConvertFrom() and ConvertFrom(), while to serialise we need to override CanConvertTo() and ConvertTo().
The two CanConvert… methods are the checks to see if the value type can be converted one way or the other. These need to be override to return true if the source/target type is a string (since this is what XAML works with.) The Convert… methods do the actual hard work of the conversion.
The final piece of the puzzle is to register the new converter so the XAML reader (or writer) will use it. This is achieved by adding a TypeConverterAttribute in the relevant place. From what I understand it can be added to either a property (XamlReader/XamlServices does not handle fields) or to a class. If used at a property level it will only be used for that property – so it is good for pre-existing types (e.g. in the BCL, etc.) or for a class that can be converted from many different values (e.g. integers, etc.) In contrast, applying it at the class level means it will be used for every occurrence of that class – unless it has been overridden at the property level.
In the end, I decided to add a new class with the converter applied to it. This saves having to apply the attribute every time we want to use the converter.
The XAML
So, my XAML for dealing with time periods changes from:
<tasks:Executable Timeout="{Time 1, Unit=Hour}" />
to:
<tasks:Executable Timeout="1h"/>
This also gives some more flexibility in that it allows for multiple units, e.g.:
<tasks:Executable Timeout="1h30m">
So in the end, it is not only more concise, it is more flexible!
The Solution
Here is how I did this. The converter class looks like this:
public class TimeIntervalConverter
: TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var stringValue = value as string;
if (stringValue == null)
{
throw new ArgumentException("value must be a string", "value");
}
var interval = new TimeInterval();
// Parsing logic omitted
// Return the converted interval
return interval;
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var timeValue = value as TimeInterval;
if (timeValue == null)
{
throw new ArgumentException("value must be a TimeValue", "value");
}
if (destinationType != typeof(string))
{
throw new ArgumentException("destinationType must be string", "destinationType");
}
timeValue = timeValue.Normalise();
return ((timeValue.Days > 0 ? timeValue.Days.ToString() + "d " : string.Empty) +
(timeValue.Hours > 0 ? timeValue.Hours.ToString() + "h " : string.Empty) +
(timeValue.Minutes > 0 ? timeValue.Minutes.ToString() + "m " : string.Empty) +
(timeValue.Seconds > 0 ? timeValue.Seconds.ToString() + "s" : string.Empty)).TrimEnd();
}
}
I have omitted the actual logic for the parsing, since that is pretty straight-forward. The main fun is around the implementation of the TypeConverter methods. I am being paranoid and double-checking the arguments – this is probably not needed as I imagine the XamlReader will not pass in invalid parameters (especially if it is using the CanConvert… methods!)
Since I added a new class to hold the values, here it is:
[TypeConverter(typeof(TimeIntervalConverter))]
public class TimeInterval
{
public double Days { get; set; }
public double Hours { get; set; }
public double Minutes { get; set; }
public double Seconds { get; set; }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public double TotalSeconds
{
get
{
return this.Days * 86400 +
this.Hours * 3600 +
this.Minutes * 60 +
this.Seconds;
}
}
}
This is even simpler – the TypeConverterAttribute at the top associates the converter, plus the attributes. I also added a helper property to convert to the total number of second, plus I marked it as hidden so the XamlReader won’t process it.
A Quick Thanks
Thanks to Rob Relyea for pointing this out – it’s good to see someone from Microsoft actively looking in the community to see what is happening.