Showing posts with label XmlSerializer. Show all posts
Showing posts with label XmlSerializer. Show all posts

Day 5: XslCompiledTransform, I miss you.

Oh man, Day 5? I'm really bad at this. But hey, I was up front that I wouldn't actually be posting on a daily basis.

It's pretty hard to write an application that doesn't touch XML in some way these days. A while ago, I found myself in several situations where I was wishing I could run an XML document through an XSLT before processing it. As a workaround, I'd load the document into an XmlDocument and do the transformation programmatically.

That's obviously a bit of a bitch, so after some thorough digging, I found that Windows Mobile 6+ ships with the unmanaged MSXML API, and that you can create wrappers for these COM objects using the MIDL compiler and adding a reference to the type library file (TLB).

My deterrence from using this is that for everything I write I need to remember to add the COM wrapper, Interop.MSXML2.dll, to my CAB setup files. This isn't a huge deal when it's a single file, obviously. But updating my setup projects every time I add a COM wrapper is very tedious.

Normally, I could just embed that DLL as a resource in my assembly, and call Assembly.Load on the resource stream. However, .NET CF does not have that particular overload of Assembly.Load. So my horrible solution: embed it as a resource, and when the static constructor on XslCompiledTransform is called, write that file out to the current directory and load it:


static XslCompiledTransform()
{
    try
    {
        Assembly.LoadFrom("Interop.MSXML2.dll");
    }
    catch (Exception)
    {
        string cwd = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
        using (FileStream fs = new FileStream(Path.Combine(cwd, "Interop.MSXML2.dll"), FileMode.Create))
        {
            using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("CompactFrameworkExtensions.Interop.MSXML2.dll"))
            {
                fs.Copy(stream);
            }
        }

    }
}

Note: This may cause "dirty" uninstallation issues.

Oh, that reminds me, the "fs.Copy(stream)" is a handy extension method that allows you to copy the contents of a stream into another.

Usage is pretty straightforward:

class Program
{
    static void Main(string[] args)
    {
        XslCompiledTransform trans = new XslCompiledTransform();
        trans.LoadString(Resource1.XSL);
        string ret = trans.TransformString(Resource1.XML);
    }
}

Resource1.XML and Resource1.XSL are simply strings that contain the documents.

Click here for the full source to XslCompiledTransform and some other handy extension methods.

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