0

I am using XamlServices as a general-purpose serialization mechanism, as described here, here and here. Although this works very well for most scenarios, I am unclear how to get it to serialize string property values that contain non-printable characters (specifically null characters).

Here's a simple example of a class that I might wish to serialize:

public class MyClass
{
    public string Value { get; set; }
}

If I create an instance of that class (note the null character in the assigned property value) ...

var instance = new MyClass
{
    Value = "Some\0Value",
};

... and serialize it using XamlServices ...

var xaml = XamlServices.Save(instance);

... it throws the exception hexadecimal value 0x00, is an invalid character.

I guess this indicates that XAML serialization doesn't natively support binary string data, so I am happy to convert the string to an encoded form (e.g. Base64) during serialization.

I have attempted achieve this conversion by creating a custom XAML value serializer that implements Base64 encoding and applying it to the relevant class properties ...

public class MyClass
{
    [ValueSerializer(typeof(Base64ValueSerializer))]
    public string Value { get; set; }
}

... but its conversion methods never get invoked, presumably because the XAML serialization mechanism thinks that no custom serializer is required when serializing a string property.

Similarly, I created a custom type converter with the same goal in mind. Once again, it's ConvertTo method does not get called during serialization although, interestingly, its ConvertFrom method does get called during deserialization and correctly populates the target property from Base64-encoded string data.

I am looking for ideas how to get XamlServices to obey my custom TypeConverter or ValueSerializer or some other means to coerce my binary string property values into a string-serializable form.

Athari
  • 33,702
  • 16
  • 105
  • 146
Tim Coulter
  • 8,705
  • 11
  • 64
  • 95
  • If you save with `var xaml = System.Windows.Markup.XamlWriter.Save(instance);` and put a breakpoint in your `Base64ValueSerializer` `CanConvertToString` method it will be hit. However, if you use `XamlServices.Save` it doesn't get into the ValueSerializer. I can't find why anywhere in the documentation. Couldn't you just save with `XamlWriter.Save`? – Adolfo Perez Nov 11 '13 at 19:56
  • @Adolfo Perez: that's an interesting observation and I would definitely be open to using XamlWriter if it solves the problem. However, having updated my code to reflect your idea, I see that, despite hitting a breakpoint in CanConvertToString, it doesn't subsequently call ConvertToString, so the Base64 conversion is still unsuccessful. That's disappointing, but thanks for your suggestion. – Tim Coulter Nov 11 '13 at 20:18
  • If you override the return: ` public override bool CanConvertToString(object value, IValueSerializerContext context) { var b = base.CanConvertToString(value, context); return true; }` It will serialize your xaml like this: `` Where is the unicode char for NULL http://www.fileformat.info/info/unicode/char/0000/index.htm – Adolfo Perez Nov 11 '13 at 20:26
  • You could put your logic there in the `CanConvertToString` method so it is more robust. Like analyzing your string value based on your rules. – Adolfo Perez Nov 11 '13 at 20:32
  • @Adolfo Perez: OK, I am happy escaping the null character with a Unicode escape sequence (or any other suitable escape sequence) but I don't see how it's possible to modify the serialized value in CanConvertToString. Would you mind posting an answer with a code snippet. If it works as you describe, I will accept it. Thank you. – Tim Coulter Nov 11 '13 at 21:04
  • In my example i didn't have to modify the serialized value. All i did was forcing the serializer to proceed without doing its internal validations by just returning true, I know its ugly and a hack, but that gets you passed the exception and automatically turns your '\0' input into a ''. I'll post my idea as an answer. I hope someone else has a better solution. – Adolfo Perez Nov 11 '13 at 23:31

2 Answers2

1
public Base64ValueSerializer : ValueSerializer
{
  public override bool CanConvertToString(object value, IValueSerializerContext context)
  { 
     //If your value string contains a '\0' then base.CanConvertToString will return false
     //var canConvert = base.CanConvertToString(value, context); 
     return IsValidString(value);
  }
  private bool IsValidString(string input)
  {
     //Check if input string contains 'invalid' characters that can be converted
     //automatically to its HTML equivalent, like '\0' to '&#x0'
     bool isValid = ...
     return isValid;
  }
}

Your IsValidString logic will depend on the different type of 'invalid characters' you are expecting and whether those can be automatically translated. Ideally you should be able to control the translation through the ConvertToString override, and I'm guessing that since your property is already a string, it doesn't even get there, but not sure.

Have you tried changing your property to 'object' type and see if the ConvertToString method in your ValueSerializer gets executed?

Adolfo Perez
  • 2,834
  • 4
  • 41
  • 61
  • Thanks for your answer. There are 2 very promising ideas here: [1] using the technique you described to bypass the serialization validation and [2] converting the property type to 'object', hoping that it will force all methods of the ValueSerializer or TypeConverter to be called. Unfortunately, although [1] works, it fails during deserialization, because apparently neither XamlReader nor XamlServices know how to handle . Option [2] also doesn't work - both mechanism treat it the same as string. :( It's a pity - we are really close to a solution, but not quite there. – Tim Coulter Nov 12 '13 at 07:09
  • I am accepting your answer, as it provided some great inspiration that eventually led me to a workable solution (which I will document as a separate answer). Thanks again for your help. – Tim Coulter Nov 12 '13 at 11:01
  • Great im interested in hearing how you ended up solving it. – Adolfo Perez Nov 12 '13 at 11:11
1

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().

Tim Coulter
  • 8,705
  • 11
  • 64
  • 95
  • Nice solution! It looks very solid. Glad I was able to help. – Adolfo Perez Nov 12 '13 at 12:41
  • I think it's beneficial to note that even if you only want to serialize your object and don't care about deserialization, you still need to override both `CanConvertTo` and `CanConvertFrom`. I guess _XAML_ serializer checks if when serialized to `string`, the value could potentially be deserialized. – Grx70 Jul 14 '17 at 09:53