Fun with IXmlSerializable

January 31st, 2009

Have you ever tried to use .NET Xml Serialization with a Hashtable or Dictionary only to discover that you get a fun error that says you can’t serialize objects that implement IDictionary? I wasn’t too thrilled. I think I’m spoiled, because these days I’m addicted to using XmlSerializer and it usually just works. Until now.

All I wanted was to store a quick little XML file structured like so: (though a lot larger)


<?xml version="1.0" encoding="utf-8" ?>
<Fields>
  <Field>
    <Position>1</Position>
    <Name>Account Number</Name>
    <Required>true</Required>
  </Field>
  <Field>
    <Position>2</Position>
    <Name>Address Line 1</Name>
    <Required>true</Required>
  </Field>
  <Field>
    <Position>3</Position>
    <Name>Address Line 2</Name>
    <Required>false</Required>
  </Field>
    <Field>
    <Position>4</Position>
    <Name>City</Name>
    <Required>true</Required>
  </Field>
  <Field>
    <Position>5</Position>
    <Name>State</Name>
    <Required>true</Required>
  </Field>
  <Field>
    <Position>6</Position>
    <Name>Zip</Name>
    <Required>true</Required>
  </Field> 
</Fields>

Then I wanted to read it into a class that had a generic Dictionary. The key would be the position, and the value would be an object that stores the name and required fields (there are actually a couple more, but I’m keeping it simple here). Honestly, should that be so hard? I want to be able to find the field at any position easily using the position as the key.

Here’s what I came up with. It works for this class, but since I’m overriding ReadXml and WriteXml, it obviously isn’t very extensible or pretty. Also, technically IXmlSerializable isn’t for public consumption, though I think enough people have cried that it might be fine to use now. In my opinion, this is rather breakable. I can’t say I’d use it for much more than this, but maybe someone else will get some use or a hint for their own utility class here. 

This is fully testable code, as long as you change the file path and have the xml file there. There is no error trapping or anything, this code is just to demo the concept. Have fun!


using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.IO;
using System.Text;
using System.Diagnostics;

namespace ConsoleApplication1
{
    public class Program
    {
        public class Field
        {
            public string Name;
            public string Required;
        }

        public class Fields : IXmlSerializable
        {
            public Dictionary<int, Field> fieldList;

            public Fields()
            {
                fieldList = new Dictionary<int, Field>();
            }

            public void WriteXml(XmlWriter writer)
            {
                writer.WriteRaw(Environment.NewLine);

                foreach (int key in fieldList.Keys)
                {
                    writer.WriteStartElement("Field");

                    writer.WriteRaw(Environment.NewLine);
                    writer.WriteStartElement("Position");
                    writer.WriteString(key.ToString());
                    writer.WriteEndElement();

                    writer.WriteRaw(Environment.NewLine);
                    writer.WriteStartElement("Name");
                    writer.WriteString(fieldList[key].Name);
                    writer.WriteEndElement();

                    writer.WriteRaw(Environment.NewLine);
                    writer.WriteStartElement("Required");
                    writer.WriteString(fieldList[key].Required);
                    writer.WriteEndElement();

                    writer.WriteRaw(Environment.NewLine);
                    writer.WriteEndElement();
                    writer.WriteRaw(Environment.NewLine);
                }
            }

            public void ReadXml(XmlReader reader)
            {
                // start with the first main node
                XmlNodeType type = reader.MoveToContent();
                if (type == XmlNodeType.Element &amp;&amp; reader.LocalName == "Fields")
                {
                    // read in each Field element
                    reader.ReadStartElement("Fields");
                    while (type == XmlNodeType.Element &amp;&amp; reader.LocalName == "Field")
                    {
                        reader.ReadStartElement("Field");
                        Field field = new Field();
                        int position = reader.ReadElementContentAsInt("Position", "");
                        field.Name = reader.ReadElementContentAsString("Name", "");
                        field.Required = reader.ReadElementContentAsString("Required", "");
                        fieldList.Add(position, field);
                        reader.ReadEndElement();
                    }
                }
            }

