Day 2: Serialization of Complex Types and XmlAttributes

This isn't really a rant about Windows Mobile per se, but it's an issue I seem to run into on a consistent basis: you can't deserialize an XmlAttribute into a struct. Consider the following code:

using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Xml.Serialization;
using System.IO;

namespace SmartDeviceProject2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            XmlSerializer ser = new XmlSerializer(typeof(Foo));

            string xml = "<Foo Moo=\"2,3\"/>";
            MemoryStream mem = new MemoryStream();
            StreamWriter writer = new StreamWriter(mem);
            writer.Write(xml);
            writer.Flush();
            mem.Seek(0, SeekOrigin.Begin);
            Foo f = (Foo)ser.Deserialize(mem);
        }
    }

    public struct Bar : IFormattable
    {
        public int X;
        public int Y;
        public static Bar Parse(string s)
        {
            string[] strings = s.Split(',');
            Bar ret = new Bar();
            ret.X = int.Parse(strings[0]);
            ret.Y = int.Parse(strings[1]);
            return ret;
        }
        public override string ToString()
        {
            return string.Format("{0},{1}", X, Y);
        }

        #region IFormattable Members

        public string ToString(string format, IFormatProvider formatProvider)
        {
            return ToString();
        }

        #endregion
    }

    public struct Foo
    {
        [XmlAttribute]
        public Bar Moo;
    }
}

Attempting to run this code on the desktop would fail on the line that instantiates the XmlSerializer: "Cannot serialize member 'Moo' of type SmartDeviceProject2.Foo. XmlAttribute/XmlText cannot be used to encode complex types.".

However, on .NET CF 2.0, it fails on deserialization with the cryptic message: "The type SmartDeviceProject2.Bar was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically."

But my gripe is the following: I have a struct, it is IFormattable, and it can be Parsed (like int.Parse, DateTime.Parse, et al). Why is that not sufficient enough for serialization to and from an XmlAttribute string?

Anyways, here is the workaround:

  1. For all types that you want to be deserialized from attributes, explicitly give them the [XmlElement] attribute. Yes, you read that right.
  2. Create an XmlSerializer. Attach a new method to the UnknownAttribute event.
  3. Whenever this event is fired, use a combination of the attribute name and reflection to find the PropertyInfo/FieldInfo for what XmlSerializer can't deserialize. Then find that object type's static Parse method, and call it on the attribute string. Then use that PropertyInfo/FieldInfo to set the property.

The new code would look something like this (you will need to add your own code to handle if it is a Property, since my sample is only looking for fields):

private void button1_Click(object sender, EventArgs e)
{
    XmlSerializer ser = new XmlSerializer(typeof(Foo));

    string xml = "<Foo Moo=\"2,3\"/>";
    MemoryStream mem = new MemoryStream();
    StreamWriter writer = new StreamWriter(mem);
    writer.Write(xml);
    writer.Flush();
    mem.Seek(0, SeekOrigin.Begin);
    ser.UnknownAttribute += new XmlAttributeEventHandler(ser_UnknownAttribute);
    Foo f = (Foo)ser.Deserialize(mem);
}

void ser_UnknownAttribute(object sender, XmlAttributeEventArgs e)
{
    FieldInfo field = e.ObjectBeingDeserialized.GetType().GetField(e.Attr.Name);
    MethodInfo parse = field.FieldType.GetMethod("Parse");
    field.SetValue(e.ObjectBeingDeserialized, parse.Invoke(null, new object[] { e.Attr.Value }));
}
public struct Foo
{
    [XmlElement]
    public Bar Moo;
}

Unfortunately I have not figured out a similar workaround for serialization...

6 comments:

pedrolamas.com said...

My friend, you should check the IXmlSerializable interface...

Just implement it on your "Bar" class and you'll have your serialization and desearialization solved!! ;)

Regards, Pedro Lamas

Koush said...

Yes, I checked that.
IXmlSerializable does not let you serialize to/from attributes with complex types either.

pedrolamas.com said...

Yes, that's true, you can use it only with xml element, not attribute, but why would you want to serialize a complexe type as an attribute?

Koush said...

Consider serialization of a color:

BackColor="#FF00FF00"

Or a point:

Location="2,3"

There are many scenarios where serializing a complex type to a string is desired. And in general, simple strings tend to be more aesthetically pleasing and readable in XML when they are attributes.
As an obvious example: XAML. Both of the above complex types can attributes off a XAML element. I've been working on a XAML type language/UI toolset, and that is when I ran into this issue.

pedrolamas.com said...

Hum... I can see your point there... :)

pbr said...

One workaround to overcome this problem without using the UnknownAttribute event is to use a 'Surrogate' property of type string in Foo and ignore the original property.
public struct Foo
{
[XmlIgnore]
public Bar Moo;

[XmlAttribute("Moo")]
private string MooSurrogate
{
get{return this.Moo.ToString();}
set{ this.Moo = Bar.Parse(value);}
}
}

This means the class (Foo) controls how it gets serialized and de-serialized and not the code which is serializing or de-serializing.

-Pramod B R