I have accepted Adolfo Perez's answer, as it was his ideas that led me to a workable solution, but I am posting here some details of exactly what I did, for anyone else who may be trying to achieve something similar.
Adolfo suggested changing the type of the problematic property from string
to object
in the hope that it would fool the XAML serialization process into using my custom TypeConverter
or ValueSerializer
implementations. In fact, this approach did not work, but it led me to experiment with using a new custom type to store the binary string data.
Since the CLR string
type is sealed, it is not possible to sub-class it, but a similar result can be achieved by creating a custom type that encapsulates a string value and the logic necessary to convert it to/from Base64, as well as implicit conversion to/from string
which enables it to be used as a plug-in replacement for the CLR string
type. Here's my implementation of this custom type:
/// <summary>
/// Implements a string type that supports XAML serialization of non-printable characters via an associated type converter that converts to Base64 format.
/// </summary>
[TypeConverter(typeof(BinaryStringConverter))]
public class BinaryString
{
/// <summary>
/// Initializes a new instance of the <see cref="BinaryString"/> class and populates it with the passed string value.
/// </summary>
/// <param name="value">A <see cref="string"/> that represents the value with which to populate this instance.</param>
public BinaryString(string value)
{
Value = value;
}
/// <summary>
/// Gets the raw value of this instance.
/// </summary>
public string Value { get; private set; }
/// <summary>
/// Implements an implicit conversion from <see cref="string"/>.
/// </summary>
/// <param name="value">A <see cref="string"/> that represents the value to convert.</param>
/// <returns>A new <see cref="BinaryString"/> that represents the converted value.</returns>
public static implicit operator BinaryString(string value)
{
return new BinaryString(value);
}
/// <summary>
/// Implements an implicit conversion to <see cref="string"/>.
/// </summary>
/// <param name="value">A <see cref="BinaryString"/> that represents the value to convert.</param>
/// <returns>The <see cref="string"/> content of the passed value.</returns>
public static implicit operator string(BinaryString value)
{
return value.Value;
}
/// <summary>
/// Returns the value of this instance in <c>Base64</c> format.
/// </summary>
/// <returns>A <see cref="string"/> that represents the <c>Base64</c> value of this instance.</returns>
public string ToBase64String()
{
return Value == null ? null : Convert.ToBase64String(Encoding.UTF8.GetBytes(Value));
}
/// <summary>
/// Creates a new instance from the passed <c>Base64</c> string.
/// </summary>
/// <param name="value">A <see cref="string"/> that represent a <c>Base64</c> value to convert from.</param>
/// <returns>A new <see cref="BinaryString"/> instance.</returns>
public static BinaryString CreateFromBase64String(string value)
{
return new BinaryString(value == null ? null : Encoding.UTF8.GetString(Convert.FromBase64String(value)));
}
}
One advantage of using a custom type in this role is that the associated type converter can then be applied directly at the class level, rather than decorating individual properties in the consuming code. Here's the type converter:
/// <summary>
/// Implements a mechanism to convert a <see cref="BinaryString"/> object to or from another object type.
/// </summary>
public class BinaryStringConverter : TypeConverter
{
/// <summary>
/// Returns whether this converter can convert the object to the specified type, using the specified context.
/// </summary>
/// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
/// <param name="destinationType">A <see cref="Type"/> that represents the type you want to convert to.</param>
/// <returns><c>true</c> if this converter can perform the conversion; otherwise, <c>false</c>.</returns>
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
/// <summary>
/// Returns whether this converter can convert an object of the given type to the type of this converter, using the specified context.
/// </summary>
/// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
/// <param name="sourceType">A <see cref="Type"/> that represents the type you want to convert from.</param>
/// <returns><c>true</c> if this converter can perform the conversion; otherwise, <c>false</c>.</returns>
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
/// <summary>
/// Converts the given value object to the specified type, using the specified context and culture information.
/// </summary>
/// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
/// <param name="culture">A <see cref="CultureInfo"/>. If null is passed, the current culture is assumed.</param>
/// <param name="value">The <see cref="object"/> to convert.</param>
/// <param name="destinationType">A <see cref="Type"/> that represents the type you want to convert to.</param>
/// <returns>An <see cref="object"/> that represents the converted value.</returns>
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
return ((BinaryString)value).ToBase64String();
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Converts the given object to the type of this converter, using the specified context and culture information.
/// </summary>
/// <param name="context">An <see cref="ITypeDescriptorContext"/> that provides a format context.</param>
/// <param name="culture">A <see cref="CultureInfo"/>. If null is passed, the current culture is assumed.</param>
/// <param name="value">The <see cref="object"/> to convert.</param>
/// <returns>An <see cref="object"/> that represents the converted value.</returns>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string)
{
return BinaryString.CreateFromBase64String((string)value);
}
return base.ConvertFrom(context, culture, value);
}
}
With these two elements in place, the consuming code is almost as before - just a little simpler, as it no longer requires any explicit type conversion or value serialization attributes ...
public class MyClass
{
public BinaryString Name { get; set; }
}
... and the serialization process is as before, except that it now properly supports non-printable characters in the assign property value:
var data = new MyClass
{
Name = "My\0Object"
};
var xaml = XamlServices.Save(data);
var deserialized = XamlServices.Parse(xaml) as MyClass;
Note that I have reverted to using XamlServices.Save()
and XamlServices.Parse()
in this example, but this technique works equally well with XamlWriter.Save()
and XamlReader.Parse()
.