            public XmlSchema GetSchema()
            {
                return (null);
            }

        }

        static void Main(string[] args)
        {
            // let's take it for a test drive with no file
            // using the debug output console to display the XML string
            Trace.Listeners.Clear();
            Trace.Listeners.Add(new DefaultTraceListener());

            // construct an object
            Fields fields = new Fields();
            Field field = new Field();
            field.Name = "Account Number";
            field.Required = "true";
            fields.fieldList.Add(1, field);

            Field field2 = new Field();
            field2.Name = "Address Line 1";
            field2.Required = "true";
            fields.fieldList.Add(2, field2);

            // serialize it out to the debug window
            XmlSerializer serializer = new XmlSerializer(typeof(Fields));
            StringBuilder sb = new StringBuilder();
            serializer.Serialize(new StringWriter(sb), fields);
            Debug.WriteLine(sb.ToString());

            // now read it back in (set a breakpoint to verify it reads it)
            Fields fields2 = serializer.Deserialize(new StringReader(sb.ToString())) as Fields;

            // let's try it with the file now (set a breakpoint to verify it reads it)
            Fields fields3 = serializer.Deserialize(new StringReader(File.ReadAllText(@"C:\XMLFile1.xml"))) as Fields;

            // just for giggles
            Debug.WriteLine(fields3.fieldList[3].Name);
        }
    }

}

XML Serialization and List

January 25th, 2009

I was writing a small wrapper class to consume a third party XML response from a web service. Using a little helper class is a lot easier than playing with DOM. One of the elements was an Errors element that could have multiple Error child elements, like so:


<Errors>
   <Error>detail</Error>
   <Error>detail</Error>
</Errors>

In my class, I wanted to use a List<string> to account for this, but since I was consuming XML, I needed to be able to tell it the element name, otherwise it serializes and deserializes as just “string”, like this.


<?xml version="1.0" encoding="utf-16"?>
<WebServiceResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<statusCode>failure</statusCode>
<recordsAffected>0</recordsAffected>
<Errors>
   <string>Foo</string>
</Errors>
</WebServiceResponse>

 

So when I tried to read in the xml, I got an error because it couldn’t find an Error element (it was string). Not so helpful.

I needed the ElementName property in my class.

[XmlArrayItem(typeof(string), ElementName = "Error")]
public List<string> Errors;

Adding this, the element was correctly deserialized so it could read something like this.


<?xml version="1.0" encoding="utf-16"?>
<WebServiceResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<statusCode>failure</statusCode>
<recordsAffected>0</recordsAffected>
<Errors>
   <Error>Foo</Error>
</Errors>
</WebServiceResponse>

Here is some code to play with so you can try it out and see how the attribute affects serialization and deserialization. Have fun!


using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System.Text;

namespace ConsoleApplication1
{
    public class Program
    {
        public class WebServiceResponse
        {
            public string statusCode;
            public string recordsAffected;

            [XmlArrayItem(typeof(string), ElementName = "Error")]
            public List<string> Errors;
        }

        static void Main(string[] args)
        {
            // if we were to set the elements themselves, it could look like this
            WebServiceResponse r = new WebServiceResponse();
            r.statusCode = "success";
            r.recordsAffected = "1";
            r.Errors = new List<string>();
            r.Errors.Add("Foo");

            XmlSerializer serializer = new XmlSerializer(typeof(WebServiceResponse));
            StringBuilder sb = new StringBuilder();
            serializer.Serialize(new StringWriter(sb), r);
            Console.WriteLine(sb.ToString());

            string s = sb.ToString();
            // more likely, we'd be reading them in from an XML response to manipulate, like so
            // (you'd have the xml response from a web service in a string or such)
            TextReader sr = new StringReader(s);
            WebServiceResponse r2 = serializer.Deserialize(sr) as WebServiceResponse;
            int numErrors = r2.Errors == null ? 0 : r2.Errors.Count;
            Console.WriteLine("There were " + numErrors + " errors");

            Console.Read();
        }
    }
}