Given a base class Color
with at least two sub-types, RgbColor
and CmykColor
:
abstract partial class Color { }
sealed class RgbColor : Color
{
public byte R { get; set; }
public byte G { get; set; }
public byte B { get; set; }
}
sealed class CmykColor : Color
{
public byte C { get; set; }
public byte M { get; set; }
public byte Y { get; set; }
public byte K { get; set; }
}
and some type that I am going to (de-) serialize to/from XAML with .NET 4's System.Xaml.XamlServices
:
class Something
{
public Color Color { get; set; }
}
I would like to be able to abbreviate just RGB colors on the XAML side like this:
<Something Color="#010203" />
instead of having to type everything out:
<Something>
<Something.Color>
<RgbColor R="1" G="2" B="3" />
</Something.Color>
</Something>
This could be easily done with a TypeConverter
. (Find my current implementation at the end of this question.) The problem is that I don't need, nor want, a special abbreviation syntax for other sub-types of Color
, such as CmykColor
.
How can I write a TypeConverter
for Color
that works only for one of its sub-types, RgbColor
?
(I have already tried to write a TypeConverter
specifically for RgbColor
instead of for Color
, but the XAML serializer doesn't appear to use it when it encounters a Color
property.)
// using System;
// using System.ComponentModel;
// using System.Globalization;
[TypeConverter(typeof(ColorConverter))]
partial class Color { }
sealed class ColorConverter : 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(string) || base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
// omitted for brevity's sake
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
Debug.Assert(value is Color);
if (destinationType == typeof(string))
{
if (value is RgbColor)
{
var color = (RgbColor)value;
return string.Format("#{0:x2}{1:x2}{2:x2}", color.R, color.G, color.B);
}
else
{
throw new NotSupportedException(); // ?
}
}
else
{
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
It turns out there is a different type of converter that can be used for just that: ValueSerializer
. Value serializers differ from type converters in two key ways:
CanConvert...
stage. (That is the important bit.)The main problem I've had with value serializers is having them invoked by XamlServices
.
Here's my solution:
First, you must apply not only a value serializer, but also a type converter to the base class:
// both types of converters must be applied!
[TypeConverter(typeof(ColorConverter))]
[ValueSerializer(typeof(ColorValueSerializer))]
partial class Color { }
Without the type converter attribute, the value serializer will never get invoked!
Next, the type converter must be written so that it only converts from strings (what MSDN calls "the load path"):
sealed class ColorConverter : TypeConverter
{
public override bool CanConvertFrom(..., Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(...);
}
public override bool CanConvertTo(..., Type destinationType)
{
return destinationType != typeof(string) && base.CanConvertTo(...);
// the above != is not a typo! We must not let
// the type converter do the conversion to string.
}
public override object ConvertFrom(...)
{
// parse a #rrggbb string and return the corresponding RgbColor
}
}
Finally, let the value serializer deal with conversions to string (the "save path"):
sealed class ColorValueSerializer : ValueSerializer
{
public override bool CanConvertToString(object value, ...)
{
return value is RgbColor;
}
public override string ConvertToString(object value, ...)
{
// convert RgbColor into "#{r}{g}{b}" string
}
}